@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.
- package/LICENSE +21 -0
- package/NativeOverlay.podspec +30 -0
- package/README.md +35 -0
- package/android/build.gradle +82 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayAnimation.kt +176 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayEntryRootView.kt +415 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayEvents.kt +38 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayHost.kt +174 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayPackage.kt +16 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayPageHostView.kt +122 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayPageHostViewManager.kt +40 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayView.kt +465 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayViewManager.kt +156 -0
- package/android/src/main/jni/CMakeLists.txt +44 -0
- package/android/src/main/jni/RNCNativeOverlay.h +19 -0
- package/common/cpp/react/renderer/components/RNCNativeOverlay/NativeOverlayComponentDescriptor.h +12 -0
- package/common/cpp/react/renderer/components/RNCNativeOverlay/NativeOverlayShadowNode.cpp +12 -0
- package/common/cpp/react/renderer/components/RNCNativeOverlay/NativeOverlayShadowNode.h +41 -0
- package/common/cpp/react/renderer/components/RNCNativeOverlay/NativeOverlayState.h +40 -0
- package/docs/SPEC.md +693 -0
- package/ios/NativeOverlayAnimation.swift +160 -0
- package/ios/NativeOverlayContainerView.swift +688 -0
- package/ios/NativeOverlayKeyboard.swift +88 -0
- package/ios/NativeOverlayPageHostView.swift +98 -0
- package/ios/NativeOverlaySheetController.swift +391 -0
- package/ios/NativeOverlayWindowManager.swift +194 -0
- package/ios/RCTNativeOverlayComponentView.h +11 -0
- package/ios/RCTNativeOverlayComponentView.mm +193 -0
- package/ios/RCTNativeOverlayPageHostComponentView.h +11 -0
- package/ios/RCTNativeOverlayPageHostComponentView.mm +64 -0
- package/lib/module/NativeOverlayNativeComponent.ts +61 -0
- package/lib/module/NativeOverlayPageHostNativeComponent.ts +13 -0
- package/lib/module/OverlayErrors.js +6 -0
- package/lib/module/OverlayErrors.js.map +1 -0
- package/lib/module/OverlayLevelContext.js +27 -0
- package/lib/module/OverlayLevelContext.js.map +1 -0
- package/lib/module/OverlayLevels.js +33 -0
- package/lib/module/OverlayLevels.js.map +1 -0
- package/lib/module/OverlayPageHost.js +24 -0
- package/lib/module/OverlayPageHost.js.map +1 -0
- package/lib/module/OverlayPageHost.web.js +49 -0
- package/lib/module/OverlayPageHost.web.js.map +1 -0
- package/lib/module/OverlayPageScope.js +41 -0
- package/lib/module/OverlayPageScope.js.map +1 -0
- package/lib/module/OverlayStore.js +305 -0
- package/lib/module/OverlayStore.js.map +1 -0
- package/lib/module/OverlayView.js +141 -0
- package/lib/module/OverlayView.js.map +1 -0
- package/lib/module/OverlayView.web.js +290 -0
- package/lib/module/OverlayView.web.js.map +1 -0
- package/lib/module/OverlayViewTypes.js +4 -0
- package/lib/module/OverlayViewTypes.js.map +1 -0
- package/lib/module/animation/motionPresets.js +57 -0
- package/lib/module/animation/motionPresets.js.map +1 -0
- package/lib/module/animation/platformMotionPresets.js +4 -0
- package/lib/module/animation/platformMotionPresets.js.map +1 -0
- package/lib/module/animation/platformMotionPresets.web.js +4 -0
- package/lib/module/animation/platformMotionPresets.web.js.map +1 -0
- package/lib/module/animation/resolveAnimation.js +80 -0
- package/lib/module/animation/resolveAnimation.js.map +1 -0
- package/lib/module/animation/spring.js +69 -0
- package/lib/module/animation/spring.js.map +1 -0
- package/lib/module/animation/types.js +2 -0
- package/lib/module/animation/types.js.map +1 -0
- package/lib/module/index.js +16 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/useOverlayController.js +127 -0
- package/lib/module/useOverlayController.js.map +1 -0
- package/lib/module/useSuspendedPageOwners.js +16 -0
- package/lib/module/useSuspendedPageOwners.js.map +1 -0
- package/lib/module/web/animateTransition.js +115 -0
- package/lib/module/web/animateTransition.js.map +1 -0
- package/lib/module/web/overlayLayers.js +147 -0
- package/lib/module/web/overlayLayers.js.map +1 -0
- package/lib/module/web/useKeyboardShift.js +110 -0
- package/lib/module/web/useKeyboardShift.js.map +1 -0
- package/lib/module/web/useSheetDrag.js +126 -0
- package/lib/module/web/useSheetDrag.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeOverlayNativeComponent.d.ts +41 -0
- package/lib/typescript/src/NativeOverlayNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/NativeOverlayPageHostNativeComponent.d.ts +9 -0
- package/lib/typescript/src/NativeOverlayPageHostNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/OverlayErrors.d.ts +4 -0
- package/lib/typescript/src/OverlayErrors.d.ts.map +1 -0
- package/lib/typescript/src/OverlayLevelContext.d.ts +12 -0
- package/lib/typescript/src/OverlayLevelContext.d.ts.map +1 -0
- package/lib/typescript/src/OverlayLevels.d.ts +13 -0
- package/lib/typescript/src/OverlayLevels.d.ts.map +1 -0
- package/lib/typescript/src/OverlayPageHost.d.ts +9 -0
- package/lib/typescript/src/OverlayPageHost.d.ts.map +1 -0
- package/lib/typescript/src/OverlayPageHost.web.d.ts +11 -0
- package/lib/typescript/src/OverlayPageHost.web.d.ts.map +1 -0
- package/lib/typescript/src/OverlayPageScope.d.ts +17 -0
- package/lib/typescript/src/OverlayPageScope.d.ts.map +1 -0
- package/lib/typescript/src/OverlayStore.d.ts +72 -0
- package/lib/typescript/src/OverlayStore.d.ts.map +1 -0
- package/lib/typescript/src/OverlayView.d.ts +3 -0
- package/lib/typescript/src/OverlayView.d.ts.map +1 -0
- package/lib/typescript/src/OverlayView.web.d.ts +3 -0
- package/lib/typescript/src/OverlayView.web.d.ts.map +1 -0
- package/lib/typescript/src/OverlayViewTypes.d.ts +68 -0
- package/lib/typescript/src/OverlayViewTypes.d.ts.map +1 -0
- package/lib/typescript/src/animation/motionPresets.d.ts +13 -0
- package/lib/typescript/src/animation/motionPresets.d.ts.map +1 -0
- package/lib/typescript/src/animation/platformMotionPresets.d.ts +2 -0
- package/lib/typescript/src/animation/platformMotionPresets.d.ts.map +1 -0
- package/lib/typescript/src/animation/platformMotionPresets.web.d.ts +2 -0
- package/lib/typescript/src/animation/platformMotionPresets.web.d.ts.map +1 -0
- package/lib/typescript/src/animation/resolveAnimation.d.ts +23 -0
- package/lib/typescript/src/animation/resolveAnimation.d.ts.map +1 -0
- package/lib/typescript/src/animation/spring.d.ts +24 -0
- package/lib/typescript/src/animation/spring.d.ts.map +1 -0
- package/lib/typescript/src/animation/types.d.ts +56 -0
- package/lib/typescript/src/animation/types.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +18 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +59 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/useOverlayController.d.ts +29 -0
- package/lib/typescript/src/useOverlayController.d.ts.map +1 -0
- package/lib/typescript/src/useSuspendedPageOwners.d.ts +7 -0
- package/lib/typescript/src/useSuspendedPageOwners.d.ts.map +1 -0
- package/lib/typescript/src/web/animateTransition.d.ts +18 -0
- package/lib/typescript/src/web/animateTransition.d.ts.map +1 -0
- package/lib/typescript/src/web/overlayLayers.d.ts +10 -0
- package/lib/typescript/src/web/overlayLayers.d.ts.map +1 -0
- package/lib/typescript/src/web/useKeyboardShift.d.ts +29 -0
- package/lib/typescript/src/web/useKeyboardShift.d.ts.map +1 -0
- package/lib/typescript/src/web/useSheetDrag.d.ts +13 -0
- package/lib/typescript/src/web/useSheetDrag.d.ts.map +1 -0
- package/package.json +188 -0
- package/react-native.config.js +16 -0
- package/src/NativeOverlayNativeComponent.ts +61 -0
- package/src/NativeOverlayPageHostNativeComponent.ts +13 -0
- package/src/OverlayErrors.ts +3 -0
- package/src/OverlayLevelContext.tsx +32 -0
- package/src/OverlayLevels.ts +40 -0
- package/src/OverlayPageHost.tsx +21 -0
- package/src/OverlayPageHost.web.tsx +53 -0
- package/src/OverlayPageScope.tsx +42 -0
- package/src/OverlayStore.ts +401 -0
- package/src/OverlayView.tsx +165 -0
- package/src/OverlayView.web.tsx +356 -0
- package/src/OverlayViewTypes.ts +81 -0
- package/src/animation/motionPresets.ts +52 -0
- package/src/animation/platformMotionPresets.ts +1 -0
- package/src/animation/platformMotionPresets.web.ts +1 -0
- package/src/animation/resolveAnimation.ts +103 -0
- package/src/animation/spring.ts +80 -0
- package/src/animation/types.ts +71 -0
- package/src/index.ts +55 -0
- package/src/types.ts +79 -0
- package/src/useOverlayController.ts +199 -0
- package/src/useSuspendedPageOwners.ts +26 -0
- package/src/web/animateTransition.ts +137 -0
- package/src/web/overlayLayers.ts +182 -0
- package/src/web/useKeyboardShift.ts +130 -0
- package/src/web/useSheetDrag.ts +143 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useSuspendedPageOwners } from './useSuspendedPageOwners';
|
|
4
|
+
import { registerOverlayPageHost } from './web/overlayLayers';
|
|
5
|
+
|
|
6
|
+
const OWNER_ATTRIBUTE = 'data-overlay-owner';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Web page host: an absolutely positioned layer inside the root-route card.
|
|
10
|
+
* Page overlays portal into it; entries of covered owners are hidden here
|
|
11
|
+
* because their own (possibly inactive) page does not re-render.
|
|
12
|
+
*/
|
|
13
|
+
export function OverlayPageHost({ hostKey }: { hostKey: string }) {
|
|
14
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
15
|
+
const suspendedOwners = useSuspendedPageOwners(hostKey);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const element = ref.current;
|
|
19
|
+
if (!element) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
return registerOverlayPageHost(hostKey, element);
|
|
23
|
+
}, [hostKey]);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const element = ref.current;
|
|
27
|
+
if (!element) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const suspended = new Set(suspendedOwners);
|
|
31
|
+
element
|
|
32
|
+
.querySelectorAll<HTMLElement>(`[${OWNER_ATTRIBUTE}]`)
|
|
33
|
+
.forEach((node) => {
|
|
34
|
+
const hidden = suspended.has(node.getAttribute(OWNER_ATTRIBUTE) ?? '');
|
|
35
|
+
node.style.visibility = hidden ? 'hidden' : '';
|
|
36
|
+
node.inert = hidden;
|
|
37
|
+
});
|
|
38
|
+
}, [suspendedOwners]);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div
|
|
42
|
+
ref={ref}
|
|
43
|
+
style={{
|
|
44
|
+
position: 'absolute',
|
|
45
|
+
inset: 0,
|
|
46
|
+
pointerEvents: 'none',
|
|
47
|
+
zIndex: 1,
|
|
48
|
+
}}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { OWNER_ATTRIBUTE as OVERLAY_OWNER_ATTRIBUTE };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createContext, useContext, useMemo } from 'react';
|
|
2
|
+
import type { PropsWithChildren } from 'react';
|
|
3
|
+
|
|
4
|
+
export interface IOverlayPageScope {
|
|
5
|
+
/** Root-route host that renders page overlays (see `OverlayPageHost`). */
|
|
6
|
+
hostKey?: string;
|
|
7
|
+
/** The nearest page; page overlays hide while it is covered. */
|
|
8
|
+
ownerKey?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const OverlayPageScopeContext = createContext<IOverlayPageScope>({});
|
|
12
|
+
|
|
13
|
+
export function useOverlayPageScope(): IOverlayPageScope {
|
|
14
|
+
return useContext(OverlayPageScopeContext);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Provided once per root route, around its navigator and `OverlayPageHost`. */
|
|
18
|
+
export function OverlayPageHostScope({
|
|
19
|
+
hostKey,
|
|
20
|
+
children,
|
|
21
|
+
}: PropsWithChildren<{ hostKey: string }>) {
|
|
22
|
+
const value = useMemo(() => ({ hostKey }), [hostKey]);
|
|
23
|
+
return (
|
|
24
|
+
<OverlayPageScopeContext.Provider value={value}>
|
|
25
|
+
{children}
|
|
26
|
+
</OverlayPageScopeContext.Provider>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Provided by each page; page overlays below it belong to `ownerKey`. */
|
|
31
|
+
export function OverlayPageOwnerScope({
|
|
32
|
+
ownerKey,
|
|
33
|
+
children,
|
|
34
|
+
}: PropsWithChildren<{ ownerKey: string }>) {
|
|
35
|
+
const { hostKey } = useOverlayPageScope();
|
|
36
|
+
const value = useMemo(() => ({ hostKey, ownerKey }), [hostKey, ownerKey]);
|
|
37
|
+
return (
|
|
38
|
+
<OverlayPageScopeContext.Provider value={value}>
|
|
39
|
+
{children}
|
|
40
|
+
</OverlayPageScopeContext.Provider>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { OverlayRequestError } from './OverlayErrors';
|
|
2
|
+
import { OVERLAY_LEVEL_ORDER, isBlockingLevel } from './OverlayLevels';
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
IOverlayDismissReason,
|
|
6
|
+
IOverlayEntry,
|
|
7
|
+
IOverlayRequest,
|
|
8
|
+
IOverlaySnapshot,
|
|
9
|
+
} from './types';
|
|
10
|
+
|
|
11
|
+
export interface IOverlayEntryHandlers {
|
|
12
|
+
/** Called once when the entry leaves the store (finalized or dropped from the queue). */
|
|
13
|
+
onRemoved?: (reason: IOverlayDismissReason) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type IOverlayBackResolution =
|
|
17
|
+
| { kind: 'dismiss'; id: string }
|
|
18
|
+
| { kind: 'block' }
|
|
19
|
+
| { kind: 'pass' };
|
|
20
|
+
|
|
21
|
+
export interface IOverlayStoreOptions {
|
|
22
|
+
/** Upper bound for a closing entry to wait for its native exit animation. */
|
|
23
|
+
closingTimeoutMs?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DEFAULT_CLOSING_TIMEOUT_MS = 5000;
|
|
27
|
+
|
|
28
|
+
function laneKeyOf(entry: IOverlayEntry): string {
|
|
29
|
+
return entry.scope === 'global'
|
|
30
|
+
? `global|${entry.level}`
|
|
31
|
+
: `page|${entry.hostKey ?? ''}|${entry.level}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function compareRenderOrder(a: IOverlayEntry, b: IOverlayEntry): number {
|
|
35
|
+
if (a.scope !== b.scope) {
|
|
36
|
+
return a.scope === 'page' ? -1 : 1;
|
|
37
|
+
}
|
|
38
|
+
if (a.scope === 'page' && a.hostKey !== b.hostKey) {
|
|
39
|
+
return (a.hostKey ?? '') < (b.hostKey ?? '') ? -1 : 1;
|
|
40
|
+
}
|
|
41
|
+
const levelDiff = OVERLAY_LEVEL_ORDER[a.level] - OVERLAY_LEVEL_ORDER[b.level];
|
|
42
|
+
return levelDiff !== 0 ? levelDiff : a.seq - b.seq;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function compareQueueOrder(a: IOverlayEntry, b: IOverlayEntry): number {
|
|
46
|
+
return b.priority !== a.priority ? b.priority - a.priority : a.seq - b.seq;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class OverlayStore {
|
|
50
|
+
private readonly closingTimeoutMs: number;
|
|
51
|
+
|
|
52
|
+
private seq = 0;
|
|
53
|
+
|
|
54
|
+
private readonly shown = new Map<string, IOverlayEntry>();
|
|
55
|
+
|
|
56
|
+
private queued: IOverlayEntry[] = [];
|
|
57
|
+
|
|
58
|
+
private readonly handlers = new Map<string, IOverlayEntryHandlers>();
|
|
59
|
+
|
|
60
|
+
private readonly closingTimers = new Map<
|
|
61
|
+
string,
|
|
62
|
+
ReturnType<typeof setTimeout>
|
|
63
|
+
>();
|
|
64
|
+
|
|
65
|
+
private readonly pageVisibility = new Map<string, boolean>();
|
|
66
|
+
|
|
67
|
+
private readonly listeners = new Set<() => void>();
|
|
68
|
+
|
|
69
|
+
private snapshot: IOverlaySnapshot = {
|
|
70
|
+
entries: [],
|
|
71
|
+
queued: [],
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
constructor(options: IOverlayStoreOptions = {}) {
|
|
75
|
+
this.closingTimeoutMs =
|
|
76
|
+
options.closingTimeoutMs ?? DEFAULT_CLOSING_TIMEOUT_MS;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
subscribe = (listener: () => void): (() => void) => {
|
|
80
|
+
this.listeners.add(listener);
|
|
81
|
+
return () => {
|
|
82
|
+
this.listeners.delete(listener);
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
getSnapshot = (): IOverlaySnapshot => this.snapshot;
|
|
87
|
+
|
|
88
|
+
getEntry(id: string): IOverlayEntry | undefined {
|
|
89
|
+
return this.shown.get(id) ?? this.queued.find((e) => e.id === id);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
request(
|
|
93
|
+
request: IOverlayRequest,
|
|
94
|
+
handlers: IOverlayEntryHandlers = {}
|
|
95
|
+
): IOverlayEntry {
|
|
96
|
+
if (request.id) {
|
|
97
|
+
const existing = this.getEntry(request.id);
|
|
98
|
+
if (existing) {
|
|
99
|
+
return existing;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const scope = request.scope ?? 'global';
|
|
103
|
+
if (scope === 'page' && (!request.hostKey || !request.ownerKey)) {
|
|
104
|
+
throw new OverlayRequestError(
|
|
105
|
+
'OverlayStore: page scope requires hostKey and ownerKey'
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
const level = request.level ?? 'modal';
|
|
109
|
+
const blocking = request.blocking ?? isBlockingLevel(level);
|
|
110
|
+
this.seq += 1;
|
|
111
|
+
const entry: IOverlayEntry = {
|
|
112
|
+
id: request.id ?? `overlay-${this.seq}`,
|
|
113
|
+
scope,
|
|
114
|
+
level,
|
|
115
|
+
strategy: request.strategy ?? 'stack',
|
|
116
|
+
priority: request.priority ?? 0,
|
|
117
|
+
replaceKey: request.replaceKey,
|
|
118
|
+
hostKey: scope === 'page' ? request.hostKey : undefined,
|
|
119
|
+
ownerKey: scope === 'page' ? request.ownerKey : undefined,
|
|
120
|
+
dismissible: request.dismissible ?? blocking,
|
|
121
|
+
statusBarStyle: request.statusBarStyle,
|
|
122
|
+
blocking,
|
|
123
|
+
phase: 'active',
|
|
124
|
+
suspended: false,
|
|
125
|
+
dismissReason: undefined,
|
|
126
|
+
seq: this.seq,
|
|
127
|
+
};
|
|
128
|
+
entry.suspended = this.isOwnerHidden(entry);
|
|
129
|
+
this.handlers.set(entry.id, handlers);
|
|
130
|
+
|
|
131
|
+
if (entry.strategy === 'replace') {
|
|
132
|
+
this.replaceIn(entry);
|
|
133
|
+
this.shown.set(entry.id, entry);
|
|
134
|
+
} else if (entry.strategy === 'queue' && !this.canPromote(entry)) {
|
|
135
|
+
entry.phase = 'queued';
|
|
136
|
+
this.queued = [...this.queued, entry].toSorted(compareQueueOrder);
|
|
137
|
+
} else {
|
|
138
|
+
this.shown.set(entry.id, entry);
|
|
139
|
+
}
|
|
140
|
+
this.emit();
|
|
141
|
+
return this.getEntry(entry.id) ?? entry;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Starts closing an entry; the native side calls `finalize` after its exit animation. */
|
|
145
|
+
dismiss(id: string, reason: IOverlayDismissReason = 'programmatic'): void {
|
|
146
|
+
if (this.dismissWithoutEmit(id, reason)) {
|
|
147
|
+
this.emit();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Removes a closing (or active) entry once its exit animation finished. */
|
|
152
|
+
finalize(id: string): void {
|
|
153
|
+
const entry = this.shown.get(id);
|
|
154
|
+
if (!entry) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
this.clearClosingTimer(id);
|
|
158
|
+
this.shown.delete(id);
|
|
159
|
+
this.promoteQueued();
|
|
160
|
+
this.emit();
|
|
161
|
+
this.notifyRemoved(id, entry.dismissReason ?? 'programmatic');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
dismissAll(
|
|
165
|
+
filter: {
|
|
166
|
+
scope?: IOverlayEntry['scope'];
|
|
167
|
+
belowLevel?: IOverlayEntry['level'];
|
|
168
|
+
reason?: IOverlayDismissReason;
|
|
169
|
+
} = {}
|
|
170
|
+
): void {
|
|
171
|
+
const reason = filter.reason ?? 'programmatic';
|
|
172
|
+
const matches = (entry: IOverlayEntry) =>
|
|
173
|
+
(!filter.scope || entry.scope === filter.scope) &&
|
|
174
|
+
(!filter.belowLevel ||
|
|
175
|
+
OVERLAY_LEVEL_ORDER[entry.level] <
|
|
176
|
+
OVERLAY_LEVEL_ORDER[filter.belowLevel]);
|
|
177
|
+
const dropped = this.queued.filter(matches);
|
|
178
|
+
this.queued = this.queued.filter((e) => !matches(e));
|
|
179
|
+
let changed = dropped.length > 0;
|
|
180
|
+
for (const entry of this.shown.values()) {
|
|
181
|
+
if (matches(entry)) {
|
|
182
|
+
changed = this.dismissWithoutEmit(entry.id, reason) || changed;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (changed) {
|
|
186
|
+
this.emit();
|
|
187
|
+
}
|
|
188
|
+
dropped.forEach((e) => this.notifyRemoved(e.id, reason));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Native / navigation signal: the owning page was covered, detached or restored. */
|
|
192
|
+
setPageVisible(ownerKey: string, visible: boolean): void {
|
|
193
|
+
if ((this.pageVisibility.get(ownerKey) ?? true) === visible) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
this.pageVisibility.set(ownerKey, visible);
|
|
197
|
+
for (const entry of this.shown.values()) {
|
|
198
|
+
if (entry.ownerKey === ownerKey) {
|
|
199
|
+
entry.suspended = !visible;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
for (const entry of this.queued) {
|
|
203
|
+
if (entry.ownerKey === ownerKey) {
|
|
204
|
+
entry.suspended = !visible;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (visible) {
|
|
208
|
+
this.promoteQueued();
|
|
209
|
+
}
|
|
210
|
+
this.emit();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** The owning page left the navigation state for good. */
|
|
214
|
+
removePage(ownerKey: string): void {
|
|
215
|
+
const dropped = this.queued.filter((e) => e.ownerKey === ownerKey);
|
|
216
|
+
this.queued = this.queued.filter((e) => e.ownerKey !== ownerKey);
|
|
217
|
+
for (const entry of this.shown.values()) {
|
|
218
|
+
if (entry.ownerKey === ownerKey) {
|
|
219
|
+
this.dismissWithoutEmit(entry.id, 'page-removed');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
this.pageVisibility.delete(ownerKey);
|
|
223
|
+
this.emit();
|
|
224
|
+
dropped.forEach((e) => this.notifyRemoved(e.id, 'page-removed'));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Decides what a back press / Escape does. Global entries win over page
|
|
229
|
+
* entries; `focusedHostKey` limits page entries to the focused root route
|
|
230
|
+
* (split view has one host per pane).
|
|
231
|
+
*/
|
|
232
|
+
resolveBack(focusedHostKey?: string): IOverlayBackResolution {
|
|
233
|
+
// Non-blocking entries take back / Escape only when they ask to be
|
|
234
|
+
// dismissible (an anchored popover); toasts do not.
|
|
235
|
+
const candidates = this.snapshot.entries.filter(
|
|
236
|
+
(e) =>
|
|
237
|
+
e.phase === 'active' &&
|
|
238
|
+
!e.suspended &&
|
|
239
|
+
(e.blocking || e.dismissible) &&
|
|
240
|
+
(e.scope === 'global' ||
|
|
241
|
+
focusedHostKey === undefined ||
|
|
242
|
+
e.hostKey === focusedHostKey)
|
|
243
|
+
);
|
|
244
|
+
const top = candidates[candidates.length - 1];
|
|
245
|
+
if (!top) {
|
|
246
|
+
return { kind: 'pass' };
|
|
247
|
+
}
|
|
248
|
+
return top.dismissible
|
|
249
|
+
? { kind: 'dismiss', id: top.id }
|
|
250
|
+
: { kind: 'block' };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Topmost visible blocking entry; everything rendered below it is inert. */
|
|
254
|
+
/** The entry whose `statusBarStyle` applies (see `findStatusBarOwner`). */
|
|
255
|
+
getStatusBarOwner(): IOverlayEntry | undefined {
|
|
256
|
+
return findStatusBarOwner(this.snapshot);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
getBlockingTop(): IOverlayEntry | undefined {
|
|
260
|
+
const { entries } = this.snapshot;
|
|
261
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
262
|
+
const entry = entries[i];
|
|
263
|
+
if (
|
|
264
|
+
entry &&
|
|
265
|
+
entry.phase !== 'closing' &&
|
|
266
|
+
!entry.suspended &&
|
|
267
|
+
entry.blocking
|
|
268
|
+
) {
|
|
269
|
+
return entry;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private dismissWithoutEmit(
|
|
276
|
+
id: string,
|
|
277
|
+
reason: IOverlayDismissReason
|
|
278
|
+
): boolean {
|
|
279
|
+
const queuedIndex = this.queued.findIndex((e) => e.id === id);
|
|
280
|
+
if (queuedIndex !== -1) {
|
|
281
|
+
this.queued = this.queued.filter((e) => e.id !== id);
|
|
282
|
+
// Queued entries never rendered, so they leave without an exit animation.
|
|
283
|
+
queueMicrotask(() => this.notifyRemoved(id, reason));
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
const entry = this.shown.get(id);
|
|
287
|
+
if (!entry || entry.phase === 'closing') {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
entry.phase = 'closing';
|
|
291
|
+
entry.dismissReason = reason;
|
|
292
|
+
this.closingTimers.set(
|
|
293
|
+
id,
|
|
294
|
+
setTimeout(() => this.finalize(id), this.closingTimeoutMs)
|
|
295
|
+
);
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private replaceIn(entry: IOverlayEntry): void {
|
|
300
|
+
const laneKey = laneKeyOf(entry);
|
|
301
|
+
const sameLane = [...this.shown.values()].filter(
|
|
302
|
+
(e) => e.phase === 'active' && laneKeyOf(e) === laneKey
|
|
303
|
+
);
|
|
304
|
+
const targets = entry.replaceKey
|
|
305
|
+
? sameLane.filter((e) => e.replaceKey === entry.replaceKey)
|
|
306
|
+
: sameLane.slice(-1);
|
|
307
|
+
targets.forEach((e) => this.dismissWithoutEmit(e.id, 'replaced'));
|
|
308
|
+
if (entry.replaceKey) {
|
|
309
|
+
const dropped = this.queued.filter(
|
|
310
|
+
(e) => laneKeyOf(e) === laneKey && e.replaceKey === entry.replaceKey
|
|
311
|
+
);
|
|
312
|
+
dropped.forEach((e) => this.dismissWithoutEmit(e.id, 'replaced'));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private isOwnerHidden(entry: IOverlayEntry): boolean {
|
|
317
|
+
return (
|
|
318
|
+
entry.scope === 'page' &&
|
|
319
|
+
entry.ownerKey !== undefined &&
|
|
320
|
+
this.pageVisibility.get(entry.ownerKey) === false
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private canPromote(entry: IOverlayEntry): boolean {
|
|
325
|
+
if (this.isOwnerHidden(entry)) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
const laneKey = laneKeyOf(entry);
|
|
329
|
+
for (const shown of this.shown.values()) {
|
|
330
|
+
if (laneKeyOf(shown) === laneKey) {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private promoteQueued(): void {
|
|
338
|
+
const promotedLanes = new Set<string>();
|
|
339
|
+
const remaining: IOverlayEntry[] = [];
|
|
340
|
+
for (const entry of this.queued) {
|
|
341
|
+
const laneKey = laneKeyOf(entry);
|
|
342
|
+
if (!promotedLanes.has(laneKey) && this.canPromote(entry)) {
|
|
343
|
+
entry.phase = 'active';
|
|
344
|
+
this.shown.set(entry.id, entry);
|
|
345
|
+
promotedLanes.add(laneKey);
|
|
346
|
+
} else {
|
|
347
|
+
remaining.push(entry);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
this.queued = remaining;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private clearClosingTimer(id: string): void {
|
|
354
|
+
const timer = this.closingTimers.get(id);
|
|
355
|
+
if (timer) {
|
|
356
|
+
clearTimeout(timer);
|
|
357
|
+
this.closingTimers.delete(id);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private notifyRemoved(id: string, reason: IOverlayDismissReason): void {
|
|
362
|
+
const handlers = this.handlers.get(id);
|
|
363
|
+
this.handlers.delete(id);
|
|
364
|
+
handlers?.onRemoved?.(reason);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private emit(): void {
|
|
368
|
+
this.snapshot = {
|
|
369
|
+
entries: [...this.shown.values()]
|
|
370
|
+
.map((e) => ({ ...e }))
|
|
371
|
+
.toSorted(compareRenderOrder),
|
|
372
|
+
queued: this.queued.map((e) => ({ ...e })),
|
|
373
|
+
};
|
|
374
|
+
this.listeners.forEach((listener) => listener());
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* The topmost shown, visible entry that styles the status bar. Entries are
|
|
380
|
+
* in render order, so a higher level (or a later request in the same level)
|
|
381
|
+
* wins; page entries render below global ones and suspended ones never own it.
|
|
382
|
+
*/
|
|
383
|
+
export function findStatusBarOwner(
|
|
384
|
+
snapshot: IOverlaySnapshot
|
|
385
|
+
): IOverlayEntry | undefined {
|
|
386
|
+
const { entries } = snapshot;
|
|
387
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
388
|
+
const entry = entries[i];
|
|
389
|
+
if (
|
|
390
|
+
entry &&
|
|
391
|
+
entry.statusBarStyle &&
|
|
392
|
+
entry.phase === 'active' &&
|
|
393
|
+
!entry.suspended
|
|
394
|
+
) {
|
|
395
|
+
return entry;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return undefined;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export const overlayStore = new OverlayStore();
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { StatusBar, StyleSheet, View, useWindowDimensions } from 'react-native';
|
|
4
|
+
|
|
5
|
+
import NativeOverlay from './NativeOverlayNativeComponent';
|
|
6
|
+
import { OverlayLevelProvider } from './OverlayLevelContext';
|
|
7
|
+
import { isBlockingLevel } from './OverlayLevels';
|
|
8
|
+
import { useOverlayController } from './useOverlayController';
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
IOverlayRequestDismissReason,
|
|
12
|
+
IOverlayViewProps,
|
|
13
|
+
} from './OverlayViewTypes';
|
|
14
|
+
import type { LayoutChangeEvent, NativeSyntheticEvent } from 'react-native';
|
|
15
|
+
|
|
16
|
+
const DEFAULT_BACKDROP_COLOR = 'rgba(0, 0, 0, 0.4)';
|
|
17
|
+
const DEFAULT_SHEET_MAX_RATIO = 0.92;
|
|
18
|
+
|
|
19
|
+
const REQUEST_REASONS: Record<string, IOverlayRequestDismissReason> = {
|
|
20
|
+
back: 'back',
|
|
21
|
+
backdrop: 'backdrop',
|
|
22
|
+
pan: 'pan',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const styles = StyleSheet.create({
|
|
26
|
+
// The host view never draws: native moves its child into the level host
|
|
27
|
+
// and clips it (zero width) while hidden. The height stays auto on
|
|
28
|
+
// purpose; a fixed 0 makes Yoga measure a fitted sheet's content at most
|
|
29
|
+
// 0 tall, so the sheet would never learn its height.
|
|
30
|
+
host: {
|
|
31
|
+
position: 'absolute',
|
|
32
|
+
top: 0,
|
|
33
|
+
left: 0,
|
|
34
|
+
width: 0,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export function OverlayView(props: IOverlayViewProps) {
|
|
39
|
+
const {
|
|
40
|
+
level = 'modal',
|
|
41
|
+
presentation = 'center',
|
|
42
|
+
backdrop,
|
|
43
|
+
blocking,
|
|
44
|
+
dismissOnBackPress,
|
|
45
|
+
sheet,
|
|
46
|
+
keepContentMounted = false,
|
|
47
|
+
statusBarStyle,
|
|
48
|
+
children,
|
|
49
|
+
testID,
|
|
50
|
+
} = props;
|
|
51
|
+
const isSheet = presentation === 'sheet';
|
|
52
|
+
const controller = useOverlayController(props);
|
|
53
|
+
const { width, height } = useWindowDimensions();
|
|
54
|
+
const {
|
|
55
|
+
entry,
|
|
56
|
+
mounted,
|
|
57
|
+
presented,
|
|
58
|
+
ownsStatusBar,
|
|
59
|
+
scope,
|
|
60
|
+
hostKey,
|
|
61
|
+
ownerKey,
|
|
62
|
+
animation,
|
|
63
|
+
onHostPresented,
|
|
64
|
+
onHostDismissed,
|
|
65
|
+
onHostRequestDismiss,
|
|
66
|
+
} = controller;
|
|
67
|
+
|
|
68
|
+
const [measuredSheetHeight, setMeasuredSheetHeight] = useState(0);
|
|
69
|
+
const maxSheetHeight = sheet?.maxHeight ?? height * DEFAULT_SHEET_MAX_RATIO;
|
|
70
|
+
const sheetHeight = Math.min(
|
|
71
|
+
sheet?.height ?? measuredSheetHeight,
|
|
72
|
+
maxSheetHeight
|
|
73
|
+
);
|
|
74
|
+
const onSheetLayout = useCallback((event: LayoutChangeEvent) => {
|
|
75
|
+
setMeasuredSheetHeight(event.nativeEvent.layout.height);
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
const animationConfig = useMemo(() => JSON.stringify(animation), [animation]);
|
|
79
|
+
const isBlocking = blocking ?? isBlockingLevel(level);
|
|
80
|
+
|
|
81
|
+
const onDismissed = useCallback(() => onHostDismissed(), [onHostDismissed]);
|
|
82
|
+
const onRequestDismiss = useCallback(
|
|
83
|
+
(event: NativeSyntheticEvent<{ reason: string }>) => {
|
|
84
|
+
onHostRequestDismiss(REQUEST_REASONS[event.nativeEvent.reason] ?? 'back');
|
|
85
|
+
},
|
|
86
|
+
[onHostRequestDismiss]
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (!keepContentMounted && (!mounted || !entry)) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// The app drives the status bar through React Native's StatusBar stack
|
|
94
|
+
// (iOS runs without view-controller-based appearance), so the owning
|
|
95
|
+
// overlay pushes onto the same stack.
|
|
96
|
+
const statusBar =
|
|
97
|
+
ownsStatusBar && statusBarStyle ? (
|
|
98
|
+
<StatusBar
|
|
99
|
+
animated
|
|
100
|
+
barStyle={statusBarStyle === 'light' ? 'light-content' : 'dark-content'}
|
|
101
|
+
/>
|
|
102
|
+
) : null;
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<>
|
|
106
|
+
{statusBar}
|
|
107
|
+
<NativeOverlay
|
|
108
|
+
style={styles.host}
|
|
109
|
+
// A fitted sheet presents once its content height is known.
|
|
110
|
+
visible={presented && (!isSheet || sheetHeight > 0)}
|
|
111
|
+
level={level}
|
|
112
|
+
scope={scope}
|
|
113
|
+
hostKey={hostKey ?? ''}
|
|
114
|
+
ownerKey={ownerKey ?? ''}
|
|
115
|
+
presentation={presentation}
|
|
116
|
+
stackOrder={entry?.seq ?? 0}
|
|
117
|
+
blocking={isBlocking}
|
|
118
|
+
dismissOnBackPress={dismissOnBackPress ?? isBlocking}
|
|
119
|
+
dismissOnBackdropPress={backdrop ? !!backdrop.dismissOnPress : false}
|
|
120
|
+
backdropColor={
|
|
121
|
+
backdrop ? backdrop.color ?? DEFAULT_BACKDROP_COLOR : undefined
|
|
122
|
+
}
|
|
123
|
+
sheetHeight={isSheet ? sheetHeight : 0}
|
|
124
|
+
sheetCornerRadius={sheet?.cornerRadius}
|
|
125
|
+
showHandle={sheet?.showHandle}
|
|
126
|
+
sheetBackgroundColor={sheet?.backgroundColor}
|
|
127
|
+
dismissOnPanDown={sheet?.dismissOnPanDown ?? true}
|
|
128
|
+
animationConfig={animationConfig}
|
|
129
|
+
onPresented={onHostPresented}
|
|
130
|
+
onDismissed={onDismissed}
|
|
131
|
+
onRequestDismiss={onRequestDismiss}
|
|
132
|
+
testID={testID}
|
|
133
|
+
>
|
|
134
|
+
{isSheet ? (
|
|
135
|
+
<View
|
|
136
|
+
collapsable={false}
|
|
137
|
+
onLayout={onSheetLayout}
|
|
138
|
+
style={{
|
|
139
|
+
width,
|
|
140
|
+
height: sheet?.height,
|
|
141
|
+
maxHeight: maxSheetHeight,
|
|
142
|
+
// The host is 0x0; without this Yoga shrinks the content to 0 and
|
|
143
|
+
// a fitted sheet never learns its height.
|
|
144
|
+
flexShrink: 0,
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
<OverlayLevelProvider value={level}>
|
|
148
|
+
{children}
|
|
149
|
+
</OverlayLevelProvider>
|
|
150
|
+
</View>
|
|
151
|
+
) : (
|
|
152
|
+
<View
|
|
153
|
+
collapsable={false}
|
|
154
|
+
pointerEvents="box-none"
|
|
155
|
+
style={{ width, height, flexShrink: 0 }}
|
|
156
|
+
>
|
|
157
|
+
<OverlayLevelProvider value={level}>
|
|
158
|
+
{children}
|
|
159
|
+
</OverlayLevelProvider>
|
|
160
|
+
</View>
|
|
161
|
+
)}
|
|
162
|
+
</NativeOverlay>
|
|
163
|
+
</>
|
|
164
|
+
);
|
|
165
|
+
}
|