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