@onekeyfe/react-native-native-overlay 3.0.162-alpha.268

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 (163) hide show
  1. package/LICENSE +21 -0
  2. package/NativeOverlay.podspec +30 -0
  3. package/README.md +35 -0
  4. package/android/build.gradle +82 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayAnimation.kt +176 -0
  7. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayEntryRootView.kt +415 -0
  8. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayEvents.kt +38 -0
  9. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayHost.kt +174 -0
  10. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayPackage.kt +16 -0
  11. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayPageHostView.kt +122 -0
  12. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayPageHostViewManager.kt +40 -0
  13. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayView.kt +465 -0
  14. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayViewManager.kt +156 -0
  15. package/android/src/main/jni/CMakeLists.txt +44 -0
  16. package/android/src/main/jni/RNCNativeOverlay.h +19 -0
  17. package/common/cpp/react/renderer/components/RNCNativeOverlay/NativeOverlayComponentDescriptor.h +12 -0
  18. package/common/cpp/react/renderer/components/RNCNativeOverlay/NativeOverlayShadowNode.cpp +12 -0
  19. package/common/cpp/react/renderer/components/RNCNativeOverlay/NativeOverlayShadowNode.h +41 -0
  20. package/common/cpp/react/renderer/components/RNCNativeOverlay/NativeOverlayState.h +40 -0
  21. package/docs/SPEC.md +693 -0
  22. package/ios/NativeOverlayAnimation.swift +160 -0
  23. package/ios/NativeOverlayContainerView.swift +688 -0
  24. package/ios/NativeOverlayKeyboard.swift +88 -0
  25. package/ios/NativeOverlayPageHostView.swift +98 -0
  26. package/ios/NativeOverlaySheetController.swift +391 -0
  27. package/ios/NativeOverlayWindowManager.swift +194 -0
  28. package/ios/RCTNativeOverlayComponentView.h +11 -0
  29. package/ios/RCTNativeOverlayComponentView.mm +193 -0
  30. package/ios/RCTNativeOverlayPageHostComponentView.h +11 -0
  31. package/ios/RCTNativeOverlayPageHostComponentView.mm +64 -0
  32. package/lib/module/NativeOverlayNativeComponent.ts +61 -0
  33. package/lib/module/NativeOverlayPageHostNativeComponent.ts +13 -0
  34. package/lib/module/OverlayErrors.js +6 -0
  35. package/lib/module/OverlayErrors.js.map +1 -0
  36. package/lib/module/OverlayLevelContext.js +27 -0
  37. package/lib/module/OverlayLevelContext.js.map +1 -0
  38. package/lib/module/OverlayLevels.js +33 -0
  39. package/lib/module/OverlayLevels.js.map +1 -0
  40. package/lib/module/OverlayPageHost.js +24 -0
  41. package/lib/module/OverlayPageHost.js.map +1 -0
  42. package/lib/module/OverlayPageHost.web.js +49 -0
  43. package/lib/module/OverlayPageHost.web.js.map +1 -0
  44. package/lib/module/OverlayPageScope.js +41 -0
  45. package/lib/module/OverlayPageScope.js.map +1 -0
  46. package/lib/module/OverlayStore.js +305 -0
  47. package/lib/module/OverlayStore.js.map +1 -0
  48. package/lib/module/OverlayView.js +141 -0
  49. package/lib/module/OverlayView.js.map +1 -0
  50. package/lib/module/OverlayView.web.js +290 -0
  51. package/lib/module/OverlayView.web.js.map +1 -0
  52. package/lib/module/OverlayViewTypes.js +4 -0
  53. package/lib/module/OverlayViewTypes.js.map +1 -0
  54. package/lib/module/animation/motionPresets.js +57 -0
  55. package/lib/module/animation/motionPresets.js.map +1 -0
  56. package/lib/module/animation/platformMotionPresets.js +4 -0
  57. package/lib/module/animation/platformMotionPresets.js.map +1 -0
  58. package/lib/module/animation/platformMotionPresets.web.js +4 -0
  59. package/lib/module/animation/platformMotionPresets.web.js.map +1 -0
  60. package/lib/module/animation/resolveAnimation.js +80 -0
  61. package/lib/module/animation/resolveAnimation.js.map +1 -0
  62. package/lib/module/animation/spring.js +69 -0
  63. package/lib/module/animation/spring.js.map +1 -0
  64. package/lib/module/animation/types.js +2 -0
  65. package/lib/module/animation/types.js.map +1 -0
  66. package/lib/module/index.js +16 -0
  67. package/lib/module/index.js.map +1 -0
  68. package/lib/module/package.json +1 -0
  69. package/lib/module/types.js +2 -0
  70. package/lib/module/types.js.map +1 -0
  71. package/lib/module/useOverlayController.js +127 -0
  72. package/lib/module/useOverlayController.js.map +1 -0
  73. package/lib/module/useSuspendedPageOwners.js +16 -0
  74. package/lib/module/useSuspendedPageOwners.js.map +1 -0
  75. package/lib/module/web/animateTransition.js +115 -0
  76. package/lib/module/web/animateTransition.js.map +1 -0
  77. package/lib/module/web/overlayLayers.js +147 -0
  78. package/lib/module/web/overlayLayers.js.map +1 -0
  79. package/lib/module/web/useKeyboardShift.js +110 -0
  80. package/lib/module/web/useKeyboardShift.js.map +1 -0
  81. package/lib/module/web/useSheetDrag.js +126 -0
  82. package/lib/module/web/useSheetDrag.js.map +1 -0
  83. package/lib/typescript/package.json +1 -0
  84. package/lib/typescript/src/NativeOverlayNativeComponent.d.ts +41 -0
  85. package/lib/typescript/src/NativeOverlayNativeComponent.d.ts.map +1 -0
  86. package/lib/typescript/src/NativeOverlayPageHostNativeComponent.d.ts +9 -0
  87. package/lib/typescript/src/NativeOverlayPageHostNativeComponent.d.ts.map +1 -0
  88. package/lib/typescript/src/OverlayErrors.d.ts +4 -0
  89. package/lib/typescript/src/OverlayErrors.d.ts.map +1 -0
  90. package/lib/typescript/src/OverlayLevelContext.d.ts +12 -0
  91. package/lib/typescript/src/OverlayLevelContext.d.ts.map +1 -0
  92. package/lib/typescript/src/OverlayLevels.d.ts +13 -0
  93. package/lib/typescript/src/OverlayLevels.d.ts.map +1 -0
  94. package/lib/typescript/src/OverlayPageHost.d.ts +9 -0
  95. package/lib/typescript/src/OverlayPageHost.d.ts.map +1 -0
  96. package/lib/typescript/src/OverlayPageHost.web.d.ts +11 -0
  97. package/lib/typescript/src/OverlayPageHost.web.d.ts.map +1 -0
  98. package/lib/typescript/src/OverlayPageScope.d.ts +17 -0
  99. package/lib/typescript/src/OverlayPageScope.d.ts.map +1 -0
  100. package/lib/typescript/src/OverlayStore.d.ts +72 -0
  101. package/lib/typescript/src/OverlayStore.d.ts.map +1 -0
  102. package/lib/typescript/src/OverlayView.d.ts +3 -0
  103. package/lib/typescript/src/OverlayView.d.ts.map +1 -0
  104. package/lib/typescript/src/OverlayView.web.d.ts +3 -0
  105. package/lib/typescript/src/OverlayView.web.d.ts.map +1 -0
  106. package/lib/typescript/src/OverlayViewTypes.d.ts +68 -0
  107. package/lib/typescript/src/OverlayViewTypes.d.ts.map +1 -0
  108. package/lib/typescript/src/animation/motionPresets.d.ts +13 -0
  109. package/lib/typescript/src/animation/motionPresets.d.ts.map +1 -0
  110. package/lib/typescript/src/animation/platformMotionPresets.d.ts +2 -0
  111. package/lib/typescript/src/animation/platformMotionPresets.d.ts.map +1 -0
  112. package/lib/typescript/src/animation/platformMotionPresets.web.d.ts +2 -0
  113. package/lib/typescript/src/animation/platformMotionPresets.web.d.ts.map +1 -0
  114. package/lib/typescript/src/animation/resolveAnimation.d.ts +23 -0
  115. package/lib/typescript/src/animation/resolveAnimation.d.ts.map +1 -0
  116. package/lib/typescript/src/animation/spring.d.ts +24 -0
  117. package/lib/typescript/src/animation/spring.d.ts.map +1 -0
  118. package/lib/typescript/src/animation/types.d.ts +56 -0
  119. package/lib/typescript/src/animation/types.d.ts.map +1 -0
  120. package/lib/typescript/src/index.d.ts +18 -0
  121. package/lib/typescript/src/index.d.ts.map +1 -0
  122. package/lib/typescript/src/types.d.ts +59 -0
  123. package/lib/typescript/src/types.d.ts.map +1 -0
  124. package/lib/typescript/src/useOverlayController.d.ts +29 -0
  125. package/lib/typescript/src/useOverlayController.d.ts.map +1 -0
  126. package/lib/typescript/src/useSuspendedPageOwners.d.ts +7 -0
  127. package/lib/typescript/src/useSuspendedPageOwners.d.ts.map +1 -0
  128. package/lib/typescript/src/web/animateTransition.d.ts +18 -0
  129. package/lib/typescript/src/web/animateTransition.d.ts.map +1 -0
  130. package/lib/typescript/src/web/overlayLayers.d.ts +10 -0
  131. package/lib/typescript/src/web/overlayLayers.d.ts.map +1 -0
  132. package/lib/typescript/src/web/useKeyboardShift.d.ts +29 -0
  133. package/lib/typescript/src/web/useKeyboardShift.d.ts.map +1 -0
  134. package/lib/typescript/src/web/useSheetDrag.d.ts +13 -0
  135. package/lib/typescript/src/web/useSheetDrag.d.ts.map +1 -0
  136. package/package.json +188 -0
  137. package/react-native.config.js +16 -0
  138. package/src/NativeOverlayNativeComponent.ts +61 -0
  139. package/src/NativeOverlayPageHostNativeComponent.ts +13 -0
  140. package/src/OverlayErrors.ts +3 -0
  141. package/src/OverlayLevelContext.tsx +32 -0
  142. package/src/OverlayLevels.ts +40 -0
  143. package/src/OverlayPageHost.tsx +21 -0
  144. package/src/OverlayPageHost.web.tsx +53 -0
  145. package/src/OverlayPageScope.tsx +42 -0
  146. package/src/OverlayStore.ts +401 -0
  147. package/src/OverlayView.tsx +165 -0
  148. package/src/OverlayView.web.tsx +356 -0
  149. package/src/OverlayViewTypes.ts +81 -0
  150. package/src/animation/motionPresets.ts +52 -0
  151. package/src/animation/platformMotionPresets.ts +1 -0
  152. package/src/animation/platformMotionPresets.web.ts +1 -0
  153. package/src/animation/resolveAnimation.ts +103 -0
  154. package/src/animation/spring.ts +80 -0
  155. package/src/animation/types.ts +71 -0
  156. package/src/index.ts +55 -0
  157. package/src/types.ts +79 -0
  158. package/src/useOverlayController.ts +199 -0
  159. package/src/useSuspendedPageOwners.ts +26 -0
  160. package/src/web/animateTransition.ts +137 -0
  161. package/src/web/overlayLayers.ts +182 -0
  162. package/src/web/useKeyboardShift.ts +130 -0
  163. package/src/web/useSheetDrag.ts +143 -0
