Skip to content

Load more button loses keyboard focus while its batch loads #2065

Description

@steilerDev

As a keyboard-only user of a list that loads in batches, I want focus to stay on the "Load more" control while its batch loads so that I can load several batches in a row without re-tabbing from the top of the page each time.

Parent Epic: none (standalone; follow-up to #2060)
Priority: Should Have

Problem

InfiniteScrollFooter sets disabled={status === 'loading'} on the Load more / Retry button (client/src/components/InfiniteScrollFooter/InfiniteScrollFooter.tsx). It is the same DOM node across the idle / loading / error states, so when a keyboard user activates it, the button they are standing on becomes disabled mid-interaction. A disabled element is removed from the tab order and loses focus, so the browser drops focus to <body>.

Consequence for the diary page (#2060's AC16 path): press Enter on "Load more", the batch loads correctly, but focus is gone. To load the next batch the user must Tab all the way back through the page — including every newly appended entry card. AC16's letter is met (one activation loads one batch) which is why #2060 was accepted, but repeated keyboard-only paging is materially worse than pointer paging.

The same defect ships to every future consumer of the shared component, not just the diary page.

Why this was deferred rather than fixed in PR #2063

  1. It is not new in round 2 — disabled={status === 'loading'} was present at round 1 and I did not flag it, so it is not a regression introduced by the round-2 fixes.
  2. The remedy is a design-system decision, not a mechanical edit: aria-disabled="true" + a no-op click guard keeps the node focusable but changes the semantics of every disabled button pattern in the app. wiki/Style-Guide.md is owned by ux-designer, who should rule on whether this becomes the house pattern for in-place async buttons or stays local to this component.

Acceptance Criteria

  • 1. Given the Load more control has keyboard focus, When I activate it and the batch request is in flight, Then the control still has focus (document.activeElement) and focus has not moved to <body> or any other element.
  • 2. Given the batch request is in flight, When I attempt to activate the control again by keyboard, Then no additional request is issued (the dedupe behaviour of Diary: replace the broken numbered pager with scroll-driven batch loading #2060 AC6 is preserved without relying on the disabled attribute).
  • 3. Given the batch request is in flight, When a screen reader reads the control, Then it is exposed as unavailable/busy (e.g. aria-disabled="true") rather than silently ignoring activation.
  • 4. Given the batch resolves and more entries remain, When the control returns to its idle state, Then it is still focused and a visible focus indicator is rendered in both light and dark mode.
  • 5. Given the batch resolves and the end of the list is reached, When the control unmounts and is replaced by the end-of-list row, Then focus is moved somewhere deliberate and announced — not silently dropped to <body>.
  • 6. Given ux-designer has ruled on the pattern, When the fix lands, Then wiki/Style-Guide.md records whether aria-disabled + click-guard is the house pattern for in-place async buttons or is scoped to InfiniteScrollFooter.
  • 7. Given the E2E suite, When it runs, Then a test asserts criterion 1 and criterion 4 by keyboard alone (the existing keyboard test in e2e/tests/diary/diary-list.spec.ts asserts the batch loads but stops short of re-asserting focus after the load).

Notes

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    user-storyUser story - individual deliverable

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions