Files
rust_browser/_bmad-output/implementation-artifacts/4-2-buttons-checkboxes-and-radio-buttons.md
Zachary D. Rowitsch f8e0c47dc7 Add buttons, checkboxes, and radio buttons with click/toggle/disabled/label support (Story 4.2)
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>
2026-03-29 08:06:22 -04:00

20 KiB
Raw Permalink Blame History

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

  1. Button click and submission: <button> and <input type="submit"> elements render with UA stylesheet chrome (centered text, border, padding). Clicking fires the element's click event. Submit buttons trigger form submission unless preventDefault() 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 its value as label and fires click only. Per HTML §4.10.6.

  2. Checkbox toggle: <input type="checkbox"> renders as a small square box (13×13px default, border, background). Clicking toggles the DOM checked property and dispatches a change event (bubbles). The visual state reflects checked (checkmark symbol) vs unchecked. The checked HTML attribute sets the initial state. Per HTML §4.10.5.1.15.

  3. Radio button selection: <input type="radio"> renders as a small circle (13×13px default). Clicking sets this radio's checked to true and unchecks all other radios in the same name group within the same form (or document if not in a form). A change event fires on the newly checked radio. Per HTML §4.10.5.1.16.

  4. Label association: <label for="id"> clicking activates the associated control (identified by matching id attribute). <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.

  5. Disabled state: Form controls with the disabled attribute do not respond to clicks, do not dispatch change events, 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.

  6. 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.

  7. Integration tests verify each control type's click behavior and event dispatch, golden tests cover rendering of checked/unchecked/disabled states, and just ci passes.

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 -- indeterminate property 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, :active pseudo-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 change event
    • 1.5 Read initial checked attribute 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 name in same form (or document), uncheck them, dispatch change on newly checked
    • 2.4 Read initial checked attribute 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>: resolve for attribute to target element by id, 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)
  • Task 4: Disabled state enforcement (AC: #5)

    • 4.1 In event_handler, check disabled attribute 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()
  • Task 5: Button type handling (AC: #1)

    • 5.1 Ensure <button> defaults to type="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")
  • Task 6: Form data collection for checkboxes/radios (AC: #2, #3)

    • 6.1 In form.rs, replace the TODO at line 60 — collect checkbox name=value (default value "on") when checked
    • 6.2 Collect radio name=value for the checked radio in each group
    • 6.3 Skip unchecked checkboxes and radios in form data
  • 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 ci and 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 body
  • find_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-urlencoded working

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 value attribute 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> and set_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 :checked exists — just needs runtime checked state to be consulted during matching

Display list and render already:

  • DisplayItem::SolidRect for filled rectangles
  • DisplayItem::Text for text rendering
  • fill_rect() with alpha blending support
  • Pattern from DisplayItem::Caret shows how to add new display items

What Needs to Be Built

  1. Checked state tracking: Add checked_states: HashMap<NodeId, bool> to AppState. Seed from DOM checked attribute on first access. Update on click toggle. Feed to layout/display_list for visual state and to selectors for :checked matching.

  2. 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.

  3. 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.

  4. Click toggle logic: In handle_click_default_actions(), add branches for input[type="checkbox"] and input[type="radio"]. Checkbox: toggle state, dispatch change. Radio: set checked, uncheck siblings by name, dispatch change.

  5. Radio group resolution: Find all <input type="radio"> elements with the same name attribute within the same <form> ancestor (or the document root if not in a form). Uncheck all except the clicked one.

  6. Label click delegation: When <label> is clicked, resolve the for attribute to an element by id, or walk descendants to find first labelable element. Trigger that element's click/focus behavior.

  7. Disabled enforcement: Check disabled attribute before allowing click default actions, focus changes, and form data collection.

  8. Form reset: Implement reset logic — clear input_values entries (revert to DOM value attributes), reset checked_states to initial DOM checked attributes.

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 ci must pass after every task, not just at the end

Git Intelligence

Recent commits confirm:

  • e9d3ffd Code review fixes for Story 4.1 (caret line-height, tests, placeholder)
  • 64a3439 Full 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 DOM checked attribute
  • Checkbox click toggles state and dispatches change event; radio click selects within name group (same name in same form/document) and dispatches change
  • Label click delegation: for attribute 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 to type="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 — Added checked_states field, ensure_checked_state(), toggle_checked(), set_radio_checked(), uncheck_radio() methods, unit tests
  • crates/app_browser/src/form_controls.rs — New module: checkbox/radio/disabled visual rendering in display list
  • crates/app_browser/src/event_handler.rs — Checkbox/radio toggle, label click delegation, disabled checks, reset form, radio group siblings, form control display list items
  • crates/app_browser/src/form.rs — Checkbox/radio form data collection, disabled exclusion, checked_states parameter threading
  • crates/app_browser/src/main.rs — Registered form_controls module, added checked_states to AppState construction
  • crates/app_browser/src/tests/form_tests.rs — Added 5 tests for checkbox/radio form data + disabled exclusion
  • tests/goldens/fixtures/305-checkbox-states.html — Golden test fixture
  • tests/goldens/fixtures/306-radio-states.html — Golden test fixture
  • tests/goldens/fixtures/307-disabled-controls.html — Golden test fixture
  • tests/goldens/fixtures/308-button-types.html — Golden test fixture
  • tests/goldens/expected/305-checkbox-states.layout.txt — Golden expected output
  • tests/goldens/expected/305-checkbox-states.dl.txt — Golden expected output
  • tests/goldens/expected/306-radio-states.layout.txt — Golden expected output
  • tests/goldens/expected/306-radio-states.dl.txt — Golden expected output
  • tests/goldens/expected/307-disabled-controls.layout.txt — Golden expected output
  • tests/goldens/expected/307-disabled-controls.dl.txt — Golden expected output
  • tests/goldens/expected/308-button-types.layout.txt — Golden expected output
  • tests/goldens/expected/308-button-types.dl.txt — Golden expected output
  • tests/goldens.rs — Registered 4 new golden tests
  • tests/js_events.rs — Added 8 integration tests for form control events
  • docs/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