Edge nav controls drop keyboard focus to the document body when a press disables them
- Dominant language
- TypeScript
- Stars
- 13.1k
- Forks
- 1.1k
- Avg merge
- 1d 14h
- Merged PRs (30d)
- 669
Description
Several components disable a previous/next control the moment its direction runs out. The browser drops focus off a control it disables, so a keyboard user who pressed the control that reached the edge lands on `` and has to Tab from the top of the document to get back. WCAG 2.4.3 Focus Order.
Driven in Chromium against the current `main` Storybook build, one press per row until the edge:
| story | control | result |
|---|---|---|
| `core-pagination--default` | Go to next page | focus on `` after 9 presses, control disabled |
| `core-calendar--min-max-boundary` | Next month | focus on `` after 2 presses, control disabled |
| `core-lightbox--gallery` | Next | focus on `` after 1 press, control **not** disabled, so this one has a second cause worth finding |
| `core-carousel--default` | Scroll right | focus on `` after 1 press, control disabled (fixed in #5601) |
`DateInput/TouchDateField.tsx` uses the same `isDisabled={!canStepBack}` / `isDisabled={!canStepForward}` shape at its stepper and was not driven.
This is filed once rather than per component because no single component's PR closes it. Each site can patch itself, and #5601 did for Carousel, but the shape is the same everywhere and the repair wants to be shared: something that notices the control the user is on is about to be disabled and moves focus somewhere sensible first.
Two things #5601 learned the hard way, so the next fix does not repeat them.
The obvious receiver, the opposite button, does not work. At the moment of the press it is still disabled, because its enabling state lands with the next render, and `focus()` on a disabled control is a no-op. Carousel hands focus to its scroll container, which is a permanent tab stop inside the same labelled region. Components without such an element need a different answer, which is part of why this is a system question.
`focus()` scrolls its element into view by default. In a scrolling component that cancels the very scroll the press started, and the content never moves. Pass `preventScroll: true`.
A shared helper cannot be an effect: firing focus from an Effect that can re-run is its own defect. The prediction has to happen in the handler that caused the state change.
Contributor guide
Research direction
Start by reproducing the listed `core-pagination--default`, `core-calendar--min-max-boundary`, `core-lightbox--gallery`, and `core-carousel--default` Storybook stories in Chromium, then inspect the handlers that disable their controls. Read `DateInput/TouchDateField.tsx` and the Carousel change in #5601 for related behavior. Done means edge presses preserve sensible keyboard focus without cancelling scrolling, including the lightbox and date-field cases.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- accessibility, frontend
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100