@@ -0,0 +1,356 @@
1
+ import { useEffect, useId, useLayoutEffect, useRef } from 'react';
2
+ import type { ReactNode, SyntheticEvent } from 'react';
3
+
4
+ import { createPortal } from 'react-dom';
5
+ import { StyleSheet, View } from 'react-native';
6
+
7
+ import { OverlayLevelProvider } from './OverlayLevelContext';
8
+ import { OVERLAY_LEVEL_ORDER, isBlockingLevel } from './OverlayLevels';
9
+ import { OVERLAY_OWNER_ATTRIBUTE } from './OverlayPageHost.web';
10
+ import { useOverlayController } from './useOverlayController';
11
+ import { animateBackdrop, animateTransition } from './web/animateTransition';
12
+ import {
13
+ ENTRY_ATTRIBUTE,
14
+ getOverlayLayerRoot,
15
+ getOverlayPageHost,
16
+ registerOverlayDismissRequester,
17
+ scheduleOverlayInertSync,
18
+ } from './web/overlayLayers';
19
+ import { useKeyboardShift } from './web/useKeyboardShift';
20
+ import { useSheetDrag } from './web/useSheetDrag';
21
+
22
+ import type { IResolvedOverlayAnimation } from './animation/resolveAnimation';
23
+ import type {
24
+ IOverlayRequestDismissReason,
25
+ IOverlaySheetOptions,
26
+ IOverlayViewProps,
27
+ } from './OverlayViewTypes';
28
+
29
+ const DEFAULT_BACKDROP_COLOR = 'rgba(0, 0, 0, 0.4)';
30
+ const DEFAULT_SHEET_MAX_HEIGHT = '92vh';
31
+ const DEFAULT_SHEET_CORNER_RADIUS = 24;
32
+
33
+ // Portaled content still bubbles React events to the caller's ancestors;
34
+ // stop them at the entry so presses cannot reach the page underneath.
35
+ // Mouse and touch events must keep bubbling natively: react-native-web's
36
+ // responder system (PanResponder, gesture responders) listens for them on
37
+ // `document`, and a React stopPropagation also stops the native event.
38
+ const stopPropagation = (event: SyntheticEvent) => event.stopPropagation();
39
+ const ISOLATED_EVENT_HANDLERS = {
40
+ onClick: stopPropagation,
41
+ onPointerDown: stopPropagation,
42
+ onPointerUp: stopPropagation,
43
+ onKeyDown: stopPropagation,
44
+ };
45
+
46
+ interface IWebOverlayEntryProps {
47
+ entryId: string;
48
+ /** Closed but kept mounted: hidden and inert. */
49
+ parked?: boolean;
50
+ stackOrder: number;
51
+ presented: boolean;
52
+ animation: IResolvedOverlayAnimation;
53
+ blocking: boolean;
54
+ backdropColor: string | undefined;
55
+ dismissOnBackdropPress: boolean;
56
+ onPresented: () => void;
57
+ onDismissed: () => void;
58
+ onRequestDismiss: (reason: IOverlayRequestDismissReason) => void;
59
+ /** Set for `presentation="sheet"`. */
60
+ sheet: IOverlaySheetOptions | undefined;
61
+ /** Page scope: stacking inside the page host and the owning page. */
62
+ zIndex: number;
63
+ ownerKey: string | undefined;
64
+ testID?: string;
65
+ children?: ReactNode;
66
+ }
67
+
68
+ function WebOverlayEntry({
69
+ entryId,
70
+ parked = false,
71
+ stackOrder,
72
+ presented,
73
+ animation,
74
+ blocking,
75
+ backdropColor,
76
+ dismissOnBackdropPress,
77
+ onPresented,
78
+ onDismissed,
79
+ onRequestDismiss,
80
+ sheet,
81
+ zIndex,
82
+ ownerKey,
83
+ testID,
84
+ children,
85
+ }: IWebOverlayEntryProps) {
86
+ const sheetRef = useRef<HTMLDivElement>(null);
87
+ // A pan dismissal already moved the sheet off screen; skip the exit run.
88
+ const pannedAwayRef = useRef(false);
89
+ const isSheet = !!sheet;
90
+ const entryRef = useRef<HTMLDivElement>(null);
91
+ const contentRef = useRef<HTMLDivElement>(null);
92
+ const keyboardLayerRef = useRef<HTMLDivElement>(null);
93
+ const backdropRef = useRef<HTMLDivElement>(null);
94
+ const directionRef = useRef<'in' | 'out' | undefined>(undefined);
95
+ const restoreFocusRef = useRef<HTMLElement | null>(null);
96
+ const callbacksRef = useRef({ onPresented, onDismissed });
97
+ callbacksRef.current = { onPresented, onDismissed };
98
+
99
+ useLayoutEffect(() => {
100
+ const content = contentRef.current;
101
+ if (!content) {
102
+ return;
103
+ }
104
+ const direction = presented ? 'in' : 'out';
105
+ if (directionRef.current === direction) {
106
+ return;
107
+ }
108
+ const neverEntered = directionRef.current === undefined;
109
+ directionRef.current = direction;
110
+ scheduleOverlayInertSync();
111
+ if (direction === 'out' && (neverEntered || pannedAwayRef.current)) {
112
+ callbacksRef.current.onDismissed();
113
+ return;
114
+ }
115
+ if (direction === 'in' && blocking) {
116
+ restoreFocusRef.current = document.activeElement as HTMLElement | null;
117
+ entryRef.current?.focus({ preventScroll: true });
118
+ }
119
+ content.getAnimations().forEach((running) => running.cancel());
120
+ backdropRef.current?.getAnimations().forEach((running) => running.cancel());
121
+ // A sheet is measured as a whole; other presentations by the children
122
+ // of the full-window React root.
123
+ const contentRoot = isSheet
124
+ ? content
125
+ : (content.firstElementChild as HTMLElement | null) ?? content;
126
+ const transition = direction === 'in' ? animation.enter : animation.exit;
127
+ const running = [
128
+ animateTransition(content, contentRoot, transition, direction),
129
+ backdropRef.current
130
+ ? animateBackdrop(
131
+ backdropRef.current,
132
+ animation.backdrop.motion,
133
+ direction
134
+ )
135
+ : undefined,
136
+ ].filter((item): item is Animation => !!item);
137
+ void Promise.all(running.map((item) => item.finished))
138
+ .then(() => {
139
+ if (directionRef.current !== direction) {
140
+ return;
141
+ }
142
+ if (direction === 'in') {
143
+ callbacksRef.current.onPresented();
144
+ } else {
145
+ restoreFocusRef.current?.focus?.({ preventScroll: true });
146
+ restoreFocusRef.current = null;
147
+ callbacksRef.current.onDismissed();
148
+ }
149
+ })
150
+ .catch(() => {
151
+ // Cancelled by a newer transition; that run reports instead.
152
+ });
153
+ }, [presented, animation, blocking, isSheet]);
154
+
155
+ useEffect(
156
+ () => registerOverlayDismissRequester(entryId, onRequestDismiss),
157
+ [entryId, onRequestDismiss]
158
+ );
159
+
160
+ // A plain DOM listener: the backdrop is a pointer target only; keyboard
161
+ // users dismiss with Escape through the layer manager.
162
+ useEffect(() => {
163
+ const backdropNode = backdropRef.current;
164
+ if (!backdropNode || !dismissOnBackdropPress) {
165
+ return;
166
+ }
167
+ const onBackdropClick = () => onRequestDismiss('backdrop');
168
+ backdropNode.addEventListener('click', onBackdropClick);
169
+ return () => backdropNode.removeEventListener('click', onBackdropClick);
170
+ }, [dismissOnBackdropPress, onRequestDismiss]);
171
+
172
+ useKeyboardShift({
173
+ layerRef: keyboardLayerRef,
174
+ sheetRef,
175
+ contentRef,
176
+ isSheet,
177
+ enabled: presented,
178
+ });
179
+
180
+ useSheetDrag({
181
+ sheetRef,
182
+ backdropRef,
183
+ enabled: !!sheet && (sheet.dismissOnPanDown ?? true) && presented,
184
+ onDismissByPan: () => {
185
+ pannedAwayRef.current = true;
186
+ onRequestDismiss('pan');
187
+ },
188
+ });
189
+
190
+ const showBackdrop = !!backdropColor || blocking;
191
+ const cornerRadius = sheet?.cornerRadius ?? DEFAULT_SHEET_CORNER_RADIUS;
192
+
193
+ return (
194
+ <div
195
+ ref={entryRef}
196
+ {...{ [ENTRY_ATTRIBUTE]: '' }}
197
+ {...(ownerKey ? { [OVERLAY_OWNER_ATTRIBUTE]: ownerKey } : {})}
198
+ data-stack-order={stackOrder}
199
+ data-testid={testID}
200
+ role={blocking ? 'dialog' : undefined}
201
+ aria-modal={blocking || undefined}
202
+ tabIndex={-1}
203
+ inert={parked || undefined}
204
+ aria-hidden={parked || undefined}
205
+ style={{
206
+ position: 'absolute',
207
+ inset: 0,
208
+ zIndex,
209
+ pointerEvents: 'none',
210
+ outline: 'none',
211
+ visibility: parked ? 'hidden' : undefined,
212
+ }}
213
+ {...ISOLATED_EVENT_HANDLERS}
214
+ >
215
+ {showBackdrop ? (
216
+ <div
217
+ ref={backdropRef}
218
+ aria-hidden
219
+ style={{
220
+ position: 'absolute',
221
+ inset: 0,
222
+ opacity: 0,
223
+ background: backdropColor ?? 'transparent',
224
+ pointerEvents: blocking ? 'auto' : 'none',
225
+ }}
226
+ />
227
+ ) : null}
228
+ {/* The keyboard lift lives on its own layer so it composes with the
229
+ enter / exit animation on the content below. It only carries a
230
+ transform while lifted: a transform would re-anchor fixed children. */}
231
+ <div
232
+ ref={keyboardLayerRef}
233
+ style={{
234
+ position: 'absolute',
235
+ inset: 0,
236
+ pointerEvents: 'none',
237
+ transition: 'transform 250ms ease-out',
238
+ }}
239
+ >
240
+ <div
241
+ ref={contentRef}
242
+ style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}
243
+ >
244
+ {sheet ? (
245
+ <div
246
+ ref={sheetRef}
247
+ style={{
248
+ position: 'absolute',
249
+ left: 0,
250
+ right: 0,
251
+ bottom: 0,
252
+ height: sheet.height,
253
+ maxHeight: sheet.maxHeight ?? DEFAULT_SHEET_MAX_HEIGHT,
254
+ display: 'flex',
255
+ flexDirection: 'column',
256
+ overflow: 'hidden',
257
+ pointerEvents: 'auto',
258
+ touchAction: 'pan-y',
259
+ background: sheet.backgroundColor
260
+ ? String(sheet.backgroundColor)
261
+ : undefined,
262
+ borderTopLeftRadius: cornerRadius,
263
+ borderTopRightRadius: cornerRadius,
264
+ }}
265
+ >
266
+ {sheet.showHandle ? (
267
+ <div
268
+ style={{
269
+ alignSelf: 'center',
270
+ width: 36,
271
+ height: 5,
272
+ marginTop: 8,
273
+ borderRadius: 2.5,
274
+ background: 'rgba(0, 0, 0, 0.2)',
275
+ flexShrink: 0,
276
+ }}
277
+ />
278
+ ) : null}
279
+ <View style={{ flexShrink: 1 }}>{children}</View>
280
+ </div>
281
+ ) : (
282
+ <View pointerEvents="box-none" style={StyleSheet.absoluteFill}>
283
+ {children}
284
+ </View>
285
+ )}
286
+ </div>
287
+ </div>
288
+ </div>
289
+ );
290
+ }
291
+
292
+ export function OverlayView(props: IOverlayViewProps) {
293
+ const {
294
+ level = 'modal',
295
+ presentation = 'center',
296
+ backdrop,
297
+ blocking,
298
+ sheet,
299
+ keepContentMounted = false,
300
+ children,
301
+ testID,
302
+ } = props;
303
+ const parkedId = useId();
304
+ const {
305
+ entry,
306
+ mounted,
307
+ presented,
308
+ animation,
309
+ onHostPresented,
310
+ onHostDismissed,
311
+ onHostRequestDismiss,
312
+ scope,
313
+ hostKey,
314
+ ownerKey,
315
+ } = useOverlayController(props);
316
+
317
+ // Closed with `keepContentMounted`: the same entry stays in the layer,
318
+ // hidden and inert, so the content keeps its state and can be measured.
319
+ const parked = !mounted || !entry;
320
+ if (parked && !keepContentMounted) {
321
+ return null;
322
+ }
323
+
324
+ const pageHost =
325
+ !parked && scope === 'page' ? getOverlayPageHost(hostKey) : undefined;
326
+ return createPortal(
327
+ <WebOverlayEntry
328
+ key="entry"
329
+ parked={parked}
330
+ entryId={entry?.id ?? parkedId}
331
+ stackOrder={entry?.seq ?? 0}
332
+ presented={!parked && presented}
333
+ animation={animation}
334
+ blocking={blocking ?? isBlockingLevel(level)}
335
+ backdropColor={
336
+ backdrop ? String(backdrop.color ?? DEFAULT_BACKDROP_COLOR) : undefined
337
+ }
338
+ dismissOnBackdropPress={backdrop ? !!backdrop.dismissOnPress : false}
339
+ onPresented={onHostPresented}
340
+ onDismissed={onHostDismissed}
341
+ onRequestDismiss={onHostRequestDismiss}
342
+ sheet={presentation === 'sheet' ? sheet ?? {} : undefined}
343
+ // Inside a page host all levels share one stacking context.
344
+ zIndex={
345
+ pageHost
346
+ ? OVERLAY_LEVEL_ORDER[level] * 1000 + (entry?.seq ?? 0)
347
+ : entry?.seq ?? 0
348
+ }
349
+ ownerKey={pageHost ? ownerKey : undefined}
350
+ testID={testID}
351
+ >
352
+ <OverlayLevelProvider value={level}>{children}</OverlayLevelProvider>
353
+ </WebOverlayEntry>,
354
+ pageHost ?? getOverlayLayerRoot(level)
355
+ );
356
+ }
@@ -0,0 +1,81 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ import type {
4
+ IOverlayAnimation,
5
+ IOverlayPresentation,
6
+ } from './animation/types';
7
+ import type {
8
+ IOverlayDismissReason,
9
+ IOverlayLevel,
10
+ IOverlayScope,
11
+ IOverlayStatusBarStyle,
12
+ IOverlayStrategy,
13
+ } from './types';
14
+ import type { ColorValue } from 'react-native';
15
+
16
+ export type IOverlayRequestDismissReason = 'back' | 'backdrop' | 'pan';
17
+
18
+ export interface IOverlayBackdrop {
19
+ /** Include alpha; e.g. the theme `$bgBackdrop`. */
20
+ color?: ColorValue;
21
+ dismissOnPress?: boolean;
22
+ }
23
+
24
+ export interface IOverlaySheetOptions {
25
+ /** Fixed height; when omitted the sheet fits its content. */
26
+ height?: number;
27
+ /** Defaults to 92% of the window height. */
28
+ maxHeight?: number;
29
+ cornerRadius?: number;
30
+ showHandle?: boolean;
31
+ backgroundColor?: ColorValue;
32
+ /** Swipe down to close. A pan dismissal cannot be vetoed. */
33
+ dismissOnPanDown?: boolean;
34
+ }
35
+
36
+ export interface IOverlayViewProps {
37
+ /** Desired open state. The overlay may still be queued by its strategy. */
38
+ visible: boolean;
39
+ /**
40
+ * `page` renders in the root-route `OverlayPageHost` and hides while the
41
+ * owning page is covered. Keys default to the nearest scope providers.
42
+ */
43
+ scope?: IOverlayScope;
44
+ hostKey?: string;
45
+ ownerKey?: string;
46
+ level?: IOverlayLevel;
47
+ strategy?: IOverlayStrategy;
48
+ priority?: number;
49
+ replaceKey?: string;
50
+ presentation?: IOverlayPresentation;
51
+ animation?: IOverlayAnimation;
52
+ /** `presentation="sheet"` only. */
53
+ sheet?: IOverlaySheetOptions;
54
+ /**
55
+ * Keep the content mounted (hidden, inert) while closed, so it keeps its
56
+ * state and callers can measure it before opening, as native-sheet did.
57
+ */
58
+ keepContentMounted?: boolean;
59
+ /** `false` renders no backdrop. */
60
+ backdrop?: IOverlayBackdrop | false;
61
+ /** Defaults by level: toast and debug pass touches through. */
62
+ blocking?: boolean;
63
+ /**
64
+ * Status bar content (iOS / Android) while this is the topmost shown
65
+ * overlay that sets one, e.g. `light` for a dark full-screen overlay.
66
+ * Unset leaves the status bar to the page below.
67
+ */
68
+ statusBarStyle?: IOverlayStatusBarStyle;
69
+ dismissOnBackPress?: boolean;
70
+ /**
71
+ * Back press or backdrop press asked to close. When omitted the overlay
72
+ * closes itself; when provided the caller decides by toggling `visible`.
73
+ * A `pan` dismissal already happened natively and always closes.
74
+ */
75
+ onRequestDismiss?: (reason: IOverlayRequestDismissReason) => void;
76
+ onPresented?: () => void;
77
+ /** The exit animation finished and the content unmounted. */
78
+ onClose?: (reason: IOverlayDismissReason) => void;
79
+ children?: ReactNode;
80
+ testID?: string;
81
+ }
@@ -0,0 +1,52 @@
1
+ import type { IOverlayMotion, IOverlayMotionPreset } from './types';
2
+
3
+ const EASE: readonly [number, number, number, number] = [0.25, 0.1, 0.25, 1];
4
+ const EASE_OUT: readonly [number, number, number, number] = [0, 0, 0.58, 1];
5
+
6
+ function spring(mass: number, stiffness: number, damping: number) {
7
+ return { type: 'spring', spring: { mass, stiffness, damping } } as const;
8
+ }
9
+
10
+ function timing(
11
+ durationMs: number,
12
+ easing: readonly [number, number, number, number]
13
+ ) {
14
+ return { type: 'timing', timing: { durationMs, easing } } as const;
15
+ }
16
+
17
+ /**
18
+ * Native tokens mirror `packages/components/tamagui.animations.native.ts`
19
+ * (Reanimated driver). `quick` intentionally uses a damping ratio of ~3.16 (no overshoot).
20
+ */
21
+ export const NATIVE_MOTION_PRESETS: Record<
22
+ IOverlayMotionPreset,
23
+ IOverlayMotion
24
+ > = {
25
+ quick: spring(0.1, 100, 20),
26
+ popoverQuick: timing(150, [0.215, 0.61, 0.355, 1]),
27
+ smooth: spring(1, 438, 42),
28
+ fast: spring(1.2, 250, 20),
29
+ medium: spring(0.9, 100, 10),
30
+ slow: spring(1, 60, 20),
31
+ // @backpackapp-io/react-native-toast: 300ms Easing.inOut(Easing.quad).
32
+ toastSlide: timing(300, [0.455, 0.03, 0.515, 0.955]),
33
+ lockFade: timing(150, EASE_OUT),
34
+ };
35
+
36
+ /**
37
+ * Web tokens mirror `packages/components/tamagui.animations.web.ts` (CSS
38
+ * driver). Web keeps its own short curves; converting the native springs
39
+ * would make web overlays several times slower than today.
40
+ */
41
+ export const WEB_MOTION_PRESETS: Record<IOverlayMotionPreset, IOverlayMotion> =
42
+ {
43
+ quick: timing(150, EASE),
44
+ popoverQuick: timing(150, [0.215, 0.61, 0.355, 1]),
45
+ smooth: timing(300, [0.2, 0, 0, 1]),
46
+ fast: timing(200, EASE_OUT),
47
+ medium: timing(300, EASE_OUT),
48
+ slow: timing(450, EASE_OUT),
49
+ // sonner: transform / opacity 400ms ease.
50
+ toastSlide: timing(400, EASE),
51
+ lockFade: timing(150, EASE_OUT),
52
+ };
@@ -0,0 +1 @@
1
+ export { NATIVE_MOTION_PRESETS as OVERLAY_MOTION_PRESETS } from './motionPresets';
@@ -0,0 +1 @@
1
+ export { WEB_MOTION_PRESETS as OVERLAY_MOTION_PRESETS } from './motionPresets';
@@ -0,0 +1,103 @@
1
+ import type {
2
+ IOverlayAnimation,
3
+ IOverlayMotion,
4
+ IOverlayMotionInput,
5
+ IOverlayMotionPreset,
6
+ IOverlayPresentation,
7
+ IOverlayTransition,
8
+ } from './types';
9
+
10
+ export interface IResolvedOverlayTransition
11
+ extends Omit<IOverlayTransition, 'motion'> {
12
+ motion: IOverlayMotion;
13
+ }
14
+
15
+ export interface IResolvedOverlayAnimation {
16
+ enter: IResolvedOverlayTransition;
17
+ exit: IResolvedOverlayTransition;
18
+ backdrop: { motion: IOverlayMotion };
19
+ }
20
+
21
+ /**
22
+ * Defaults match today's visuals: Dialog center (scale 0.85 + fade, quick),
23
+ * Tamagui Sheet (slide from bottom, quick), message toasts (slide from top,
24
+ * 300ms quad), Popover floating content (scale 0.95 + fade, popoverQuick).
25
+ */
26
+ export const DEFAULT_OVERLAY_ANIMATIONS: Record<
27
+ IOverlayPresentation,
28
+ { enter: IOverlayTransition; backdrop: IOverlayMotionPreset }
29
+ > = {
30
+ sheet: {
31
+ enter: { type: 'slide', edge: 'bottom', motion: 'quick' },
32
+ backdrop: 'quick',
33
+ },
34
+ center: {
35
+ enter: { type: 'scale', scale: 0.85, fade: true, motion: 'quick' },
36
+ backdrop: 'quick',
37
+ },
38
+ toast: {
39
+ enter: { type: 'slide', edge: 'top', fade: true, motion: 'toastSlide' },
40
+ backdrop: 'quick',
41
+ },
42
+ fullscreen: {
43
+ enter: { type: 'fade', motion: 'quick' },
44
+ backdrop: 'quick',
45
+ },
46
+ anchored: {
47
+ enter: {
48
+ type: 'scale',
49
+ scale: 0.95,
50
+ fade: true,
51
+ origin: 'anchor',
52
+ motion: 'popoverQuick',
53
+ },
54
+ backdrop: 'popoverQuick',
55
+ },
56
+ };
57
+
58
+ function resolveMotion(
59
+ input: IOverlayMotionInput | undefined,
60
+ fallback: IOverlayMotionInput,
61
+ presets: Record<IOverlayMotionPreset, IOverlayMotion>
62
+ ): IOverlayMotion {
63
+ const value = input ?? fallback;
64
+ return typeof value === 'string' ? presets[value] : value;
65
+ }
66
+
67
+ function resolveTransition(
68
+ transition: IOverlayTransition,
69
+ fallbackMotion: IOverlayMotionInput,
70
+ presets: Record<IOverlayMotionPreset, IOverlayMotion>
71
+ ): IResolvedOverlayTransition {
72
+ return {
73
+ ...transition,
74
+ fade: transition.fade ?? transition.type === 'scale',
75
+ motion: resolveMotion(transition.motion, fallbackMotion, presets),
76
+ };
77
+ }
78
+
79
+ /** Fills presentation defaults and turns preset names into concrete curves. */
80
+ export function resolveOverlayAnimation(
81
+ presentation: IOverlayPresentation,
82
+ animation: IOverlayAnimation | undefined,
83
+ presets: Record<IOverlayMotionPreset, IOverlayMotion>
84
+ ): IResolvedOverlayAnimation {
85
+ const defaults = DEFAULT_OVERLAY_ANIMATIONS[presentation];
86
+ const enterInput = animation?.enter ?? defaults.enter;
87
+ const fallbackMotion = defaults.enter.motion ?? 'quick';
88
+ const enter = resolveTransition(enterInput, fallbackMotion, presets);
89
+ const exit = animation?.exit
90
+ ? resolveTransition(animation.exit, fallbackMotion, presets)
91
+ : enter;
92
+ return {
93
+ enter,
94
+ exit,
95
+ backdrop: {
96
+ motion: resolveMotion(
97
+ animation?.backdrop?.motion,
98
+ defaults.backdrop,
99
+ presets
100
+ ),
101
+ },
102
+ };
103
+ }
@@ -0,0 +1,80 @@
1
+ import type { IOverlaySpring } from './types';
2
+
3
+ export function springNaturalFrequency(spring: IOverlaySpring): number {
4
+ return Math.sqrt(spring.stiffness / spring.mass);
5
+ }
6
+
7
+ export function springDampingRatio(spring: IOverlaySpring): number {
8
+ return spring.damping / (2 * Math.sqrt(spring.stiffness * spring.mass));
9
+ }
10
+
11
+ /**
12
+ * Android `SpringForce` assumes unit mass, so it takes
13
+ * stiffness / mass and the damping ratio.
14
+ */
15
+ export function springToAndroidSpringForce(spring: IOverlaySpring): {
16
+ stiffness: number;
17
+ dampingRatio: number;
18
+ } {
19
+ return {
20
+ stiffness: spring.stiffness / spring.mass,
21
+ dampingRatio: springDampingRatio(spring),
22
+ };
23
+ }
24
+
25
+ /** Normalized step response: progress from 0 to 1 at `tMs`, zero start velocity. */
26
+ export function springProgressAt(spring: IOverlaySpring, tMs: number): number {
27
+ const t = tMs / 1000;
28
+ const w0 = springNaturalFrequency(spring);
29
+ const zeta = springDampingRatio(spring);
30
+ let progress: number;
31
+ if (zeta < 1) {
32
+ const wd = w0 * Math.sqrt(1 - zeta * zeta);
33
+ progress =
34
+ 1 -
35
+ Math.exp(-zeta * w0 * t) *
36
+ (Math.cos(wd * t) + ((zeta * w0) / wd) * Math.sin(wd * t));
37
+ } else if (zeta === 1) {
38
+ progress = 1 - Math.exp(-w0 * t) * (1 + w0 * t);
39
+ } else {
40
+ const root = Math.sqrt(zeta * zeta - 1);
41
+ const r1 = -w0 * (zeta - root);
42
+ const r2 = -w0 * (zeta + root);
43
+ progress = 1 - (r2 * Math.exp(r1 * t) - r1 * Math.exp(r2 * t)) / (r2 - r1);
44
+ }
45
+ return spring.overshootClamping ? Math.min(progress, 1) : progress;
46
+ }
47
+
48
+ const MAX_SETTLE_MS = 10_000;
49
+
50
+ /** First time after which the response stays within `epsilon` of the target. */
51
+ export function springSettleTimeMs(
52
+ spring: IOverlaySpring,
53
+ epsilon = 0.001
54
+ ): number {
55
+ let lastOutside = 0;
56
+ for (let t = 0; t <= MAX_SETTLE_MS; t += 1) {
57
+ if (Math.abs(1 - springProgressAt(spring, t)) >= epsilon) {
58
+ lastOutside = t;
59
+ }
60
+ }
61
+ return lastOutside + 1;
62
+ }
63
+
64
+ /**
65
+ * CSS `linear()` easing sampled from the spring, for the Web Animations API when a
66
+ * caller passes a custom spring. Presets use `WEB_MOTION_PRESETS` instead.
67
+ */
68
+ export function springToCssLinear(
69
+ spring: IOverlaySpring,
70
+ stops = 40
71
+ ): { durationMs: number; easing: string } {
72
+ const durationMs = springSettleTimeMs(spring);
73
+ const points: string[] = [];
74
+ for (let i = 0; i <= stops; i += 1) {
75
+ const value =
76
+ i === stops ? 1 : springProgressAt(spring, (durationMs * i) / stops);
77
+ points.push(String(Math.round(value * 10_000) / 10_000));
78
+ }
79
+ return { durationMs, easing: `linear(${points.join(', ')})` };
80
+ }