You're viewing the readable version of this site. The interactive extras (search, diagrams, read-aloud) need JavaScript and a current browser. Enable JavaScript; if it is already enabled, update your browser.

Learn · Around the Algorithm

budding

Who Owns the Next Key?

A key event is not a broadcast. It is a request that one visible layer must own.

search, keyboard, focus, accessibility, state-machines, interaction, algorithms, learn

“DOM Focus is maintained on the combobox and the assistive technology focus is moved within the listbox using aria-activedescendant.”

— W3C Web Accessibility Initiative, Combobox Pattern

This is the twenty-second chapter in a book about search from first principles. You will reproduce a double-handling failure, model nested transient surfaces as a finite stack, derive exclusive guarded dispatch and last-opened-first- dismissed behavior, and prove that keyboard selection agrees with visible selection. You will distinguish DOM focus from semantic selection, compare roving tabindex with aria-activedescendant, preserve focus across pointer activation, and state the liveness obligation left after the safety proof. The next chapter gives every query the same kind of exclusive publication right.

Two correct listeners make one wrong interaction

The previous chapter kept the caret still while results changed. Now place a spelling popup above those results. Both surfaces recognize Down Arrow. Both recognize Escape. Both handlers are locally reasonable:

  • the suggestion popup moves to its next suggestion;
  • the result list moves to its next result;
  • the popup closes on Escape; and
  • the containing search dialog also closes on Escape.

Broadcast one Down Arrow and two highlights move. Broadcast one Escape and two surfaces disappear. No handler is individually broken. The missing object is ownership.

Prediction — find the shortest failing trace.

A flag such as popupHandledArrow can patch this trace. Add recent searches, contextual help, or a nested dialog and flags multiply pairwise. The repair is not another exception. It is a rule that decides before any transition which layer owns the event.

A finite stack names the visible owners

Let the active transient layers be a finite stack

L=[0,1,,m1], L=[\ell_0,\ell_1,\ldots,\ell_{m-1}],

where m1\ell_{m-1} is the most recently opened visible layer. A layer records four things:

  • the events it accepts in its current state;
  • its transition for an accepted event;
  • whether it can be dismissed; and
  • the focus destination after commit or dismissal.

Opening a nested transient surface pushes one layer. Dismissing the top dismissible surface pops one. Persistent peer panes are not layers in this stack; they need an explicit product state because neither is temporarily above the other.

Dispatch scans from the top downward and stops at the first accepting guard. The scan returns a decision value before effects run. Rendering, focus movement, and announcement project the one chosen transition.

A guard is a predicate that says whether a transition is legal in the current state. Learn more in W3C State Chart XML.

Algorithm — exclusive guarded dispatch

DISPATCH(EVENT, LAYERS)
Input:  one EVENT and finite LAYERS ordered bottom to top
Output: one owner decision or unhandled

i  LENGTH(LAYERS) - 1
while i  0
    layer  LAYERS[i]
    if VISIBLE(layer) and ACCEPTS(layer, EVENT)
        transition  STEP(layer, EVENT)
        return OWNED(layer.identity, transition)
    i  i - 1
return UNHANDLED

The algorithm is deliberately sequential even if rendering elsewhere is parallel. Ownership is a priority decision. Running guards concurrently cannot justify publishing more than the highest accepting decision, and the layer count is bounded by the surface design rather than the result count.

Three safety laws fall out of one stop

The early return carries most of the proof.

Exclusive ownership

For every event, at most one layer transition is admitted. The scan either finds no accepting layer or returns at the first one. It cannot reach a second accepting layer after return.

Induct over event sequences. The empty sequence admits no conflicting transition. For the inductive step, the next event admits at most one transition; applying that transition produces another finite stack. Therefore every finite trace preserves exclusive ownership.

Last opened, first dismissed

Let Escape be accepted by the topmost dismissible transient layer. Dispatch encounters that layer before any layer below it. Its transition removes only itself, so Escape cannot remove a lower layer while a dismissible layer remains above it.

This is last-opened, first-closed behavior, not the broader claim that every Escape always closes something. An input may use Escape to abandon an active edit, a nondismissible commitment dialog may refuse it, and an already empty field need not invent an action.

Visible-state agreement

Store one semantic selection identity in the owning layer. The renderer derives both aria-selected and the visual selected style from that identity. Because dispatch admits only one selection transition and projection has one source, two rows cannot both claim the sole selection.

This law fails if hover, focus, and selection share one CSS class. Color and outline then stop communicating state. Category styling may say “command” or “page”; state styling must say focus, current selection, disabled, or pending consistently across categories.

Safety does not prove an exit exists

The three laws say bad things cannot happen. They do not say the reader can finish. Each opened layer also needs a liveness witness:

  1. a finite keyboard path commits or dismisses it;
  2. that path does not require a pointer;
  3. the terminal transition names a focus successor; and
  4. the successor is present, focusable, and semantically appropriate.

If a suggestion commits, focus normally remains in or returns to the input. If the containing dialog closes, focus returns to the control that opened it when that control still exists. If it no longer exists, the design chooses a nearby stable successor; “whatever the browser happens to focus” is not a policy.

Reveal — dismiss exactly one layer.

Clearing the query and closing the dialog are different commands. They need different accessible names, hit regions, state transitions, and undo stories. Overloading one unlabeled × icon or one Escape branch makes ownership invisible to both the reader and the implementation.

Focus is not selection

An editable search field needs DOM focus to receive text, maintain its caret, and preserve native editing keys. The currently navigated suggestion is a semantic active item. Those states may refer to different elements without contradiction.

The WAI-ARIA combobox pattern describes exactly this arrangement: DOM focus can remain on the combobox while aria-activedescendant identifies the active option in its controlled popup. Down Arrow changes the active identity; it need not move DOM focus out of the input. Enter commits that identity. Escape closes the popup without requiring the text value to change.

