@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,182 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OVERLAY_LEVELS,
|
|
3
|
+
OVERLAY_LEVEL_ORDER,
|
|
4
|
+
OVERLAY_WEB_Z_INDEX_BASE,
|
|
5
|
+
} from '../OverlayLevels';
|
|
6
|
+
import { overlayStore } from '../OverlayStore';
|
|
7
|
+
|
|
8
|
+
import type { IOverlayRequestDismissReason } from '../OverlayViewTypes';
|
|
9
|
+
import type { IOverlayLevel } from '../types';
|
|
10
|
+
|
|
11
|
+
const LAYER_ATTRIBUTE = 'data-onekey-overlay-layer';
|
|
12
|
+
export const ENTRY_ATTRIBUTE = 'data-onekey-overlay-entry';
|
|
13
|
+
|
|
14
|
+
const APP_ROOT_ID = 'root';
|
|
15
|
+
|
|
16
|
+
const layerRoots = new Map<IOverlayLevel, HTMLDivElement>();
|
|
17
|
+
const pageHosts = new Map<string, HTMLDivElement>();
|
|
18
|
+
const dismissRequesters = new Map<
|
|
19
|
+
string,
|
|
20
|
+
(reason: IOverlayRequestDismissReason) => void
|
|
21
|
+
>();
|
|
22
|
+
let installed = false;
|
|
23
|
+
|
|
24
|
+
function createLayerRoot(level: IOverlayLevel): HTMLDivElement {
|
|
25
|
+
const root = document.createElement('div');
|
|
26
|
+
root.setAttribute(LAYER_ATTRIBUTE, level);
|
|
27
|
+
Object.assign(root.style, {
|
|
28
|
+
position: 'fixed',
|
|
29
|
+
inset: '0',
|
|
30
|
+
pointerEvents: 'none',
|
|
31
|
+
zIndex: String(OVERLAY_WEB_Z_INDEX_BASE[level]),
|
|
32
|
+
isolation: 'isolate',
|
|
33
|
+
});
|
|
34
|
+
// Keep DOM order equal to level order so equal z-index ties stay correct.
|
|
35
|
+
const next = OVERLAY_LEVELS.slice(OVERLAY_LEVELS.indexOf(level) + 1)
|
|
36
|
+
.map((l) => layerRoots.get(l))
|
|
37
|
+
.find(Boolean);
|
|
38
|
+
document.body.insertBefore(root, next ?? null);
|
|
39
|
+
return root;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function setInert(element: HTMLElement | null | undefined, inert: boolean) {
|
|
43
|
+
if (element && element.inert !== inert) {
|
|
44
|
+
element.inert = inert;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Elements outside a page host made inert for a blocking page overlay.
|
|
49
|
+
let pageInertElements = new Set<HTMLElement>();
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A page host lives inside the app root, so a blocking page overlay cannot
|
|
53
|
+
* inert the app root; inert every sibling on the path from the host up to it
|
|
54
|
+
* instead (other routes, the page itself, navigation chrome).
|
|
55
|
+
*/
|
|
56
|
+
function siblingsOutside(host: HTMLElement, root: HTMLElement) {
|
|
57
|
+
const siblings = new Set<HTMLElement>();
|
|
58
|
+
let node: HTMLElement = host;
|
|
59
|
+
while (node !== root && node.parentElement) {
|
|
60
|
+
const parent: HTMLElement = node.parentElement;
|
|
61
|
+
for (const sibling of Array.from(parent.children)) {
|
|
62
|
+
if (sibling !== node && sibling instanceof HTMLElement) {
|
|
63
|
+
siblings.add(sibling);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
node = parent;
|
|
67
|
+
}
|
|
68
|
+
return siblings;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function inertEntriesBelow(container: ParentNode, seq: number) {
|
|
72
|
+
container
|
|
73
|
+
.querySelectorAll<HTMLElement>(`[${ENTRY_ATTRIBUTE}]`)
|
|
74
|
+
.forEach((node) => {
|
|
75
|
+
setInert(node, Number(node.dataset.stackOrder) < seq);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Everything rendered below the topmost blocking entry becomes inert. */
|
|
80
|
+
function syncInert() {
|
|
81
|
+
const top = overlayStore.getBlockingTop();
|
|
82
|
+
const appRoot = document.getElementById(APP_ROOT_ID);
|
|
83
|
+
let nextPageInert = new Set<HTMLElement>();
|
|
84
|
+
const pageHost =
|
|
85
|
+
top?.scope === 'page' ? getOverlayPageHost(top.hostKey) : undefined;
|
|
86
|
+
if (top && pageHost && appRoot?.contains(pageHost)) {
|
|
87
|
+
setInert(appRoot, false);
|
|
88
|
+
nextPageInert = siblingsOutside(pageHost, appRoot);
|
|
89
|
+
inertEntriesBelow(pageHost, top.seq);
|
|
90
|
+
} else {
|
|
91
|
+
setInert(appRoot, !!top);
|
|
92
|
+
}
|
|
93
|
+
pageInertElements.forEach((element) => {
|
|
94
|
+
if (!nextPageInert.has(element)) {
|
|
95
|
+
setInert(element, false);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
nextPageInert.forEach((element) => setInert(element, true));
|
|
99
|
+
pageInertElements = nextPageInert;
|
|
100
|
+
|
|
101
|
+
for (const [level, root] of layerRoots) {
|
|
102
|
+
// Global layers render above every page overlay.
|
|
103
|
+
const below =
|
|
104
|
+
top?.scope === 'global' &&
|
|
105
|
+
OVERLAY_LEVEL_ORDER[level] < OVERLAY_LEVEL_ORDER[top.level];
|
|
106
|
+
setInert(root, below);
|
|
107
|
+
if (top?.scope === 'global' && level === top.level) {
|
|
108
|
+
inertEntriesBelow(root, top.seq);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function onKeyDown(event: KeyboardEvent) {
|
|
114
|
+
// Escape while composing belongs to the IME.
|
|
115
|
+
if (event.key !== 'Escape' || event.isComposing || event.keyCode === 229) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const resolution = overlayStore.resolveBack();
|
|
119
|
+
if (resolution.kind === 'pass') {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
event.stopPropagation();
|
|
123
|
+
event.preventDefault();
|
|
124
|
+
if (resolution.kind === 'dismiss') {
|
|
125
|
+
dismissRequesters.get(resolution.id)?.('back');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function installOverlayWebManager() {
|
|
130
|
+
if (installed || typeof document === 'undefined') {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
installed = true;
|
|
134
|
+
overlayStore.subscribe(syncInert);
|
|
135
|
+
document.addEventListener('keydown', onKeyDown, true);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function getOverlayLayerRoot(level: IOverlayLevel): HTMLDivElement {
|
|
139
|
+
installOverlayWebManager();
|
|
140
|
+
let root = layerRoots.get(level);
|
|
141
|
+
if (!root || !root.isConnected) {
|
|
142
|
+
root = createLayerRoot(level);
|
|
143
|
+
layerRoots.set(level, root);
|
|
144
|
+
}
|
|
145
|
+
return root;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function registerOverlayDismissRequester(
|
|
149
|
+
id: string,
|
|
150
|
+
requester: (reason: IOverlayRequestDismissReason) => void
|
|
151
|
+
): () => void {
|
|
152
|
+
dismissRequesters.set(id, requester);
|
|
153
|
+
return () => {
|
|
154
|
+
if (dismissRequesters.get(id) === requester) {
|
|
155
|
+
dismissRequesters.delete(id);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Re-run after entry DOM nodes mount, since the store emits before commit. */
|
|
161
|
+
export function scheduleOverlayInertSync() {
|
|
162
|
+
requestAnimationFrame(syncInert);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function registerOverlayPageHost(
|
|
166
|
+
hostKey: string,
|
|
167
|
+
element: HTMLDivElement
|
|
168
|
+
): () => void {
|
|
169
|
+
installOverlayWebManager();
|
|
170
|
+
pageHosts.set(hostKey, element);
|
|
171
|
+
return () => {
|
|
172
|
+
if (pageHosts.get(hostKey) === element) {
|
|
173
|
+
pageHosts.delete(hostKey);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function getOverlayPageHost(
|
|
179
|
+
hostKey: string | undefined
|
|
180
|
+
): HTMLDivElement | undefined {
|
|
181
|
+
return hostKey ? pageHosts.get(hostKey) : undefined;
|
|
182
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import type { RefObject } from 'react';
|
|
3
|
+
|
|
4
|
+
/** Gap between centered content and the keyboard; matches native. */
|
|
5
|
+
export const KEYBOARD_MARGIN = 16;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* How far to lift overlay content above the software keyboard (>= 0), the
|
|
9
|
+
* same rule as iOS and Android: a sheet until its bottom clears the
|
|
10
|
+
* keyboard, other content until its bottom is `KEYBOARD_MARGIN` above it,
|
|
11
|
+
* never above the top edge. Full-window content never moves.
|
|
12
|
+
*/
|
|
13
|
+
export function computeKeyboardShift({
|
|
14
|
+
keyboardTop,
|
|
15
|
+
contentTop,
|
|
16
|
+
contentBottom,
|
|
17
|
+
isSheet,
|
|
18
|
+
safeTop = 0,
|
|
19
|
+
}: {
|
|
20
|
+
keyboardTop: number | undefined;
|
|
21
|
+
contentTop: number;
|
|
22
|
+
contentBottom: number;
|
|
23
|
+
isSheet: boolean;
|
|
24
|
+
safeTop?: number;
|
|
25
|
+
}): number {
|
|
26
|
+
if (keyboardTop === undefined) {
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
const needed = isSheet
|
|
30
|
+
? contentBottom - keyboardTop
|
|
31
|
+
: contentBottom + KEYBOARD_MARGIN - keyboardTop;
|
|
32
|
+
const room = contentTop - safeTop;
|
|
33
|
+
return Math.max(0, Math.min(needed, room));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Vertical extent of the rendered children of the full-window React root,
|
|
38
|
+
* relative to the content wrapper, from layout offsets (no transforms).
|
|
39
|
+
*/
|
|
40
|
+
function layoutExtent(
|
|
41
|
+
content: HTMLElement | null
|
|
42
|
+
): { top: number; bottom: number } | undefined {
|
|
43
|
+
const root = content?.firstElementChild as HTMLElement | null | undefined;
|
|
44
|
+
if (!root) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
const children = Array.from(root.children).filter(
|
|
48
|
+
(child): child is HTMLElement => child instanceof HTMLElement
|
|
49
|
+
);
|
|
50
|
+
if (children.length === 0) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
top: Math.min(...children.map((c) => root.offsetTop + c.offsetTop)),
|
|
55
|
+
bottom: Math.max(
|
|
56
|
+
...children.map((c) => root.offsetTop + c.offsetTop + c.offsetHeight)
|
|
57
|
+
),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Mobile browsers shrink the visual viewport for the keyboard while fixed
|
|
63
|
+
* layers keep the layout viewport; lift the entry's keyboard layer by the
|
|
64
|
+
* covered part.
|
|
65
|
+
*/
|
|
66
|
+
export function useKeyboardShift({
|
|
67
|
+
layerRef,
|
|
68
|
+
sheetRef,
|
|
69
|
+
contentRef,
|
|
70
|
+
isSheet,
|
|
71
|
+
enabled,
|
|
72
|
+
}: {
|
|
73
|
+
layerRef: RefObject<HTMLDivElement | null>;
|
|
74
|
+
sheetRef: RefObject<HTMLDivElement | null>;
|
|
75
|
+
contentRef: RefObject<HTMLDivElement | null>;
|
|
76
|
+
isSheet: boolean;
|
|
77
|
+
enabled: boolean;
|
|
78
|
+
}) {
|
|
79
|
+
const shiftRef = useRef(0);
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
const viewport = globalThis.visualViewport;
|
|
83
|
+
const layer = layerRef.current;
|
|
84
|
+
if (!viewport || !layer || !enabled) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const update = () => {
|
|
88
|
+
const keyboardBottom = viewport.offsetTop + viewport.height;
|
|
89
|
+
const covered = globalThis.innerHeight - keyboardBottom;
|
|
90
|
+
const current = shiftRef.current;
|
|
91
|
+
// Layout geometry (offsets ignore transforms), placed from the entry,
|
|
92
|
+
// which never transforms: neither the lift (maybe mid-transition) nor
|
|
93
|
+
// the enter / exit animation may skew the resting frame.
|
|
94
|
+
const origin = layer.parentElement?.getBoundingClientRect().top ?? 0;
|
|
95
|
+
let top = origin;
|
|
96
|
+
let bottom = origin + layer.offsetHeight;
|
|
97
|
+
const sheet = sheetRef.current;
|
|
98
|
+
if (isSheet && sheet) {
|
|
99
|
+
top = origin + sheet.offsetTop;
|
|
100
|
+
bottom = top + sheet.offsetHeight;
|
|
101
|
+
} else {
|
|
102
|
+
const extent = layoutExtent(contentRef.current);
|
|
103
|
+
if (extent) {
|
|
104
|
+
top = origin + extent.top;
|
|
105
|
+
bottom = origin + extent.bottom;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const shift = computeKeyboardShift({
|
|
109
|
+
keyboardTop: covered > 1 ? keyboardBottom : undefined,
|
|
110
|
+
contentTop: top,
|
|
111
|
+
contentBottom: bottom,
|
|
112
|
+
isSheet,
|
|
113
|
+
});
|
|
114
|
+
if (Math.abs(shift - current) < 0.5) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
shiftRef.current = shift;
|
|
118
|
+
layer.style.transform = shift ? `translate3d(0, ${-shift}px, 0)` : '';
|
|
119
|
+
};
|
|
120
|
+
viewport.addEventListener('resize', update);
|
|
121
|
+
viewport.addEventListener('scroll', update);
|
|
122
|
+
update();
|
|
123
|
+
return () => {
|
|
124
|
+
viewport.removeEventListener('resize', update);
|
|
125
|
+
viewport.removeEventListener('scroll', update);
|
|
126
|
+
shiftRef.current = 0;
|
|
127
|
+
layer.style.transform = '';
|
|
128
|
+
};
|
|
129
|
+
}, [contentRef, enabled, isSheet, layerRef, sheetRef]);
|
|
130
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import type { RefObject } from 'react';
|
|
3
|
+
|
|
4
|
+
const DRAG_START_THRESHOLD = 6;
|
|
5
|
+
const DISMISS_DISTANCE_RATIO = 0.4;
|
|
6
|
+
const DISMISS_VELOCITY = 0.5; // px per ms
|
|
7
|
+
const UPWARD_RESISTANCE = 0.2;
|
|
8
|
+
const SETTLE_TRANSITION = 'transform 200ms cubic-bezier(0.32, 0.72, 0, 1)';
|
|
9
|
+
|
|
10
|
+
function scrolledAncestorWithin(
|
|
11
|
+
target: EventTarget | null,
|
|
12
|
+
sheet: HTMLElement
|
|
13
|
+
) {
|
|
14
|
+
let node = target instanceof HTMLElement ? target : null;
|
|
15
|
+
while (node && node !== sheet) {
|
|
16
|
+
if (node.scrollTop > 0 && node.scrollHeight > node.clientHeight) {
|
|
17
|
+
return node;
|
|
18
|
+
}
|
|
19
|
+
node = node.parentElement;
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Pointer-driven drag-to-dismiss for the web bottom sheet: follows the
|
|
26
|
+
* finger downward, resists upward, and dismisses past 40% of the height or
|
|
27
|
+
* on a fast flick. A drag never starts from inside scrolled content.
|
|
28
|
+
*/
|
|
29
|
+
export function useSheetDrag({
|
|
30
|
+
sheetRef,
|
|
31
|
+
backdropRef,
|
|
32
|
+
enabled,
|
|
33
|
+
onDismissByPan,
|
|
34
|
+
}: {
|
|
35
|
+
sheetRef: RefObject<HTMLDivElement | null>;
|
|
36
|
+
backdropRef: RefObject<HTMLDivElement | null>;
|
|
37
|
+
enabled: boolean;
|
|
38
|
+
onDismissByPan: () => void;
|
|
39
|
+
}) {
|
|
40
|
+
const onDismissRef = useRef(onDismissByPan);
|
|
41
|
+
onDismissRef.current = onDismissByPan;
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const sheet = sheetRef.current;
|
|
45
|
+
if (!sheet || !enabled) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
let startY = 0;
|
|
49
|
+
let lastY = 0;
|
|
50
|
+
let lastTime = 0;
|
|
51
|
+
let velocity = 0;
|
|
52
|
+
let tracking = false;
|
|
53
|
+
let dragging = false;
|
|
54
|
+
let pointerId = -1;
|
|
55
|
+
|
|
56
|
+
const setOffset = (offset: number) => {
|
|
57
|
+
sheet.style.transform = `translate3d(0, ${offset}px, 0)`;
|
|
58
|
+
const backdrop = backdropRef.current;
|
|
59
|
+
if (backdrop && offset > 0) {
|
|
60
|
+
backdrop.style.opacity = String(
|
|
61
|
+
Math.max(0, 1 - offset / sheet.offsetHeight)
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const onPointerDown = (event: PointerEvent) => {
|
|
67
|
+
if (event.button !== 0 || scrolledAncestorWithin(event.target, sheet)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
tracking = true;
|
|
71
|
+
dragging = false;
|
|
72
|
+
pointerId = event.pointerId;
|
|
73
|
+
startY = event.clientY;
|
|
74
|
+
lastY = event.clientY;
|
|
75
|
+
lastTime = event.timeStamp;
|
|
76
|
+
velocity = 0;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const onPointerMove = (event: PointerEvent) => {
|
|
80
|
+
if (!tracking || event.pointerId !== pointerId) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const dy = event.clientY - startY;
|
|
84
|
+
if (!dragging) {
|
|
85
|
+
if (dy < DRAG_START_THRESHOLD) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
dragging = true;
|
|
89
|
+
sheet.setPointerCapture(pointerId);
|
|
90
|
+
sheet.style.transition = 'none';
|
|
91
|
+
}
|
|
92
|
+
const dt = Math.max(1, event.timeStamp - lastTime);
|
|
93
|
+
velocity = (event.clientY - lastY) / dt;
|
|
94
|
+
lastY = event.clientY;
|
|
95
|
+
lastTime = event.timeStamp;
|
|
96
|
+
setOffset(dy > 0 ? dy : dy * UPWARD_RESISTANCE);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const onPointerUp = (event: PointerEvent) => {
|
|
100
|
+
if (!tracking || event.pointerId !== pointerId) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
tracking = false;
|
|
104
|
+
if (!dragging) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
dragging = false;
|
|
108
|
+
const dy = event.clientY - startY;
|
|
109
|
+
const height = sheet.offsetHeight;
|
|
110
|
+
sheet.style.transition = SETTLE_TRANSITION;
|
|
111
|
+
if (dy > height * DISMISS_DISTANCE_RATIO || velocity > DISMISS_VELOCITY) {
|
|
112
|
+
setOffset(height);
|
|
113
|
+
onDismissRef.current();
|
|
114
|
+
} else {
|
|
115
|
+
setOffset(0);
|
|
116
|
+
if (backdropRef.current) {
|
|
117
|
+
backdropRef.current.style.opacity = '1';
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Swallow the click that follows a drag so buttons under the finger
|
|
121
|
+
// do not fire.
|
|
122
|
+
sheet.addEventListener(
|
|
123
|
+
'click',
|
|
124
|
+
(clickEvent) => {
|
|
125
|
+
clickEvent.stopPropagation();
|
|
126
|
+
clickEvent.preventDefault();
|
|
127
|
+
},
|
|
128
|
+
{ capture: true, once: true }
|
|
129
|
+
);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
sheet.addEventListener('pointerdown', onPointerDown);
|
|
133
|
+
sheet.addEventListener('pointermove', onPointerMove);
|
|
134
|
+
sheet.addEventListener('pointerup', onPointerUp);
|
|
135
|
+
sheet.addEventListener('pointercancel', onPointerUp);
|
|
136
|
+
return () => {
|
|
137
|
+
sheet.removeEventListener('pointerdown', onPointerDown);
|
|
138
|
+
sheet.removeEventListener('pointermove', onPointerMove);
|
|
139
|
+
sheet.removeEventListener('pointerup', onPointerUp);
|
|
140
|
+
sheet.removeEventListener('pointercancel', onPointerUp);
|
|
141
|
+
};
|
|
142
|
+
}, [sheetRef, backdropRef, enabled]);
|
|
143
|
+
}
|