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

263 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Story 4.2: Buttons, Checkboxes & Radio Buttons
Status: done
<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
## 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
- [x] Task 1: Checkbox rendering and toggle (AC: #2, #6)
- [x] 1.1 Add checkbox-specific UA stylesheet rules (13×13px box, border, display: inline-block, vertical-align: middle)
- [x] 1.2 In display_list builder, render checkbox box as bordered rect; when checked, render checkmark (✓ text or line segments)
- [x] 1.3 Add `checked_states: HashMap<NodeId, bool>` to AppState for runtime checked tracking
- [x] 1.4 In event_handler click default action, if target is checkbox: toggle checked_states entry, dispatch `change` event
- [x] 1.5 Read initial `checked` attribute from DOM to seed checked_states on first interaction
- [x] 1.6 Feed checked_states to layout/display_list for visual rendering
- [x] Task 2: Radio button rendering and group logic (AC: #3, #6)
- [x] 2.1 Add radio-specific UA stylesheet rules (13×13px circle via border-radius: 50%, border, display: inline-block)
- [x] 2.2 In display_list builder, render radio as circle; when checked, render filled inner dot
- [x] 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
- [x] 2.4 Read initial `checked` attribute for radio group initial state
- [x] Task 3: Label association and click delegation (AC: #4)
- [x] 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
- [x] 3.2 Activate the resolved control: simulate click for buttons/checkboxes/radios, focus for text inputs
- [x] 3.3 Ensure label click does not double-fire when label wraps the control (event already targets control)
- [x] Task 4: Disabled state enforcement (AC: #5)
- [x] 4.1 In event_handler, check `disabled` attribute before processing click default actions for form controls
- [x] 4.2 Add disabled visual style in UA stylesheet or display_list (reduced opacity or gray color)
- [x] 4.3 Exclude disabled controls from form submission in `form.rs`
- [x] 4.4 Prevent focus on disabled elements in `set_content_focus()`
- [x] Task 5: Button type handling (AC: #1)
- [x] 5.1 Ensure `<button>` defaults to `type="submit"` per spec; `<input type="submit">` fires form submission
- [x] 5.2 `<button type="button">` and `<input type="button">` fire click only, no form submission
- [x] 5.3 `<button type="reset">` / `<input type="reset">` resets form inputs to initial values (clear input_values, uncheck checked_states where initial was unchecked)
- [x] 5.4 Ensure button label comes from: `<button>` text content, `<input>` value attribute, or default ("Submit"/"Reset")
- [x] Task 6: Form data collection for checkboxes/radios (AC: #2, #3)
- [x] 6.1 In `form.rs`, replace the TODO at line 60 — collect checkbox `name=value` (default value "on") when checked
- [x] 6.2 Collect radio `name=value` for the checked radio in each group
- [x] 6.3 Skip unchecked checkboxes and radios in form data
- [x] Task 7: Integration tests and golden tests (AC: #7)
- [x] 7.1 Add golden test for checkbox rendering (checked and unchecked states)
- [x] 7.2 Add golden test for radio button rendering (selected and unselected)
- [x] 7.3 Add golden test for disabled form controls (visual distinction)
- [x] 7.4 Add golden test for button rendering (submit, button, reset types)
- [x] 7.5 Add integration tests for checkbox toggle + change event dispatch
- [x] 7.6 Add integration tests for radio group selection + change event dispatch
- [x] 7.7 Add integration tests for label click delegation (for attribute and wrapping)
- [x] 7.8 Add integration tests for disabled control rejection
- [x] 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