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>
263 lines
20 KiB
Markdown
263 lines
20 KiB
Markdown
# 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
|