diff --git a/fixtures/html/checkbox-radio-demo.html b/fixtures/html/checkbox-radio-demo.html new file mode 100644 index 000000000..3e64bfab6 --- /dev/null +++ b/fixtures/html/checkbox-radio-demo.html @@ -0,0 +1,214 @@ + + + + Checkbox and Radio Button Demo + + + +

JSAR Runtime: Checkbox and Radio Button Demo

+ +
+

Checkbox Examples

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+

Radio Button Examples

+ +
+ Choose your preferred theme:
+ +
+ + +
+ + + +
+ +
+ Subscription level:
+ +
+ + +
+ + + +
+
+ +
+

Form Validation Demo

+
+
+ + +
+ +
+ Select your age group: *
+ +
+ + +
+ + + +
+ + +
+
+ +
+

Demo Controls

+

These buttons demonstrate programmatic control of checkboxes and radio buttons:

+ + + + + + +
+
+ + + + \ No newline at end of file diff --git a/fixtures/html/checkbox-radio-states.html b/fixtures/html/checkbox-radio-states.html new file mode 100644 index 000000000..9b8f88ef0 --- /dev/null +++ b/fixtures/html/checkbox-radio-states.html @@ -0,0 +1,288 @@ + + + + Checkbox and Radio Button Visual States Reference + + + +
+

Checkbox & Radio Button Visual States

+

JSAR Runtime Implementation Reference

+ +
+

Implementation Notes

+

This reference shows all visual states supported by the JSAR runtime checkbox and radio button implementation. + Each control is rendered using Skia canvas with pixel-perfect appearance that matches standard browser rendering.

+ +
+ +

Checkbox States

+
+
+

Unchecked

+
+ + +
+
+ Empty checkbox with border and white background +
+
+ +
+

Checked

+
+ + +
+
+ Checkbox with checkmark symbol +
+
+ +
+

Indeterminate

+
+ + +
+
+ Checkbox with horizontal bar (set via JavaScript) +
+
+ +
+

Disabled Unchecked

+
+ + +
+
+ Grayed out, non-interactive +
+
+ +
+

Disabled Checked

+
+ + +
+
+ Grayed out with visible checkmark +
+
+ +
+

Required

+
+ + +
+
+ Required for form validation +
+
+ +
+

Focused

+
+ + +
+
+ Click or tab to see focus outline +
+
+
+ +

Radio Button States

+
+
+

Unselected

+
+ + +
+
+ Empty circle with border +
+
+ +
+

Selected

+
+ + +
+
+ Circle with filled dot +
+
+ +
+

Disabled Unselected

+
+ + +
+
+ Grayed out, non-interactive +
+
+ +
+

Disabled Selected

+
+ + +
+
+ Grayed out with visible dot +
+
+ +
+

Required Group

+
+ +
+ + +
+
+ Required radio group +
+
+ +
+

Focused

+
+ + +
+
+ Click or tab to see focus outline +
+
+
+ +
+

Technical Implementation Details

+

Rendering Engine: Skia Canvas API for high-quality graphics

+

Color Scheme:

+ +

Dimensions: 1em × 1em (adjustable via CSS)

+

Border Radius: 2px for checkboxes, 50% for radio buttons

