Skip to content

WEB-UX-004 — Move focus to main content after SPA path navigation #657

Description

@daliu

Officer impact: No live-site change is claimed at issue creation. After a future approved publication, keyboard users who navigate between client-side pages will move into the destination main content unless that page or the user has already established newer focus.

Officer documentation: Update only docs/officers/PUBLISH_AND_CHECK.md with one separately named source/later-publication verification procedure.

Deployment evidence: None at issue creation. Source and synthetic tests only are authorized. No website publication, Firebase deployment, outside-provider configuration, account/sign-in action, production-data action, or live-behavior verification is authorized or claimed.

Atomic outcome

After a committed client-side pathname transition, hand otherwise-stale keyboard focus once to the existing #main-content landmark. Preserve the existing pathname-only scroll reset, preserve newer destination/user focus, and never focus main on initial load.

Exact base commit: 3265a55cb92df1ebb5401f2647ff81e4ca3af0b8

Exact base tree: 322468cc75a57f7ea392eb9f57a201fb11c4b35a

Current defect

src/components/ScrollToTop.jsx currently calls only window.scrollTo(0, 0) when pathname changes. src/App.jsx renders a persistent <main id="main-content"> without programmatic focusability. A desktop Navbar or Footer link therefore remains focused in persistent old-page chrome after its route content changes. On the phone layout, the selected menu link becomes hidden when released #490 closes the menu, and browser focus can fall to the document body. Assistive-technology and keyboard users are not taken into the new page content.

Invariant and transitions

  • Keep the current window.scrollTo(0, 0) behavior on initial render and every pathname change.
  • Initial direct load or reload never moves focus.
  • Search-only, hash-only, location-state-only, and same-path history changes neither scroll again nor create a focus handoff; the effect remains keyed only by pathname.
  • Give the existing main landmark tabIndex={-1} so code can focus it without adding it to ordinary Tab order. Preserve its exact ID and the existing skip-link destination.
  • Each real pathname transition creates only an opaque latest-transition token and a transient reference or opaque focus-version snapshot sufficient to distinguish the unchanged navigation origin from newer focus. Never read, copy, serialize, log, or persist element text, values, IDs, query, hash, route state, credentials, or member data.
  • Capture the exact transition target and origin in a layout-phase pathname effect, then schedule that exact token's one cancelable animation-frame settlement from a pathname-keyed passive effect. Identity-specific cleanup must prevent an older passive cleanup from clearing a newer layout token. This ordering allows destination layout/passive focus and synchronous redirect commits to settle before the handoff.
  • At settlement, consume before target checks. Require the latest still-mounted transition and the exact connected main target.
  • Leave main or a connected main descendant focused when it already owns focus.
  • Focus main exactly once with { preventScroll: true } only when focus is still the captured navigation origin or has fallen to null, body, the document root, or a disconnected element.
  • Preserve any different connected focus established after the transition by the destination or user. Never retry after consuming a missing, disconnected, superseded, or no-steal target.
  • A newer pathname transition cancels and generation-invalidates the older frame. A forcibly invoked stale callback cannot act on or consume a newer intent. Unmount is inert.
  • An asynchronously committed later navigation is a separate pathname transition. Do not claim that unrelated later navigation is coalesced with an earlier completed handoff.
  • Preserve released WEB-UX-002 — Restore visible keyboard focus across the site #291's global focus-visible rule and skip-link behavior byte-for-byte, and preserve released WEB-UX-003 — Make mobile navigation state explicit and deterministic #490 mobile disclosure/close behavior byte-for-byte. The committed-build browser check at 1280×900 and 390×844 proved that focused main is exactly x=0, y=0, and viewport-wide, so the existing outward WEB-UX-002 — Restore visible keyboard focus across the site #291 decoration is clipped and visually absent. Add only one scoped, fixed, pointer-inert #main-content:focus-visible::after two-tone cue inset inside the viewport and below the fixed navigation; it must not move layout or enter the accessibility tree.
  • Add no route, call, request, retry, analytics event, log, storage, authentication, membership, directory, photo/search, Firebase, provider, or production-data behavior.

Trustworthy RED

On the unchanged exact-base runtime, add a focused test in the existing src/App.test.jsx: focus a persistent synthetic navigation link, commit another pathname, flush a controlled animation frame, and require the existing main landmark to own focus. The old source must fail only because it leaves focus on the persistent link or body. Record the test-only diff digest and focused output before changing runtime.

