diff --git a/packages/next/client/components/layout-router.tsx b/packages/next/client/components/layout-router.tsx index 35898fd9dfda6de..050ed4a0271af80 100644 --- a/packages/next/client/components/layout-router.tsx +++ b/packages/next/client/components/layout-router.tsx @@ -1,10 +1,4 @@ 'use client' - -import React, { useContext, useEffect, useRef, use } from 'react' -import type { - ChildProp, - //Segment -} from '../../server/app-render' import type { AppRouterInstance, ChildSegmentMap, @@ -12,9 +6,13 @@ import type { import type { FlightRouterState, FlightSegmentPath, - // FlightDataPath, } from '../../server/app-render' import type { ErrorComponent } from './error-boundary' +import type { FocusAndScrollRef } from './reducer' + +import React, { useContext, useEffect, use } from 'react' +import { findDOMNode as ReactDOMfindDOMNode } from 'react-dom' +import type { ChildProp } from '../../server/app-render' import { CacheStates, LayoutRouterContext, @@ -77,6 +75,31 @@ function walkAddRefetch( return treeToRecreate } +// TODO-APP: Replace with new React API for finding dom nodes without a `ref` when available +/** + * Wraps ReactDOM.findDOMNode with additional logic to hide React Strict Mode warning + */ +function findDOMNode( + instance: Parameters[0] +): ReturnType { + // Only apply strict mode warning when not in production + if (process.env.NODE_ENV !== 'production') { + const originalConsoleError = console.error + try { + console.error = (...messages) => { + // Ignore strict mode warning for the findDomNode call below + if (!messages[0].includes('Warning: %s is deprecated in StrictMode.')) { + originalConsoleError(...messages) + } + } + return ReactDOMfindDOMNode(instance) + } finally { + console.error = originalConsoleError! + } + } + return ReactDOMfindDOMNode(instance) +} + /** * Check if the top of the HTMLElement is in the viewport. */ @@ -85,6 +108,36 @@ function topOfElementInViewport(element: HTMLElement) { return rect.top >= 0 } +class ScrollAndFocusHandler extends React.Component<{ + focusAndScrollRef: FocusAndScrollRef + children: React.ReactNode +}> { + componentDidMount() { + // Handle scroll and focus, it's only applied once in the first useEffect that triggers that changed. + const { focusAndScrollRef } = this.props + const domNode = findDOMNode(this) + + if (focusAndScrollRef.apply && domNode instanceof HTMLElement) { + // State is mutated to ensure that the focus and scroll is applied only once. + focusAndScrollRef.apply = false + // Set focus on the element + domNode.focus() + // Only scroll into viewport when the layout is not visible currently. + if (!topOfElementInViewport(domNode)) { + const htmlElement = document.documentElement + const existing = htmlElement.style.scrollBehavior + htmlElement.style.scrollBehavior = 'auto' + domNode.scrollIntoView() + htmlElement.style.scrollBehavior = existing + } + } + } + + render() { + return this.props.children + } +} + /** * InnerLayoutRouter handles rendering the provided segment based on the cache. */ @@ -117,26 +170,6 @@ export function InnerLayoutRouter({ const { changeByServerResponse, tree: fullTree, focusAndScrollRef } = context - const focusAndScrollElementRef = useRef(null) - - useEffect(() => { - // Handle scroll and focus, it's only applied once in the first useEffect that triggers that changed. - if (focusAndScrollRef.apply && focusAndScrollElementRef.current) { - // State is mutated to ensure that the focus and scroll is applied only once. - focusAndScrollRef.apply = false - // Set focus on the element - 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 - } - } - }, [focusAndScrollRef]) - // Read segment path from the parallel router cache node. let childNode = childNodes.get(path) @@ -257,9 +290,9 @@ export function InnerLayoutRouter({ // Ensure root layout is not wrapped in a div as the root layout renders `` return rootLayoutIncluded ? ( -
+ {subtree} -
+ ) : ( subtree )