@react-navigation/native 8.0.0-alpha.35 → 8.0.0-alpha.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Link.tsx CHANGED
@@ -9,47 +9,58 @@ import {
9
9
 
10
10
  import { type LinkProps, useLinkProps } from './useLinkProps';
11
11
 
12
- type Props<
13
- ParamList extends {} = RootParamList,
14
- RouteName extends keyof ParamList = keyof ParamList,
15
- > = LinkProps<ParamList, RouteName> &
16
- Omit<TextProps, 'disabled'> & {
17
- target?: string;
18
- onPress?: (
19
- e: React.MouseEvent<HTMLAnchorElement, MouseEvent> | GestureResponderEvent
20
- ) => void;
21
- disabled?: boolean | null;
22
- children: React.ReactNode;
23
- };
12
+ type PressEvent =
13
+ | React.MouseEvent<HTMLAnchorElement, MouseEvent>
14
+ | GestureResponderEvent;
15
+
16
+ type LinkBaseProps = Omit<TextProps, 'disabled'> & {
17
+ target?: string;
18
+ onPress?: (e: PressEvent) => void;
19
+ disabled?: boolean | undefined;
20
+ children: React.ReactNode;
21
+ };
24
22
 
25
23
  /**
26
24
  * Component to render link to another screen using a path.
27
25
  * Uses an anchor tag on the web.
28
26
  *
27
+ * @param props.in Name of the current or parent screen whose navigator contains the target screen.
29
28
  * @param props.screen Name of the screen to navigate to (e.g. `'Feeds'`).
30
29
  * @param props.params Params to pass to the screen to navigate to (e.g. `{ sort: 'hot' }`).
31
30
  * @param props.href Optional absolute path to use for the href (e.g. `/feeds/hot`).
32
- * @param props.action Optional action to use for in-page navigation. By default, the path is parsed to an action based on linking config.
31
+ * @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`).
33
32
  * @param props.children Child elements to render the content.
34
33
  */
35
34
  export function Link<
36
- const ParamList extends {} = RootParamList,
37
- const RouteName extends keyof ParamList = keyof ParamList,
35
+ ParamList extends {} = RootParamList,
36
+ RouteName extends Extract<keyof ParamList, string> = Extract<
37
+ keyof ParamList,
38
+ string
39
+ >,
38
40
  >({
41
+ in: parent,
39
42
  screen,
40
43
  params,
41
44
  action,
42
45
  href,
43
46
  style,
47
+ target,
44
48
  ...rest
45
- }: Props<ParamList, RouteName>) {
49
+ }: LinkProps<NoInfer<ParamList>, RouteName> & LinkBaseProps) {
50
+ // @ts-expect-error: destructuring loses the relationship between target props
51
+ const props = useLinkProps({ in: parent, screen, params, action, href });
52
+
53
+ // Keep usage of `useTheme` after `useLinkProps`
54
+ // This ensures proper error when used outside of navigation container
46
55
  const { colors, fonts } = useTheme();
47
- // @ts-expect-error: This is already type-checked by the prop types
48
- const props = useLinkProps<ParamList>({ screen, params, action, href });
49
56
 
50
- const onPress = (
51
- e: React.MouseEvent<HTMLAnchorElement, MouseEvent> | GestureResponderEvent
52
- ) => {
57
+ const onPress = (e: PressEvent) => {
58
+ if (rest.disabled) {
59
+ e.preventDefault();
60
+ e.stopPropagation();
61
+ return;
62
+ }
63
+
53
64
  if ('onPress' in rest) {
54
65
  rest.onPress?.(e);
55
66
  }
@@ -64,7 +75,17 @@ export function Link<
64
75
  ...props,
65
76
  ...rest,
66
77
  ...Platform.select({
67
- web: { onClick: onPress } as any,
78
+ web: {
79
+ 'aria-disabled': rest.disabled,
80
+ onAuxClick: rest.disabled
81
+ ? (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
82
+ e.preventDefault();
83
+ e.stopPropagation();
84
+ }
85
+ : undefined,
86
+ onClick: onPress,
87
+ hrefAttrs: { target },
88
+ },
68
89
  default: { onPress },
69
90
  }),
70
91
  style: [{ color: colors.primary }, fonts.regular, style],
@@ -195,6 +195,8 @@ export function createMemoryHistory() {
195
195
  // But on Firefox, it seems to take much longer, around 50ms from our testing
196
196
  // We're using a hacky timeout since there doesn't seem to be way to know for sure
197
197
  const timer = setTimeout(() => {
198
+ window.removeEventListener('popstate', onPopState);
199
+
198
200
  const foundIndex = pending.findIndex((it) => it.ref === done);
199
201
 
200
202
  if (foundIndex > -1) {
@@ -29,4 +29,4 @@ export default codegenNativeComponent<NativeProps>(
29
29
  {
30
30
  interfaceOnly: true,
31
31
  }
32
- ) as HostComponent<NativeProps>;
32
+ );
@@ -0,0 +1,12 @@
1
+ import isEqual from 'fast-deep-equal';
2
+ import * as React from 'react';
3
+
4
+ export function useDeepStableValue<T>(value: T): T {
5
+ const valueRef = React.useRef(value);
6
+
7
+ if (!isEqual(valueRef.current, value)) {
8
+ valueRef.current = value;
9
+ }
10
+
11
+ return valueRef.current;
12
+ }
@@ -1,151 +1,436 @@
1
1
  import {
2
+ CommonActions,
3
+ type FocusedRouteState,
2
4
  getPathFromState,
3
5
  type NavigationAction,
6
+ type NavigationContainerRef,
4
7
  NavigationContainerRefContext,
8
+ NavigationContext,
9
+ NavigationFocusedRouteStateContext,
10
+ type NavigationHelpers,
5
11
  NavigationHelpersContext,
12
+ type NavigationProp,
13
+ type NavigatorScreenParams,
14
+ type ParamListBase,
15
+ type ParamListForNavigator,
6
16
  type RootParamList,
7
17
  } from '@react-navigation/core';
8
18
  import * as React from 'react';
9
19
  import { type GestureResponderEvent, Platform } from 'react-native';
20
+ import useLatestCallback from 'use-latest-callback';
10
21
 
11
22
  import { LinkingContext } from './LinkingContext';
23
+ import { useDeepStableValue } from './useDeepStableValue';
24
+
25
+ type LinkScreenTargetProps<
26
+ ParamList extends {},
27
+ RouteName extends Extract<keyof ParamList, string> = Extract<
28
+ keyof ParamList,
29
+ string
30
+ >,
31
+ > = RouteName extends unknown
32
+ ? undefined extends ParamList[RouteName]
33
+ ? {
34
+ href?: string | undefined;
35
+ action?: NavigationAction | undefined;
36
+ in?: Extract<keyof ParamList, string> | undefined;
37
+ screen: RouteName;
38
+ params?: ParamList[RouteName] | undefined;
39
+ }
40
+ : {
41
+ href?: string | undefined;
42
+ action?: NavigationAction | undefined;
43
+ in?: Extract<keyof ParamList, string> | undefined;
44
+ screen: RouteName;
45
+ params: ParamList[RouteName];
46
+ }
47
+ : never;
48
+
49
+ type LinkScreenTargetPropsWithIn<
50
+ ParamList extends {},
51
+ RouteName extends Extract<keyof ParamList, string> = Extract<
52
+ keyof ParamList,
53
+ string
54
+ >,
55
+ > = RouteName extends unknown
56
+ ? undefined extends ParamList[RouteName]
57
+ ? {
58
+ href?: string | undefined;
59
+ action?: NavigationAction | undefined;
60
+ in: Extract<keyof ParamList, string>;
61
+ screen: RouteName;
62
+ params?: ParamList[RouteName] | undefined;
63
+ }
64
+ : {
65
+ href?: string | undefined;
66
+ action?: NavigationAction | undefined;
67
+ in: Extract<keyof ParamList, string>;
68
+ screen: RouteName;
69
+ params: ParamList[RouteName];
70
+ }
71
+ : never;
72
+
73
+ type LinkPropsWithInForParams<Params> = [Params] extends [never]
74
+ ? never
75
+ : [Params] extends [NavigatorScreenParams<infer Navigator>]
76
+ ? ParamListForNavigator<Navigator> extends infer NestedParamList extends {}
77
+ ?
78
+ | LinkScreenTargetPropsWithIn<NestedParamList>
79
+ | LinkPropsWithIn<NestedParamList>
80
+ : never
81
+ : never;
82
+
83
+ type LinkPropsWithIn<ParamList extends {}> = string extends keyof ParamList
84
+ ? never
85
+ : {
86
+ [RouteName in keyof ParamList]: LinkPropsWithInForParams<
87
+ Exclude<ParamList[RouteName], undefined>
88
+ >;
89
+ }[keyof ParamList];
12
90
 
13
91
  export type LinkProps<
14
92
  ParamList extends {} = RootParamList,
15
- RouteName extends keyof ParamList = keyof ParamList,
93
+ RouteName extends Extract<keyof ParamList, string> = Extract<
94
+ keyof ParamList,
95
+ string
96
+ >,
16
97
  > =
17
- | ({
18
- href?: string | undefined;
19
- action?: NavigationAction | undefined;
20
- } & (RouteName extends unknown
21
- ? undefined extends ParamList[RouteName]
22
- ? { screen: RouteName; params?: ParamList[RouteName] | undefined }
23
- : { screen: RouteName; params: ParamList[RouteName] }
24
- : never))
98
+ | LinkScreenTargetProps<ParamList, RouteName>
99
+ | LinkPropsWithIn<ParamList>
25
100
  | {
26
101
  href?: string | undefined;
27
102
  action: NavigationAction;
103
+ in?: undefined;
28
104
  screen?: undefined;
29
105
  params?: undefined;
30
106
  };
31
107
 
108
+ const NESTED_KEYS = ['payload', 'params', 'state', 'routes'] as const;
109
+ const NESTED_PARAMS_KEYS = ['params', 'state'] as const;
110
+ const NAVIGATE_ACTION_TYPES = [
111
+ 'NAVIGATE',
112
+ 'PUSH',
113
+ 'REPLACE',
114
+ 'POP_TO',
115
+ 'JUMP_TO',
116
+ ];
117
+ /**
118
+ * Nested params are tracked as consumed after handling
119
+ * So navigating with same params again won't work
120
+ * This can happen if the action or params are memoized
121
+ * Or component hasn't re-renderd to re-create inline objects
122
+ * So we clone the action and params when necessary before dispatch
123
+ */
124
+ function clone<T>(value: T): T;
125
+ function clone(
126
+ value: unknown,
127
+ keys: typeof NESTED_KEYS | typeof NESTED_PARAMS_KEYS,
128
+ clones: WeakMap<object, object>
129
+ ): unknown;
130
+ function clone(
131
+ value: unknown,
132
+ keys: typeof NESTED_KEYS | typeof NESTED_PARAMS_KEYS = NESTED_KEYS,
133
+ clones?: WeakMap<object, object>
134
+ ): unknown {
135
+ if (typeof value !== 'object' || value == null) {
136
+ return value;
137
+ }
138
+
139
+ const existing = clones?.get(value);
140
+
141
+ if (existing) {
142
+ return existing;
143
+ }
144
+
145
+ const isArray = Array.isArray(value);
146
+
147
+ if (
148
+ keys === NESTED_PARAMS_KEYS &&
149
+ 'screen' in value &&
150
+ typeof value.screen !== 'string' &&
151
+ !(
152
+ 'state' in value &&
153
+ typeof value.state === 'object' &&
154
+ value.state != null &&
155
+ 'routes' in value.state &&
156
+ Array.isArray(value.state.routes)
157
+ )
158
+ ) {
159
+ return value;
160
+ }
161
+
162
+ let copy: object | undefined =
163
+ keys === NESTED_PARAMS_KEYS ? { ...value } : undefined;
164
+
165
+ clones ??= new WeakMap();
166
+ clones.set(value, copy ?? value);
167
+
168
+ for (const key of isArray ? value.keys() : keys) {
169
+ const nested: unknown = Reflect.get(value, key);
170
+
171
+ const cloned = clone(
172
+ nested,
173
+ key === 'params' ? NESTED_PARAMS_KEYS : NESTED_KEYS,
174
+ clones
175
+ );
176
+
177
+ if (cloned !== nested) {
178
+ copy ??= isArray ? [...value] : { ...value };
179
+ clones.set(value, copy);
180
+
181
+ Reflect.set(copy, key, cloned);
182
+ }
183
+ }
184
+
185
+ return copy ?? value;
186
+ }
187
+
188
+ const getTargetFromAction = (
189
+ action: NavigationAction | undefined,
190
+ screens: Record<string, unknown> | undefined
191
+ ) => {
192
+ if (action === undefined || screens === undefined) {
193
+ return undefined;
194
+ }
195
+
196
+ const { payload } = action;
197
+
198
+ if (
199
+ !NAVIGATE_ACTION_TYPES.includes(action.type) ||
200
+ payload === undefined ||
201
+ !('name' in payload) ||
202
+ typeof payload.name !== 'string' ||
203
+ !(payload.name in screens)
204
+ ) {
205
+ return undefined;
206
+ }
207
+
208
+ return {
209
+ screen: payload.name,
210
+ params:
211
+ 'params' in payload &&
212
+ typeof payload.params === 'object' &&
213
+ payload.params != null
214
+ ? payload.params
215
+ : undefined,
216
+ };
217
+ };
218
+
219
+ const getStateForParent = (
220
+ parent: string | undefined,
221
+ targetState: FocusedRouteState,
222
+ focusedRouteState: FocusedRouteState | undefined
223
+ ): FocusedRouteState | undefined => {
224
+ if (parent === undefined) {
225
+ return targetState;
226
+ }
227
+
228
+ if (focusedRouteState === undefined) {
229
+ return undefined;
230
+ }
231
+
232
+ const route = focusedRouteState.routes[0];
233
+
234
+ const childState = getStateForParent(parent, targetState, route.state);
235
+
236
+ if (childState !== undefined) {
237
+ return {
238
+ routes: [
239
+ {
240
+ ...route,
241
+ state: childState,
242
+ },
243
+ ],
244
+ };
245
+ }
246
+
247
+ if (route.name === parent) {
248
+ return targetState;
249
+ }
250
+
251
+ return undefined;
252
+ };
253
+
32
254
  /**
33
255
  * Hook to get props for an anchor tag so it can work with in page navigation.
34
256
  *
257
+ * @param props.in Name of the current or parent screen whose navigator contains the target screen.
35
258
  * @param props.screen Name of the screen to navigate to (e.g. `'Feeds'`).
36
259
  * @param props.params Params to pass to the screen to navigate to (e.g. `{ sort: 'hot' }`).
37
260
  * @param props.href Optional absolute path to use for the href (e.g. `/feeds/hot`).
38
- * @param props.action Optional action to use for in-page navigation. By default, the path is parsed to an action based on linking config.
261
+ * @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`).
39
262
  */
40
263
  export function useLinkProps<
41
- const ParamList extends {} = RootParamList,
42
- const RouteName extends keyof ParamList = keyof ParamList,
43
- >({ screen, params, href, action }: LinkProps<ParamList, RouteName>) {
264
+ ParamList extends {} = RootParamList,
265
+ RouteName extends Extract<keyof ParamList, string> = Extract<
266
+ keyof ParamList,
267
+ string
268
+ >,
269
+ >({
270
+ in: parent,
271
+ screen,
272
+ params,
273
+ action,
274
+ ...rest
275
+ }: LinkProps<NoInfer<ParamList>, RouteName>) {
44
276
  const root = React.use(NavigationContainerRefContext);
45
- const navigation = React.use(NavigationHelpersContext);
277
+
278
+ let navigation:
279
+ | NavigationHelpers<ParamListBase>
280
+ | NavigationProp<ParamListBase>
281
+ | NavigationContainerRef<ParamListBase>
282
+ | undefined = React.use(NavigationContext);
283
+
284
+ if (parent != null) {
285
+ navigation = navigation?.getParent(parent);
286
+ } else if (action != null) {
287
+ const helpers = React.use(NavigationHelpersContext);
288
+
289
+ // Handle custom actions in the immediate parent
290
+ navigation = helpers ?? navigation ?? root;
291
+ } else {
292
+ // The ref dispatches in currently focused screen
293
+ // So prefer the top-most navigator if available
294
+ let parent = navigation?.getParent();
295
+
296
+ while (parent !== undefined) {
297
+ navigation = parent;
298
+ parent = navigation.getParent();
299
+ }
300
+
301
+ navigation = navigation ?? root;
302
+ }
303
+
304
+ if (navigation == null) {
305
+ if (parent != null) {
306
+ throw new Error(
307
+ `Couldn't find a navigation object for '${parent}' in current or any parent screens. Is your component inside the correct screen?`
308
+ );
309
+ }
310
+
311
+ throw new Error(
312
+ "Couldn't find a navigation object. Is your component inside NavigationContainer?"
313
+ );
314
+ }
315
+
46
316
  const { options } = React.use(LinkingContext);
47
317
 
48
- const onPress = (
49
- e?: React.MouseEvent<HTMLAnchorElement, MouseEvent> | GestureResponderEvent
50
- ) => {
51
- let shouldHandle = false;
52
-
53
- if (Platform.OS !== 'web' || !e) {
54
- e?.preventDefault?.();
55
- shouldHandle = true;
56
- } else {
57
- // ignore clicks with modifier keys
58
- const hasModifierKey =
59
- ('metaKey' in e && e.metaKey) ||
60
- ('altKey' in e && e.altKey) ||
61
- ('ctrlKey' in e && e.ctrlKey) ||
62
- ('shiftKey' in e && e.shiftKey);
63
-
64
- // only handle left clicks
65
- const isLeftClick =
66
- 'button' in e ? e.button == null || e.button === 0 : true;
67
-
68
- // let browser handle "target=_blank" etc.
69
- const isSelfTarget =
70
- e.currentTarget && 'target' in e.currentTarget
71
- ? [undefined, null, '', 'self'].includes(e.currentTarget.target)
72
- : true;
73
-
74
- if (!hasModifierKey && isLeftClick && isSelfTarget) {
75
- e.preventDefault?.();
76
- shouldHandle = true;
318
+ const onPress = useLatestCallback(
319
+ (
320
+ e?:
321
+ | React.MouseEvent<HTMLAnchorElement, MouseEvent>
322
+ | GestureResponderEvent
323
+ ) => {
324
+ if (Platform.OS === 'web' && e) {
325
+ // ignore clicks with modifier keys
326
+ const hasModifierKey =
327
+ ('metaKey' in e && e.metaKey) ||
328
+ ('altKey' in e && e.altKey) ||
329
+ ('ctrlKey' in e && e.ctrlKey) ||
330
+ ('shiftKey' in e && e.shiftKey);
331
+
332
+ // only handle left clicks
333
+ const isLeftClick =
334
+ 'button' in e ? e.button == null || e.button === 0 : true;
335
+
336
+ // let browser handle "target=_blank" etc.
337
+ const isSelfTarget =
338
+ e.currentTarget && 'target' in e.currentTarget
339
+ ? [undefined, null, '', '_self'].includes(e.currentTarget.target)
340
+ : true;
341
+
342
+ // let the browser handle the interaction
343
+ if (hasModifierKey || !isLeftClick || !isSelfTarget) {
344
+ return;
345
+ }
77
346
  }
78
- }
79
347
 
80
- if (shouldHandle) {
81
- if (action) {
82
- if (navigation) {
83
- navigation.dispatch(action);
84
- } else if (root) {
85
- root.dispatch(action);
86
- } else {
348
+ e?.preventDefault();
349
+
350
+ let cloned: NavigationAction;
351
+
352
+ if (action != null) {
353
+ cloned = clone(action);
354
+ } else {
355
+ if (screen == null) {
87
356
  throw new Error(
88
- "Couldn't find a navigation object. Is your component inside NavigationContainer?"
357
+ "Couldn't find a screen to navigate to. Make sure to provide a screen name."
89
358
  );
90
359
  }
91
- } else {
92
- // @ts-expect-error This is already type-checked by the prop types
93
- navigation?.navigate(screen, params);
360
+
361
+ cloned = clone(
362
+ CommonActions.navigate(
363
+ screen,
364
+ // @ts-expect-error This is already type-checked by the prop types
365
+ params
366
+ )
367
+ );
368
+
369
+ if (parent != null) {
370
+ cloned = {
371
+ ...cloned,
372
+ target: navigation.getState()?.key,
373
+ };
374
+ }
94
375
  }
376
+
377
+ navigation.dispatch(cloned);
95
378
  }
96
- };
379
+ );
97
380
 
98
- const getPathFromStateHelper = options?.getPathFromState ?? getPathFromState;
99
-
100
- if (Platform.OS === 'web') {
101
- if (screen == null && action != null && options?.config != null) {
102
- switch (action.type) {
103
- case 'NAVIGATE':
104
- case 'PUSH':
105
- case 'REPLACE':
106
- case 'POP_TO':
107
- case 'JUMP_TO': {
108
- if (
109
- action.payload != null &&
110
- 'name' in action.payload &&
111
- typeof action.payload.name === 'string' &&
112
- action.payload.name in options.config.screens
113
- ) {
114
- screen = action.payload.name;
115
-
116
- if (
117
- 'params' in action.payload &&
118
- typeof action.payload.params === 'object' &&
119
- action.payload.params != null
120
- ) {
121
- // @ts-expect-error this is fine 🔥
122
- params = action.payload.params;
123
- }
381
+ const target = useDeepStableValue(
382
+ Platform.OS === 'web'
383
+ ? typeof screen === 'string'
384
+ ? {
385
+ screen,
386
+ params:
387
+ typeof params === 'object' && params != null ? params : undefined,
124
388
  }
125
- }
126
- }
389
+ : getTargetFromAction(action, options?.config?.screens)
390
+ : undefined
391
+ );
392
+
393
+ const state =
394
+ Platform.OS === 'web' && rest.href == null && target != null
395
+ ? React.use(NavigationFocusedRouteStateContext)
396
+ : undefined;
397
+
398
+ const href = React.useMemo(() => {
399
+ if (Platform.OS !== 'web' || rest.href != null || target == null) {
400
+ return rest.href;
127
401
  }
128
- }
402
+
403
+ const stateForParent = getStateForParent(
404
+ parent,
405
+ {
406
+ routes: [
407
+ {
408
+ name: target.screen,
409
+ params: target.params,
410
+ },
411
+ ],
412
+ },
413
+ state
414
+ );
415
+
416
+ if (stateForParent) {
417
+ const getPath = options?.getPathFromState ?? getPathFromState;
418
+
419
+ return getPath(stateForParent, options?.config);
420
+ }
421
+
422
+ return undefined;
423
+ }, [
424
+ rest.href,
425
+ target,
426
+ parent,
427
+ state,
428
+ options?.getPathFromState,
429
+ options?.config,
430
+ ]);
129
431
 
130
432
  return {
131
- href:
132
- href ??
133
- (Platform.OS === 'web' && typeof screen === 'string'
134
- ? getPathFromStateHelper(
135
- {
136
- routes: [
137
- {
138
- name: screen,
139
- params:
140
- typeof params === 'object' && params != null
141
- ? params
142
- : undefined,
143
- },
144
- ],
145
- },
146
- options?.config
147
- )
148
- : undefined),
433
+ href,
149
434
  role: 'link' as const,
150
435
  onPress,
151
436
  };