Required GREEN coverage

  1. Initial / and an initial deep URL with search/hash scroll once but never focus main.
  2. Query-only, hash-only, state-only, and same-path replacement/navigation do not schedule focus or scroll again.
  3. A focused persistent navigation origin transfers once to connected main with exactly { preventScroll: true }.
  4. Body, document-root, null, removed, and disconnected origins recover to main.
  5. Main already focused is not redundantly focused; a connected focused descendant is preserved.
  6. Destination layout focus, destination passive focus, and deliberate connected focus selected before settlement are preserved.
  7. The real released phone menu closes after a public destination, its old menu link is no longer the page destination, and main receives focus without changing WEB-UX-003 — Make mobile navigation state explicit and deterministic #490 ARIA/classes/routes.
  8. Rapid A-to-B-to-C or synchronous redirect commits leave only the newest frame eligible. A forcibly invoked canceled callback is inert and cannot consume the newer intent.
  9. Missing or disconnected main consumes without delayed focus after rerender.
  10. Unmount cancels and generation-fences even a forcibly invoked old callback.
  11. StrictMode initial replay stays focus-inert; POP/back pathname navigation gets the same current handoff.
  12. The real app shell preserves Skip to content -> #main-content, the exact main ID, tabIndex={-1}, and zero additional service calls.
  13. Run the named focused block, full App and frontend suites, repository Node tests, TypeScript, scoped ESLint, unchanged lint baseline, diagnostic build, workflow/security checks, diff check, and independent accessibility/security/officer reviews.
  14. On the exact-head signed-out Deploy Preview, inspect keyboard behavior at 390-pixel and 1280-pixel widths. Require the scoped cue to be visible, fully inside the viewport below navigation, pointer-inert, non-disruptive, and layout-neutral; stop if it is clipped, absent, overlays controls materially, or changes layout.

Exact ownership

  • src/components/ScrollToTop.jsx: only the pathname focus-intent/settlement lifecycle while retaining the existing scroll effect.
  • src/App.jsx: only programmatic focusability and the stable exact-main target relationship.
  • src/App.test.jsx: one separately named WEB-UX-004 focused component matrix plus the minimum real-shell integration assertion. No new test file, so the 120-file lint inventory remains unchanged.
  • src/index.css: only the scoped bounded #main-content:focus-visible::after cue; preserve the global WEB-UX-002 — Restore visible keyboard focus across the site #291 selector and declarations byte-for-byte.
  • src/headerClearance.test.js: only mutation-sensitive assertions for the scoped main cue, its viewport/nav insets, pointer/layout containment, and preservation of the one global WEB-UX-002 — Restore visible keyboard focus across the site #291 rule.
  • docs/officers/PUBLISH_AND_CHECK.md: one separately named no-terminal source/later-publication route-focus verification procedure adjacent to the historical WEB-UX-002 — Restore visible keyboard focus across the site #291 keyboard check.

No .github/lint-baseline.json, Navbar, Footer, route-page, Account, directory, service, root design/security, map, Mermaid, backend, release-control, or provider path is owned.

Officer procedure requirements

The new guide section must name purpose, approver, prerequisites, one-action steps, expected result, stop conditions, success proof, undo, and escalation. It must use only signed-out public pages and made-up/no submitted data; cover initial load, desktop and phone navigation, browser back/forward, destination-owned focus, main focus/next-Tab order, 390-pixel and 1280-pixel widths, and visible unclipped #291 focus treatment; and separately record source, tests, merge, website publication, exact runmprc.com revision, Firebase, providers, accounts/sign-in, production data, and live behavior. Live proof remains NOT AVAILABLE YET until an approved publication is separately verified.

No Mermaid change is required because the existing main landmark, page hierarchy, routes, destinations, permissions, ownership, data movement, and deployment topology do not change.

Explicit exclusions

  • No new test file or lint-baseline/file-count update.
  • No focus movement on initial load or same-path search/hash/state cleanup.
  • No unconditional immediate layout/passive focus, layout-scheduled frame, autoFocus, timeout, live region, page-heading rewrite, route-label change, or ordinary Tab stop.
  • No Navbar state/markup/CSS change, no Footer change, no global focus-rule change, no outline suppression, and no weakening of WEB-UX-002 — Restore visible keyboard focus across the site #291/WEB-UX-003 — Make mobile navigation state explicit and deterministic #490 behavior.
  • No Account editor/retry/validation work and no member-directory runtime, availability, or connection work.
  • No Functions, Firestore Rules, indexes, schemas, services, packages, workflows, provider consoles, accounts, sign-in, production data, deployment, publication, or live action.
  • No membership, role, entitlement, payment, roster, photo query, facial recognition, matching, embedding, similarity, or biometrics.

Migration and residual risk

No migration. This browser focus handoff proves only source behavior on a committed SPA pathname transition. It does not prove publication, authorization, data correctness, backend availability, or production behavior. A delayed asynchronous redirect is its own transition. Rendered preview review must stop for a clipped or disruptive main focus indicator rather than suppressing the global indicator.

Coordination and claim protocol

Open PRs were empty at audit. Searches found no open exact-path or route-focus claimant. #291 and #490 are closed/released foundations. Active #504 coordinates only the disconnected directory; #507 retains all future directory connection/release gates; #616 owns only its two Strava Function paths plus its separate OAUTH/RISK-024 SECURITY hunk. #133, #136, and #460 mention docs/officers/PUBLISH_AND_CHECK.md for different future procedures and have no active exact hunk claim. Post and reread disjointness notices before editing.

Issue creation is not a claim. Before the first repository edit: create a unique worktree/branch from the exact base; re-read main, open PRs, and notices; assign the issue; post a timestamped claim naming the exact branch/base/tree/six paths; and set status:in-progress. Hold the claim through reviewed merge, exact-main audit, closure, and explicit release.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:webWeb application and hostingbugSomething isn't workingpriority:P1High-priority follow-upsize:SSmall focused issuetype:reliabilityReliability and recoverytype:testingTest infrastructure and quality gates

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions