securityos/node_modules/next/dist/esm/client/components/layout-router.js

407 lines
20 KiB
JavaScript

"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 <link/> 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 `<html/>` 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 `<Offscreen>` 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 `<html>`
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 <Offscreen> 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 `<Offscreen>`.
*/ // TODO-APP: Add handling of `<Offscreen>` 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