Repository navigation
WEB-UX-004 — Move focus to main content after SPA path navigation #657
Description
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
mainis exactlyx=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::aftertwo-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
- Initial
/and an initial deep URL with search/hash scroll once but never focus main. - Query-only, hash-only, state-only, and same-path replacement/navigation do not schedule focus or scroll again.
- A focused persistent navigation origin transfers once to connected main with exactly
{ preventScroll: true }. - Body, document-root, null, removed, and disconnected origins recover to main.
- Main already focused is not redundantly focused; a connected focused descendant is preserved.
- Destination layout focus, destination passive focus, and deliberate connected focus selected before settlement are preserved.
- 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.
- 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.
- Missing or disconnected main consumes without delayed focus after rerender.
- Unmount cancels and generation-fences even a forcibly invoked old callback.
- StrictMode initial replay stays focus-inert; POP/back pathname navigation gets the same current handoff.
- The real app shell preserves
Skip to content->#main-content, the exact main ID,tabIndex={-1}, and zero additional service calls. - 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.
- 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 namedWEB-UX-004focused 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::aftercue; 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.
Activity
daliu commentedon Aug 14, 2026
CLAIMED by /root at
2026-08-14T05:42:43Z; branchcodex/issue-657-spa-route-focus; exact base3265a55cb92df1ebb5401f2647ff81e4ca3af0b8; exact base tree322468cc75a57f7ea392eb9f57a201fb11c4b35a.Exact ownership:
src/components/ScrollToTop.jsx: only a pathname-bound, cancelable, latest-transition focus settlement; preserve the existing pathname scroll effect.src/App.jsx: onlytabIndex={-1}on the existing exactmain#main-content; preserve skip-link target, routes, hierarchy, and all children.src/App.test.jsx: only one separately named WEB-UX-004 controlled-frame component matrix plus the minimum real-shell assertion; no new test file or lint-inventory change.docs/officers/PUBLISH_AND_CHECK.md: only one separately named no-terminal source/later-publication route-focus verification procedure adjacent to historical WEB-UX-002 — Restore visible keyboard focus across the site #291.The implementation will capture the exact main element and active navigation origin at the committed pathname transition, schedule one requestAnimationFrame from a layout effect, consume/fence it by object identity, preserve newer connected focus, and focus main only from the unchanged origin or null/body/document-root/disconnected focus with
{ preventScroll: true }. Initial and same-path search/hash/state changes remain focus-inert. Stale, canceled, superseded, missing/replaced-main, and unmounted callbacks cannot act or retry.Excluded and immutable: new test files,
.github/lint-baseline.json, CSS,src/headerClearance.test.js, Navbar/Footer/route-page/Account/directory/service code, root design/security/maps/Mermaid, Functions, Rules, indexes, schemas, packages, workflows, providers, accounts/sign-in, production data, deployment, publication, and live behavior. Released #291/#490 behavior, #504/#507 boundaries, literal-false directory availability, and every #616 path/hunk remain unchanged.No source or documentation edit preceded this valid claim. Hold this claim through exact-head review, merge, exact-main audit, closure, and explicit release. STOP on any exact collision or violated invariant.
daliu commentedon Aug 14, 2026
CLAIM DESIGN CORRECTION at
2026-08-14T05:51:48Z; exact four-path ownership is unchanged.React 18 can defer destination passive effects until after paint while a frame scheduled during layout runs before paint. Therefore a layout-scheduled frame could transiently focus main before a destination
useEffectestablishes its own focus, violating the no-steal invariant even if final focus later appears correct.Corrected owned lifecycle: the pathname
useLayoutEffectcaptures the exact main/origin and installs the identity token; a separate pathname-keyed passive effect schedules that exact token's single cancelablerequestAnimationFrameafter the existing scroll effect. Cleanup is identity-specific, so an older passive cleanup cannot clear or act on a newer layout token. Tests must prove a destination passive-focus route causes zero main-focus calls, not merely the correct final active element.The issue body now records this correction. No scope, route, data, service, CSS, documentation path, backend, provider, deployment, or live boundary changed. Runtime review is still in progress; no claim is released.
daliu commentedon Aug 14, 2026
CLAIM SCOPE CORRECTION at 2026-08-14T06:23:16Z; exact base, branch, runtime lifecycle, and backend/live boundaries are unchanged.
A read-only browser check of the committed local production build hit the issue's explicit visual stop condition at both required widths:
main#main-contentwas exactlyx=0, y=0, width=1280; computed WEB-UX-002 — Restore visible keyboard focus across the site #291 decoration was a 3px secondary outline with 3px outward offset and a 6px dark spread. Focus reached main and scroll stayed0,0, but the decoration lay outside the viewport/top under the fixed navigation and was visually absent.x=0, y=0, width=390; the phone menu closed tohide__nav, focus reached main, and the same decoration was visually absent.This is local committed-build evidence only, not Deploy Preview, publication, runmprc.com, or live evidence. The exact-head Deploy Preview at both widths remains mandatory.
The claim is expanded from four to exactly six paths before any CSS/test edit:
src/components/ScrollToTop.jsx,src/App.jsx,src/App.test.jsx,docs/officers/PUBLISH_AND_CHECK.mdsrc/index.cssonly a scoped fixed pointer-inert#main-content:focus-visible::aftertwo-tone cue inside the viewport and below the fixed navigation; preserve the global WEB-UX-002 — Restore visible keyboard focus across the site #291 rule byte-for-bytesrc/headerClearance.test.jsonly mutation-sensitive assertions for cue insets, fixed/layout-neutral/pointer-inert containment, and preservation of the single global WEB-UX-002 — Restore visible keyboard focus across the site #291 ruleNo outline suppression, global selector/declaration change, Navbar/Footer/route/data/service/backend/provider/deployment/live change, or new test/lint file is authorized. The issue body now records this correction. No CSS or header-clearance edit preceded it; the current local commit remains the reviewed four-path source snapshot pending this bounded follow-up.
daliu commentedon Aug 14, 2026
LOCAL SIX-PATH CANDIDATE READY at 2026-08-14T06:38:45Z; claim remains active and nothing is pushed.
a411cb4ebcfb4f1f05b3883721aa55f3d72bc701eac1fe8b86f0e871d2fc41e5338dceace38a04ec3265a55cb92df1ebb5401f2647ff81e4ca3af0b8+731/-6ea9c32ae30493a11e42206327055cbd0001c21b0e378ed09caa6e2a55bead576d50ff54aab64b1bdf73bea756946238df80453d11bfc7ba540cecf4e8557e5ef39166cdd26c2de8e1c0ef5b483058dab54da463c2ed2f2265e25b7df290c5e96fcccd40bfb210bc10e2a6ee31f03b2c6b5d021f5a441280aa7dc2424e25e7c36Trustworthy RED evidence:
ea14897d70e85f27f2a5c80d4422a4faad31ddaa4055fee99672d2252829f733; 1 failed / 362 skipped because persistent navigation retained focus5d5f37a666ee61f4ef9b3481f345ed9371002a9635a5aa2b84518fb059727102; focused cue test failed only because the scoped rule was absentFinal local evidence:
This is source/local evidence only. There is no remote branch, pull request, hosted CI, Deploy Preview, publication, runmprc.com change, Firebase/provider/account/data action, or live proof. The environment requires explicit user authorization before pushing source to the GitHub origin. Exact-head hosted checks and both required Deploy Preview widths remain mandatory after that authorization.
daliu commentedon Aug 14, 2026
COMPLETE / RELEASED by
/rootat 2026-08-14T07:08:04Z.PR #658 merged as exact main
95880748e15c03b0ee58da6e1ed11ac6c9526529; treeeac1fe8b86f0e871d2fc41e5338dceace38a04ecis byte-identical to reviewed candidate tree; parents are prior main3265a55cb92df1ebb5401f2647ff81e4ca3af0b8and reviewed heada411cb4ebcfb4f1f05b3883721aa55f3d72bc701. The first-parent diff remains exactly six paths,+731/-6, with SHA-256fcccd40bfb210bc10e2a6ee31f03b2c6b5d021f5a441280aa7dc2424e25e7c36.Exact-head CI run
31778085226and exact-main CI run31778506870each passed all five jobs. Netlify preview6a7ebc3388b62b0008ee950fwas ready at exact head, unpublished/noindex, with no Functions or edge functions; signed-out 1280×900 and 390×844 checks passed. The automatic main attempt6a7ebdd8f69c0b00080353a8endederrorwithpublished_at:nulland did not move production.Published production remains inert #623 deploy
6a7e072f8f346b0008510d29, control9d5cc8612b4321172370bd949d307e7e4ac0ec7d, sourcec2d87d1f69f15e128a0bc9b1b9f915b7c8417aec, and tree411aa6ec9a9459f5d923030533ffc7c007fe6908. Directory availability remains literalfalsewith blob295909b0df8dd7c54a0f164f99d4943656aece4b.No Firebase deployment, outside-provider configuration, account/sign-in action, production-data action, directory connection, or live WEB-UX-004 behavior occurred. The exact six claimed paths are released. The user's later explicit deployment authorization will be handled by a separate reviewed one-shot Netlify release; it must not publish the accumulated 84-path web delta from live #623 to current main.
daliu commentedon Aug 14, 2026
PRE-CLAIM coordination for #659 / WEB-002D from exact current main
95880748e15c03b0ee58da6e1ed11ac6c9526529and exact live sourcec2d87d1f69f15e128a0bc9b1b9f915b7c8417aec.The approved release will project only the already-reviewed #291 visible-focus source, #490 deterministic/ARIA phone-navigation source, and #657 SPA route-focus source into a frozen source ref based directly on the current live source. Exact source ownership is six paths:
src/App.jsx,src/App.test.jsx,src/components/Navbar.jsx,src/components/ScrollToTop.jsx,src/index.css, and the live-pathsrc/headerClearance.test.jsx. The ref will never merge tomain. No accumulated-main route/content, package, service, backend, provider, data, or directory code is included. No source edit has occurred; only isolated branches/worktrees exist. Send STOP for an exact behavioral/hunk dependency or collision.