From 8664ffe26dafd7529d5fc674294c0f53709d51c3 Mon Sep 17 00:00:00 2001 From: Zack Tanner Date: Wed, 19 Jul 2023 23:13:04 -0700 Subject: [PATCH] fix: allow smooth scrolling if only hash changes (pages & app) (#52915) We were preventing smooth scrolling to avoid jarring UX when `scroll-behavior: smooth` was set and the user navigates to another route ([PR](https://github.com/vercel/next.js/pull/40642) and [related comment](https://github.com/vercel/next.js/issues/51721#issuecomment-1623416600)). This updates both pages & app router to restore smooth scroll functionality if the only the route hash changes. Fixes #51721 Closes NEXT-1406 --- .../src/client/components/layout-router.tsx | 5 +- packages/next/src/shared/lib/router/router.ts | 48 +++++++++++-------- .../lib/router/utils/handle-smooth-scroll.ts | 8 +++- 3 files changed, 38 insertions(+), 23 deletions(-) diff --git a/packages/next/src/client/components/layout-router.tsx b/packages/next/src/client/components/layout-router.tsx index f45ff5af30589..d3c9a0733383d 100644 --- a/packages/next/src/client/components/layout-router.tsx +++ b/packages/next/src/client/components/layout-router.tsx @@ -158,7 +158,7 @@ interface ScrollAndFocusHandlerProps { segmentPath: FlightSegmentPath } class InnerScrollAndFocusHandler extends React.Component { - handlePotentialScroll = () => { + handlePotentialScroll = (isUpdate?: boolean) => { // Handle scroll and focus, it's only applied once in the first useEffect that triggers that changed. const { focusAndScrollRef, segmentPath } = this.props @@ -247,6 +247,7 @@ class InnerScrollAndFocusHandler extends React.Component window.scrollTo(0, 0)) - return - } - // Decode hash to make non-latin anchor works. - const rawHash = decodeURIComponent(hash) - // First we check if the element by id is found - const idEl = document.getElementById(rawHash) - if (idEl) { - handleSmoothScroll(() => idEl.scrollIntoView()) - return - } - // If there's no element with the id, we check the `name` property - // To mirror browsers - const nameEl = document.getElementsByName(rawHash)[0] - if (nameEl) { - handleSmoothScroll(() => nameEl.scrollIntoView()) - } + handleSmoothScroll( + () => { + // Scroll to top if the hash is just `#` with no value or `#top` + // To mirror browsers + if (hash === '' || hash === 'top') { + window.scrollTo(0, 0) + return + } + + // Decode hash to make non-latin anchor works. + const rawHash = decodeURIComponent(hash) + // First we check if the element by id is found + const idEl = document.getElementById(rawHash) + if (idEl) { + idEl.scrollIntoView() + return + } + // If there's no element with the id, we check the `name` property + // To mirror browsers + const nameEl = document.getElementsByName(rawHash)[0] + if (nameEl) { + nameEl.scrollIntoView() + } + }, + { + onlyHashChange: this.onlyAHashChange(as), + } + ) } urlIsNew(asPath: string): boolean { diff --git a/packages/next/src/shared/lib/router/utils/handle-smooth-scroll.ts b/packages/next/src/shared/lib/router/utils/handle-smooth-scroll.ts index fd7c6afcdaa0a..7c19e3a340034 100644 --- a/packages/next/src/shared/lib/router/utils/handle-smooth-scroll.ts +++ b/packages/next/src/shared/lib/router/utils/handle-smooth-scroll.ts @@ -4,8 +4,14 @@ */ export function handleSmoothScroll( fn: () => void, - options: { dontForceLayout?: boolean } = {} + options: { dontForceLayout?: boolean; onlyHashChange?: boolean } = {} ) { + // if only the hash is changed, we don't need to disable smooth scrolling + // we only care to prevent smooth scrolling when navigating to a new page to avoid jarring UX + if (options.onlyHashChange) { + fn() + return + } const htmlElement = document.documentElement const existing = htmlElement.style.scrollBehavior htmlElement.style.scrollBehavior = 'auto'