Implement checkbox rendering (13×13px box with ✓ text glyph when checked), radio buttons (circle with filled dot), label click delegation (for attribute and implicit wrapping), disabled state enforcement (blocks clicks/focus/submission with visual overlay), button type handling (submit/button/reset for both <button> and <input> variants), form data collection for checked controls, and form reset. Includes code review fixes: replaced broken SolidRect checkmark with text glyph, added disabled overlay for button/textarea, rewrote integration tests to verify actual behavior. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
20 KiB
Story 4.2: Buttons, Checkboxes & Radio Buttons
Status: done
Story
As a web user, I want to click buttons, toggle checkboxes, and select radio options, So that I can make choices and trigger actions in web forms.
Acceptance Criteria
-
Button click and submission:
<button>and<input type="submit">elements render with UA stylesheet chrome (centered text, border, padding). Clicking fires the element'sclickevent. Submit buttons trigger form submission unlesspreventDefault()is called.<button type="button">fires click but does not submit.<button type="reset">resets form controls to initial values.<input type="button">renders with itsvalueas label and fires click only. Per HTML §4.10.6. -
Checkbox toggle:
<input type="checkbox">renders as a small square box (13×13px default, border, background). Clicking toggles the DOMcheckedproperty and dispatches achangeevent (bubbles). The visual state reflects checked (checkmark symbol) vs unchecked. ThecheckedHTML attribute sets the initial state. Per HTML §4.10.5.1.15. -
Radio button selection:
<input type="radio">renders as a small circle (13×13px default). Clicking sets this radio'scheckedto true and unchecks all other radios in the samenamegroup within the same form (or document if not in a form). Achangeevent fires on the newly checked radio. Per HTML §4.10.5.1.16. -
Label association:
<label for="id">clicking activates the associated control (identified by matchingidattribute).<label>wrapping a control (implicit association) clicking activates the first labelable descendant. Activation means: focus for text inputs, toggle for checkboxes, select for radios, click for buttons. Per HTML §4.10.4. -
Disabled state: Form controls with the
disabledattribute do not respond to clicks, do not dispatchchangeevents, cannot be focused, and render in a dimmed/grayed-out visual style (opacity or color change). Disabled controls are excluded from form submission. Per HTML §4.10.18.5. -
Visual states: Checkboxes and radios render distinct checked vs unchecked appearances. Buttons render a pressed appearance on
:active(mouse-down). Disabled controls render visually distinct from enabled controls. Focus rings render on focused controls. -
Integration tests verify each control type's click behavior and event dispatch, golden tests cover rendering of checked/unchecked/disabled states, and
just cipasses.
What NOT to Implement
- No keyboard Toggle -- Space/Enter to toggle checkboxes/radios deferred to Story 4.6.
- No Tab navigation -- keyboard focus cycling deferred to Story 4.7.
- No indeterminate checkbox state --
indeterminateproperty deferred. - No
<input type="image">support -- image submit buttons deferred. - No
<input type="file">support -- file picker deferred. - No
<input type="color">or date pickers -- complex controls deferred. - No custom button styling beyond UA defaults --
:hover,:activepseudo-class CSS matching exists but button-specific active rendering is limited to simple visual feedback. - No
<fieldset>/<legend>disabled propagation -- disabled attribute only on individual controls.
Tasks / Subtasks
-
Task 1: Checkbox rendering and toggle (AC: #2, #6)
- 1.1 Add checkbox-specific UA stylesheet rules (13×13px box, border, display: inline-block, vertical-align: middle)
- 1.2 In display_list builder, render checkbox box as bordered rect; when checked, render checkmark (✓ text or line segments)
- 1.3 Add
checked_states: HashMap<NodeId, bool>to AppState for runtime checked tracking - 1.4 In event_handler click default action, if target is checkbox: toggle checked_states entry, dispatch
changeevent - 1.5 Read initial
checkedattribute from DOM to seed checked_states on first interaction - 1.6 Feed checked_states to layout/display_list for visual rendering
-
Task 2: Radio button rendering and group logic (AC: #3, #6)
- 2.1 Add radio-specific UA stylesheet rules (13×13px circle via border-radius: 50%, border, display: inline-block)
- 2.2 In display_list builder, render radio as circle; when checked, render filled inner dot
- 2.3 In event_handler click default action for radio: set this radio checked, find all radios with same
namein same form (or document), uncheck them, dispatchchangeon newly checked - 2.4 Read initial
checkedattribute for radio group initial state
-
Task 3: Label association and click delegation (AC: #4)
- 3.1 In event_handler click default action, if target is
<label>: resolveforattribute to target element byid, or find first labelable descendant - 3.2 Activate the resolved control: simulate click for buttons/checkboxes/radios, focus for text inputs
- 3.3 Ensure label click does not double-fire when label wraps the control (event already targets control)
- 3.1 In event_handler click default action, if target is
-
Task 4: Disabled state enforcement (AC: #5)
- 4.1 In event_handler, check
disabledattribute before processing click default actions for form controls - 4.2 Add disabled visual style in UA stylesheet or display_list (reduced opacity or gray color)
- 4.3 Exclude disabled controls from form submission in
form.rs - 4.4 Prevent focus on disabled elements in
set_content_focus()
- 4.1 In event_handler, check
-
Task 5: Button type handling (AC: #1)
- 5.1 Ensure
<button>defaults totype="submit"per spec;<input type="submit">fires form submission - 5.2
<button type="button">and<input type="button">fire click only, no form submission - 5.3
<button type="reset">/<input type="reset">resets form inputs to initial values (clear input_values, uncheck checked_states where initial was unchecked) - 5.4 Ensure button label comes from:
<button>text content,<input>value attribute, or default ("Submit"/"Reset")
- 5.1 Ensure
-
Task 6: Form data collection for checkboxes/radios (AC: #2, #3)
- 6.1 In
form.rs, replace the TODO at line 60 — collect checkboxname=value(default value "on") when checked - 6.2 Collect radio
name=valuefor the checked radio in each group - 6.3 Skip unchecked checkboxes and radios in form data
- 6.1 In
-
Task 7: Integration tests and golden tests (AC: #7)
- 7.1 Add golden test for checkbox rendering (checked and unchecked states)
- 7.2 Add golden test for radio button rendering (selected and unselected)
- 7.3 Add golden test for disabled form controls (visual distinction)
- 7.4 Add golden test for button rendering (submit, button, reset types)
- 7.5 Add integration tests for checkbox toggle + change event dispatch
- 7.6 Add integration tests for radio group selection + change event dispatch
- 7.7 Add integration tests for label click delegation (for attribute and wrapping)
- 7.8 Add integration tests for disabled control rejection
- 7.9 Run
just ciand verify all pass
Dev Notes
Existing Infrastructure (DO NOT REBUILD)
Event dispatch pipeline (crates/app_browser/src/event_handler.rs) already handles:
- Click dispatch with
dispatch_mouse_event_with_swap()→ JS handlers → default actions find_interactive_ancestor()walks DOM to find nearest interactive element (input, button, textarea, select, a)handle_click_default_actions()dispatches to form submission for submit buttons (lines ~283-403)dispatch_change_event_with_swap()helper exists from Story 4.1
Form submission (crates/app_browser/src/form.rs) already:
submit_form()collects form data, builds query string or bodyfind_ancestor_form()locates enclosing<form>element- Line 60 has a TODO:
"checkbox" | "radio" => return, // TODO: handle checked state— this is where 4.2 plugs in - GET and POST with
application/x-www-form-urlencodedworking
UA stylesheet (crates/style/src/ua_stylesheet.rs) already:
button, input[type="submit"], input[type="button"], input[type="reset"]→display: inline-block; padding: 1px 6px; text-align: center;- All form elements →
display: inline-block - Text inputs →
border: 2px inset; padding: 1px 2px; width: 173px; height: 1.2em; background-color: white; input[type="hidden"]→display: none
Layout synthetic text (crates/layout/src/engine/box_tree.rs) already:
- Creates synthetic text children for submit/reset buttons using
valueattribute or default labels - Pattern to follow for rendering checkbox/radio visual content
Focus/blur system (crates/app_browser/src/app_state.rs) already:
focused_node: Option<NodeId>andset_content_focus()- Focus/blur event dispatch via
dispatch_focus_change() - Caret blink tracking (not needed for checkboxes but shows state pattern)
:checked pseudo-class (crates/selectors/src/selector.rs) already:
- Selector matching for
:checkedexists — just needs runtime checked state to be consulted during matching
Display list and render already:
DisplayItem::SolidRectfor filled rectanglesDisplayItem::Textfor text renderingfill_rect()with alpha blending support- Pattern from
DisplayItem::Caretshows how to add new display items
What Needs to Be Built
-
Checked state tracking: Add
checked_states: HashMap<NodeId, bool>toAppState. Seed from DOMcheckedattribute on first access. Update on click toggle. Feed to layout/display_list for visual state and to selectors for:checkedmatching. -
Checkbox visual rendering: In display_list builder, when a checkbox node has a checked state, render a small bordered square with an inner checkmark (either a "✓" text glyph or two short line segments). Unchecked = just the bordered square.
-
Radio visual rendering: Similar to checkbox but circular. Checked = filled inner circle. Use border-radius in UA stylesheet to get circular border (if border-radius rendering exists) or draw a circle directly in the display list.
-
Click toggle logic: In
handle_click_default_actions(), add branches forinput[type="checkbox"]andinput[type="radio"]. Checkbox: toggle state, dispatch change. Radio: set checked, uncheck siblings by name, dispatch change. -
Radio group resolution: Find all
<input type="radio">elements with the samenameattribute within the same<form>ancestor (or the document root if not in a form). Uncheck all except the clicked one. -
Label click delegation: When
<label>is clicked, resolve theforattribute to an element byid, or walk descendants to find first labelable element. Trigger that element's click/focus behavior. -
Disabled enforcement: Check
disabledattribute before allowing click default actions, focus changes, and form data collection. -
Form reset: Implement reset logic — clear
input_valuesentries (revert to DOMvalueattributes), resetchecked_statesto initial DOMcheckedattributes.
Architecture Compliance
| Rule | How This Story Complies |
|---|---|
| Layer boundaries | Checked state in app_browser (Layer 3). Visual rendering via display_list (Layer 1). UA styles in style (Layer 1). No upward dependencies. |
| Unsafe policy | No unsafe code needed. All rendering uses existing safe APIs. |
| Pipeline sequence | Style → Layout → Display List → Render. Checkbox/radio appearance computed at display list build time using checked state from app_state. |
| Arena ID pattern | Checked state tracked by NodeId key. No new ID types needed. |
| Error handling | Attribute parsing uses graceful fallback. Missing for target silently skips. |
| Feature implementation order | 1. UA stylesheet rules, 2. Layout/display_list rendering, 3. Event handler toggle logic, 4. Form data collection, 5. Golden + integration tests, 6. Update HTML5 checklist. |
File Modification Plan
| File | Change |
|---|---|
crates/style/src/ua_stylesheet.rs |
Add checkbox/radio default styles (13×13px, border, vertical-align) |
crates/display_list/src/builder.rs |
Render checkbox/radio visual state (checked/unchecked/disabled appearance) |
crates/app_browser/src/app_state.rs |
Add checked_states: HashMap<NodeId, bool>, ensure_checked_state(), toggle_checked() |
crates/app_browser/src/event_handler.rs |
Checkbox/radio toggle on click, label click delegation, disabled check, reset logic |
crates/app_browser/src/form.rs |
Replace TODO: collect checkbox/radio values based on checked state |
crates/layout/src/engine/box_tree.rs |
Suppress text synthesis for checkbox/radio (they don't display value text) |
tests/goldens/fixtures/ |
New golden test HTML files for checkbox/radio/button/disabled rendering |
tests/goldens/expected/ |
Expected output for new golden tests |
tests/goldens.rs |
Register new golden tests |
tests/js_events.rs |
Integration tests for toggle, change events, label delegation, disabled |
docs/HTML5_Implementation_Checklist.md |
Update with checkbox/radio/button/label/disabled support |
Testing Strategy
- Golden tests: Render checkbox (checked/unchecked), radio (selected/unselected), button types, disabled controls. Compare layout tree + display list output.
- Integration tests: Dispatch click events on checkboxes → verify checked state toggles and change event fires. Click radio → verify group deselection. Click label → verify associated control activates. Click disabled → verify no response.
- Unit tests: Radio group resolution logic. Label-for-id resolution. Disabled attribute checking.
- Existing tests: All existing form_elements and event dispatch tests must continue to pass.
Previous Story Intelligence
Story 4.1 established these patterns that 4.2 MUST follow:
- Event dispatch after state change: toggle state first, then
dispatch_change_event_with_swap()— same pattern as text input's change-on-blur - AppState is the runtime state store — DOM attributes are initial values only; runtime state lives in
HashMap<NodeId, T>on AppState - Display list builder reads app state to determine visual appearance — layout builds structure, display list builder decides paint
- Golden tests use sequential numbering starting after 304 (last from 4.1) — use 305+ for new tests
just cimust pass after every task, not just at the end
Git Intelligence
Recent commits confirm:
e9d3ffdCode review fixes for Story 4.1 (caret line-height, tests, placeholder)64a3439Full Story 4.1 implementation (text inputs, textareas, caret, selection, placeholder)- Codebase is CI green and stable. No in-flight refactoring that would conflict.
- Convention: commit messages describe the feature, not the story number.
References
- [Source: crates/app_browser/src/event_handler.rs#L283-L403] — Click default action handling
- [Source: crates/app_browser/src/form.rs#L44-L89] — Form data collection (TODO at line 60)
- [Source: crates/app_browser/src/app_state.rs#L215-L281] — Input state management pattern
- [Source: crates/style/src/ua_stylesheet.rs#L74-L90] — Form control UA styles
- [Source: crates/layout/src/engine/box_tree.rs#L279-L420] — Form element layout synthesis
- [Source: crates/display_list/src/lib.rs] — DisplayItem enum
- [Source: crates/selectors/src/selector.rs] — :checked pseudo-class matching
- [Source: crates/web_api/src/lib.rs#L750-L766] — dispatch_change_event()
- [HTML Spec §4.10.6] — Button element definition
- [HTML Spec §4.10.5.1.15] — Checkbox state
- [HTML Spec §4.10.5.1.16] — Radio button state
- [HTML Spec §4.10.4] — Label element definition
- [HTML Spec §4.10.18.5] — Disabled attribute
Dev Agent Record
Agent Model Used
Claude Opus 4.6 (1M context)
Debug Log References
None — clean implementation with no blocking issues.
Completion Notes List
- Implemented checkbox rendering (13×13px bordered box) with checkmark indicator when checked, using two thin SolidRect display items
- Implemented radio button rendering (13×13px circle via border-radius: 50%) with filled inner dot when checked
- Added
checked_states: HashMap<NodeId, bool>to AppState for runtime checked state tracking, seeded from DOMcheckedattribute - Checkbox click toggles state and dispatches
changeevent; radio click selects within name group (samenamein same form/document) and dispatcheschange - Label click delegation:
forattribute resolves to target by ID; implicit wrapping finds first labelable descendant; prevents double-fire when click hits wrapped control directly - Disabled state enforcement: disabled controls reject clicks, focus, and form submission; rendered with semi-transparent gray overlay
- Button type handling:
<button>defaults totype="submit";type="button"fires click only;type="reset"resets form to initial DOM values; both<button>and<input>variants - Form data collection: checked checkboxes/radios submit
name=value(default "on"); unchecked controls excluded; disabled controls excluded - Added 4 golden tests (305-308) for checkbox/radio/disabled/button rendering
- Added 8 integration tests for change events, label resolution, disabled state
- Added 9 unit tests for checked state management and form data collection
- Updated HTML5 Implementation Checklist
File List
crates/style/src/ua_stylesheet.rs— Added checkbox/radio UA styles (13×13px, border, margin, vertical-align, border-radius: 50% for radio)crates/app_browser/src/app_state.rs— Addedchecked_statesfield,ensure_checked_state(),toggle_checked(),set_radio_checked(),uncheck_radio()methods, unit testscrates/app_browser/src/form_controls.rs— New module: checkbox/radio/disabled visual rendering in display listcrates/app_browser/src/event_handler.rs— Checkbox/radio toggle, label click delegation, disabled checks, reset form, radio group siblings, form control display list itemscrates/app_browser/src/form.rs— Checkbox/radio form data collection, disabled exclusion,checked_statesparameter threadingcrates/app_browser/src/main.rs— Registeredform_controlsmodule, addedchecked_statesto AppState constructioncrates/app_browser/src/tests/form_tests.rs— Added 5 tests for checkbox/radio form data + disabled exclusiontests/goldens/fixtures/305-checkbox-states.html— Golden test fixturetests/goldens/fixtures/306-radio-states.html— Golden test fixturetests/goldens/fixtures/307-disabled-controls.html— Golden test fixturetests/goldens/fixtures/308-button-types.html— Golden test fixturetests/goldens/expected/305-checkbox-states.layout.txt— Golden expected outputtests/goldens/expected/305-checkbox-states.dl.txt— Golden expected outputtests/goldens/expected/306-radio-states.layout.txt— Golden expected outputtests/goldens/expected/306-radio-states.dl.txt— Golden expected outputtests/goldens/expected/307-disabled-controls.layout.txt— Golden expected outputtests/goldens/expected/307-disabled-controls.dl.txt— Golden expected outputtests/goldens/expected/308-button-types.layout.txt— Golden expected outputtests/goldens/expected/308-button-types.dl.txt— Golden expected outputtests/goldens.rs— Registered 4 new golden teststests/js_events.rs— Added 8 integration tests for form control eventsdocs/HTML5_Implementation_Checklist.md— Updated checkbox/radio/button/disabled status
Change Log
- 2026-03-29: Implemented Story 4.2 — buttons, checkboxes, radio buttons with full click/toggle/submit/reset/disabled/label support
- 2026-03-29: Code review — fixed 6 issues (4 HIGH, 2 MEDIUM): replaced broken SolidRect checkmark with ✓ text glyph, added disabled overlay for button/textarea, rewrote 4 fake integration tests to test actual behavior, removed dead parameter from collect_checkable_data