UNPKG

next

Version:

The React Framework

355 lines (354 loc) • 16.6 kB
'use client'; import { jsx as _jsx } from "react/jsx-runtime"; import React, { createContext, useContext, useOptimistic, useRef } from 'react'; import { formatUrl } from '../../shared/lib/router/utils/format-url'; import { AppRouterContext } from '../../shared/lib/app-router-context.shared-runtime'; import { PrefetchKind } from '../components/router-reducer/router-reducer-types'; import { useMergedRef } from '../use-merged-ref'; import { isAbsoluteUrl } from '../../shared/lib/utils'; import { addBasePath } from '../add-base-path'; import { warnOnce } from '../../shared/lib/utils/warn-once'; import { IDLE_LINK_STATUS, mountLinkInstance, onNavigationIntent, unmountLinkForCurrentNavigation, unmountPrefetchableInstance } from '../components/links'; import { isLocalURL } from '../../shared/lib/router/utils/is-local-url'; import { dispatchNavigateAction } from '../components/app-router-instance'; import { errorOnce } from '../../shared/lib/utils/error-once'; function isModifiedEvent(event) { const eventTarget = event.currentTarget; const target = eventTarget.getAttribute('target'); return target && target !== '_self' || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || // triggers resource download event.nativeEvent && event.nativeEvent.which === 2; } function linkClicked(e, href, as, linkInstanceRef, replace, scroll, onNavigate) { const { nodeName } = e.currentTarget; // anchors inside an svg have a lowercase nodeName const isAnchorNodeName = nodeName.toUpperCase() === 'A'; if (isAnchorNodeName && isModifiedEvent(e) || e.currentTarget.hasAttribute('download')) { // ignore click for browser’s default behavior return; } if (!isLocalURL(href)) { if (replace) { // browser default behavior does not replace the history state // so we need to do it manually e.preventDefault(); location.replace(href); } // ignore click for browser’s default behavior return; } e.preventDefault(); const navigate = ()=>{ if (onNavigate) { let isDefaultPrevented = false; onNavigate({ preventDefault: ()=>{ isDefaultPrevented = true; } }); if (isDefaultPrevented) { return; } } dispatchNavigateAction(as || href, replace ? 'replace' : 'push', scroll != null ? scroll : true, linkInstanceRef.current); }; React.startTransition(navigate); } function formatStringOrUrl(urlObjOrString) { if (typeof urlObjOrString === 'string') { return urlObjOrString; } return formatUrl(urlObjOrString); } /** * A React component that extends the HTML `<a>` element to provide * [prefetching](https://nextjs.org/docs/app/building-your-application/routing/linking-and-navigating#2-prefetching) * and client-side navigation. This is the primary way to navigate between routes in Next.js. * * @remarks * - Prefetching is only enabled in production. * * @see https://nextjs.org/docs/app/api-reference/components/link */ export default function LinkComponent(props) { const [linkStatus, setOptimisticLinkStatus] = useOptimistic(IDLE_LINK_STATUS); let children; const linkInstanceRef = useRef(null); const { href: hrefProp, as: asProp, children: childrenProp, prefetch: prefetchProp = null, passHref, replace, shallow, scroll, onClick, onMouseEnter: onMouseEnterProp, onTouchStart: onTouchStartProp, legacyBehavior = false, onNavigate, ref: forwardedRef, unstable_dynamicOnHover, ...restProps } = props; children = childrenProp; if (legacyBehavior && (typeof children === 'string' || typeof children === 'number')) { children = /*#__PURE__*/ _jsx("a", { children: children }); } const router = React.useContext(AppRouterContext); const prefetchEnabled = prefetchProp !== false; /** * The possible states for prefetch are: * - null: this is the default "auto" mode, where we will prefetch partially if the link is in the viewport * - true: we will prefetch if the link is visible and prefetch the full page, not just partially * - false: we will not prefetch if in the viewport at all * - 'unstable_dynamicOnHover': this starts in "auto" mode, but switches to "full" when the link is hovered */ const appPrefetchKind = prefetchProp === null ? PrefetchKind.AUTO : PrefetchKind.FULL; if (process.env.NODE_ENV !== 'production') { function createPropError(args) { return Object.defineProperty(new Error("Failed prop type: The prop `" + args.key + "` expects a " + args.expected + " in `<Link>`, but got `" + args.actual + "` instead." + (typeof window !== 'undefined' ? "\nOpen your browser's console to view the Component stack trace." : '')), "__NEXT_ERROR_CODE", { value: "E319", enumerable: false, configurable: true }); } // TypeScript trick for type-guarding: const requiredPropsGuard = { href: true }; const requiredProps = Object.keys(requiredPropsGuard); requiredProps.forEach((key)=>{ if (key === 'href') { if (props[key] == null || typeof props[key] !== 'string' && typeof props[key] !== 'object') { throw createPropError({ key, expected: '`string` or `object`', actual: props[key] === null ? 'null' : typeof props[key] }); } } else { // TypeScript trick for type-guarding: // eslint-disable-next-line @typescript-eslint/no-unused-vars const _ = key; } }); // TypeScript trick for type-guarding: const optionalPropsGuard = { as: true, replace: true, scroll: true, shallow: true, passHref: true, prefetch: true, unstable_dynamicOnHover: true, onClick: true, onMouseEnter: true, onTouchStart: true, legacyBehavior: true, onNavigate: true }; const optionalProps = Object.keys(optionalPropsGuard); optionalProps.forEach((key)=>{ const valType = typeof props[key]; if (key === 'as') { if (props[key] && valType !== 'string' && valType !== 'object') { throw createPropError({ key, expected: '`string` or `object`', actual: valType }); } } else if (key === 'onClick' || key === 'onMouseEnter' || key === 'onTouchStart' || key === 'onNavigate') { if (props[key] && valType !== 'function') { throw createPropError({ key, expected: '`function`', actual: valType }); } } else if (key === 'replace' || key === 'scroll' || key === 'shallow' || key === 'passHref' || key === 'prefetch' || key === 'legacyBehavior' || key === 'unstable_dynamicOnHover') { if (props[key] != null && valType !== 'boolean') { throw createPropError({ key, expected: '`boolean`', actual: valType }); } } else { // TypeScript trick for type-guarding: // eslint-disable-next-line @typescript-eslint/no-unused-vars const _ = key; } }); } if (process.env.NODE_ENV !== 'production') { if (props.locale) { warnOnce('The `locale` prop is not supported in `next/link` while using the `app` router. Read more about app router internalization: https://nextjs.org/docs/app/building-your-application/routing/internationalization'); } if (!asProp) { let href; if (typeof hrefProp === 'string') { href = hrefProp; } else if (typeof hrefProp === 'object' && typeof hrefProp.pathname === 'string') { href = hrefProp.pathname; } if (href) { const hasDynamicSegment = href.split('/').some((segment)=>segment.startsWith('[') && segment.endsWith(']')); if (hasDynamicSegment) { throw Object.defineProperty(new Error("Dynamic href `" + href + "` found in <Link> while using the `/app` router, this is not supported. Read more: https://nextjs.org/docs/messages/app-dir-dynamic-href"), "__NEXT_ERROR_CODE", { value: "E267", enumerable: false, configurable: true }); } } } } const { href, as } = React.useMemo(()=>{ const resolvedHref = formatStringOrUrl(hrefProp); return { href: resolvedHref, as: asProp ? formatStringOrUrl(asProp) : resolvedHref }; }, [ hrefProp, asProp ]); // This will return the first child, if multiple are provided it will throw an error let child; if (legacyBehavior) { if (process.env.NODE_ENV === 'development') { if (onClick) { console.warn('"onClick" was passed to <Link> with `href` of `' + hrefProp + '` but "legacyBehavior" was set. The legacy behavior requires onClick be set on the child of next/link'); } if (onMouseEnterProp) { console.warn('"onMouseEnter" was passed to <Link> with `href` of `' + hrefProp + '` but "legacyBehavior" was set. The legacy behavior requires onMouseEnter be set on the child of next/link'); } try { child = React.Children.only(children); } catch (err) { if (!children) { throw Object.defineProperty(new Error("No children were passed to <Link> with `href` of `" + hrefProp + "` but one child is required https://nextjs.org/docs/messages/link-no-children"), "__NEXT_ERROR_CODE", { value: "E320", enumerable: false, configurable: true }); } throw Object.defineProperty(new Error("Multiple children were passed to <Link> with `href` of `" + hrefProp + "` but only one child is supported https://nextjs.org/docs/messages/link-multiple-children" + (typeof window !== 'undefined' ? " \nOpen your browser's console to view the Component stack trace." : '')), "__NEXT_ERROR_CODE", { value: "E266", enumerable: false, configurable: true }); } } else { child = React.Children.only(children); } } else { if (process.env.NODE_ENV === 'development') { if ((children == null ? void 0 : children.type) === 'a') { throw Object.defineProperty(new Error('Invalid <Link> with <a> child. Please remove <a> or use <Link legacyBehavior>.\nLearn more: https://nextjs.org/docs/messages/invalid-new-link-with-extra-anchor'), "__NEXT_ERROR_CODE", { value: "E209", enumerable: false, configurable: true }); } } } const childRef = legacyBehavior ? child && typeof child === 'object' && child.ref : forwardedRef; // Use a callback ref to attach an IntersectionObserver to the anchor tag on // mount. In the future we will also use this to keep track of all the // currently mounted <Link> instances, e.g. so we can re-prefetch them after // a revalidation or refresh. const observeLinkVisibilityOnMount = React.useCallback((element)=>{ if (router !== null) { linkInstanceRef.current = mountLinkInstance(element, href, router, appPrefetchKind, prefetchEnabled, setOptimisticLinkStatus); } return ()=>{ if (linkInstanceRef.current) { unmountLinkForCurrentNavigation(linkInstanceRef.current); linkInstanceRef.current = null; } unmountPrefetchableInstance(element); }; }, [ prefetchEnabled, href, router, appPrefetchKind, setOptimisticLinkStatus ]); const mergedRef = useMergedRef(observeLinkVisibilityOnMount, childRef); const childProps = { ref: mergedRef, onClick (e) { if (process.env.NODE_ENV !== 'production') { if (!e) { throw Object.defineProperty(new Error('Component rendered inside next/link has to pass click event to "onClick" prop.'), "__NEXT_ERROR_CODE", { value: "E312", enumerable: false, configurable: true }); } } if (!legacyBehavior && typeof onClick === 'function') { onClick(e); } if (legacyBehavior && child.props && typeof child.props.onClick === 'function') { child.props.onClick(e); } if (!router) { return; } if (e.defaultPrevented) { return; } linkClicked(e, href, as, linkInstanceRef, replace, scroll, onNavigate); }, onMouseEnter (e) { if (!legacyBehavior && typeof onMouseEnterProp === 'function') { onMouseEnterProp(e); } if (legacyBehavior && child.props && typeof child.props.onMouseEnter === 'function') { child.props.onMouseEnter(e); } if (!router) { return; } if (!prefetchEnabled || process.env.NODE_ENV === 'development') { return; } const upgradeToDynamicPrefetch = unstable_dynamicOnHover === true; onNavigationIntent(e.currentTarget, upgradeToDynamicPrefetch); }, onTouchStart: process.env.__NEXT_LINK_NO_TOUCH_START ? undefined : function onTouchStart(e) { if (!legacyBehavior && typeof onTouchStartProp === 'function') { onTouchStartProp(e); } if (legacyBehavior && child.props && typeof child.props.onTouchStart === 'function') { child.props.onTouchStart(e); } if (!router) { return; } if (!prefetchEnabled) { return; } const upgradeToDynamicPrefetch = unstable_dynamicOnHover === true; onNavigationIntent(e.currentTarget, upgradeToDynamicPrefetch); } }; // If child is an <a> tag and doesn't have a href attribute, or if the 'passHref' property is // defined, we specify the current 'href', so that repetition is not needed by the user. // If the url is absolute, we can bypass the logic to prepend the basePath. if (isAbsoluteUrl(as)) { childProps.href = as; } else if (!legacyBehavior || passHref || child.type === 'a' && !('href' in child.props)) { childProps.href = addBasePath(as); } let link; if (legacyBehavior) { if (process.env.NODE_ENV === 'development') { errorOnce('`legacyBehavior` is deprecated and will be removed in a future ' + 'release. A codemod is available to upgrade your components:\n\n' + 'npx @next/codemod@latest new-link .\n\n' + 'Learn more: https://nextjs.org/docs/app/building-your-application/upgrading/codemods#remove-a-tags-from-link-components'); } link = /*#__PURE__*/ React.cloneElement(child, childProps); } else { link = /*#__PURE__*/ _jsx("a", { ...restProps, ...childProps, children: children }); } return /*#__PURE__*/ _jsx(LinkStatusContext.Provider, { value: linkStatus, children: link }); } const LinkStatusContext = /*#__PURE__*/ createContext(IDLE_LINK_STATUS); export const useLinkStatus = ()=>{ return useContext(LinkStatusContext); }; //# sourceMappingURL=link.js.map