SIGN IN SIGN UP

Put the keyboard and screen-reader layer behind ?a11y=1

The layer from 472e3e76b (and the tip cards' keyboard pass in 5a87c445e) broke
too much for people who never use it, so it is off by default and comes back
with ?a11y=1 (Modes.a11y, read at call time; `.a11y` on the root for CSS,
mirrored into Document PiP). Without the flag the client looks and behaves as
it did before the layer; with it, exactly as it does now.

Off without the flag: focus traps and focus restoration in popups and menus,
menus taking the focus, roving tab stops and the tab stops added to bubbles, the
chat history, rows, plates and tiles, Enter/Space on role="button", arrows in
menus, tabs and picker grids, the key routing that kept keys on a focused
control instead of the composer, `inert` on off-screen columns and panels, the
skip link, reduced motion, Increase Contrast (its row, a stored value and its
settings search result), the focus rings, and every element swapped to a native
<button> — the div, span or link it was comes back, because a native button
brings the browser's box and takes the focus on a click. Those controls are one
A11yButton (Button's asDiv where they are built imperatively) rather than a
ternary at each site. index.html pins maximum-scale again: without it iOS zooms
into any field under 16px; the flag lifts it for pinch zoom. ARIA names, roles
and .sr-only text stay on either way — nobody without assistive technology can
tell they are there.

The shared helpers check the flag themselves (createFocusTrap, buttonKeyDown,
ensureButtonSemantics, attachTabList, attachPickerGrid, handleMenuKeyDown,
updateScrollRegionFocusable, attachClickEvent's keyboard click), and CSS is
scoped with :where(.a11y), which leaves every selector's specificity as it was.

Two later changes leaned on the layer and are gated with it: the composer
commit's input check no longer counts a range as an input, so a seek bar that
took the focus on a click sent the draft on Enter; and a toast let a keyboard
click through to the button under it. The audio row's toggle is built once
now: gated inline in the element prop, the flag-off div was rebuilt on every
read, and the row dressed one element and mounted another.

The a11y and focus suites, and the composer's keyboard specs, open ?a11y=1; the
layer's unit tests import tests/helpers/a11yLayer. jsdom here has
`ontouchstart`, so attachClickEvent listens to mousedown in unit tests and an
`el.click()` reached it only through the keyboard-click path: the Instant View
button test uses simulateClickEvent now.

test:focus ran on the lottie config, one worker, one test walking every story
with fixed 300ms pauses twice a stop: a quarter of an hour. It now runs on the
popup suite's setup (playwright.focus.config.ts), split into a part per worker
but one, which the sign-in sweep takes, so everything goes in one wave (a part
per worker, or more parts, were both slower). A stop no longer waits by the
clock: the transitions it sets off are run to their end and the control has to
hold still for a frame; a closed popup is not sat out through its hide
(closeStory, shared with test:popups). ~50 s, same 248 stories and ~1140 stops.
Sign-in cards move the focus into their field a moment after showing, so
non-popup surfaces keep their 400ms.

The faster sweep caught a real bug the old pauses hid: Tab out of the country
field closed its list, and the focus landed on the list's own scroller (Chrome
makes scrollers focusable) just as it vanished. The closing list is inert now.

Verified: tsc, lint and the full unit suite on this tree alone (the checkout
carries unrelated work), test:popups (flag off), test:a11y and test:focus (flag
on), and both modes in a signed-in preview — with the flag off bubbles take no
focus, the context menu leaves it on the body and Escape does not scroll the
chat, a popup focuses nothing; with it on, all of that is back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
E
Eduard Kuzmenko committed
a3f94ab7a7fecd8d31918ecd50bb5a831a1ce22b
Parent: 08e0c3b