+
+
+ + + + \ No newline at end of file diff --git a/src/bindings/dom/html_input_element.cpp b/src/bindings/dom/html_input_element.cpp index 7f5c82af2..a8d47a415 100644 --- a/src/bindings/dom/html_input_element.cpp +++ b/src/bindings/dom/html_input_element.cpp @@ -18,6 +18,7 @@ namespace dombinding T::InstanceAccessor("placeholder", &T::PlaceholderGetter, &T::PlaceholderSetter), T::InstanceAccessor("name", &T::NameGetter, &T::NameSetter), T::InstanceAccessor("valueAsNumber", &T::ValueAsNumberGetter, &T::ValueAsNumberSetter), + T::InstanceAccessor("indeterminate", &T::IndeterminateGetter, &T::IndeterminateSetter), // Methods T::InstanceMethod("checkValidity", &T::CheckValidity), @@ -179,6 +180,23 @@ namespace dombinding } } + Napi::Value HTMLInputElement::IndeterminateGetter(const Napi::CallbackInfo &info) + { + Napi::Env env = info.Env(); + Napi::HandleScope scope(env); + return Napi::Boolean::New(env, node->indeterminate()); + } + + void HTMLInputElement::IndeterminateSetter(const Napi::CallbackInfo &info, const Napi::Value &value) + { + Napi::Env env = info.Env(); + Napi::HandleScope scope(env); + if (value.IsBoolean()) + { + node->setIndeterminate(value.As().Value()); + } + } + // Method implementations Napi::Value HTMLInputElement::CheckValidity(const Napi::CallbackInfo &info) { diff --git a/src/bindings/dom/html_input_element.hpp b/src/bindings/dom/html_input_element.hpp index 05d2685f6..a42cdd4af 100644 --- a/src/bindings/dom/html_input_element.hpp +++ b/src/bindings/dom/html_input_element.hpp @@ -40,6 +40,9 @@ namespace dombinding Napi::Value ValueAsNumberGetter(const Napi::CallbackInfo &info); void ValueAsNumberSetter(const Napi::CallbackInfo &info, const Napi::Value &value); + Napi::Value IndeterminateGetter(const Napi::CallbackInfo &info); + void IndeterminateSetter(const Napi::CallbackInfo &info, const Napi::Value &value); + private: // Methods Napi::Value CheckValidity(const Napi::CallbackInfo &info); diff --git a/src/client/html/html_input_element.cpp b/src/client/html/html_input_element.cpp index 760fafc76..a8e14fc94 100644 --- a/src/client/html/html_input_element.cpp +++ b/src/client/html/html_input_element.cpp @@ -204,4 +204,81 @@ namespace dom } } } + + void HTMLInputElement::handleClick() + { + // Only handle click for checkbox and radio buttons + std::string input_type = type(); + if (input_type != "checkbox" && input_type != "radio") + return; + + // Don't handle click if disabled + if (disabled()) + return; + + if (input_type == "checkbox") + { + // Toggle checked state for checkbox + setChecked(!checked_); + // Clear indeterminate state when user clicks + setIndeterminate(false); + } + else if (input_type == "radio") + { + // For radio buttons, uncheck all other radio buttons with the same name + // and check this one + if (!checked_) + { + // Find other radio buttons with the same name and uncheck them + std::string radio_name = name(); + if (!radio_name.empty()) + { + // TODO: Implement radio group handling + // For now, just set this radio button as checked + } + setChecked(true); + } + } + + // Trigger change event + triggerChangeEvent(); + } + + void HTMLInputElement::triggerChangeEvent() + { + // TODO: Implement proper event dispatching + // For now, this is a placeholder for change event triggering + // This should create and dispatch a 'change' event + } + + void HTMLInputElement::click() + { + // Handle checkbox/radio interaction first + handleClick(); + + // Then call parent click implementation for any additional behavior + HTMLElement::click(); + } + + void HTMLInputElement::updateVisualRepresentation() + { + // This method will be called by the layout system + // The actual visual update will be handled by LayoutHTMLInput + // We just need to mark the layout as needing an update + // TODO: Add proper layout invalidation mechanism + } + + void HTMLInputElement::focus() + { + focused_ = true; + updateVisualRepresentation(); + HTMLElement::focus(); + } + + void HTMLInputElement::blur() + { + focused_ = false; + updateVisualRepresentation(); + HTMLElement::blur(); + } } diff --git a/src/client/html/html_input_element.hpp b/src/client/html/html_input_element.hpp index 98c4dd47c..e01c3f83c 100644 --- a/src/client/html/html_input_element.hpp +++ b/src/client/html/html_input_element.hpp @@ -42,7 +42,12 @@ namespace dom } void setChecked(bool value) { - checked_ = value; + if (checked_ != value) + { + checked_ = value; + // Update visual representation when checked state changes + updateVisualRepresentation(); + } } bool disabled() const @@ -104,15 +109,41 @@ namespace dom double valueAsNumber() const; void setValueAsNumber(double value); + // Interaction handling + void handleClick(); + void click() override; // Override click to handle checkbox/radio interaction + + // Support for indeterminate state (checkbox only) + bool indeterminate() const + { + return indeterminate_; + } + void setIndeterminate(bool value) + { + indeterminate_ = value; + } + + // Focus state support + bool focused() const + { + return focused_; + } + void focus() override; + void blur() override; + private: void createdCallback(bool from_scripting) override; void attributeChangedCallback(const std::string &name, const std::string &oldValue, const std::string &newValue) override; + void triggerChangeEvent(); + void updateVisualRepresentation(); private: std::string value_; bool checked_ = false; + bool indeterminate_ = false; + bool focused_ = false; std::string custom_validity_message_; }; } diff --git a/src/client/layout/layout_html_input.cpp b/src/client/layout/layout_html_input.cpp index e09c22f69..16753d88e 100644 --- a/src/client/layout/layout_html_input.cpp +++ b/src/client/layout/layout_html_input.cpp @@ -1,20 +1,236 @@ #include "./layout_html_input.hpp" #include +#include +#include +#include +#include +#include +#include +#include namespace client_layout { + using namespace builtin_scene; + using namespace dom; + void LayoutHTMLInput::entityDidCreate(builtin_scene::ecs::EntityId entity) { LayoutReplaced::entityDidCreate(entity); - // Add any input-specific entity components here - // For now, we'll use the basic layout handling + // Add WebContent component for checkbox/radio rendering + if (needsCustomRendering()) + { + auto addWebContentComponent = [&entity](Scene &scene) + { + scene.addComponent(entity, WebContent("input-control", 20.0f, 20.0f)); + }; + useSceneWithCallback(addWebContentComponent); + + // Trigger initial render + updateInputVisuals(); + } } void LayoutHTMLInput::entityWillBeDestroyed(builtin_scene::ecs::EntityId entity) { - // Clean up any input-specific entity components here + auto removeWebContentComponent = [&entity](Scene &scene) + { + if (scene.hasComponent(entity)) + { + scene.removeComponent(entity); + } + }; + useSceneWithCallback(removeWebContentComponent); LayoutReplaced::entityWillBeDestroyed(entity); } + + bool LayoutHTMLInput::needsCustomRendering() const + { + const auto &input_element = Node::AsChecked(node()); + std::string type = input_element.type(); + return type == "checkbox" || type == "radio"; + } + + void LayoutHTMLInput::updateInputVisuals() + { + if (!needsCustomRendering()) + return; + + auto updateVisuals = [this](Scene &scene) + { + if (!scene.hasComponent(entity())) + return; + + WebContent &webContent = scene.getComponentChecked(entity()); + webContent.setContentDirty(true); + + renderCheckboxOrRadio(); + }; + useSceneWithCallback(updateVisuals); + } + + void LayoutHTMLInput::renderCheckboxOrRadio() + { + auto renderControl = [this](Scene &scene) + { + if (!scene.hasComponent(entity())) + return; + + WebContent &webContent = scene.getComponentChecked(entity()); + SkCanvas *canvas = webContent.canvas(); + if (!canvas) + return; + + const auto &input_element = Node::AsChecked(node()); + std::string type = input_element.type(); + bool checked = input_element.checked(); + bool disabled = input_element.disabled(); + bool indeterminate = input_element.indeterminate(); + bool focused = input_element.focused(); + + // Clear the canvas + canvas->clear(SK_ColorTRANSPARENT); + + // Get control dimensions + float width = webContent.logicalWidth(); + float height = webContent.logicalHeight(); + float size = std::min(width, height); + + // Center the control + float x = (width - size) * 0.5f; + float y = (height - size) * 0.5f; + SkRect controlRect = SkRect::MakeXYWH(x, y, size, size); + + // Define colors + SkColor borderColor = disabled ? SkColorSetARGB(255, 170, 170, 170) : SkColorSetARGB(255, 118, 118, 118); + SkColor backgroundColor = disabled ? SkColorSetARGB(255, 245, 245, 245) : SK_ColorWHITE; + SkColor checkColor = disabled ? SkColorSetARGB(255, 170, 170, 170) : SkColorSetARGB(255, 51, 51, 51); + SkColor focusColor = SkColorSetARGB(255, 0, 123, 255); // Blue focus outline + + if (type == "checkbox") + { + // Draw checkbox background + SkPaint bgPaint; + bgPaint.setColor(backgroundColor); + bgPaint.setStyle(SkPaint::kFill_Style); + bgPaint.setAntiAlias(true); + + SkRRect roundedRect = SkRRect::MakeRectXY(controlRect, 2.0f, 2.0f); + canvas->drawRRect(roundedRect, bgPaint); + + // Draw checkbox border + SkPaint borderPaint; + borderPaint.setColor(borderColor); + borderPaint.setStyle(SkPaint::kStroke_Style); + borderPaint.setStrokeWidth(1.0f); + borderPaint.setAntiAlias(true); + canvas->drawRRect(roundedRect, borderPaint); + + // Draw checkmark if checked, or indeterminate bar if indeterminate + if (indeterminate) + { + // Draw indeterminate bar (horizontal line) + SkPaint indeterminatePaint; + indeterminatePaint.setColor(checkColor); + indeterminatePaint.setStyle(SkPaint::kStroke_Style); + indeterminatePaint.setStrokeWidth(2.0f); + indeterminatePaint.setStrokeCap(SkPaint::kRound_Cap); + indeterminatePaint.setAntiAlias(true); + + float barInset = size * 0.3f; + float barY = y + size * 0.5f; + canvas->drawLine(x + barInset, barY, x + size - barInset, barY, indeterminatePaint); + } + else if (checked) + { + // Draw checkmark + SkPaint checkPaint; + checkPaint.setColor(checkColor); + checkPaint.setStyle(SkPaint::kStroke_Style); + checkPaint.setStrokeWidth(2.0f); + checkPaint.setStrokeCap(SkPaint::kRound_Cap); + checkPaint.setStrokeJoin(SkPaint::kRound_Join); + checkPaint.setAntiAlias(true); + + // Draw checkmark path + SkPath checkPath; + float checkInset = size * 0.25f; + float checkX = x + checkInset; + float checkY = y + size * 0.5f; + float checkW = size - 2 * checkInset; + float checkH = checkW * 0.6f; + + checkPath.moveTo(checkX, checkY); + checkPath.lineTo(checkX + checkW * 0.4f, checkY + checkH * 0.6f); + checkPath.lineTo(checkX + checkW, checkY - checkH * 0.4f); + + canvas->drawPath(checkPath, checkPaint); + } + } + else if (type == "radio") + { + // Draw radio button background + SkPaint bgPaint; + bgPaint.setColor(backgroundColor); + bgPaint.setStyle(SkPaint::kFill_Style); + bgPaint.setAntiAlias(true); + + float centerX = controlRect.centerX(); + float centerY = controlRect.centerY(); + float radius = size * 0.5f; + + canvas->drawCircle(centerX, centerY, radius, bgPaint); + + // Draw radio button border + SkPaint borderPaint; + borderPaint.setColor(borderColor); + borderPaint.setStyle(SkPaint::kStroke_Style); + borderPaint.setStrokeWidth(1.0f); + borderPaint.setAntiAlias(true); + canvas->drawCircle(centerX, centerY, radius, borderPaint); + + // Draw radio dot if checked + if (checked) + { + SkPaint dotPaint; + dotPaint.setColor(checkColor); + dotPaint.setStyle(SkPaint::kFill_Style); + dotPaint.setAntiAlias(true); + + float dotRadius = radius * 0.4f; + canvas->drawCircle(centerX, centerY, dotRadius, dotPaint); + } + } + + // Draw focus outline if focused and not disabled + if (focused && !disabled) + { + SkPaint focusPaint; + focusPaint.setColor(focusColor); + focusPaint.setStyle(SkPaint::kStroke_Style); + focusPaint.setStrokeWidth(2.0f); + focusPaint.setAntiAlias(true); + + if (type == "checkbox") + { + SkRRect focusRect = SkRRect::MakeRectXY( + controlRect.makeInset(-3.0f, -3.0f), 3.0f, 3.0f); + canvas->drawRRect(focusRect, focusPaint); + } + else if (type == "radio") + { + float centerX = controlRect.centerX(); + float centerY = controlRect.centerY(); + float focusRadius = size * 0.5f + 3.0f; + canvas->drawCircle(centerX, centerY, focusRadius, focusPaint); + } + } + + webContent.setContentDirty(false); + webContent.setSurfaceDirty(true); + }; + + useSceneWithCallback(renderControl); + } } diff --git a/src/client/layout/layout_html_input.hpp b/src/client/layout/layout_html_input.hpp index 63da37c78..fa49289c9 100644 --- a/src/client/layout/layout_html_input.hpp +++ b/src/client/layout/layout_html_input.hpp @@ -21,5 +21,10 @@ namespace client_layout private: void entityDidCreate(builtin_scene::ecs::EntityId entity) override; void entityWillBeDestroyed(builtin_scene::ecs::EntityId entity) override; + + // Methods for rendering checkbox/radio controls + void updateInputVisuals(); + void renderCheckboxOrRadio(); + bool needsCustomRendering() const; }; } diff --git a/tests/client/html_input_element_tests.cpp b/tests/client/html_input_element_tests.cpp new file mode 100644 index 000000000..f04960370 --- /dev/null +++ b/tests/client/html_input_element_tests.cpp @@ -0,0 +1,95 @@ +#define CATCH_CONFIG_MAIN +#include "../catch2/catch_amalgamated.hpp" +#include +#include + +using namespace dom; + +TEST_CASE("HTMLInputElement checkbox functionality", "[HTMLInputElement]") +{ + auto document = std::make_shared(); + HTMLInputElement checkbox(document); + + // Set type to checkbox + checkbox.setType("checkbox"); + REQUIRE(checkbox.type() == "checkbox"); + + // Test initial unchecked state + REQUIRE(checkbox.checked() == false); + REQUIRE(checkbox.indeterminate() == false); + REQUIRE(checkbox.focused() == false); + + // Test setting checked state + checkbox.setChecked(true); + REQUIRE(checkbox.checked() == true); + + // Test indeterminate state + checkbox.setIndeterminate(true); + REQUIRE(checkbox.indeterminate() == true); + + // Test focus state + checkbox.focus(); + REQUIRE(checkbox.focused() == true); + + checkbox.blur(); + REQUIRE(checkbox.focused() == false); + + // Test that click toggles checked state when not disabled + checkbox.setChecked(false); + checkbox.handleClick(); + REQUIRE(checkbox.checked() == true); + + // Test that click clears indeterminate state + checkbox.setIndeterminate(true); + checkbox.handleClick(); + REQUIRE(checkbox.indeterminate() == false); + + // Test disabled state prevents interaction + checkbox.setDisabled(true); + checkbox.setChecked(false); + checkbox.handleClick(); + REQUIRE(checkbox.checked() == false); // Should not change when disabled +} + +TEST_CASE("HTMLInputElement radio functionality", "[HTMLInputElement]") +{ + auto document = std::make_shared(); + HTMLInputElement radio(document); + + // Set type to radio + radio.setType("radio"); + REQUIRE(radio.type() == "radio"); + + // Test initial unchecked state + REQUIRE(radio.checked() == false); + + // Test that click sets checked state for radio + radio.handleClick(); + REQUIRE(radio.checked() == true); + + // Test that clicking checked radio doesn't uncheck it + radio.handleClick(); + REQUIRE(radio.checked() == true); // Radio should remain checked + + // Test disabled state prevents interaction + radio.setDisabled(true); + radio.setChecked(false); + radio.handleClick(); + REQUIRE(radio.checked() == false); // Should not change when disabled +} + +TEST_CASE("HTMLInputElement validation with checkbox/radio", "[HTMLInputElement]") +{ + auto document = std::make_shared(); + HTMLInputElement checkbox(document); + + checkbox.setType("checkbox"); + checkbox.setRequired(true); + + // Required checkbox should be invalid when unchecked + REQUIRE(checkbox.checkValidity() == false); + + // Required checkbox should be valid when checked + checkbox.setChecked(true); + REQUIRE(checkbox.checkValidity() == true); +} \ No newline at end of file