There are two common composite-focus strategies:

Strategy DOM focus What moves Useful when
roving tabindex active child one child has 0; peers have -1 children receive native focus and scrolling
aria-activedescendant container or input referenced semantic identity typing must remain in an editable field

Neither strategy is decorative ARIA. Each is an interaction algorithm. With roving tabindex, the implementation updates tab stops and calls focus. With aria-activedescendant, it updates the reference, visible indicator, and scroll position while DOM focus stays put.

The active element must satisfy the required DOM relationship to the focused owner, commonly as a descendant of the controlled listbox. Virtualization must therefore keep the active option represented; an identifier that points to a recycled or absent node breaks the projection even if the semantic state is correct.

Pointer and keyboard write the same state

A pointer press on a suggestion should commit through the same semantic transition as Enter. Otherwise a reader who alternates mouse and keyboard can see one highlight while assistive technology reports another.

The event order matters. Pointer down can move focus before click. If blur dismisses the popup first, the later click has no target to commit. A robust flow decides ownership on the pointer event, prevents only the unwanted focus transfer when that is safe and necessary, and routes commitment through the same layer transition. It does not create a second pointer-only source of selection truth.

Pointer hover is preview, not keyboard selection. Merely crossing a row should not steal the active descendant from a keyboard user. A deliberate press may change selection because it expresses activation intent.

The boundary between suggestions and results is semantic

Suppose the system retains five suggestions and ten results under separate caps. Concatenating the fifteen visible rows does not prove they form one ranked order. It also does not answer what Down Arrow does at the fifth row.

There are two honest designs:

  • suggestions are a separate caret-pointed layer, so Escape dismisses them and returns navigation to the result owner; or
  • suggestions are rows in one result algebra, so one comparator, selection, cap policy, and navigation order govern the combined list.

Visual placement declares ownership. A popup floating above results promises a separate transient layer. An inline row promises participation in the list. Using popup appearance with inline-list behavior makes the boundary impossible to predict.

Transfer — choose a navigation algebra for two capped sources.

Browser reality is an effect boundary

The state machine owns the law; the browser adapter observes and performs it. That separation matters at several edges:

  • composed events can cross shadow boundaries and expose a retargeted node;
  • a popover enters the browser's top layer, but top-layer placement does not define the product's selection algebra;
  • focus and blur events have an order that can race pointer commitment;
  • a visually departing animation may still be logically active unless removal and ownership share one terminal transition;
  • assistive technology can change focus or activation without reproducing the keyboard event sequence a test author expected; and
  • reduced motion changes projection timing, never the transition graph.

The adapter reports attributed events to the machine. The machine chooses one owner and one transition. The adapter then updates DOM state, focus, scrolling, and announcements. Listening on every descendant and hoping propagation order matches visual depth reverses that ownership.

Test traces, not isolated handlers

The shortest useful oracle consumes whole attributed traces. Generate bounded sequences containing open, Arrow, Tab, Enter, Escape, pointer press, focus loss, and removal. After every step assert:

  • zero or one transition consumed the event;
  • Escape removed no layer below a dismissible top layer;
  • at most one semantic identity is selected;
  • visual and accessibility projection name that same identity;
  • every terminal transition names an existing focus successor; and
  • stack depth remains within the declared product bound.

When a trace fails, shrink the sequence while preserving preconditions. The broadcast failure shrinks to open suggestions; Down Arrow. The double-dismiss failure shrinks to open dialog; open suggestions; Escape. These small traces explain the law better than a screenshot of the final wrong state.

Negative results

Temptation Failure
let every visible component listen one intent produces several transitions
stop propagation inside arbitrary DOM nodes event-tree position masquerades as semantic ownership
use one boolean per popup pair combinations grow while the ownership rule stays implicit
move DOM focus into every suggestion editable input loses its caret and native text behavior
preserve focus by row number reranking gives the focus identity to another result
let hover set keyboard selection pointer motion steals assistive and keyboard context
clear and close with one command two effects lose distinct names and exit paths
animate a layer after logically removing it visible state and input ownership disagree

Lessons

  • One input event admits at most one layer transition.
  • Transient nested surfaces form a finite last-opened-first-dismissed stack.
  • Top-down guarded dispatch makes exclusive ownership structural.
  • Safety still needs a liveness witness: every layer can commit or dismiss and reach a defined focus successor.
  • DOM focus, semantic active identity, selection, and activation are distinct.
  • Editable comboboxes can keep DOM focus in the input while aria-activedescendant moves through suggestions.
  • Pointer and keyboard activation update the same semantic state.
  • Independently capped sources need either one combined navigation algebra or an explicit layer boundary.
  • Appearance communicates ownership; it is not merely decoration.

Practice

  1. Prove exclusive ownership for a trace with three nested accepting layers.
  2. Give a counterexample showing why exclusive ownership does not prove that a popup can be dismissed.
  3. Specify the focus successor when a dialog opener is removed while the dialog is open.
  4. Compare roving tabindex and aria-activedescendant for an editable search field containing a virtualized result list.
  5. Design the transition for pointer down followed by blur and click without creating a second selection source.
  6. Decide whether spelling corrections are a transient layer or rows in one combined result order, then state the Arrow and Escape laws.

References

  1. W3C Web Accessibility Initiative. “Combobox Pattern.” ARIA Authoring Practices Guide.
  2. W3C Web Accessibility Initiative. “Developing a Keyboard Interface.” ARIA Authoring Practices Guide.
  3. WHATWG. “Interaction.” HTML Living Standard, focus and popover sections.
  4. W3C. “UI Events.” Focus, input, pointer, and keyboard event ordering.