UNPKG

@react-navigation/native

Version:

React Native integration for React Navigation

136 lines (132 loc) 5.2 kB
"use strict"; import { CommonActions, getPathFromState, NavigationContainerRefContext, NavigationHelpersContext } from '@react-navigation/core'; import * as React from 'react'; import { Platform } from 'react-native'; import useLatestCallback from 'use-latest-callback'; import { LinkingContext } from "./LinkingContext.js"; import { useDeepStableValue } from "./useDeepStableValue.js"; const NESTED_KEYS = ['payload', 'params', 'state', 'routes']; const NESTED_PARAMS_KEYS = ['params', 'state']; const NAVIGATE_ACTION_TYPES = ['NAVIGATE', 'PUSH', 'REPLACE', 'POP_TO', 'JUMP_TO']; /** * Nested params are tracked as consumed after handling * So navigating with same params again won't work * This can happen if the action or params are memoized * Or component hasn't re-renderd to re-create inline objects * So we clone the action and params when necessary before dispatch */ function clone(value, keys = NESTED_KEYS, clones) { if (typeof value !== 'object' || value == null) { return value; } const existing = clones?.get(value); if (existing) { return existing; } const isArray = Array.isArray(value); if (keys === NESTED_PARAMS_KEYS && 'screen' in value && typeof value.screen !== 'string' && !('state' in value && typeof value.state === 'object' && value.state != null && 'routes' in value.state && Array.isArray(value.state.routes))) { return value; } let copy = keys === NESTED_PARAMS_KEYS ? { ...value } : undefined; clones ??= new WeakMap(); clones.set(value, copy ?? value); for (const key of isArray ? value.keys() : keys) { const nested = Reflect.get(value, key); const cloned = clone(nested, key === 'params' ? NESTED_PARAMS_KEYS : NESTED_KEYS, clones); if (cloned !== nested) { copy ??= isArray ? [...value] : { ...value }; clones.set(value, copy); Object.assign(copy, { [key]: cloned }); } } return copy ?? value; } const getTargetFromAction = (action, screens) => { if (action === undefined || screens === undefined) { return undefined; } const { payload } = action; if (!NAVIGATE_ACTION_TYPES.includes(action.type) || payload === undefined || !('name' in payload) || typeof payload.name !== 'string' || !(payload.name in screens)) { return undefined; } return { screen: payload.name, params: 'params' in payload && typeof payload.params === 'object' && payload.params != null ? payload.params : undefined }; }; /** * Hook to get props for an anchor tag so it can work with in page navigation. * * @param props.screen Name of the screen to navigate to (e.g. `'Feeds'`). * @param props.params Params to pass to the screen to navigate to (e.g. `{ sort: 'hot' }`). * @param props.href Optional absolute path to use for the href (e.g. `/feeds/hot`). * @param props.action Optional action to override the in-page navigation. The `href` is still derived from `screen`, so this can be used to render a link while dispatching a different action (e.g. a `replace`). */ export function useLinkProps({ screen, params, action, ...rest }) { const root = React.useContext(NavigationContainerRefContext); const navigation = React.useContext(NavigationHelpersContext) ?? root; if (navigation == null) { throw new Error("Couldn't find a navigation object. Is your component inside NavigationContainer?"); } const { options } = React.useContext(LinkingContext); const onPress = useLatestCallback(e => { if (Platform.OS === 'web' && e) { const hasModifierKey = 'metaKey' in e && e.metaKey || 'altKey' in e && e.altKey || 'ctrlKey' in e && e.ctrlKey || 'shiftKey' in e && e.shiftKey; const isLeftClick = 'button' in e ? e.button == null || e.button === 0 : true; const isSelfTarget = e.currentTarget && 'target' in e.currentTarget ? [undefined, null, '', '_self'].includes(e.currentTarget.target) : true; if (hasModifierKey || !isLeftClick || !isSelfTarget) { return; } } e?.preventDefault(); let cloned; if (action != null) { cloned = clone(action); } else { if (screen == null) { throw new Error("Couldn't find a screen to navigate to. Make sure to provide a screen name."); } cloned = clone( // @ts-expect-error This is already type-checked by the prop types CommonActions.navigate(screen, params)); } navigation.dispatch(cloned); }); const target = useDeepStableValue(Platform.OS === 'web' ? typeof screen === 'string' ? { screen, params: typeof params === 'object' && params != null ? params : undefined } : getTargetFromAction(action, options?.config?.screens) : undefined); const href = React.useMemo(() => { if (Platform.OS !== 'web' || rest.href != null || target == null) { return rest.href; } const getPath = options?.getPathFromState ?? getPathFromState; return getPath({ routes: [{ name: target.screen, params: target.params }] }, options?.config); }, [rest.href, target, options?.getPathFromState, options?.config]); return { href, role: 'link', onPress }; } //# sourceMappingURL=useLinkProps.js.map