fix(router): decode hash scroll targets#1255
Conversation
Hash-only client navigation looked up raw URI fragments directly. That meant ids such as "hello world" and browser-compatible named anchors were skipped when links used encoded fragments or legacy anchors. The router shims now share the decoded id, named-anchor fallback, and top-fragment behavior already used by the app browser popstate path. Regression coverage exercises hash-only Pages Router pushes for decoded ids and name fallback.
commit: |
Match Next.js Pages Router behavior by gating hash target scrolling behind the scroll option for both hash-only and hash-bearing navigations. Add regression coverage for hash-only Router.push with scroll disabled.
|
/bigbonk review |
There was a problem hiding this comment.
Solid fix. The shared helper matches Next.js's scrollToHash behavior precisely (decode → #top/empty → getElementById → getElementsByName), the scroll: false guard that was missing on hash-only paths is now correctly applied, and the test coverage is thorough.
I verified:
scrollToHashTargetinhash-scroll.tsmatches the Next.js Pages RouterscrollToHash: decode, handle""and"top", checkid, fall back toname.- Next.js guards hash scrolling with
if (scroll)in the hash-only branch (wherescrolldefaults totruefromconst { scroll = true } = options). The old vinext code unconditionally calledscrollToHash— this PR correctly adds thescroll !== falseguard to all hash-only paths. - The
app-browser-entry.tsrefactor preserves therequestAnimationFramescheduling viascrollToHashTargetOnNextFrame. - The three new tests cover the key scenarios: URI-decoded id,
namefallback, andscroll: falseopt-out.
One minor observation inline, but not blocking.
| return; | ||
| } | ||
|
|
||
| document.getElementsByName(fragment)[0]?.scrollIntoView({ behavior: "auto" }); |
There was a problem hiding this comment.
Nit: Next.js calls scrollIntoView() (no args) rather than scrollIntoView({ behavior: "auto" }). They're functionally equivalent per spec, so this is fine — just noting the difference in case someone ever diffs against Next.js source and wonders.
|
Review submitted with approval. Here's a summary: Approved — this is a clean, well-scoped fix that:
|
Overview
next/navigation,next/router, and app browser popstate restoration.packages/vinext/src/shims/hash-scroll.ts,packages/vinext/src/shims/navigation.ts,packages/vinext/src/shims/router.ts,packages/vinext/src/server/app-browser-entry.ts,tests/shims.test.tsid="hello world"via#hello%20worldand can fall back to legacynameanchors.Why
Hash navigation is a browser-visible compatibility surface. Next.js resolves hash fragments by decoding the URI fragment, checking
id, and then falling back toname; vinext only checked raw ids in the router shims, so encoded ids and named anchors were skipped despite the app browser popstate path already having the correct behavior.decodeURIComponentpass before DOM lookup.idfirst, thenname.document.getElementsByName()fallback to router-driven hash navigation.What changed
Router.push("#hello%20world")withid="hello world"hello%20world, did not scroll.hello world, scrolls the id target.Router.push("#legacy-anchor")with<a name="legacy-anchor">nameafter id miss and scrolls the first match.#topand empty fragmentMaintainer review path
packages/vinext/src/shims/hash-scroll.tsfor the exact target resolution order and malformed-fragment recovery.packages/vinext/src/shims/router.tsandpackages/vinext/src/shims/navigation.tsto confirm all old raw-id call sites use the shared helper.packages/vinext/src/server/app-browser-entry.tsto confirm popstate restoration keeps itsrequestAnimationFramescheduling while delegating the target decision.tests/shims.test.tsfor the focused red/green coverage through the public Pages Router singleton.Validation
vp test run tests/shims.test.ts -t "hash-only pushes"failed becausegetElementByIdreceivedhello%20worldandgetElementsByNamewas never called.vp test run tests/shims.test.ts -t "hash-only pushes"passed after the fix.vp test run tests/shims.test.ts tests/app-browser-entry.test.tspassed, 1003 tests.vp checkpassed formatting, lint, and type checks.knip.Risk / compatibility
References
scrollToHash#top, checksid, then falls back toname.top, thenid, thennamein the scroll/focus handler.namefallback is expected behavior.scroll={false}behavior.