A focus trap is the loop that keeps Tab inside an open dialog. There is one set of things every focus-trap test asserts:
- Tab never leaves the dialog
- Tab from the last control wraps to the first
- Shift+Tab from the first wraps to the last
Try it: https://dev48.infy.uk/design/day77-focus-trap.html
All three hold for seven of eight traps. Six of those seven are wrong.
And it could not be otherwise. The same assertions pass for 3,600 of 3,600 lists drawn at random from the dialog, in random order — because a cycle is a cycle whatever is in it. The suite is testing that a loop closes, not that it visits the right things in the right order.
What that lets through
The trap that appears in the most shipped code sends the keyboard somewhere the reader cannot see on 1,184 of 1,500 generated dialogs:
16.4% of every Tab press lands on an invisible target
6 presses in a row on the worst dialog, focus ring never moving
1,132 of those dialogs hold a control no amount of Tab can reach at all
None of that violates any of the three assertions. The loop closes. It just closes around the wrong set.
The fix is fixtures, not assertions
Five more fixtures and sixteen extra elements of markup take the suite from accepting seven traps to accepting one. The assertions never changed — what changed is what the dialog contains: a hidden control, a display:none sibling, an element with a negative tabindex, a control inside a closed <details>.
The general shape: a property that holds for a random permutation of your elements is not a property of your ordering. If your suite would pass on shuffled input, it is not testing order — and focus management is entirely about order.
Verifier 298 asserts, 1,583,935 in-page assertions on load, 0 failures. Vanilla JavaScript, one file, no build step.
Top comments (0)