"use client"; import React, { useContext, use, startTransition, Suspense } from "react"; import ReactDOM from "react-dom"; import { CacheStates, LayoutRouterContext, GlobalLayoutRouterContext, TemplateContext } from "../../shared/lib/app-router-context.shared-runtime"; import { fetchServerResponse } from "./router-reducer/fetch-server-response"; import { createInfinitePromise } from "./infinite-promise"; import { ErrorBoundary } from "./error-boundary"; import { matchSegment } from "./match-segments"; import { handleSmoothScroll } from "../../shared/lib/router/utils/handle-smooth-scroll"; import { RedirectBoundary } from "./redirect-boundary"; import { NotFoundBoundary } from "./not-found-boundary"; import { getSegmentValue } from "./router-reducer/reducers/get-segment-value"; import { createRouterCacheKey } from "./router-reducer/create-router-cache-key"; import { createRecordFromThenable } from "./router-reducer/create-record-from-thenable"; /** * Add refetch marker to router state at the point of the current layout segment. * This ensures the response returned is not further down than the current layout segment. */ function walkAddRefetch(segmentPathToWalk, treeToRecreate) { if (segmentPathToWalk) { const [segment, parallelRouteKey] = segmentPathToWalk; const isLast = segmentPathToWalk.length === 2; if (matchSegment(treeToRecreate[0], segment)) { if (treeToRecreate[1].hasOwnProperty(parallelRouteKey)) { if (isLast) { const subTree = walkAddRefetch(undefined, treeToRecreate[1][parallelRouteKey]); return [ treeToRecreate[0], { ...treeToRecreate[1], [parallelRouteKey]: [ subTree[0], subTree[1], subTree[2], "refetch" ] } ]; } return [ treeToRecreate[0], { ...treeToRecreate[1], [parallelRouteKey]: walkAddRefetch(segmentPathToWalk.slice(2), treeToRecreate[1][parallelRouteKey]) } ]; } } } 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) { // Tree-shake for server bundle if (typeof window === "undefined") return null; // Only apply strict mode warning when not in production if (process.env.NODE_ENV !== "production") { const originalConsoleError = console.error; try { console.error = function() { for(var _len = arguments.length, messages = new Array(_len), _key = 0; _key < _len; _key++){ messages[_key] = arguments[_key]; } // Ignore strict mode warning for the findDomNode call below if (!messages[0].includes("Warning: %s is deprecated in StrictMode.")) { originalConsoleError(...messages); } }; return ReactDOM.findDOMNode(instance); } finally{ console.error = originalConsoleError; } } return ReactDOM.findDOMNode(instance); } const rectProperties = [ "bottom", "height", "left", "right", "top", "width", "x", "y" ]; /** * Check if a HTMLElement is hidden or fixed/sticky position */ function shouldSkipElement(element) { // we ignore fixed or sticky positioned elements since they'll likely pass the "in-viewport" check // and will result in a situation we bail on scroll because of something like a fixed nav, // even though the actual page content is offscreen if ([ "sticky", "fixed" ].includes(getComputedStyle(element).position)) { if (process.env.NODE_ENV === "development") { console.warn("Skipping auto-scroll behavior due to `position: sticky` or `position: fixed` on element:", element); } return true; } // Uses `getBoundingClientRect` to check if the element is hidden instead of `offsetParent` // because `offsetParent` doesn't consider document/body const rect = element.getBoundingClientRect(); return rectProperties.every((item)=>rect[item] === 0); } /** * Check if the top corner of the HTMLElement is in the viewport. */ function topOfElementInViewport(element, viewportHeight) { const rect = element.getBoundingClientRect(); return rect.top >= 0 && rect.top <= viewportHeight; } /** * Find the DOM node for a hash fragment. * If `top` the page has to scroll to the top of the page. This mirrors the browser's behavior. * If the hash fragment is an id, the page has to scroll to the element with that id. * If the hash fragment is a name, the page has to scroll to the first element with that name. */ function getHashFragmentDomNode(hashFragment) { // If the hash fragment is `top` the page has to scroll to the top of the page. if (hashFragment === "top") { return document.body; } var _document_getElementById; // If the hash fragment is an id, the page has to scroll to the element with that id. return (_document_getElementById = document.getElementById(hashFragment)) != null ? _document_getElementById : // If the hash fragment is a name, the page has to scroll to the first element with that name. document.getElementsByName(hashFragment)[0]; } class InnerScrollAndFocusHandler extends React.Component { componentDidMount() { this.handlePotentialScroll(); } componentDidUpdate() { // Because this property is overwritten in handlePotentialScroll it's fine to always run it when true as it'll be set to false for subsequent renders. if (this.props.focusAndScrollRef.apply) { this.handlePotentialScroll(); } } render() { return this.props.children; } constructor(...args){ super(...args); this.handlePotentialScroll = ()=>{ // Handle scroll and focus, it's only applied once in the first useEffect that triggers that changed. const { focusAndScrollRef, segmentPath } = this.props; if (focusAndScrollRef.apply) { // segmentPaths is an array of segment paths that should be scrolled to // if the current segment path is not in the array, the scroll is not applied // unless the array is empty, in which case the scroll is always applied if (focusAndScrollRef.segmentPaths.length !== 0 && !focusAndScrollRef.segmentPaths.some((scrollRefSegmentPath)=>segmentPath.every((segment, index)=>matchSegment(segment, scrollRefSegmentPath[index])))) { return; } let domNode = null; const hashFragment = focusAndScrollRef.hashFragment; if (hashFragment) { domNode = getHashFragmentDomNode(hashFragment); } // `findDOMNode` is tricky because it returns just the first child if the component is a fragment. // This already caused a bug where the first child was a in head. if (!domNode) { domNode = findDOMNode(this); } // If there is no DOM node this layout-router level is skipped. It'll be handled higher-up in the tree. if (!(domNode instanceof Element)) { return; } // Verify if the element is a HTMLElement and if we want to consider it for scroll behavior. // If the element is skipped, try to select the next sibling and try again. while(!(domNode instanceof HTMLElement) || shouldSkipElement(domNode)){ // No siblings found that match the criteria are found, so handle scroll higher up in the tree instead. if (domNode.nextElementSibling === null) { return; } domNode = domNode.nextElementSibling; } // State is mutated to ensure that the focus and scroll is applied only once. focusAndScrollRef.apply = false; focusAndScrollRef.hashFragment = null; focusAndScrollRef.segmentPaths = []; handleSmoothScroll(()=>{ // In case of hash scroll, we only need to scroll the element into view if (hashFragment) { domNode.scrollIntoView(); return; } // Store the current viewport height because reading `clientHeight` causes a reflow, // and it won't change during this function. const htmlElement = document.documentElement; const viewportHeight = htmlElement.clientHeight; // If the element's top edge is already in the viewport, exit early. if (topOfElementInViewport(domNode, viewportHeight)) { return; } // Otherwise, try scrolling go the top of the document to be backward compatible with pages // scrollIntoView() called on `` element scrolls horizontally on chrome and firefox (that shouldn't happen) // We could use it to scroll horizontally following RTL but that also seems to be broken - it will always scroll left // scrollLeft = 0 also seems to ignore RTL and manually checking for RTL is too much hassle so we will scroll just vertically htmlElement.scrollTop = 0; // Scroll to domNode if domNode is not in viewport when scrolled to top of document if (!topOfElementInViewport(domNode, viewportHeight)) { domNode.scrollIntoView(); } }, { // We will force layout by querying domNode position dontForceLayout: true, onlyHashChange: focusAndScrollRef.onlyHashChange }); // Mutate after scrolling so that it can be read by `handleSmoothScroll` focusAndScrollRef.onlyHashChange = false; // Set focus on the element domNode.focus(); } }; } } function ScrollAndFocusHandler(param) { let { segmentPath, children } = param; const context = useContext(GlobalLayoutRouterContext); if (!context) { throw new Error("invariant global layout router not mounted"); } return /*#__PURE__*/ React.createElement(InnerScrollAndFocusHandler, { segmentPath: segmentPath, focusAndScrollRef: context.focusAndScrollRef }, children); } /** * InnerLayoutRouter handles rendering the provided segment based on the cache. */ function InnerLayoutRouter(param) { let { parallelRouterKey, url, childNodes, childProp, segmentPath, tree, // TODO-APP: implement `` when available. // isActive, cacheKey } = param; const context = useContext(GlobalLayoutRouterContext); if (!context) { throw new Error("invariant global layout router not mounted"); } const { buildId, changeByServerResponse, tree: fullTree } = context; // Read segment path from the parallel router cache node. let childNode = childNodes.get(cacheKey); // If childProp is available this means it's the Flight / SSR case. if (childProp && // TODO-APP: verify if this can be null based on user code childProp.current !== null) { if (!childNode) { // Add the segment's subTreeData to the cache. // This writes to the cache when there is no item in the cache yet. It never *overwrites* existing cache items which is why it's safe in concurrent mode. childNode = { status: CacheStates.READY, data: null, subTreeData: childProp.current, parallelRoutes: new Map() }; childNodes.set(cacheKey, childNode); } else { if (childNode.status === CacheStates.LAZY_INITIALIZED) { // @ts-expect-error we're changing it's type! childNode.status = CacheStates.READY; // @ts-expect-error childNode.subTreeData = childProp.current; } } } // When childNode is not available during rendering client-side we need to fetch it from the server. if (!childNode || childNode.status === CacheStates.LAZY_INITIALIZED) { /** * Router state with refetch marker added */ // TODO-APP: remove '' const refetchTree = walkAddRefetch([ "", ...segmentPath ], fullTree); childNode = { status: CacheStates.DATA_FETCH, data: createRecordFromThenable(fetchServerResponse(new URL(url, location.origin), refetchTree, context.nextUrl, buildId)), subTreeData: null, head: childNode && childNode.status === CacheStates.LAZY_INITIALIZED ? childNode.head : undefined, parallelRoutes: childNode && childNode.status === CacheStates.LAZY_INITIALIZED ? childNode.parallelRoutes : new Map() }; /** * Flight data fetch kicked off during render and put into the cache. */ childNodes.set(cacheKey, childNode); } // This case should never happen so it throws an error. It indicates there's a bug in the Next.js. if (!childNode) { throw new Error("Child node should always exist"); } // This case should never happen so it throws an error. It indicates there's a bug in the Next.js. if (childNode.subTreeData && childNode.data) { throw new Error("Child node should not have both subTreeData and data"); } // If cache node has a data request we have to unwrap response by `use` and update the cache. if (childNode.data) { /** * Flight response data */ // When the data has not resolved yet `use` will suspend here. const [flightData, overrideCanonicalUrl] = use(childNode.data); // segmentPath from the server does not match the layout's segmentPath childNode.data = null; // setTimeout is used to start a new transition during render, this is an intentional hack around React. setTimeout(()=>{ startTransition(()=>{ changeByServerResponse(fullTree, flightData, overrideCanonicalUrl); }); }); // Suspend infinitely as `changeByServerResponse` will cause a different part of the tree to be rendered. use(createInfinitePromise()); } // If cache node has no subTreeData and no data request we have to infinitely suspend as the data will likely flow in from another place. // TODO-APP: double check users can't return null in a component that will kick in here. if (!childNode.subTreeData) { use(createInfinitePromise()); } const subtree = // The layout router context narrows down tree and childNodes at each level. /*#__PURE__*/ React.createElement(LayoutRouterContext.Provider, { value: { tree: tree[1][parallelRouterKey], childNodes: childNode.parallelRoutes, // TODO-APP: overriding of url for parallel routes url: url } }, childNode.subTreeData); // Ensure root layout is not wrapped in a div as the root layout renders `` return subtree; } /** * Renders suspense boundary with the provided "loading" property as the fallback. * If no loading property is provided it renders the children without a suspense boundary. */ function LoadingBoundary(param) { let { children, loading, loadingStyles, hasLoading } = param; if (hasLoading) { return /*#__PURE__*/ React.createElement(Suspense, { fallback: /*#__PURE__*/ React.createElement(React.Fragment, null, loadingStyles, loading) }, children); } return /*#__PURE__*/ React.createElement(React.Fragment, null, children); } /** * OuterLayoutRouter handles the current segment as well as rendering of other segments. * It can be rendered next to each other with a different `parallelRouterKey`, allowing for Parallel routes. */ export default function OuterLayoutRouter(param) { let { parallelRouterKey, segmentPath, childProp, error, errorStyles, templateStyles, loading, loadingStyles, hasLoading, template, notFound, notFoundStyles, styles } = param; const context = useContext(LayoutRouterContext); if (!context) { throw new Error("invariant expected layout router to be mounted"); } const { childNodes, tree, url } = context; // Get the current parallelRouter cache node let childNodesForParallelRouter = childNodes.get(parallelRouterKey); // If the parallel router cache node does not exist yet, create it. // This writes to the cache when there is no item in the cache yet. It never *overwrites* existing cache items which is why it's safe in concurrent mode. if (!childNodesForParallelRouter) { childNodesForParallelRouter = new Map(); childNodes.set(parallelRouterKey, childNodesForParallelRouter); } // Get the active segment in the tree // The reason arrays are used in the data format is that these are transferred from the server to the browser so it's optimized to save bytes. const treeSegment = tree[1][parallelRouterKey][0]; const childPropSegment = childProp.segment; // If segment is an array it's a dynamic route and we want to read the dynamic route value as the segment to get from the cache. const currentChildSegmentValue = getSegmentValue(treeSegment); /** * Decides which segments to keep rendering, all segments that are not active will be wrapped in ``. */ // TODO-APP: Add handling of `` when it's available. const preservedSegments = [ treeSegment ]; return /*#__PURE__*/ React.createElement(React.Fragment, null, styles, preservedSegments.map((preservedSegment)=>{ const isChildPropSegment = matchSegment(preservedSegment, childPropSegment); const preservedSegmentValue = getSegmentValue(preservedSegment); const cacheKey = createRouterCacheKey(preservedSegment); return(/* - Error boundary - Only renders error boundary if error component is provided. - Rendered for each segment to ensure they have their own error state. - Loading boundary - Only renders suspense boundary if loading components is provided. - Rendered for each segment to ensure they have their own loading state. - Passed to the router during rendering to ensure it can be immediately rendered when suspending on a Flight fetch. */ /*#__PURE__*/ React.createElement(TemplateContext.Provider, { key: createRouterCacheKey(preservedSegment, true), value: /*#__PURE__*/ React.createElement(ScrollAndFocusHandler, { segmentPath: segmentPath }, /*#__PURE__*/ React.createElement(ErrorBoundary, { errorComponent: error, errorStyles: errorStyles }, /*#__PURE__*/ React.createElement(LoadingBoundary, { hasLoading: hasLoading, loading: loading, loadingStyles: loadingStyles }, /*#__PURE__*/ React.createElement(NotFoundBoundary, { notFound: notFound, notFoundStyles: notFoundStyles }, /*#__PURE__*/ React.createElement(RedirectBoundary, null, /*#__PURE__*/ React.createElement(InnerLayoutRouter, { parallelRouterKey: parallelRouterKey, url: url, tree: tree, childNodes: childNodesForParallelRouter, childProp: isChildPropSegment ? childProp : null, segmentPath: segmentPath, cacheKey: cacheKey, isActive: currentChildSegmentValue === preservedSegmentValue })))))) }, templateStyles, template)); })); } //# sourceMappingURL=layout-router.js.map