Files
rust_browser/_bmad-output/implementation-artifacts/3-9-event-dispatch-completeness.md
Zachary D. Rowitsch fb64ca1d34
All checks were successful
ci / fast (linux) (push) Successful in 7m9s
Create story files for Epic 3 stories 3.5-3.10
Create comprehensive implementation-ready story files for the remaining
Epic 3 (JavaScript Engine Maturity) stories and update sprint status
from backlog to ready-for-dev:

- 3.5: Built-in Completeness (Array/String/Object)
- 3.6: Built-in Completeness (Date/RegExp/Map/Set)
- 3.7: WeakRef, FinalizationRegistry & Strict Mode Edge Cases
- 3.8: DOM Bindings via web_api
- 3.9: Event Dispatch Completeness
- 3.10: Web API Exposure (fetch, Math, setInterval, rAF)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 08:06:03 -04:00

22 KiB

Story 3.9: Event Dispatch Completeness

Status: ready-for-dev

Story

As a web developer using JavaScript, I want all event types and dispatch phases to work correctly, So that interactive elements respond to user actions as expected.

Acceptance Criteria

  1. Capture phase: addEventListener(type, handler, {capture: true}) or addEventListener(type, handler, true) registers a capture listener. Events propagate Document → root → ... → target during capture phase before target and bubble phases. Capture listeners fire with eventPhase === 1. Per DOM §2.9.

  2. stopImmediatePropagation: event.stopImmediatePropagation() prevents remaining listeners on the current target from firing (unlike stopPropagation() which only prevents ancestors). Per DOM §2.5.

  3. Mouse events: mousedown, mouseup, mousemove, mouseenter, mouseleave, mouseover, mouseout dispatched to JS handlers. MouseEvent objects include clientX, clientY, button, buttons, ctrlKey, shiftKey, altKey, metaKey. mousedown/mouseup/mouseover/mouseout bubble; mouseenter/mouseleave do not. Per UI Events §3.

  4. Keyboard events: keydown, keyup dispatched to JS handlers on the focused element (or document if nothing focused). KeyboardEvent objects include key, code, ctrlKey, shiftKey, altKey, metaKey, repeat. Per UI Events §5.

  5. Focus events: focus, blur (don't bubble), focusin, focusout (bubble) dispatched when focus changes between elements. Events include relatedTarget (element losing/gaining focus). Per UI Events §6.

  6. Custom events: new Event(type, {bubbles, cancelable}) constructor works from JS. element.dispatchEvent(event) dispatches a synthetic event through the full capture → target → bubble path. event.isTrusted is true for user-initiated events, false for synthetic. Per DOM §2.2.

  7. Integration tests verify each event type and dispatch phase, and just ci passes.

What NOT to Implement

  • No input/change events -- form input events deferred to Epic 4 (User Input & Forms).
  • No touch events -- touchstart, touchmove, touchend out of scope (desktop browser).
  • No wheel/scroll events -- scroll events deferred to Epic 5.
  • No resize event -- window resize deferred to Epic 5.
  • No DragEvent -- drag and drop out of scope.
  • No CompositionEvent -- IME input events out of scope.
  • No keypress -- deprecated event, skip. Only keydown/keyup.
  • No MouseEvent.offsetX/offsetY -- layout-relative coordinates require layout tree access from event dispatch. Defer.
  • No Event.composedPath() -- Shadow DOM not implemented.

Files to Modify

File Change
crates/web_api/src/event.rs Add EventData enum (Mouse, Keyboard, Focus variants) to DomEvent. Add immediate_propagation_stopped, is_trusted flags.
crates/web_api/src/event_dispatch.rs Add capture phase loop (reverse propagation path). Check listener.capture flag during dispatch. Add stopImmediatePropagation check in invoke_listeners().
crates/web_api/src/event_target.rs Ensure capture listeners are stored and retrievable separately for capture vs bubble phases.
crates/web_api/src/dom_host/host_environment.rs Expose MouseEvent/KeyboardEvent/FocusEvent properties via get_property(). Add stopImmediatePropagation() to call_method(). Add dispatchEvent() to Element/Document methods. Add Event constructor.
crates/web_api/src/lib.rs Add dispatch_mouse_event(), dispatch_keyboard_event(), dispatch_focus_event() methods to WebApiFacade. Add dispatch_synthetic_event() for custom events.
crates/app_browser/src/event_handler.rs Dispatch mousedown/mouseup/mousemove on mouse input events. Dispatch keydown/keyup on keyboard input events. Calculate client coordinates from window position. Track hovered element for mouseenter/mouseleave/mouseover/mouseout.
crates/browser_runtime/src/lib.rs Add dispatch_mouse_event(), dispatch_keyboard_event(), dispatch_focus_event() wrappers.
crates/platform/src/event_loop.rs Verify raw keyboard/mouse events are forwarded to event handler (may already be done).
tests/js_events.rs Add tests for capture phase, keyboard events, mouse events, focus events, custom events, stopImmediatePropagation.
docs/HTML5_Implementation_Checklist.md Check off capture phase, event types.

Tasks / Subtasks

  • Task 1: Capture phase and stopImmediatePropagation (AC: #1, #2)

    • 1.1 Add immediate_propagation_stopped: bool and is_trusted: bool to DomEvent in event.rs:
      • Initialize immediate_propagation_stopped = false
      • Initialize is_trusted = true for user events, false for synthetic
    • 1.2 Implement capture phase in dispatch_event() in event_dispatch.rs:
      • Current flow: Target → Bubble
      • New flow: Capture → Target → Bubble (per DOM §2.9)
      • Build propagation path as before: [target, parent, ..., root, Document]
      • Capture phase: iterate path in REVERSE (Document → root → ... → parent of target), skip target itself
        • Set event.phase = EventPhase::Capture
        • For each ancestor, invoke listeners where listener.capture == true
        • Check propagation_stopped after each node
      • Target phase: invoke ALL listeners on target (both capture and non-capture)
        • Set event.phase = EventPhase::Target
      • Bubble phase: iterate path forward (parent → ... → root → Document), if event.bubbles
        • Set event.phase = EventPhase::Bubble
        • Invoke listeners where listener.capture == false
    • 1.3 Add stopImmediatePropagation() to event methods in host_environment.rs:
      • Set event.immediate_propagation_stopped = true
      • Also sets event.propagation_stopped = true (per spec)
    • 1.4 Check immediate_propagation_stopped in invoke_listeners():
      • After each listener invocation, if immediate_propagation_stopped == true, break out of listener loop (not just node loop)
    • 1.5 Update listener snapshot collection in event_target.rs:
      • collect_for_path() should include capture flag so dispatch can filter
      • Return Vec<(EventTargetId, Vec<EventListener>)> with capture flag preserved
    • 1.6 Add tests:
      • Capture handler fires before target handler
      • Capture handler fires before bubble handler
      • Multiple capture handlers fire in registration order
      • stopImmediatePropagation() stops remaining handlers on same element
      • stopPropagation() in capture phase prevents target and bubble phases
  • Task 2: Mouse events (AC: #3)

    • 2.1 Add EventData enum to DomEvent in event.rs:
      pub enum EventData {
          None,
          Mouse { client_x: f64, client_y: f64, button: u16, buttons: u16,
                  ctrl_key: bool, shift_key: bool, alt_key: bool, meta_key: bool },
          Keyboard { key: String, code: String, repeat: bool,
                     ctrl_key: bool, shift_key: bool, alt_key: bool, meta_key: bool },
          Focus { related_target: Option<EventTargetId> },
      }
      
      • Add data: EventData field to DomEvent
      • Default to EventData::None for existing events (click, DOMContentLoaded, etc.)
    • 2.2 Expose MouseEvent properties via get_property() in host_environment.rs:
      • For Event type with EventData::Mouse: expose clientX, clientY, button, buttons, ctrlKey, shiftKey, altKey, metaKey
      • For non-mouse events, these properties return undefined
    • 2.3 Add dispatch_mouse_event() to WebApiFacade:
      • Parameters: event_type: &str, target: NodeId, client_x: f64, client_y: f64, button: u16, modifiers: Modifiers
      • Create DomEvent with EventData::Mouse { ... }
      • Set bubbles based on event type: mousedown/mouseup/mouseover/mouseout/click → true; mouseenter/mouseleave → false
      • Dispatch via dispatch_event()
      • For non-bubbling events (mouseenter/mouseleave): fire only on target, no propagation path
    • 2.4 Dispatch mouse events from event_handler.rs:
      • On WindowEvent::MousePressed → dispatch mousedown then (on release or same frame) prepare mouseup/click sequence
      • On WindowEvent::MouseReleased → dispatch mouseup, then click if same target
      • On WindowEvent::CursorMoved → dispatch mousemove on element under cursor
      • Track previously hovered element for enter/leave:
        • If element changes: dispatch mouseout (old, bubbles) → mouseleave (old, no bubble) → mouseover (new, bubbles) → mouseenter (new, no bubble)
      • Calculate clientX/clientY relative to viewport (subtract window chrome/scroll offsets)
    • 2.5 Update existing dispatch_click() to use the new mouse event infrastructure:
      • click should include MouseEvent properties (clientX, clientY, button=0)
      • Preserve backwards compatibility with existing click tests
    • 2.6 Add tests:
      • mousedownmouseupclick sequence on element
      • MouseEvent properties (clientX, clientY, button) accessible in handler
      • mouseover/mouseout bubble; mouseenter/mouseleave don't
      • Modifier keys (ctrlKey, shiftKey) passed through
  • Task 3: Keyboard events (AC: #4)

    • 3.1 Expose KeyboardEvent properties via get_property():
      • For Event with EventData::Keyboard: expose key, code, repeat, ctrlKey, shiftKey, altKey, metaKey
    • 3.2 Add dispatch_keyboard_event() to WebApiFacade:
      • Parameters: event_type: &str, target: NodeId, key: &str, code: &str, modifiers: Modifiers, repeat: bool
      • keydown and keyup both bubble and are cancelable
      • Target: focused element if one exists, otherwise document
    • 3.3 Dispatch keyboard events from event_handler.rs:
      • On WindowEvent::KeyPressed → dispatch keydown
      • On WindowEvent::KeyReleased → dispatch keyup
      • Map platform key codes to key (logical key like "Enter", "a", "ArrowUp") and code (physical key like "KeyA", "Enter", "ArrowUp")
      • Check winit key event for key and code mappings -- winit provides event.logical_key and event.physical_key
      • Pass modifier state from winit::event::Modifiers
    • 3.4 Add focused element tracking:
      • Add focused_element: Option<NodeId> to WebApiFacade (or EventHandler state)
      • Default to None (keyboard events target document)
      • Updated by focus events (Task 4) and click events (clicking an element focuses it)
    • 3.5 Add tests:
      • keydown fires on focused element
      • keydown fires on document when nothing focused
      • KeyboardEvent properties (key, code, ctrlKey) accessible
      • keydown bubbles to parent
      • preventDefault() on keydown prevents default action
  • Task 4: Focus events (AC: #5)

    • 4.1 Expose FocusEvent properties via get_property():
      • For Event with EventData::Focus: expose relatedTarget (HostObject or Null)
    • 4.2 Add dispatch_focus_change() to WebApiFacade:
      • Parameters: old_focus: Option<NodeId>, new_focus: Option<NodeId>
      • Dispatch sequence (per UI Events §6.4.2):
        1. focusout on old element (bubbles, relatedTarget = new element)
        2. focusin on new element (bubbles, relatedTarget = old element)
        3. blur on old element (no bubble, relatedTarget = new element)
        4. focus on new element (no bubble, relatedTarget = old element)
      • focus/blur are NOT cancelable; focusin/focusout are NOT cancelable
    • 4.3 Trigger focus changes from click events:
      • When element is clicked, if it's focusable (inputs, buttons, elements with tabindex), update focused_element and dispatch focus events
      • Only certain elements are focusable: <input>, <textarea>, <select>, <button>, <a> with href, elements with tabindex
      • For this story, treat any element as focusable when clicked (refinement in Epic 4)
    • 4.4 Add tests:
      • Click on element A, then click on element B → blur on A, focus on B
      • focus doesn't bubble; focusin does
      • relatedTarget is correct on both focus and blur events
  • Task 5: Custom events and dispatchEvent (AC: #6)

    • 5.1 Add Event constructor to JS globals:
      • new Event(type) → create event with type, bubbles=false, cancelable=false
      • new Event(type, { bubbles: true, cancelable: true }) → set options
      • Implement via construct() in host_environment.rs for constructor name "Event"
      • Return HostObject with type "Event" and a new event ID
      • Store pending custom events in WebApiFacade (similar to active_events)
    • 5.2 Add dispatchEvent(event) to Element/Document methods:
      • Accept a custom Event HostObject
      • Set event.is_trusted = false (synthetic event)
      • Set event.target to the element
      • Dispatch through full capture → target → bubble path
      • Return !event.defaultPrevented (boolean, per DOM §2.6)
    • 5.3 Add event.isTrusted property:
      • Expose via get_property() for Event type
      • true for events created by user interaction, false for dispatchEvent()
    • 5.4 Add tests:
      • new Event("custom", {bubbles: true}) creates event
      • element.dispatchEvent(event) triggers handlers
      • Custom event bubbles if bubbles: true
      • event.isTrusted === false for custom events
      • event.isTrusted === true for click events
  • Task 6: Testing and validation (AC: #7)

    • 6.1 Add comprehensive integration tests in tests/js_events.rs:
      • Capture → target → bubble ordering
      • stopImmediatePropagation vs stopPropagation
      • Mouse event sequence (mousedown → mouseup → click)
      • Keyboard events on focused element
      • Focus/blur on element change
      • Custom events via dispatchEvent
      • All event properties accessible in handlers
    • 6.2 Run all existing test suites:
      • cargo test -p web_api
      • cargo test -p rust_browser --test js_events
      • cargo test -p rust_browser --test js_dom_tests
      • cargo test -p rust_browser --test js_tests
      • cargo test -p rust_browser --test goldens
    • 6.3 Update docs/HTML5_Implementation_Checklist.md:
      • Check off capture phase
      • Check off keyboard/mouse/focus event types
    • 6.4 Run just ci -- full validation pass

Dev Notes

Key Architecture Decisions

Extend DomEvent with EventData enum, don't subclass. The existing DomEvent struct is used everywhere. Rather than creating MouseEvent/KeyboardEvent subclasses (which would require trait objects or generics throughout dispatch), add an EventData enum field. This keeps the dispatch path monomorphic and simple.

Capture phase is a reverse walk of the same propagation path. The existing build_propagation_path() returns [target, parent, ..., root, Document]. For capture: iterate indices (len-1)..1 (Document → root → parent of target). For target: index 0. For bubble: indices 1..len. No new path building needed.

Mouse event sequence matters. Per UI Events, clicking produces: mousedownmouseupclick. All three fire on the same target. mousedown is dispatched on press, mouseup + click on release. If mouse moves between press and release (different target), click is suppressed.

Focus tracking is shared state. focused_element: Option<NodeId> must be accessible from both event dispatch (to target keyboard events) and DOM operations (to determine which element receives focus events). Store in WebApiFacade.

Implementation Patterns from Existing Code

Event dispatch (in event_dispatch.rs):

pub fn dispatch_event(
    event: &mut DomEvent,
    document: &Document,
    listener_registry: &EventListenerRegistry,
    js_engine: &mut JsEngine,
    host: &mut DomHost<'_>,
) -> DispatchResult {
    let path = build_propagation_path(document, target);
    let snapshot = listener_registry.collect_for_path(&path, &event.event_type);
    // Currently: target phase, then bubble phase
    // Add: capture phase BEFORE target phase
}

Listener registration (in host_environment.rs):

// addEventListener(type, callback, capture)
"addEventListener" => {
    let capture = args.get(2).map_or(false, |v| v.to_boolean());
    // capture is stored but currently ignored during dispatch
}

Platform key events (in platform/src/event_loop.rs):

// winit events mapped to WindowEvent variants
// Check if KeyPressed/KeyReleased carry key information

Critical Implementation Details

mouseenter/mouseleave DON'T bubble. Unlike mouseover/mouseout, these events fire only on the target, not ancestors. In dispatch_event(), check if event type is mouseenter or mouseleave and skip propagation path (fire only on target).

focus/blur DON'T bubble either. But focusin/focusout DO. The focus change sequence dispatches both pairs to ensure both bubbling and non-bubbling listeners are supported.

Custom Event storage. When new Event(type) is called, create a DomEvent and store it with a unique ID. When dispatchEvent(eventHostObject) is called, look up the event by ID, set target, and dispatch. After dispatch, the event remains accessible (properties can be read after dispatch).

isTrusted is read-only. Scripts cannot set event.isTrusted. Only the engine sets it. User events get true, dispatchEvent events get false.

Element hover tracking for mouseenter/mouseleave. Track hovered_element: Option<NodeId> in event handler state. On CursorMoved, hit-test to find element under cursor. If different from previous, dispatch the mouseout → mouseleave → mouseover → mouseenter sequence.

Dependencies

Story 3.8 (DOM Bindings): Focus events need parentNode navigation for bubble path building (already exists in dispatch). dispatchEvent() needs to be a method on Element host objects (added in 3.8 or here).

No dependency on Stories 3.5-3.7. Event dispatch doesn't require property descriptors, Date/Map/Set, or WeakRef.

Previous Story Patterns

From Story 3.8 (DOM Bindings):

  • HostObject types with ID ranges -- custom events need their own ID range
  • get_property() dispatch on type_name -- extend for MouseEvent/KeyboardEvent properties

From existing event tests (tests/js_events.rs):

  • Tests use dispatch_click() on WebApiFacade directly
  • Integration tests run JS code and check results via script output

Risk Assessment

MEDIUM: Mouse event dispatch from platform layer. Requires understanding how winit delivers mouse position, button state, and modifier keys. The mapping from winit::event::MouseButton/KeyEvent to our EventData structs needs careful attention to coordinate systems.

MEDIUM: Hover tracking for mouseenter/mouseleave. Requires hit-testing on every CursorMoved event, which could be performance-sensitive. The hit-test infrastructure already exists for click handling -- reuse it.

LOW: Capture phase. Well-defined spec behavior. Straightforward reversal of propagation path iteration.

LOW: stopImmediatePropagation. Single boolean flag check in listener loop.

LOW: Custom events. Small API surface. Main complexity is Event constructor and storage.

Phased Implementation Strategy

Phase A -- Capture + stopImmediatePropagation (Task 1): Foundation fix for dispatch. All subsequent events benefit from correct 3-phase dispatch.

Phase B -- Mouse Events (Task 2): Extend DomEvent with EventData, wire from platform layer. Largest task.

Phase C -- Keyboard Events (Task 3): Similar pattern to mouse events but simpler (no coordinate tracking).

Phase D -- Focus Events (Task 4): Depends on focused element tracking. Ties into keyboard event targeting.

Phase E -- Custom Events (Task 5): New API surface. Independent of other tasks.

Phase F -- Testing + Validation (Task 6): After all event types implemented.

Project Structure Notes

  • Event system changes in crates/web_api/src/ (Layer 1) -- no layer violations
  • Platform input mapping in crates/app_browser/src/event_handler.rs (Layer 3) -- reads from platform, calls web_api
  • browser_runtime (Layer 2) -- thin wrappers forwarding to web_api
  • No unsafe code needed
  • No new external dependencies

References

  • DOM Living Standard §2 -- Events -- Event dispatch algorithm
  • DOM Living Standard §2.9 -- Dispatching events -- Capture → target → bubble
  • UI Events §3 -- Mouse Events -- MouseEvent interface
  • UI Events §5 -- Keyboard Events -- KeyboardEvent interface
  • UI Events §6 -- Focus Events -- FocusEvent interface
  • [Source: crates/web_api/src/event.rs] -- DomEvent struct, EventPhase enum
  • [Source: crates/web_api/src/event_dispatch.rs] -- dispatch_event(), build_propagation_path(), invoke_listeners()
  • [Source: crates/web_api/src/event_target.rs] -- EventListenerRegistry, listener deduplication
  • [Source: crates/web_api/src/dom_host/host_environment.rs] -- addEventListener, event property access
  • [Source: crates/app_browser/src/event_handler.rs] -- Platform event handling, click dispatch
  • [Source: _bmad-output/planning-artifacts/epics.md#Story 3.9] -- Story requirements

Dev Agent Record

Agent Model Used

{{agent_model_name_version}}

Debug Log References

Completion Notes List

File List