expo-router 5.2.0-canary-20250630-547cd82 → 5.2.0-canary-20250709-136b77f

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.
Files changed (64) hide show
  1. package/assets/modal.module.css +124 -0
  2. package/build/fork/useLinking.d.ts.map +1 -1
  3. package/build/fork/useLinking.js +7 -3
  4. package/build/fork/useLinking.js.map +1 -1
  5. package/build/layouts/DrawerClient.d.ts +2 -2
  6. package/build/layouts/Stack.web.d.ts +17 -0
  7. package/build/layouts/Stack.web.d.ts.map +1 -0
  8. package/build/layouts/Stack.web.js +17 -0
  9. package/build/layouts/Stack.web.js.map +1 -0
  10. package/build/layouts/StackClient.d.ts +41 -3
  11. package/build/layouts/StackClient.d.ts.map +1 -1
  12. package/build/layouts/StackClient.js +79 -2
  13. package/build/layouts/StackClient.js.map +1 -1
  14. package/build/layouts/TabsClient.d.ts +3 -3
  15. package/build/link/BaseExpoRouterLink.d.ts +2 -1
  16. package/build/link/BaseExpoRouterLink.d.ts.map +1 -1
  17. package/build/link/BaseExpoRouterLink.js +37 -1
  18. package/build/link/BaseExpoRouterLink.js.map +1 -1
  19. package/build/link/ExpoLink.d.ts +2 -1
  20. package/build/link/ExpoLink.d.ts.map +1 -1
  21. package/build/link/ExpoLink.js +44 -3
  22. package/build/link/ExpoLink.js.map +1 -1
  23. package/build/link/Link.d.ts +3 -3
  24. package/build/link/Link.d.ts.map +1 -1
  25. package/build/link/Link.js.map +1 -1
  26. package/build/link/LinkWithPreview.d.ts +19 -6
  27. package/build/link/LinkWithPreview.d.ts.map +1 -1
  28. package/build/link/LinkWithPreview.js +53 -18
  29. package/build/link/LinkWithPreview.js.map +1 -1
  30. package/build/link/preview/HrefPreview.d.ts.map +1 -1
  31. package/build/link/preview/HrefPreview.js +2 -2
  32. package/build/link/preview/HrefPreview.js.map +1 -1
  33. package/build/link/preview/native.d.ts +2 -0
  34. package/build/link/preview/native.d.ts.map +1 -1
  35. package/build/link/preview/native.js.map +1 -1
  36. package/build/modal/web/ModalStack.web.d.ts +25 -0
  37. package/build/modal/web/ModalStack.web.d.ts.map +1 -0
  38. package/build/modal/web/ModalStack.web.js +86 -0
  39. package/build/modal/web/ModalStack.web.js.map +1 -0
  40. package/build/modal/web/ModalStackRouteDrawer.web.d.ts +14 -0
  41. package/build/modal/web/ModalStackRouteDrawer.web.d.ts.map +1 -0
  42. package/build/modal/web/ModalStackRouteDrawer.web.js +161 -0
  43. package/build/modal/web/ModalStackRouteDrawer.web.js.map +1 -0
  44. package/build/modal/web/TransparentModalStackRouteDrawer.web.d.ts +10 -0
  45. package/build/modal/web/TransparentModalStackRouteDrawer.web.d.ts.map +1 -0
  46. package/build/modal/web/TransparentModalStackRouteDrawer.web.js +28 -0
  47. package/build/modal/web/TransparentModalStackRouteDrawer.web.js.map +1 -0
  48. package/build/modal/web/modalStyles.d.ts +3 -0
  49. package/build/modal/web/modalStyles.d.ts.map +1 -0
  50. package/build/modal/web/modalStyles.js +8 -0
  51. package/build/modal/web/modalStyles.js.map +1 -0
  52. package/build/modal/web/types.d.ts +14 -0
  53. package/build/modal/web/types.d.ts.map +1 -0
  54. package/build/modal/web/types.js +3 -0
  55. package/build/modal/web/types.js.map +1 -0
  56. package/build/modal/web/utils.d.ts +70 -0
  57. package/build/modal/web/utils.d.ts.map +1 -0
  58. package/build/modal/web/utils.js +117 -0
  59. package/build/modal/web/utils.js.map +1 -0
  60. package/ios/ExpoHead.podspec +1 -1
  61. package/ios/LinkPreview/LinkPreviewNativeActionView.swift +22 -0
  62. package/ios/LinkPreview/LinkPreviewNativeModule.swift +3 -0
  63. package/ios/LinkPreview/LinkPreviewNativeView.swift +30 -11
  64. package/package.json +9 -7
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modalStyles.js","sourceRoot":"","sources":["../../../src/modal/web/modalStyles.ts"],"names":[],"mappings":";;;;;AAAA,wFAAsD;AAEtD,kBAAe,0BAAM,CAAC","sourcesContent":["import styles from '../../../assets/modal.module.css';\n\nexport default styles;\n"]}
@@ -0,0 +1,14 @@
1
+ import { ParamListBase, StackActionHelpers, StackNavigationState, StackRouterOptions, useNavigationBuilder } from '@react-navigation/native';
2
+ import { NativeStackNavigationEventMap, NativeStackNavigationOptions } from '@react-navigation/native-stack';
3
+ import { ExtendedStackNavigationOptions } from '../../layouts/StackClient';
4
+ export type ModalStackNavigatorProps = {
5
+ initialRouteName?: string;
6
+ screenOptions?: ExtendedStackNavigationOptions;
7
+ children: React.ReactNode;
8
+ };
9
+ export type ModalStackViewProps = Omit<ReturnType<typeof useNavigationBuilder<StackNavigationState<ParamListBase>, StackRouterOptions, StackActionHelpers<ParamListBase>, NativeStackNavigationOptions, NativeStackNavigationEventMap>>, 'NavigationContent'>;
10
+ export type CSSWithVars = React.CSSProperties & {
11
+ [key: `--${string}`]: string | number;
12
+ };
13
+ export type PresentationOptions = Partial<Pick<ExtendedStackNavigationOptions, 'presentation'>>;
14
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/modal/web/types.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,kBAAkB,EAClB,oBAAoB,EACpB,kBAAkB,EAClB,oBAAoB,EACrB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,6BAA6B,EAC7B,4BAA4B,EAC7B,MAAM,gCAAgC,CAAC;AAExC,OAAO,EAAE,8BAA8B,EAAE,MAAM,2BAA2B,CAAC;AAE3E,MAAM,MAAM,wBAAwB,GAAG;IACrC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,8BAA8B,CAAC;IAC/C,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,IAAI,CACpC,UAAU,CACR,OAAO,oBAAoB,CACzB,oBAAoB,CAAC,aAAa,CAAC,EACnC,kBAAkB,EAClB,kBAAkB,CAAC,aAAa,CAAC,EACjC,4BAA4B,EAC5B,6BAA6B,CAC9B,CACF,EACD,mBAAmB,CACpB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,KAAK,CAAC,aAAa,GAAG;IAC9C,CAAC,GAAG,EAAE,KAAK,MAAM,EAAE,GAAG,MAAM,GAAG,MAAM,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,8BAA8B,EAAE,cAAc,CAAC,CAAC,CAAC"}
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/modal/web/types.ts"],"names":[],"mappings":"","sourcesContent":["import {\n ParamListBase,\n StackActionHelpers,\n StackNavigationState,\n StackRouterOptions,\n useNavigationBuilder,\n} from '@react-navigation/native';\nimport {\n NativeStackNavigationEventMap,\n NativeStackNavigationOptions,\n} from '@react-navigation/native-stack';\n\nimport { ExtendedStackNavigationOptions } from '../../layouts/StackClient';\n\nexport type ModalStackNavigatorProps = {\n initialRouteName?: string;\n screenOptions?: ExtendedStackNavigationOptions;\n children: React.ReactNode;\n};\n\nexport type ModalStackViewProps = Omit<\n ReturnType<\n typeof useNavigationBuilder<\n StackNavigationState<ParamListBase>,\n StackRouterOptions,\n StackActionHelpers<ParamListBase>,\n NativeStackNavigationOptions,\n NativeStackNavigationEventMap\n >\n >,\n 'NavigationContent'\n>;\n\nexport type CSSWithVars = React.CSSProperties & {\n [key: `--${string}`]: string | number;\n};\n\nexport type PresentationOptions = Partial<Pick<ExtendedStackNavigationOptions, 'presentation'>>;\n"]}
@@ -0,0 +1,70 @@
1
+ import { ParamListBase, StackNavigationState } from '@react-navigation/native';
2
+ import { ExtendedStackNavigationOptions } from '../../layouts/StackClient';
3
+ /**
4
+ * A minimal subset of `ExtendedStackNavigationOptions` needed for the helper
5
+ * @internal
6
+ */
7
+ export type PresentationOptions = Partial<Pick<ExtendedStackNavigationOptions, 'presentation'>>;
8
+ /**
9
+ * Helper to determine if a given screen should be treated as a modal-type presentation
10
+ *
11
+ * @param options - The navigation options.
12
+ * @returns Whether the screen should be treated as a modal-type presentation.
13
+ *
14
+ * @internal
15
+ */
16
+ export declare function isModalPresentation(options?: PresentationOptions | null): boolean;
17
+ /**
18
+ * Helper to determine if a given screen should be treated as a transparent modal-type presentation
19
+ *
20
+ * @param options - The navigation options.
21
+ * @returns Whether the screen should be treated as a transparent modal-type presentation.
22
+ *
23
+ * @internal
24
+ */
25
+ export declare function isTransparentModalPresentation(options?: PresentationOptions | null): boolean;
26
+ /**
27
+ * SSR-safe viewport detection: initial render always returns `false` so that
28
+ * server and client markup match. The actual media query evaluation happens
29
+ * after mount.
30
+ *
31
+ * @internal
32
+ */
33
+ export declare function useIsDesktop(breakpoint?: number): boolean;
34
+ /**
35
+ * Returns a copy of the given Stack navigation state with any modal-type routes removed
36
+ * (only when running on the web) and a recalculated `index` that still points at the
37
+ * currently active non-modal route. If the active route *is* a modal that gets
38
+ * filtered out, we fall back to the last remaining route – this matches the logic
39
+ * used inside `ModalStackView` so that the underlying `NativeStackView` never tries
40
+ * to render a modal screen that is simultaneously being shown in the overlay.
41
+ *
42
+ * This helper is exported primarily for unit-testing; it should be considered
43
+ * internal to `ModalStack.web` and not a public API.
44
+ *
45
+ * @param state - The navigation state.
46
+ * @param descriptors - The navigation descriptors.
47
+ * @param isWeb - Whether the current platform is web.
48
+ * @returns The navigation state with any modal-type routes removed.
49
+ *
50
+ * @internal
51
+ */
52
+ export declare function convertStackStateToNonModalState(state: StackNavigationState<ParamListBase>, descriptors: Record<string, {
53
+ options: ExtendedStackNavigationOptions;
54
+ }>, isWeb: boolean): {
55
+ routes: import("@react-navigation/native").NavigationRoute<ParamListBase, string>[];
56
+ index: number;
57
+ };
58
+ /**
59
+ * Returns the index of the last route in the stack that is *not* a modal.
60
+ *
61
+ * @param state - The navigation state.
62
+ * @param descriptors - The navigation descriptors.
63
+ * @returns The index of the last non-modal route.
64
+ *
65
+ * @internal
66
+ */
67
+ export declare function findLastNonModalIndex(state: StackNavigationState<ParamListBase>, descriptors: Record<string, {
68
+ options: ExtendedStackNavigationOptions;
69
+ }>): number;
70
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/modal/web/utils.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAG/E,OAAO,EAAE,8BAA8B,EAAE,MAAM,2BAA2B,CAAC;AAE3E;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,8BAA8B,EAAE,cAAc,CAAC,CAAC,CAAC;AAEhG;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,CAAC,EAAE,mBAAmB,GAAG,IAAI,WAUvE;AAED;;;;;;;GAOG;AACH,wBAAgB,8BAA8B,CAAC,OAAO,CAAC,EAAE,mBAAmB,GAAG,IAAI,WAGlF;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,UAAU,GAAE,MAAY,WAoBpD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,gCAAgC,CAC9C,KAAK,EAAE,oBAAoB,CAAC,aAAa,CAAC,EAC1C,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,OAAO,EAAE,8BAA8B,CAAA;CAAE,CAAC,EACxE,KAAK,EAAE,OAAO;;;EAmBf;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,oBAAoB,CAAC,aAAa,CAAC,EAC1C,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,OAAO,EAAE,8BAA8B,CAAA;CAAE,CAAC,UASzE"}
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ 'use client';
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.isModalPresentation = isModalPresentation;
8
+ exports.isTransparentModalPresentation = isTransparentModalPresentation;
9
+ exports.useIsDesktop = useIsDesktop;
10
+ exports.convertStackStateToNonModalState = convertStackStateToNonModalState;
11
+ exports.findLastNonModalIndex = findLastNonModalIndex;
12
+ const react_1 = __importDefault(require("react"));
13
+ /**
14
+ * Helper to determine if a given screen should be treated as a modal-type presentation
15
+ *
16
+ * @param options - The navigation options.
17
+ * @returns Whether the screen should be treated as a modal-type presentation.
18
+ *
19
+ * @internal
20
+ */
21
+ function isModalPresentation(options) {
22
+ const presentation = options?.presentation;
23
+ return (presentation === 'modal' ||
24
+ presentation === 'formSheet' ||
25
+ presentation === 'fullScreenModal' ||
26
+ presentation === 'containedModal' ||
27
+ presentation === 'transparentModal' ||
28
+ presentation === 'containedTransparentModal');
29
+ }
30
+ /**
31
+ * Helper to determine if a given screen should be treated as a transparent modal-type presentation
32
+ *
33
+ * @param options - The navigation options.
34
+ * @returns Whether the screen should be treated as a transparent modal-type presentation.
35
+ *
36
+ * @internal
37
+ */
38
+ function isTransparentModalPresentation(options) {
39
+ const presentation = options?.presentation;
40
+ return presentation === 'transparentModal' || presentation === 'containedTransparentModal';
41
+ }
42
+ /**
43
+ * SSR-safe viewport detection: initial render always returns `false` so that
44
+ * server and client markup match. The actual media query evaluation happens
45
+ * after mount.
46
+ *
47
+ * @internal
48
+ */
49
+ function useIsDesktop(breakpoint = 768) {
50
+ const isWeb = process.env.EXPO_OS === 'web';
51
+ // Ensure server-side and initial client render agree (mobile first).
52
+ const [isDesktop, setIsDesktop] = react_1.default.useState(false);
53
+ react_1.default.useEffect(() => {
54
+ if (!isWeb || typeof window === 'undefined')
55
+ return;
56
+ const mql = window.matchMedia(`(min-width: ${breakpoint}px)`);
57
+ const listener = (e) => setIsDesktop(e.matches);
58
+ // Update immediately after mount
59
+ setIsDesktop(mql.matches);
60
+ mql.addEventListener('change', listener);
61
+ return () => mql.removeEventListener('change', listener);
62
+ }, [breakpoint, isWeb]);
63
+ return isDesktop;
64
+ }
65
+ /**
66
+ * Returns a copy of the given Stack navigation state with any modal-type routes removed
67
+ * (only when running on the web) and a recalculated `index` that still points at the
68
+ * currently active non-modal route. If the active route *is* a modal that gets
69
+ * filtered out, we fall back to the last remaining route – this matches the logic
70
+ * used inside `ModalStackView` so that the underlying `NativeStackView` never tries
71
+ * to render a modal screen that is simultaneously being shown in the overlay.
72
+ *
73
+ * This helper is exported primarily for unit-testing; it should be considered
74
+ * internal to `ModalStack.web` and not a public API.
75
+ *
76
+ * @param state - The navigation state.
77
+ * @param descriptors - The navigation descriptors.
78
+ * @param isWeb - Whether the current platform is web.
79
+ * @returns The navigation state with any modal-type routes removed.
80
+ *
81
+ * @internal
82
+ */
83
+ function convertStackStateToNonModalState(state, descriptors, isWeb) {
84
+ if (!isWeb) {
85
+ return { routes: state.routes, index: state.index };
86
+ }
87
+ // Remove every modal-type route from the stack on web.
88
+ const routes = state.routes.filter((route) => {
89
+ return !isModalPresentation(descriptors[route.key].options);
90
+ });
91
+ // Recalculate the active index so it still points at the same non-modal route, or –
92
+ // if that route was filtered out – at the last remaining route.
93
+ let index = routes.findIndex((r) => r.key === state.routes[state.index]?.key);
94
+ if (index < 0) {
95
+ index = routes.length > 0 ? routes.length - 1 : 0;
96
+ }
97
+ return { routes, index };
98
+ }
99
+ /**
100
+ * Returns the index of the last route in the stack that is *not* a modal.
101
+ *
102
+ * @param state - The navigation state.
103
+ * @param descriptors - The navigation descriptors.
104
+ * @returns The index of the last non-modal route.
105
+ *
106
+ * @internal
107
+ */
108
+ function findLastNonModalIndex(state, descriptors) {
109
+ // Iterate backwards through the stack to find the last non-modal route.
110
+ for (let i = state.routes.length - 1; i >= 0; i--) {
111
+ if (!isModalPresentation(descriptors[state.routes[i].key].options)) {
112
+ return i;
113
+ }
114
+ }
115
+ return -1;
116
+ }
117
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../../src/modal/web/utils.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\nimport { ParamListBase, StackNavigationState } from '@react-navigation/native';\nimport React from 'react';\n\nimport { ExtendedStackNavigationOptions } from '../../layouts/StackClient';\n\n/**\n * A minimal subset of `ExtendedStackNavigationOptions` needed for the helper\n * @internal\n */\nexport type PresentationOptions = Partial<Pick<ExtendedStackNavigationOptions, 'presentation'>>;\n\n/**\n * Helper to determine if a given screen should be treated as a modal-type presentation\n *\n * @param options - The navigation options.\n * @returns Whether the screen should be treated as a modal-type presentation.\n *\n * @internal\n */\nexport function isModalPresentation(options?: PresentationOptions | null) {\n const presentation = options?.presentation;\n return (\n presentation === 'modal' ||\n presentation === 'formSheet' ||\n presentation === 'fullScreenModal' ||\n presentation === 'containedModal' ||\n presentation === 'transparentModal' ||\n presentation === 'containedTransparentModal'\n );\n}\n\n/**\n * Helper to determine if a given screen should be treated as a transparent modal-type presentation\n *\n * @param options - The navigation options.\n * @returns Whether the screen should be treated as a transparent modal-type presentation.\n *\n * @internal\n */\nexport function isTransparentModalPresentation(options?: PresentationOptions | null) {\n const presentation = options?.presentation;\n return presentation === 'transparentModal' || presentation === 'containedTransparentModal';\n}\n\n/**\n * SSR-safe viewport detection: initial render always returns `false` so that\n * server and client markup match. The actual media query evaluation happens\n * after mount.\n *\n * @internal\n */\nexport function useIsDesktop(breakpoint: number = 768) {\n const isWeb = process.env.EXPO_OS === 'web';\n\n // Ensure server-side and initial client render agree (mobile first).\n const [isDesktop, setIsDesktop] = React.useState<boolean>(false);\n\n React.useEffect(() => {\n if (!isWeb || typeof window === 'undefined') return;\n\n const mql = window.matchMedia(`(min-width: ${breakpoint}px)`);\n const listener = (e: MediaQueryListEvent) => setIsDesktop(e.matches);\n\n // Update immediately after mount\n setIsDesktop(mql.matches);\n\n mql.addEventListener('change', listener);\n return () => mql.removeEventListener('change', listener);\n }, [breakpoint, isWeb]);\n\n return isDesktop;\n}\n\n/**\n * Returns a copy of the given Stack navigation state with any modal-type routes removed\n * (only when running on the web) and a recalculated `index` that still points at the\n * currently active non-modal route. If the active route *is* a modal that gets\n * filtered out, we fall back to the last remaining route – this matches the logic\n * used inside `ModalStackView` so that the underlying `NativeStackView` never tries\n * to render a modal screen that is simultaneously being shown in the overlay.\n *\n * This helper is exported primarily for unit-testing; it should be considered\n * internal to `ModalStack.web` and not a public API.\n *\n * @param state - The navigation state.\n * @param descriptors - The navigation descriptors.\n * @param isWeb - Whether the current platform is web.\n * @returns The navigation state with any modal-type routes removed.\n *\n * @internal\n */\nexport function convertStackStateToNonModalState(\n state: StackNavigationState<ParamListBase>,\n descriptors: Record<string, { options: ExtendedStackNavigationOptions }>,\n isWeb: boolean\n) {\n if (!isWeb) {\n return { routes: state.routes, index: state.index };\n }\n\n // Remove every modal-type route from the stack on web.\n const routes = state.routes.filter((route) => {\n return !isModalPresentation(descriptors[route.key].options);\n });\n\n // Recalculate the active index so it still points at the same non-modal route, or –\n // if that route was filtered out – at the last remaining route.\n let index = routes.findIndex((r) => r.key === state.routes[state.index]?.key);\n if (index < 0) {\n index = routes.length > 0 ? routes.length - 1 : 0;\n }\n\n return { routes, index };\n}\n\n/**\n * Returns the index of the last route in the stack that is *not* a modal.\n *\n * @param state - The navigation state.\n * @param descriptors - The navigation descriptors.\n * @returns The index of the last non-modal route.\n *\n * @internal\n */\nexport function findLastNonModalIndex(\n state: StackNavigationState<ParamListBase>,\n descriptors: Record<string, { options: ExtendedStackNavigationOptions }>\n) {\n // Iterate backwards through the stack to find the last non-modal route.\n for (let i = state.routes.length - 1; i >= 0; i--) {\n if (!isModalPresentation(descriptors[state.routes[i].key].options)) {\n return i;\n }\n }\n return -1;\n}\n"]}
@@ -13,7 +13,7 @@ Pod::Spec.new do |s|
13
13
  s.platforms = {
14
14
  :ios => '15.1'
15
15
  }
16
- s.swift_version = '5.4'
16
+ s.swift_version = '5.9'
17
17
  s.source = { git: 'https://github.com/expo/expo.git' }
18
18
  s.static_framework = true
19
19
 
@@ -4,9 +4,31 @@ import WebKit
4
4
  class LinkPreviewNativeActionView: ExpoView {
5
5
  var id: String = ""
6
6
  var title: String = ""
7
+ var icon: String?
8
+ var subActions: [LinkPreviewNativeActionView] = []
7
9
 
8
10
  required init(appContext: AppContext? = nil) {
9
11
  super.init(appContext: appContext)
10
12
  clipsToBounds = true
11
13
  }
14
+
15
+ override func mountChildComponentView(_ childComponentView: UIView, index: Int) {
16
+ if let childActionView = childComponentView as? LinkPreviewNativeActionView {
17
+ subActions.append(childActionView)
18
+ } else {
19
+ print(
20
+ "ExpoRouter: Unknown child component view (\(childComponentView)) mounted to NativeLinkPreviewActionView"
21
+ )
22
+ }
23
+ }
24
+
25
+ override func unmountChildComponentView(_ child: UIView, index: Int) {
26
+ if let childActionView = child as? LinkPreviewNativeActionView {
27
+ subActions.removeAll(where: { $0 == childActionView })
28
+ } else {
29
+ print(
30
+ "ExpoRouter: Unknown child component view (\(child)) unmounted from NativeLinkPreviewActionView"
31
+ )
32
+ }
33
+ }
12
34
  }
@@ -43,6 +43,9 @@ public class LinkPreviewNativeModule: Module {
43
43
  Prop("title") { (view: LinkPreviewNativeActionView, title: String) in
44
44
  view.title = title
45
45
  }
46
+ Prop("icon") { (view: LinkPreviewNativeActionView, icon: String) in
47
+ view.icon = icon
48
+ }
46
49
  }
47
50
 
48
51
  View(NativeLinkPreviewTrigger.self) {}
@@ -25,7 +25,7 @@ class NativeLinkPreviewView: ExpoView, UIContextMenuInteractionDelegate {
25
25
 
26
26
  func setNextScreenId(_ screenId: String) {
27
27
  self.nextScreenId = screenId
28
- linkPreviewNativeNavigation.updatePreloadedView(screenId, with: self)
28
+ linkPreviewNativeNavigation.updatePreloadedView(screenId, with: self)
29
29
  }
30
30
 
31
31
  // MARK: - Children
@@ -138,8 +138,8 @@ class NativeLinkPreviewView: ExpoView, UIContextMenuInteractionDelegate {
138
138
  animator: UIContextMenuInteractionCommitAnimating
139
139
  ) {
140
140
  linkPreviewNativeNavigation.pushPreloadedView()
141
+ onPreviewTapped()
141
142
  animator.addCompletion { [weak self] in
142
- self?.onPreviewTapped()
143
143
  }
144
144
  }
145
145
 
@@ -159,17 +159,36 @@ class NativeLinkPreviewView: ExpoView, UIContextMenuInteractionDelegate {
159
159
  }
160
160
 
161
161
  private func createContextMenu() -> UIMenu {
162
- let uiActions = actions.map { action in
163
- return UIAction(
164
- title: action.title
165
- ) { _ in
166
- self.onActionSelected([
167
- "id": action.id
168
- ])
169
- }
162
+ if actions.count == 1, let menu = convertActionViewToUiAction(actions[0]) as? UIMenu {
163
+ return menu
170
164
  }
165
+ return UIMenu(
166
+ title: "",
167
+ children: actions.map { action in
168
+ self.convertActionViewToUiAction(action)
169
+ }
170
+ )
171
+ }
171
172
 
172
- return UIMenu(title: "", children: uiActions)
173
+ private func convertActionViewToUiAction(_ action: LinkPreviewNativeActionView) -> UIMenuElement {
174
+ if !action.subActions.isEmpty {
175
+ let subActions = action.subActions.map { subAction in
176
+ self.convertActionViewToUiAction(subAction)
177
+ }
178
+ return UIMenu(
179
+ title: action.title,
180
+ image: action.icon.flatMap { UIImage(systemName: $0) },
181
+ children: subActions
182
+ )
183
+ }
184
+ return UIAction(
185
+ title: action.title,
186
+ image: action.icon.flatMap { UIImage(systemName: $0) }
187
+ ) { _ in
188
+ self.onActionSelected([
189
+ "id": action.id
190
+ ])
191
+ }
173
192
  }
174
193
  }
175
194
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-router",
3
- "version": "5.2.0-canary-20250630-547cd82",
3
+ "version": "5.2.0-canary-20250709-136b77f",
4
4
  "description": "Expo Router is a file-based router for React Native and web applications.",
5
5
  "author": "650 Industries, Inc.",
6
6
  "license": "MIT",
@@ -76,9 +76,9 @@
76
76
  ],
77
77
  "peerDependencies": {
78
78
  "@react-navigation/drawer": "^7.3.9",
79
- "expo": "54.0.0-canary-20250630-547cd82",
80
- "expo-constants": "17.1.7-canary-20250630-547cd82",
81
- "expo-linking": "7.1.6-canary-20250630-547cd82",
79
+ "expo": "54.0.0-canary-20250709-136b77f",
80
+ "expo-constants": "17.1.8-canary-20250709-136b77f",
81
+ "expo-linking": "7.1.8-canary-20250709-136b77f",
82
82
  "react-native-reanimated": "*",
83
83
  "react-native-safe-area-context": "*",
84
84
  "react-native-screens": "*"
@@ -105,8 +105,8 @@
105
105
  "tsd": "^0.28.1"
106
106
  },
107
107
  "dependencies": {
108
- "@expo/metro-runtime": "6.0.0-canary-20250630-547cd82",
109
- "@expo/server": "0.6.4-canary-20250630-547cd82",
108
+ "@expo/metro-runtime": "6.0.0-canary-20250709-136b77f",
109
+ "@expo/server": "0.6.4-canary-20250709-136b77f",
110
110
  "@radix-ui/react-slot": "1.2.0",
111
111
  "@react-navigation/bottom-tabs": "^7.3.10",
112
112
  "@react-navigation/native": "^7.1.6",
@@ -118,6 +118,8 @@
118
118
  "schema-utils": "^4.0.1",
119
119
  "semver": "~7.6.3",
120
120
  "server-only": "^0.0.1",
121
- "shallowequal": "^1.1.0"
121
+ "sf-symbols-typescript": "^2.1.0",
122
+ "shallowequal": "^1.1.0",
123
+ "vaul": "^0.9.0"
122
124
  }
123
125
  }