@@ -12,10 +12,13 @@ import type { FocusAndScrollRef } from './router-reducer/router-reducer-types'
1212
1313import React , {
1414 Activity ,
15+ Fragment ,
1516 useContext ,
1617 use ,
1718 Suspense ,
1819 useDeferredValue ,
20+ useLayoutEffect ,
21+ type FragmentInstance ,
1922 type JSX ,
2023 type ActivityProps ,
2124} from 'react'
@@ -45,6 +48,8 @@ import { getParamValueFromCacheKey } from '../route-params'
4548import type { Params } from '../../server/request/params'
4649import { isDeferredRsc } from './router-reducer/ppr-navigations'
4750
51+ const enableNewScrollHandler = process . env . __NEXT_APP_NEW_SCROLL_HANDLER
52+
4853const __DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = (
4954 ReactDOM as any
5055) . __DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE
@@ -96,9 +101,23 @@ function shouldSkipElement(element: HTMLElement) {
96101/**
97102 * Check if the top corner of the HTMLElement is in the viewport.
98103 */
99- function topOfElementInViewport ( element : HTMLElement , viewportHeight : number ) {
100- const rect = element . getBoundingClientRect ( )
101- return rect . top >= 0 && rect . top <= viewportHeight
104+ function topOfElementInViewport (
105+ instance : HTMLElement | FragmentInstance ,
106+ viewportHeight : number
107+ ) : boolean {
108+ const rects = instance . getClientRects ( )
109+ if ( rects . length === 0 ) {
110+ // Just to be explicit.
111+ return false
112+ }
113+ let elementTop = Number . POSITIVE_INFINITY
114+ for ( let i = 0 ; i < rects . length ; i ++ ) {
115+ const rect = rects [ i ]
116+ if ( rect . top < elementTop ) {
117+ elementTop = rect . top
118+ }
119+ }
120+ return elementTop >= 0 && elementTop <= viewportHeight
102121}
103122
104123/**
@@ -125,7 +144,7 @@ interface ScrollAndFocusHandlerProps {
125144 children : React . ReactNode
126145 segmentPath : FlightSegmentPath
127146}
128- class InnerScrollAndFocusHandler extends React . Component < ScrollAndFocusHandlerProps > {
147+ class InnerScrollAndFocusHandlerOld extends React . Component < ScrollAndFocusHandlerProps > {
129148 handlePotentialScroll = ( ) => {
130149 // Handle scroll and focus, it's only applied once.
131150 const { focusAndScrollRef, segmentPath } = this . props
@@ -170,9 +189,9 @@ class InnerScrollAndFocusHandler extends React.Component<ScrollAndFocusHandlerPr
170189 while ( ! ( domNode instanceof HTMLElement ) || shouldSkipElement ( domNode ) ) {
171190 if ( process . env . NODE_ENV !== 'production' ) {
172191 if ( domNode . parentElement ?. localName === 'head' ) {
173- // TODO: We enter this state when metadata was rendered as part of the page or via Next.js.
192+ // We enter this state when metadata was rendered as part of the page or via Next.js.
174193 // This is always a bug in Next.js and caused by React hoisting metadata.
175- // We need to replace `findDOMNode` in favor of Fragment Refs (when available) so that we can skip over metadata.
194+ // Fixed with `experimental.appNewScrollHandler`
176195 }
177196 }
178197
@@ -246,6 +265,108 @@ class InnerScrollAndFocusHandler extends React.Component<ScrollAndFocusHandlerPr
246265 }
247266}
248267
268+ function InnerScrollAndFocusHandlerNew ( props : ScrollAndFocusHandlerProps ) {
269+ const childrenRef = React . useRef < FragmentInstance > ( null )
270+
271+ useLayoutEffect (
272+ ( ) => {
273+ const { focusAndScrollRef, segmentPath } = props
274+ // Handle scroll and focus, it's only applied once in the first useEffect that triggers that changed.
275+
276+ if ( focusAndScrollRef . apply ) {
277+ // segmentPaths is an array of segment paths that should be scrolled to
278+ // if the current segment path is not in the array, the scroll is not applied
279+ // unless the array is empty, in which case the scroll is always applied
280+ if (
281+ focusAndScrollRef . segmentPaths . length !== 0 &&
282+ ! focusAndScrollRef . segmentPaths . some ( ( scrollRefSegmentPath ) =>
283+ segmentPath . every ( ( segment , index ) =>
284+ matchSegment ( segment , scrollRefSegmentPath [ index ] )
285+ )
286+ )
287+ ) {
288+ return
289+ }
290+
291+ let instance : FragmentInstance | HTMLElement | null = null
292+ const hashFragment = focusAndScrollRef . hashFragment
293+
294+ if ( hashFragment ) {
295+ instance = getHashFragmentDomNode ( hashFragment )
296+ }
297+
298+ if ( ! instance ) {
299+ instance = childrenRef . current
300+ }
301+
302+ // If there is no DOM node this layout-router level is skipped. It'll be handled higher-up in the tree.
303+ if ( instance === null ) {
304+ return
305+ }
306+
307+ // State is mutated to ensure that the focus and scroll is applied only once.
308+ focusAndScrollRef . apply = false
309+ focusAndScrollRef . hashFragment = null
310+ focusAndScrollRef . segmentPaths = [ ]
311+
312+ disableSmoothScrollDuringRouteTransition (
313+ ( ) => {
314+ // In case of hash scroll, we only need to scroll the element into view
315+ if ( hashFragment ) {
316+ instance . scrollIntoView ( )
317+
318+ return
319+ }
320+ // Store the current viewport height because reading `clientHeight` causes a reflow,
321+ // and it won't change during this function.
322+ const htmlElement = document . documentElement
323+ const viewportHeight = htmlElement . clientHeight
324+
325+ // If the element's top edge is already in the viewport, exit early.
326+ if ( topOfElementInViewport ( instance , viewportHeight ) ) {
327+ return
328+ }
329+
330+ // Otherwise, try scrolling go the top of the document to be backward compatible with pages
331+ // scrollIntoView() called on `<html/>` element scrolls horizontally on chrome and firefox (that shouldn't happen)
332+ // We could use it to scroll horizontally following RTL but that also seems to be broken - it will always scroll left
333+ // scrollLeft = 0 also seems to ignore RTL and manually checking for RTL is too much hassle so we will scroll just vertically
334+ htmlElement . scrollTop = 0
335+
336+ // Scroll to domNode if domNode is not in viewport when scrolled to top of document
337+ if ( ! topOfElementInViewport ( instance , viewportHeight ) ) {
338+ // Scroll into view doesn't scroll horizontally by default when not needed
339+ instance . scrollIntoView ( )
340+ }
341+ } ,
342+ {
343+ // We will force layout by querying domNode position
344+ dontForceLayout : true ,
345+ onlyHashChange : focusAndScrollRef . onlyHashChange ,
346+ }
347+ )
348+
349+ // Mutate after scrolling so that it can be read by `disableSmoothScrollDuringRouteTransition`
350+ focusAndScrollRef . onlyHashChange = false
351+
352+ // Set focus on the element but don't scroll since we already did that.
353+ // The focus might have targetted a deep element outside of the instances
354+ // top edge.
355+ instance . focus ( { preventScroll : true } )
356+ }
357+ } ,
358+ // Used to run on every commit. We may be able to be smarter about this
359+ // but be prepared for lots of manual testing.
360+ undefined
361+ )
362+
363+ return < Fragment ref = { childrenRef } > { props . children } </ Fragment >
364+ }
365+
366+ const InnerScrollAndFocusHandler = enableNewScrollHandler
367+ ? InnerScrollAndFocusHandlerNew
368+ : InnerScrollAndFocusHandlerOld
369+
249370function ScrollAndFocusHandler ( {
250371 segmentPath,
251372 children,
0 commit comments