Implement z-index-aware hit testing, overflow clipping, keyboard default behaviors (Enter/Space on links/buttons/checkboxes), horizontal and keyboard page scrolling, container-level overflow:auto/scroll with scroll chaining, and scroll event dispatch. Code review fixes: scroll events target actual scrolled container element, deduplicated link navigation via navigate_link_href() helper, and reference-based scroll offset API. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
25 KiB
Story 4.6: Click, Keyboard & Scroll Interaction
Status: done
Story
As a web user, I want my clicks, key presses, and scroll actions to work correctly on all page content, so that I can interact with web pages naturally.
Acceptance Criteria
-
Click hit testing accuracy: Clicking on an element delivers the click event to the topmost visible element at the click coordinates. Hit testing accounts for z-index stacking order and
overflow: hiddenclipping. Clicking on overlapping elements correctly targets the topmost per stacking context. -
Keyboard event dispatch: Pressing keys while an element is focused dispatches
keydownandkeyupevents with correctkey,code,repeat, and modifier properties (ctrlKey, shiftKey, altKey, metaKey) per UI Events spec. Events target the focused element ordocumentif nothing is focused. -
Keyboard default behaviors: Enter on a focused
<a>element navigates to the link href. Space on a checkbox toggles it. Space on a button triggers click. Arrow keys in text inputs move the cursor (already implemented). These default behaviors are suppressed ifpreventDefault()is called on thekeydownevent. -
Page-level scroll improvement: Mouse wheel and trackpad scrolling works vertically (already exists) and horizontally. Keyboard scrolling: Space/Shift+Space scroll by viewport height, Page Up/Page Down scroll by viewport height, Home/End scroll to top/bottom of page. Arrow Up/Down scroll by a line height (~40px) when no text input is focused.
-
Scroll chaining for nested containers: An element with
overflow: autooroverflow: scrollthat has content exceeding its bounds becomes a scrollable container. Scroll events first apply to the innermost scrollable container at the pointer position. When the inner container reaches its scroll boundary, excess scroll propagates to the parent scrollable container (scroll chaining). -
Scroll event dispatch: After a scroll position changes, a non-cancelable, non-bubbling
scrollevent is dispatched on the scrolled element (ordocumentfor page-level scroll). -
Integration tests verify hit testing, keyboard dispatch with default behaviors, page scrolling (wheel + keyboard), container scrolling, scroll chaining, and
just cipasses.
What NOT to Implement
- No pointer events (pointerdown, pointermove, pointerup) -- beyond scope, mouse events are sufficient.
- No touch/gesture events -- desktop browser only.
- No smooth scroll CSS property (
scroll-behavior: smooth) -- immediate scroll only. - No scrollbar click/drag interaction -- ScrollIndicator remains visual only.
- No scroll snap (
scroll-snap-type) -- deferred. - No
element.scrollTop/element.scrollLeftJS properties -- deferred to Web API story. - No
element.scrollIntoView()JS method -- deferred. - No
keypressevent -- deprecated per UI Events spec. - No IME composition events (
compositionstart/compositionupdate/compositionend) -- deferred. - No Tab key focus navigation -- that is Story 4.7.
- No horizontal scrollbar rendering -- vertical only for now.
Tasks / Subtasks
-
Task 1: Improve hit testing for stacking contexts and overflow clipping (AC: #1)
- 1.1 Update
find_element_at_position()inhit_test.rsto respect z-index ordering: when testing children of a box withposition: relative/absolute/fixed, iterate in reverse paint order (later-painted elements first) so topmost stacking context wins - 1.2 Add overflow clipping to hit testing: if a layout box has
overflow: hidden/auto/scroll, clip the hit test region to the box's content area -- clicks outside the clip rect should not target children of that container - 1.3 Add unit tests: overlapping positioned elements (higher z-index wins), overflow:hidden clips child hit targets, basic hit testing for nested containers
- 1.1 Update
-
Task 2: Add keyboard default behaviors for non-input elements (AC: #3)
- 2.1 In
event_handler.rs, after dispatchingkeydownevent, checkdefault_preventedflag. If not prevented, execute default behavior based on focused element type and key - 2.2 Enter key on focused
<a>element: trigger navigation to href (followhandle_link_click()pattern) - 2.3 Space key on focused
<input type="checkbox">: toggle checked state (follow existing checkbox click handler) - 2.4 Space key on focused
<button>or<input type="submit">: trigger click event dispatch (synthetic click) - 2.5 Space key on focused
<a>element: trigger navigation (same as Enter) - 2.6 Add unit/integration tests: Enter on link navigates, Space on checkbox toggles, Space on button fires click, preventDefault suppresses default
- 2.1 In
-
Task 3: Improve page-level scrolling with horizontal and keyboard support (AC: #4)
- 3.1 Add
scroll_xandmax_scroll_xtoAppState(alongside existingscroll_y/max_scroll_y) - 3.2 Handle
delta_xin scroll wheel handler (event_handler.rs ~line 1590): apply toscroll_x, clamp tomax_scroll_x - 3.3 Update
build_display_list_with_scroll()to accept and apply both scroll_x and scroll_y offsets - 3.4 Add keyboard scroll handlers (when no text input focused and keydown not prevented): Space/PageDown scroll down by viewport height, Shift+Space/PageUp scroll up by viewport height, Home scroll to top (scroll_y=0), End scroll to bottom (scroll_y=max), ArrowUp/ArrowDown scroll by 40px
- 3.5 Calculate
max_scroll_xasmax(0, doc_width - viewport_width)during layout - 3.6 Add tests: horizontal wheel scroll, keyboard scroll (Space, PageDown, PageUp, Home, End, ArrowUp, ArrowDown), keyboard scroll suppressed when text input focused
- 3.1 Add
-
Task 4: Implement container-level scrolling for overflow:auto/scroll (AC: #5)
- 4.1 Add
scroll_offsets: HashMap<NodeId, (f32, f32)>toAppStatefor per-container scroll positions - 4.2 Implement
find_scroll_container_at_position()inhit_test.rs: given mouse coordinates, find the innermost element withoverflow: auto/scrollwhose content exceeds its bounds - 4.3 In scroll wheel handler, determine scroll target: if pointer is over a scroll container, scroll that container first. Apply remaining delta to parent containers or page (scroll chaining)
- 4.4 Update display list builder to apply per-container scroll offsets: when building items for a scrollable container, offset child items by the container's scroll offset and clip to container bounds
- 4.5 Update hit testing to account for container scroll offsets: adjust coordinates by container scroll offset before testing children
- 4.6 Calculate per-container max scroll values from content overflow (content_height - container_height, content_width - container_width)
- 4.7 Add tests: div with overflow:auto scrolls its content, scroll chaining when inner container is at boundary, hit testing within scrolled container
- 4.1 Add
-
Task 5: Dispatch scroll events (AC: #6)
- 5.1 Add
dispatch_scroll_event()toweb_api: event type "scroll", bubbles: false, cancelable: false, target is the scrolled element (or document for page scroll) - 5.2 Add
dispatch_scroll_event()wrapper tobrowser_runtime - 5.3 Fire scroll event after any scroll position change (page-level or container-level) in event_handler.rs
- 5.4 Add integration tests: scroll event fires on page scroll, scroll event fires on container scroll, scroll event has correct target, scroll event is not cancelable
- 5.1 Add
-
Task 6: Integration tests and CI (AC: #7)
- 6.1 Hit testing integration tests: click on overlapping elements targets correct one, click outside overflow:hidden region does not target clipped child
- 6.2 Keyboard integration tests: keydown/keyup events fire with correct properties, Enter on link navigates, Space on checkbox toggles, default behavior prevented by preventDefault
- 6.3 Scroll integration tests: page scrolls via mouse wheel, keyboard scrolling works (Space, Page keys, Home/End), container scrolls independently, scroll chaining propagates to parent
- 6.4 Scroll event integration tests: scroll event fires with correct properties
- 6.5 Regression: all existing form tests, event tests, and golden tests pass unchanged
- 6.6
just cipasses -- all tests, lint, fmt, policy clean - 6.7 Update
docs/HTML5_Implementation_Checklist.mdwith click/keyboard/scroll interaction status
Dev Notes
Existing Infrastructure (DO NOT REBUILD)
Hit testing already works (crates/app_browser/src/hit_test.rs):
find_link_at_position()(line 22-37): recursively finds<a>elements at (x, y)find_element_at_position()(line 129-137): finds deepest element at positionfind_element_in_box()(line 140-191): depth-limited recursion (MAX_HIT_TEST_DEPTH=256), checks inline fragments then children- These need enhancement for stacking order and overflow clipping, NOT replacement
Keyboard events already dispatch (crates/app_browser/src/event_handler.rs):
key_code_to_key_and_code()(line 71-91): maps platform KeyCode to UI Events spec stringsdispatch_keyboard_event_with_swap()(line 95-151): dispatches keydown/keyup to JS via browser_runtime- Events target focused element or document root
- Text input handling (cursor movement, selection, backspace, delete) all working
- Missing: default behavior for non-input elements (links, buttons, checkboxes) after dispatch
Page scroll already works (crates/app_browser/src/event_handler.rs ~line 1590-1604):
ScrollWheel { delta_x, delta_y }received from platform event loop- Only
delta_yis used;delta_xis ignored -- add horizontal support - Scroll offset applied in display list builder via
build_display_list_with_scroll(tree, scroll_y) max_scroll_ycalculated asdoc_height - viewport_height- Fragment scroll (
scroll_to_fragment) exists inapp_state.rs
Event dispatch infrastructure (crates/web_api/src/event_dispatch.rs):
- Full three-phase DOM event dispatch: Capture -> Target -> Bubble
dispatch_mouse_event(),dispatch_keyboard_event(),dispatch_focus_change(),dispatch_input_event(),dispatch_change_event(),dispatch_submit_event(),dispatch_invalid_event()all availabledefault_preventedflag checked after dispatch -- pattern for conditional default behavior
Focus system (crates/app_browser/src/app_state.rs):
focused_node: Option<NodeId>tracks current focusset_content_focus()manages transitions, returns dirty node- Focus outline rendered via
focus_outline.rs - Four-event focus sequence (focusout, focusin, blur, focus) properly dispatched
ScrollIndicator (crates/display_list/src/lib.rs line 77-86):
- Visual-only scrollbar:
track_rect,thumb_rect,verticalflag - Already in display list rendering pipeline -- no changes needed for this story
Overflow properties (crates/layout/src/types.rs line 252-253):
overflow_xandoverflow_yfields exist on LayoutBox- Values:
Overflow::Visible,Overflow::Scroll,Overflow::Auto,Overflow::Hidden - Already parsed from CSS and stored -- just not used for scroll behavior yet
Platform keyboard support (crates/platform/src/event_loop.rs):
- KeyCode enum includes: Enter, Backspace, Delete, Left, Right, Home, End, Tab, Escape, A, F5, R, L, BracketLeft, BracketRight, Period, Other
- Missing from enum: Space, PageUp, PageDown, ArrowUp, ArrowDown -- add these to KeyCode and winit mapping
Modifiersstruct tracks ctrl, alt, shift, metaCharacterInput(char)for text typing
What Needs to Be Built
-
Hit test improvements (
hit_test.rs): Add z-index stacking order respect (reverse paint order iteration). Add overflow clipping check (reject hits outside clip rect of overflow:hidden/auto/scroll containers). -
Keyboard default behaviors (
event_handler.rs): After keydown dispatch, if not prevented, check element type: Enter/Space on links -> navigate, Space on checkbox -> toggle, Space on button -> click. Follows existingdefault_preventedcheck pattern. -
Horizontal page scroll (
app_state.rs+event_handler.rs+display_list/builder.rs): Addscroll_x/max_scroll_x, handledelta_xin wheel handler, pass both offsets to display list builder. -
Keyboard scroll (
event_handler.rs): When no text input focused and keydown not prevented, handle Space/PageUp/PageDown/Home/End/ArrowUp/ArrowDown for page scrolling. -
Container scroll (
app_state.rs+hit_test.rs+event_handler.rs+display_list/builder.rs): Per-container scroll offsets, find scroll container at position, scroll chaining logic, display list clipping/offsetting for scrolled containers. -
Platform KeyCode expansion (
platform/src/event_loop.rs): Add Space, PageUp, PageDown, ArrowUp, ArrowDown to KeyCode enum. Map from winit virtual key codes. -
Scroll event dispatch (
web_api+browser_runtime): New event type "scroll", non-cancelable, non-bubbling. Follow existing event dispatch patterns.
Architecture Compliance
| Rule | How This Story Complies |
|---|---|
| Layer boundaries | Hit testing, scroll handling, keyboard routing in app_browser (Layer 3). Scroll event dispatch in web_api/browser_runtime (Layer 1/2). KeyCode additions in platform (Layer 1). Display list changes in display_list (Layer 1). No upward dependencies. |
| Unsafe policy | No unsafe code needed. All changes are safe Rust. |
| Pipeline sequence | Scroll offset application during display list building (existing pattern). Hit testing operates on layout tree. No changes to style/layout computation. |
| Arena ID pattern | Uses existing NodeId for scroll container identification. No new ID types. |
| Existing patterns | Follows dispatch_submit_event pattern for scroll events. Follows checked_states/select_states pattern for scroll offsets HashMap. Follows default_prevented check pattern for keyboard defaults. |
| Single-threaded model | All scroll and event handling on main thread. No threading changes. |
File Modification Plan
| File | Change |
|---|---|
crates/platform/src/event_loop.rs |
Add Space, PageUp, PageDown, ArrowUp, ArrowDown to KeyCode enum; map from winit |
crates/app_browser/src/hit_test.rs |
Add z-index stacking order, overflow clipping to hit testing |
crates/app_browser/src/event_handler.rs |
Add keyboard default behaviors (Enter/Space on links/buttons/checkboxes), horizontal scroll support, keyboard scrolling, container scroll with chaining, scroll event dispatch |
crates/app_browser/src/app_state.rs |
Add scroll_x, max_scroll_x, scroll_offsets: HashMap<NodeId, (f32, f32)> |
crates/display_list/src/builder.rs |
Accept and apply scroll_x offset; apply per-container scroll offsets with clipping |
crates/web_api/src/lib.rs |
Add dispatch_scroll_event() (bubbles: false, cancelable: false) |
crates/browser_runtime/src/lib.rs |
Add dispatch_scroll_event() wrapper |
crates/app_browser/src/main.rs |
Initialize scroll_x, scroll_offsets in AppState |
tests/js_events.rs |
Integration tests for keyboard defaults, scroll events |
docs/HTML5_Implementation_Checklist.md |
Update interaction event status |
Testing Strategy
- Unit tests (in
hit_test.rsor newhit_test_tests.rs): z-index ordering in hit testing, overflow clipping in hit testing - Unit tests (in new
scroll_tests.rsor existing test modules): container scroll offset clamping, scroll chaining logic, keyboard scroll calculation - Integration tests (in
js_events.rs): keydown/keyup properties, Enter on link navigates, Space on checkbox toggles, preventDefault suppresses default, scroll event fires on scroll, scroll event target is correct - Integration tests (in new
scroll_interaction.rsor existing): page keyboard scroll (Space/PageDown/PageUp/Home/End), container overflow scroll, horizontal scroll - Regression tests: All existing form tests, event tests, and golden tests must pass unchanged
Previous Story Intelligence
Story 4.5 established patterns this story MUST follow:
- Parameter threading: When adding params (like scroll_x), update all call sites consistently (display list building, event handler, tests)
- Runtime state over DOM: Use AppState fields for scroll positions, not DOM attributes
- Event dispatch pattern: Follow
dispatch_submit_event_with_swap()ordispatch_invalid_event_with_swap()patterns fordispatch_scroll_event_with_swap() just ciafter every task: Don't batch -- verify incrementally- Deferred items are OK: Mark as deferred with clear rationale (e.g., scrollbar interaction deferred)
- Review bug patterns: 4.5 review found readonly fields missed in validation -- similarly ensure keyboard defaults cover all interactive element types (links, buttons, checkboxes, radio buttons)
Git Intelligence
Recent commits show stable Epic 4 progression:
be22671Story 4.5: client-side form validation9ca0a12Story 4.4: form submission71f263fStory 4.3: select menusf8e0c47Story 4.2: buttons, checkboxes, radio buttons- Convention: descriptive commit messages with (Story X.Y) suffix
- All stories complete in single commits (no multi-commit stories in Epic 4)
Key Implementation Notes
-
KeyCode enum expansion: The platform crate's
KeyCodeenum needs Space, PageUp, PageDown, ArrowUp, ArrowDown. The winit mapping is inevent_loop.rs. Follow the existing pattern: add enum variants, addVirtualKeyCode::Space => KeyCode::Spacemappings. -
Scroll container detection: A container is scrollable if
overflow_xoroverflow_yisAutoorScrollAND content exceeds container bounds. CheckLayoutBoxdimensions: ifcontent_height > box_height(for vertical) orcontent_width > box_width(for horizontal), the container is scrollable. -
Scroll chaining pattern: When scroll delta is received, find innermost scroll container at pointer position. Apply delta. If container hits its bound (scroll_y == 0 or scroll_y == max), compute remaining delta and propagate to parent scroll container. Repeat until all delta consumed or page level reached.
-
Display list per-container scroll: When building display list items for children of a scrollable container, push a clip rect (container bounds), then offset all child coordinates by
-scroll_offset. Pop clip after children. This is similar to existingPushClip/PopClippattern. -
Hit testing with container scroll: When hit testing into a scrollable container, add the container's scroll offset to the test coordinates before checking children. This reverses the display list offset.
-
Default behavior prevention pattern: After
dispatch_keyboard_event_with_swap()returns, checkevent.default_prevented. If not prevented, match on(focused_element_type, key)to execute defaults. This mirrors how form submission checksdefault_preventedafter submit event dispatch. -
Scroll event coalescing: Multiple scroll wheel events can arrive per frame. For simplicity, dispatch one scroll event per scroll position change. No need to coalesce in this story.
References
- [Source: crates/app_browser/src/hit_test.rs#find_element_at_position] -- Hit testing entry point
- [Source: crates/app_browser/src/hit_test.rs#find_element_in_box] -- Recursive hit test logic
- [Source: crates/app_browser/src/event_handler.rs#~L71-91] -- key_code_to_key_and_code mapping
- [Source: crates/app_browser/src/event_handler.rs#~L95-151] -- Keyboard event dispatch
- [Source: crates/app_browser/src/event_handler.rs#~L1590-1604] -- Current scroll wheel handler
- [Source: crates/app_browser/src/app_state.rs#~L54-56] -- scroll_y and max_scroll_y
- [Source: crates/app_browser/src/app_state.rs#~L393] -- scroll_to_fragment
- [Source: crates/display_list/src/builder.rs#~L97-100] -- Scroll offset in display list building
- [Source: crates/display_list/src/lib.rs#~L77-86] -- ScrollIndicator display item
- [Source: crates/layout/src/types.rs#~L252-253] -- overflow_x, overflow_y on LayoutBox
- [Source: crates/platform/src/event_loop.rs#~L18-36] -- KeyCode enum
- [Source: crates/web_api/src/event_dispatch.rs] -- Three-phase DOM event dispatch
- [Source: crates/web_api/src/lib.rs#dispatch_mouse_event] -- Mouse event dispatch pattern
- [Source: crates/web_api/src/lib.rs#dispatch_keyboard_event] -- Keyboard event dispatch pattern
- [HTML Living Standard - Scrolling] -- Scroll processing model
- [UI Events W3C Spec] -- KeyboardEvent key/code values, default actions
- [CSSOM View Module] -- Scroll APIs and overflow scrolling behavior
Dev Agent Record
Agent Model Used
Claude Opus 4.6 (1M context)
Debug Log References
No blocking issues encountered.
Completion Notes List
- Task 1: Improved hit testing in
hit_test.rs— z-index stacking order (positioned children sorted by z-index, highest checked first), overflow clipping (children of overflow:hidden/auto/scroll containers only tested within padding box). 7 new unit tests covering z-index priority, negative z-index, overflow:hidden clipping, overflow:auto clipping, and overflow:visible no-clip. - Task 2: Keyboard default behaviors — Enter on focused
<a>navigates to href, Space on checkbox toggles, Space on radio selects, Space on button/submit triggers synthetic click. Space defaults handled inCharacterInputpath (since space is printable), Enter defaults inKeyPressed. Added Space, Up, Down, PageUp, PageDown to platformKeyCodeenum with winit mappings. - Task 3: Page-level scroll improvements — added
scroll_x/max_scroll_xto AppState, horizontal wheel scroll viadelta_x, keyboard scrolling (Space/Shift+Space for page, PageUp/Down, Home/End, ArrowUp/Down for line). Display list builder accepts both scroll_x and scroll_y. Hit testing accounts for scroll_x in mouse coordinates. - Task 4: Container-level scrolling —
scroll_offsets: HashMap<NodeId, (f32, f32)>in AppState,find_scroll_containers_at_position()finds scrollable containers at pointer, scroll chaining propagates remaining delta to parent containers then page level. Display list builder applies per-container scroll offsets and clips children.last_mouse_x/last_mouse_ytrack pointer for scroll target detection. - Task 5: Scroll event dispatch —
dispatch_scroll_event()in web_api (bubbles: false, cancelable: false) + browser_runtime wrapper.dispatch_scroll_event_on_document()helper fires on document root after any scroll change. 3 integration tests: fires on target, does not bubble, not cancelable. - Task 6: 9 new integration tests in js_events.rs (3 scroll event + 6 keyboard). All 308 app_browser unit tests pass. All existing integration tests pass.
just ciclean. HTML5 checklist updated.
Change Log
- 2026-04-02: Implemented Story 4.6 Click, Keyboard & Scroll Interaction — hit testing z-index/overflow improvements, keyboard default behaviors for links/buttons/checkboxes, horizontal page scroll, keyboard page scroll, container-level overflow:auto/scroll scrolling with scroll chaining, scroll event dispatch. 7 new hit test unit tests, 9 new integration tests. All ACs satisfied,
just cipasses. - 2026-04-02: Code review fixes — (H1) Scroll events now dispatch on the actual scrolled container element, not always document root (AC #6 fully compliant). (H2) Extracted
navigate_link_href()helper to deduplicate Enter/Space link navigation code. (H3) Changedbuild_display_list_with_container_scrollAPI to accept&HashMapreference instead of owned clone. (L2) Removed redundant nestedstate.documentchecks.just cipasses.
File List
crates/platform/src/event_loop.rs— Added Space, Up, Down, PageUp, PageDown to KeyCode enum with winit mappingscrates/app_browser/src/hit_test.rs— Added z-index stacking order, overflow clipping,is_scroll_container(),content_overflow(),find_scroll_containers_at_position(),find_layout_box_by_id()crates/app_browser/src/event_handler.rs— Keyboard default behaviors (Enter/Space on links/buttons/checkboxes), horizontal scroll, keyboard scrolling, container scroll with chaining, scroll event dispatch, scroll_x in hit testingcrates/app_browser/src/app_state.rs— Addedscroll_x,max_scroll_x,scroll_offsets,last_mouse_x,last_mouse_y; reset on navigationcrates/app_browser/src/main.rs— Initialize new AppState fieldscrates/display_list/src/builder.rs— Addedscroll_offset_x,container_scroll_offsets,build_display_list_with_container_scroll(); applied container scroll offsets in render pipelinecrates/display_list/src/lib.rs— Exportbuild_display_list_with_container_scrollcrates/web_api/src/lib.rs— Addeddispatch_scroll_event()(bubbles: false, cancelable: false)crates/browser_runtime/src/lib.rs— Addeddispatch_scroll_event()wrappercrates/app_browser/src/tests/hit_test_tests.rs— 7 new tests: z-index ordering (3), overflow clipping (4)crates/display_list/src/tests/scroll_offset_tests.rs— Updated for new scroll_x parametercrates/display_list/src/tests/clipping_tests.rs— Updated for new scroll_x parametercrates/display_list/src/tests/sticky_positioning_tests.rs— Updated for new scroll_x parametertests/js_events.rs— 9 new integration tests: scroll event (3), keyboard events (6)docs/HTML5_Implementation_Checklist.md— Updated interaction event status