diff --git a/packages/next/client/components/layout-router.tsx b/packages/next/client/components/layout-router.tsx index 36bcaaf325927..9aff85ccf8233 100644 --- a/packages/next/client/components/layout-router.tsx +++ b/packages/next/client/components/layout-router.tsx @@ -117,6 +117,9 @@ export function InnerLayoutRouter({ const { changeByServerResponse, tree: fullTree, focusAndScrollRef } = context + const router = useRouter() + const refScroll = useRef(null) + const focusAndScrollElementRef = useRef(null) useEffect(() => { @@ -128,13 +131,39 @@ export function InnerLayoutRouter({ focusAndScrollElementRef.current.focus() // Only scroll into viewport when the layout is not visible currently. if (!topOfElementInViewport(focusAndScrollElementRef.current)) { - const htmlElement = document.documentElement - const existing = htmlElement.style.scrollBehavior - htmlElement.style.scrollBehavior = 'auto' focusAndScrollElementRef.current.scrollIntoView() - htmlElement.style.scrollBehavior = existing } } + + /** + * 1. The route change starts + * Switch the scroll behavior to 'auto' + * Scroll immediately to the top and hold the value until the route change finishes. + */ + const handleRouteChangeStart = () => { + refScroll.current = document.documentElement.style.scrollBehavior; + document.documentElement.style.scrollBehavior = 'auto'; + } + + /** + * 2. The route change finishes + * Switch back to the default value specified in global css for the html element. + * For smooth-scrolling smooth, the behavior is 'smooth'. Hash changes are no route + * changes; the result is smooth scrolling on hash changes. + */ + const handleRouteChangeComplete = () => { + document.documentElement.style.scrollBehavior = refScroll.current; + } + + // Subscribe to routeChangeStart, routeChangeComplete events + router.events.on('routeChangeStart', handleRouteChangeStart); + router.events.on('routeChangeComplete', handleRouteChangeComplete); + + return () => { + router.events.off('routeChangeStart', handleRouteChangeStart); + router.events.off('routeChangeComplete', handleRouteChangeComplete); + }; + }, [focusAndScrollRef]) // Read segment path from the parallel router cache node. diff --git a/packages/next/client/index.tsx b/packages/next/client/index.tsx index a5baeae1648c6..7db5f37db2bf5 100644 --- a/packages/next/client/index.tsx +++ b/packages/next/client/index.tsx @@ -523,9 +523,11 @@ function renderReactElement( function Root({ callbacks, + router, children, }: React.PropsWithChildren<{ - callbacks: Array<() => void> + callbacks: Array<() => void>, + router: Router }>): React.ReactElement { // We use `useLayoutEffect` to guarantee the callbacks are executed // as soon as React flushes the update @@ -533,10 +535,43 @@ function Root({ () => callbacks.forEach((callback) => callback()), [callbacks] ) + + const refScroll = React.useRef(null) + // We should ask to measure the Web Vitals after rendering completes so we // don't cause any hydration delay: React.useEffect(() => { measureWebVitals(onPerfEntry) + + /** + * 1. The route change starts + * Switch the scroll behavior to 'auto' + * Scroll immediately to the top and hold the value until the route change finishes. + */ + const handleRouteChangeStart = () => { + refScroll.current = document.documentElement.style.scrollBehavior; + document.documentElement.style.scrollBehavior = 'auto'; + } + + /** + * 2. The route change finishes + * Switch back to the default value specified in global css for the html element. + * For smooth-scrolling smooth, the behavior is 'smooth'. Hash changes are no route + * changes; the result is smooth scrolling on hash changes. + */ + const handleRouteChangeComplete = () => { + document.documentElement.style.scrollBehavior = refScroll.current; + } + + // Subscribe to routeChangeStart, routeChangeComplete events + router.events.on('routeChangeStart', handleRouteChangeStart); + router.events.on('routeChangeComplete', handleRouteChangeComplete); + + return () => { + router.events.off('routeChangeStart', handleRouteChangeStart); + router.events.off('routeChangeComplete', handleRouteChangeComplete); + }; + }, []) if (process.env.__NEXT_TEST_MODE) { @@ -692,13 +727,8 @@ function doRender(input: RenderRouteInfo): Promise { el.parentNode!.removeChild(el) }) } - if (input.scroll) { - const htmlElement = document.documentElement - const existing = htmlElement.style.scrollBehavior - htmlElement.style.scrollBehavior = 'auto' - window.scrollTo(input.scroll.x, input.scroll.y) - htmlElement.style.scrollBehavior = existing + window.scrollTo(input.scroll.x, input.scroll.y); } } @@ -722,7 +752,7 @@ function doRender(input: RenderRouteInfo): Promise { // We catch runtime errors using componentDidCatch which will trigger renderError renderReactElement(appElement!, (callback) => ( - + {process.env.__NEXT_STRICT_MODE ? ( {elem} ) : ( diff --git a/packages/next/shared/lib/router/router.ts b/packages/next/shared/lib/router/router.ts index 0815955205b79..5fc3bc4efea50 100644 --- a/packages/next/shared/lib/router/router.ts +++ b/packages/next/shared/lib/router/router.ts @@ -655,14 +655,6 @@ interface FetchNextDataParams { unstable_skipClientCache?: boolean } -function handleSmoothScroll(fn: () => void) { - const htmlElement = document.documentElement - const existing = htmlElement.style.scrollBehavior - htmlElement.style.scrollBehavior = 'auto' - fn() - htmlElement.style.scrollBehavior = existing -} - function tryToParseAsJSON(text: string) { try { return JSON.parse(text) @@ -2228,7 +2220,7 @@ export default class Router implements BaseRouter { // Scroll to top if the hash is just `#` with no value or `#top` // To mirror browsers if (hash === '' || hash === 'top') { - handleSmoothScroll(() => window.scrollTo(0, 0)) + window.scrollTo(0, 0) return } @@ -2237,14 +2229,14 @@ export default class Router implements BaseRouter { // First we check if the element by id is found const idEl = document.getElementById(rawHash) if (idEl) { - handleSmoothScroll(() => idEl.scrollIntoView()) + 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()) + nameEl.scrollIntoView() } }