expo-router 5.2.0-canary-20250701-6a945c5 → 5.2.0-canary-20250713-8f814f8

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 (72) 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 +47 -17
  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/Modal.d.ts +2 -0
  37. package/build/modal/Modal.d.ts.map +1 -1
  38. package/build/modal/Modal.js +9 -0
  39. package/build/modal/Modal.js.map +1 -1
  40. package/build/modal/utils.d.ts +3 -0
  41. package/build/modal/utils.d.ts.map +1 -0
  42. package/build/modal/utils.js +14 -0
  43. package/build/modal/utils.js.map +1 -0
  44. package/build/modal/web/ModalStack.web.d.ts +25 -0
  45. package/build/modal/web/ModalStack.web.d.ts.map +1 -0
  46. package/build/modal/web/ModalStack.web.js +86 -0
  47. package/build/modal/web/ModalStack.web.js.map +1 -0
  48. package/build/modal/web/ModalStackRouteDrawer.web.d.ts +14 -0
  49. package/build/modal/web/ModalStackRouteDrawer.web.d.ts.map +1 -0
  50. package/build/modal/web/ModalStackRouteDrawer.web.js +161 -0
  51. package/build/modal/web/ModalStackRouteDrawer.web.js.map +1 -0
  52. package/build/modal/web/TransparentModalStackRouteDrawer.web.d.ts +10 -0
  53. package/build/modal/web/TransparentModalStackRouteDrawer.web.d.ts.map +1 -0
  54. package/build/modal/web/TransparentModalStackRouteDrawer.web.js +28 -0
  55. package/build/modal/web/TransparentModalStackRouteDrawer.web.js.map +1 -0
  56. package/build/modal/web/modalStyles.d.ts +3 -0
  57. package/build/modal/web/modalStyles.d.ts.map +1 -0
  58. package/build/modal/web/modalStyles.js +8 -0
  59. package/build/modal/web/modalStyles.js.map +1 -0
  60. package/build/modal/web/types.d.ts +14 -0
  61. package/build/modal/web/types.d.ts.map +1 -0
  62. package/build/modal/web/types.js +3 -0
  63. package/build/modal/web/types.js.map +1 -0
  64. package/build/modal/web/utils.d.ts +70 -0
  65. package/build/modal/web/utils.d.ts.map +1 -0
  66. package/build/modal/web/utils.js +117 -0
  67. package/build/modal/web/utils.js.map +1 -0
  68. package/ios/ExpoHead.podspec +1 -1
  69. package/ios/LinkPreview/LinkPreviewNativeActionView.swift +22 -0
  70. package/ios/LinkPreview/LinkPreviewNativeModule.swift +3 -0
  71. package/ios/LinkPreview/LinkPreviewNativeView.swift +30 -11
  72. package/package.json +9 -7
@@ -0,0 +1,161 @@
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.ModalStackRouteDrawer = ModalStackRouteDrawer;
8
+ const react_1 = __importDefault(require("react"));
9
+ const vaul_1 = require("vaul");
10
+ const modalStyles_1 = __importDefault(require("./modalStyles"));
11
+ const utils_1 = require("./utils");
12
+ function ModalStackRouteDrawer({ routeKey, options, renderScreen, onDismiss, themeColors, }) {
13
+ const [open, setOpen] = react_1.default.useState(true);
14
+ // Determine sheet vs. modal with an SSR-safe hook. The first render (during
15
+ // hydration) always assumes mobile/sheet to match the server markup; an
16
+ // effect then updates the state after mount if the viewport is desktop.
17
+ const isDesktop = (0, utils_1.useIsDesktop)();
18
+ const isSheet = !isDesktop;
19
+ // Resolve snap points logic.
20
+ const allowed = options.sheetAllowedDetents;
21
+ const isArrayDetents = Array.isArray(allowed);
22
+ const useCustomSnapPoints = isArrayDetents && !(allowed.length === 1 && allowed[0] === 1);
23
+ let snapPoints = useCustomSnapPoints
24
+ ? allowed
25
+ : undefined;
26
+ if (!isSheet) {
27
+ snapPoints = [1];
28
+ }
29
+ const [snap, setSnap] = react_1.default.useState(useCustomSnapPoints && isArrayDetents ? allowed[0] : 1);
30
+ // Update the snap value when custom snap points change.
31
+ react_1.default.useEffect(() => {
32
+ if (isSheet) {
33
+ const next = useCustomSnapPoints && isArrayDetents ? allowed[0] : 1;
34
+ setSnap(next);
35
+ }
36
+ else {
37
+ // Desktop modal always fixed snap at 1
38
+ setSnap(1);
39
+ }
40
+ }, [isSheet, useCustomSnapPoints, isArrayDetents, allowed]);
41
+ // Map react-native-screens ios sheet undimmed logic to Vaul's fadeFromIndex
42
+ const fadeFromIndex = isSheet
43
+ ? options.sheetLargestUndimmedDetentIndex === 'last'
44
+ ? (snapPoints?.length ?? 0)
45
+ : typeof options.sheetLargestUndimmedDetentIndex === 'number'
46
+ ? options.sheetLargestUndimmedDetentIndex + 1
47
+ : 0
48
+ : 0;
49
+ // --- Styling -----------------------------------------------------------
50
+ // Using CSS variables so defaults live in CSS and can be overridden via props.
51
+ const modalStyleVars = {
52
+ backgroundColor: themeColors.background,
53
+ };
54
+ if (!isSheet) {
55
+ if (options.webModalStyle?.width) {
56
+ modalStyleVars['--expo-router-modal-width'] =
57
+ typeof options.webModalStyle.width === 'number'
58
+ ? `${options.webModalStyle.width}px`
59
+ : options.webModalStyle.width;
60
+ modalStyleVars['--expo-router-modal-max-width'] =
61
+ typeof options.webModalStyle.width === 'number'
62
+ ? `${options.webModalStyle.width}px`
63
+ : options.webModalStyle.width;
64
+ // Also set explicit width so browsers that ignore CSS vars in `width` prop still work.
65
+ modalStyleVars.width =
66
+ typeof options.webModalStyle.width === 'number'
67
+ ? `${options.webModalStyle.width}px`
68
+ : options.webModalStyle.width;
69
+ }
70
+ // Min width override
71
+ if (options.webModalStyle?.minWidth) {
72
+ const mw = typeof options.webModalStyle.minWidth === 'number'
73
+ ? `${options.webModalStyle.minWidth}px`
74
+ : options.webModalStyle.minWidth;
75
+ modalStyleVars['--expo-router-modal-min-width'] = mw;
76
+ modalStyleVars.minWidth = mw;
77
+ }
78
+ if (options.webModalStyle?.height) {
79
+ const h = typeof options.webModalStyle.height === 'number'
80
+ ? `${options.webModalStyle.height}px`
81
+ : options.webModalStyle.height;
82
+ modalStyleVars['--expo-router-modal-height'] = h;
83
+ modalStyleVars.maxHeight = h;
84
+ modalStyleVars.height = h;
85
+ modalStyleVars.minHeight = h;
86
+ }
87
+ // Separate min-height override (takes precedence over modalHeight)
88
+ if (options.webModalStyle?.minHeight) {
89
+ const mh = typeof options.webModalStyle.minHeight === 'number'
90
+ ? `${options.webModalStyle.minHeight}px`
91
+ : options.webModalStyle.minHeight;
92
+ modalStyleVars['--expo-router-modal-min-height'] = mh;
93
+ modalStyleVars.minHeight = mh;
94
+ }
95
+ }
96
+ const fitToContents = isSheet && options.sheetAllowedDetents === 'fitToContents';
97
+ if (fitToContents) {
98
+ modalStyleVars.height = 'auto';
99
+ modalStyleVars.minHeight = 'auto';
100
+ // Allow sheet to grow with content but never exceed viewport height
101
+ modalStyleVars.maxHeight = 'calc(100vh)';
102
+ }
103
+ // Apply corner radius (default 10px)
104
+ const radiusValue = options.sheetCornerRadius ?? 10;
105
+ const radiusCss = typeof radiusValue === 'number' ? `${radiusValue}px` : radiusValue;
106
+ if (options.webModalStyle?.border) {
107
+ modalStyleVars['--expo-router-modal-border'] = options.webModalStyle.border;
108
+ }
109
+ if (isSheet) {
110
+ // Only top corners for mobile sheet
111
+ modalStyleVars.borderTopLeftRadius = radiusCss;
112
+ modalStyleVars.borderTopRightRadius = radiusCss;
113
+ // Only apply CSS var override if a custom corner radius was provided
114
+ if (options.sheetCornerRadius) {
115
+ modalStyleVars['--expo-router-modal-border-radius'] = radiusCss;
116
+ }
117
+ }
118
+ else {
119
+ // All corners for desktop modal
120
+ if (options.sheetCornerRadius) {
121
+ modalStyleVars.borderRadius = radiusCss;
122
+ modalStyleVars['--expo-router-modal-border-radius'] = radiusCss;
123
+ }
124
+ }
125
+ // --- End Styling -----------------------------------------------------------
126
+ const handleOpenChange = (open) => {
127
+ if (!open)
128
+ onDismiss();
129
+ };
130
+ // Props that only make sense for sheets
131
+ const sheetProps = isSheet
132
+ ? {
133
+ snapPoints: snapPoints,
134
+ activeSnapPoint: snap,
135
+ setActiveSnapPoint: setSnap,
136
+ fadeFromIndex,
137
+ }
138
+ : {};
139
+ return (<vaul_1.Drawer.Root key={`${routeKey}-${isSheet ? 'sheet' : 'modal'}`} open={open} dismissible={options.gestureEnabled ?? true} onAnimationEnd={handleOpenChange} shouldScaleBackground autoFocus onOpenChange={setOpen} {...sheetProps}>
140
+ <vaul_1.Drawer.Portal>
141
+ <vaul_1.Drawer.Overlay className={modalStyles_1.default.overlay} style={options.webModalStyle?.overlayBackground
142
+ ? {
143
+ '--expo-router-modal-overlay-background': options.webModalStyle.overlayBackground,
144
+ }
145
+ : undefined}/>
146
+ <vaul_1.Drawer.Content aria-describedby="modal-description" className={modalStyles_1.default.drawerContent} style={{
147
+ pointerEvents: 'none',
148
+ ...(fitToContents ? { height: 'auto' } : null),
149
+ }}>
150
+ <div className={modalStyles_1.default.modal} data-presentation={isSheet ? 'formSheet' : 'modal'} style={modalStyleVars}>
151
+ {/* TODO:(@Hirbod) Figure out how to add title and description to the modal for screen readers in a meaningful way */}
152
+ <vaul_1.Drawer.Title about="" aria-describedby="" className={modalStyles_1.default.srOnly}/>
153
+ <vaul_1.Drawer.Description about="" className={modalStyles_1.default.srOnly}/>
154
+ {/* Render the screen content */}
155
+ <div className={modalStyles_1.default.modalBody}>{renderScreen()}</div>
156
+ </div>
157
+ </vaul_1.Drawer.Content>
158
+ </vaul_1.Drawer.Portal>
159
+ </vaul_1.Drawer.Root>);
160
+ }
161
+ //# sourceMappingURL=ModalStackRouteDrawer.web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ModalStackRouteDrawer.web.js","sourceRoot":"","sources":["../../../src/modal/web/ModalStackRouteDrawer.web.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\nimport React from 'react';\nimport { Drawer } from 'vaul';\n\nimport modalStyles from './modalStyles';\nimport { CSSWithVars } from './types';\nimport { useIsDesktop } from './utils';\nimport { ExtendedStackNavigationOptions } from '../../layouts/StackClient';\n\nfunction ModalStackRouteDrawer({\n routeKey,\n options,\n renderScreen,\n onDismiss,\n themeColors,\n}: {\n routeKey: string;\n options: ExtendedStackNavigationOptions;\n renderScreen: () => React.ReactNode;\n onDismiss: () => void;\n themeColors: { card: string; background: string };\n}) {\n const [open, setOpen] = React.useState(true);\n // Determine sheet vs. modal with an SSR-safe hook. The first render (during\n // hydration) always assumes mobile/sheet to match the server markup; an\n // effect then updates the state after mount if the viewport is desktop.\n const isDesktop = useIsDesktop();\n const isSheet = !isDesktop;\n\n // Resolve snap points logic.\n const allowed = options.sheetAllowedDetents;\n\n const isArrayDetents = Array.isArray(allowed);\n const useCustomSnapPoints = isArrayDetents && !(allowed.length === 1 && allowed[0] === 1);\n\n let snapPoints: (number | string)[] | undefined = useCustomSnapPoints\n ? (allowed as (number | string)[])\n : undefined;\n\n if (!isSheet) {\n snapPoints = [1];\n }\n\n const [snap, setSnap] = React.useState<number | string | null>(\n useCustomSnapPoints && isArrayDetents ? allowed[0] : 1\n );\n\n // Update the snap value when custom snap points change.\n React.useEffect(() => {\n if (isSheet) {\n const next = useCustomSnapPoints && isArrayDetents ? allowed[0] : 1;\n setSnap(next);\n } else {\n // Desktop modal always fixed snap at 1\n setSnap(1);\n }\n }, [isSheet, useCustomSnapPoints, isArrayDetents, allowed]);\n\n // Map react-native-screens ios sheet undimmed logic to Vaul's fadeFromIndex\n const fadeFromIndex = isSheet\n ? options.sheetLargestUndimmedDetentIndex === 'last'\n ? (snapPoints?.length ?? 0)\n : typeof options.sheetLargestUndimmedDetentIndex === 'number'\n ? options.sheetLargestUndimmedDetentIndex + 1\n : 0\n : 0;\n\n // --- Styling -----------------------------------------------------------\n\n // Using CSS variables so defaults live in CSS and can be overridden via props.\n const modalStyleVars: CSSWithVars = {\n backgroundColor: themeColors.background,\n };\n\n if (!isSheet) {\n if (options.webModalStyle?.width) {\n modalStyleVars['--expo-router-modal-width'] =\n typeof options.webModalStyle.width === 'number'\n ? `${options.webModalStyle.width}px`\n : options.webModalStyle.width;\n\n modalStyleVars['--expo-router-modal-max-width'] =\n typeof options.webModalStyle.width === 'number'\n ? `${options.webModalStyle.width}px`\n : options.webModalStyle.width;\n\n // Also set explicit width so browsers that ignore CSS vars in `width` prop still work.\n modalStyleVars.width =\n typeof options.webModalStyle.width === 'number'\n ? `${options.webModalStyle.width}px`\n : options.webModalStyle.width;\n }\n\n // Min width override\n if (options.webModalStyle?.minWidth) {\n const mw =\n typeof options.webModalStyle.minWidth === 'number'\n ? `${options.webModalStyle.minWidth}px`\n : options.webModalStyle.minWidth;\n modalStyleVars['--expo-router-modal-min-width'] = mw;\n modalStyleVars.minWidth = mw;\n }\n\n if (options.webModalStyle?.height) {\n const h =\n typeof options.webModalStyle.height === 'number'\n ? `${options.webModalStyle.height}px`\n : options.webModalStyle.height;\n modalStyleVars['--expo-router-modal-height'] = h;\n modalStyleVars.maxHeight = h;\n modalStyleVars.height = h;\n modalStyleVars.minHeight = h;\n }\n\n // Separate min-height override (takes precedence over modalHeight)\n if (options.webModalStyle?.minHeight) {\n const mh =\n typeof options.webModalStyle.minHeight === 'number'\n ? `${options.webModalStyle.minHeight}px`\n : options.webModalStyle.minHeight;\n modalStyleVars['--expo-router-modal-min-height'] = mh;\n modalStyleVars.minHeight = mh;\n }\n }\n\n const fitToContents = isSheet && options.sheetAllowedDetents === 'fitToContents';\n\n if (fitToContents) {\n modalStyleVars.height = 'auto';\n modalStyleVars.minHeight = 'auto';\n // Allow sheet to grow with content but never exceed viewport height\n modalStyleVars.maxHeight = 'calc(100vh)';\n }\n\n // Apply corner radius (default 10px)\n const radiusValue = options.sheetCornerRadius ?? 10;\n const radiusCss = typeof radiusValue === 'number' ? `${radiusValue}px` : radiusValue;\n\n if (options.webModalStyle?.border) {\n modalStyleVars['--expo-router-modal-border'] = options.webModalStyle.border;\n }\n\n if (isSheet) {\n // Only top corners for mobile sheet\n modalStyleVars.borderTopLeftRadius = radiusCss;\n modalStyleVars.borderTopRightRadius = radiusCss;\n\n // Only apply CSS var override if a custom corner radius was provided\n if (options.sheetCornerRadius) {\n modalStyleVars['--expo-router-modal-border-radius'] = radiusCss;\n }\n } else {\n // All corners for desktop modal\n if (options.sheetCornerRadius) {\n modalStyleVars.borderRadius = radiusCss;\n modalStyleVars['--expo-router-modal-border-radius'] = radiusCss;\n }\n }\n // --- End Styling -----------------------------------------------------------\n\n const handleOpenChange = (open: boolean) => {\n if (!open) onDismiss();\n };\n\n // Props that only make sense for sheets\n const sheetProps = isSheet\n ? {\n snapPoints: snapPoints as (number | string)[],\n activeSnapPoint: snap,\n setActiveSnapPoint: setSnap,\n fadeFromIndex,\n }\n : {};\n\n return (\n <Drawer.Root\n key={`${routeKey}-${isSheet ? 'sheet' : 'modal'}`}\n open={open}\n dismissible={options.gestureEnabled ?? true}\n onAnimationEnd={handleOpenChange}\n shouldScaleBackground\n autoFocus\n onOpenChange={setOpen}\n {...sheetProps}>\n <Drawer.Portal>\n <Drawer.Overlay\n className={modalStyles.overlay}\n style={\n options.webModalStyle?.overlayBackground\n ? ({\n '--expo-router-modal-overlay-background': options.webModalStyle.overlayBackground,\n } as React.CSSProperties)\n : undefined\n }\n />\n <Drawer.Content\n aria-describedby=\"modal-description\"\n className={modalStyles.drawerContent}\n style={{\n pointerEvents: 'none',\n ...(fitToContents ? { height: 'auto' } : null),\n }}>\n <div\n className={modalStyles.modal}\n data-presentation={isSheet ? 'formSheet' : 'modal'}\n style={modalStyleVars}>\n {/* TODO:(@Hirbod) Figure out how to add title and description to the modal for screen readers in a meaningful way */}\n <Drawer.Title about=\"\" aria-describedby=\"\" className={modalStyles.srOnly} />\n <Drawer.Description about=\"\" className={modalStyles.srOnly} />\n {/* Render the screen content */}\n <div className={modalStyles.modalBody}>{renderScreen()}</div>\n </div>\n </Drawer.Content>\n </Drawer.Portal>\n </Drawer.Root>\n );\n}\n\nexport { ModalStackRouteDrawer };\n"]}
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import { ExtendedStackNavigationOptions } from '../../layouts/StackClient';
3
+ declare function TransparentModalStackRouteDrawer({ routeKey, options, renderScreen, onDismiss, }: {
4
+ routeKey: string;
5
+ options: ExtendedStackNavigationOptions;
6
+ renderScreen: () => React.ReactNode;
7
+ onDismiss: () => void;
8
+ }): React.JSX.Element;
9
+ export { TransparentModalStackRouteDrawer };
10
+ //# sourceMappingURL=TransparentModalStackRouteDrawer.web.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TransparentModalStackRouteDrawer.web.d.ts","sourceRoot":"","sources":["../../../src/modal/web/TransparentModalStackRouteDrawer.web.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,OAAO,EAAE,8BAA8B,EAAE,MAAM,2BAA2B,CAAC;AAE3E,iBAAS,gCAAgC,CAAC,EACxC,QAAQ,EACR,OAAO,EACP,YAAY,EACZ,SAAS,GACV,EAAE;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,8BAA8B,CAAC;IACxC,YAAY,EAAE,MAAM,KAAK,CAAC,SAAS,CAAC;IACpC,SAAS,EAAE,MAAM,IAAI,CAAC;CACvB,qBAuBA;AAED,OAAO,EAAE,gCAAgC,EAAE,CAAC"}
@@ -0,0 +1,28 @@
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.TransparentModalStackRouteDrawer = TransparentModalStackRouteDrawer;
8
+ const react_1 = __importDefault(require("react"));
9
+ const vaul_1 = require("vaul");
10
+ const modalStyles_1 = __importDefault(require("./modalStyles"));
11
+ function TransparentModalStackRouteDrawer({ routeKey, options, renderScreen, onDismiss, }) {
12
+ const handleOpenChange = (open) => {
13
+ if (!open)
14
+ onDismiss();
15
+ };
16
+ return (<vaul_1.Drawer.Root defaultOpen autoFocus key={`${routeKey}-transparent`} dismissible={options.gestureEnabled ?? false} onAnimationEnd={handleOpenChange}>
17
+ <vaul_1.Drawer.Portal>
18
+ <vaul_1.Drawer.Content className={modalStyles_1.default.transparentDrawerContent}>
19
+ {/* TODO:(@Hirbod) Figure out how to add title and description to the modal for screen readers in a meaningful way */}
20
+ <vaul_1.Drawer.Title about="" aria-describedby="" className={modalStyles_1.default.srOnly}/>
21
+ <vaul_1.Drawer.Description about="" className={modalStyles_1.default.srOnly}/>
22
+ {/* Render the screen content */}
23
+ <div className={modalStyles_1.default.modalBody}>{renderScreen()}</div>
24
+ </vaul_1.Drawer.Content>
25
+ </vaul_1.Drawer.Portal>
26
+ </vaul_1.Drawer.Root>);
27
+ }
28
+ //# sourceMappingURL=TransparentModalStackRouteDrawer.web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TransparentModalStackRouteDrawer.web.js","sourceRoot":"","sources":["../../../src/modal/web/TransparentModalStackRouteDrawer.web.tsx"],"names":[],"mappings":";AAAA,YAAY,CAAC;;;;;AA0CJ,4EAAgC;AAzCzC,kDAA0B;AAC1B,+BAA8B;AAE9B,gEAAwC;AAGxC,SAAS,gCAAgC,CAAC,EACxC,QAAQ,EACR,OAAO,EACP,YAAY,EACZ,SAAS,GAMV;IACC,MAAM,gBAAgB,GAAG,CAAC,IAAa,EAAE,EAAE;QACzC,IAAI,CAAC,IAAI;YAAE,SAAS,EAAE,CAAC;IACzB,CAAC,CAAC;IAEF,OAAO,CACL,CAAC,aAAM,CAAC,IAAI,CACV,WAAW,CACX,SAAS,CACT,GAAG,CAAC,CAAC,GAAG,QAAQ,cAAc,CAAC,CAC/B,WAAW,CAAC,CAAC,OAAO,CAAC,cAAc,IAAI,KAAK,CAAC,CAC7C,cAAc,CAAC,CAAC,gBAAgB,CAAC,CACjC;MAAA,CAAC,aAAM,CAAC,MAAM,CACZ;QAAA,CAAC,aAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,qBAAW,CAAC,wBAAwB,CAAC,CAC9D;UAAA,CAAC,oHAAoH,CACrH;UAAA,CAAC,aAAM,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,gBAAgB,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,qBAAW,CAAC,MAAM,CAAC,EACzE;UAAA,CAAC,aAAM,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,qBAAW,CAAC,MAAM,CAAC,EAC3D;UAAA,CAAC,+BAA+B,CAChC;UAAA,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,qBAAW,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY,EAAE,CAAC,EAAE,GAAG,CAC9D;QAAA,EAAE,aAAM,CAAC,OAAO,CAClB;MAAA,EAAE,aAAM,CAAC,MAAM,CACjB;IAAA,EAAE,aAAM,CAAC,IAAI,CAAC,CACf,CAAC;AACJ,CAAC","sourcesContent":["'use client';\nimport React from 'react';\nimport { Drawer } from 'vaul';\n\nimport modalStyles from './modalStyles';\nimport { ExtendedStackNavigationOptions } from '../../layouts/StackClient';\n\nfunction TransparentModalStackRouteDrawer({\n routeKey,\n options,\n renderScreen,\n onDismiss,\n}: {\n routeKey: string;\n options: ExtendedStackNavigationOptions;\n renderScreen: () => React.ReactNode;\n onDismiss: () => void;\n}) {\n const handleOpenChange = (open: boolean) => {\n if (!open) onDismiss();\n };\n\n return (\n <Drawer.Root\n defaultOpen\n autoFocus\n key={`${routeKey}-transparent`}\n dismissible={options.gestureEnabled ?? false}\n onAnimationEnd={handleOpenChange}>\n <Drawer.Portal>\n <Drawer.Content className={modalStyles.transparentDrawerContent}>\n {/* TODO:(@Hirbod) Figure out how to add title and description to the modal for screen readers in a meaningful way */}\n <Drawer.Title about=\"\" aria-describedby=\"\" className={modalStyles.srOnly} />\n <Drawer.Description about=\"\" className={modalStyles.srOnly} />\n {/* Render the screen content */}\n <div className={modalStyles.modalBody}>{renderScreen()}</div>\n </Drawer.Content>\n </Drawer.Portal>\n </Drawer.Root>\n );\n}\n\nexport { TransparentModalStackRouteDrawer };\n"]}
@@ -0,0 +1,3 @@
1
+ import styles from '../../../assets/modal.module.css';
2
+ export default styles;
3
+ //# sourceMappingURL=modalStyles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modalStyles.d.ts","sourceRoot":"","sources":["../../../src/modal/web/modalStyles.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,kCAAkC,CAAC;AAEtD,eAAe,MAAM,CAAC"}
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const modal_module_css_1 = __importDefault(require("../../../assets/modal.module.css"));
7
+ exports.default = modal_module_css_1.default;
8
+ //# sourceMappingURL=modalStyles.js.map
@@ -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