@onekeyfe/react-native-native-overlay 3.0.162-alpha.270 → 6.7.0-11
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/android/build.gradle +6 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayAccessibilityIsolation.kt +37 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayAnimation.kt +5 -10
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayEntryRootView.kt +23 -2
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayHost.kt +31 -32
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayPageHostView.kt +21 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayView.kt +10 -3
- package/android/src/test/java/com/onekey/nativeoverlay/NativeOverlayAccessibilityIsolationTest.kt +112 -0
- package/android/src/test/java/com/onekey/nativeoverlay/NativeOverlayBackTest.kt +295 -0
- package/android/src/test/java/com/onekey/nativeoverlay/NativeOverlayMotionTest.kt +101 -0
- package/docs/SPEC.md +198 -30
- package/ios/NativeOverlayContainerView.swift +20 -7
- package/ios/NativeOverlaySheetController.swift +37 -1
- package/lib/module/OverlayPageHost.web.js +19 -6
- package/lib/module/OverlayPageHost.web.js.map +1 -1
- package/lib/module/OverlayStore.js +13 -1
- package/lib/module/OverlayStore.js.map +1 -1
- package/lib/module/OverlayView.web.js +107 -25
- package/lib/module/OverlayView.web.js.map +1 -1
- package/lib/module/useOverlayController.js +11 -3
- package/lib/module/useOverlayController.js.map +1 -1
- package/lib/module/web/animateTransition.js +20 -12
- package/lib/module/web/animateTransition.js.map +1 -1
- package/lib/module/web/overlayLayers.js +27 -9
- package/lib/module/web/overlayLayers.js.map +1 -1
- package/lib/module/web/useKeyboardShift.js +1 -1
- package/lib/module/web/useKeyboardShift.js.map +1 -1
- package/lib/module/web/useSheetDrag.js +22 -2
- package/lib/module/web/useSheetDrag.js.map +1 -1
- package/lib/typescript/src/OverlayPageHost.web.d.ts.map +1 -1
- package/lib/typescript/src/OverlayStore.d.ts +2 -0
- package/lib/typescript/src/OverlayStore.d.ts.map +1 -1
- package/lib/typescript/src/OverlayView.web.d.ts.map +1 -1
- package/lib/typescript/src/useOverlayController.d.ts.map +1 -1
- package/lib/typescript/src/web/animateTransition.d.ts +2 -2
- package/lib/typescript/src/web/animateTransition.d.ts.map +1 -1
- package/lib/typescript/src/web/overlayLayers.d.ts +3 -0
- package/lib/typescript/src/web/overlayLayers.d.ts.map +1 -1
- package/lib/typescript/src/web/useSheetDrag.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/OverlayPageHost.web.tsx +24 -9
- package/src/OverlayStore.ts +13 -1
- package/src/OverlayView.web.tsx +197 -56
- package/src/useOverlayController.ts +11 -4
- package/src/web/animateTransition.ts +30 -11
- package/src/web/overlayLayers.ts +48 -10
- package/src/web/useKeyboardShift.ts +1 -1
- package/src/web/useSheetDrag.ts +23 -2
package/src/OverlayView.web.tsx
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useId,
|
|
5
|
+
useLayoutEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useSyncExternalStore,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import type { KeyboardEvent, ReactNode, SyntheticEvent } from 'react';
|
|
3
10
|
|
|
4
11
|
import { createPortal } from 'react-dom';
|
|
5
12
|
import { StyleSheet, View } from 'react-native';
|
|
@@ -7,6 +14,7 @@ import { StyleSheet, View } from 'react-native';
|
|
|
7
14
|
import { OverlayLevelProvider } from './OverlayLevelContext';
|
|
8
15
|
import { OVERLAY_LEVEL_ORDER, isBlockingLevel } from './OverlayLevels';
|
|
9
16
|
import { OVERLAY_OWNER_ATTRIBUTE } from './OverlayPageHost.web';
|
|
17
|
+
import { overlayStore } from './OverlayStore';
|
|
10
18
|
import { useOverlayController } from './useOverlayController';
|
|
11
19
|
import { animateBackdrop, animateTransition } from './web/animateTransition';
|
|
12
20
|
import {
|
|
@@ -15,6 +23,7 @@ import {
|
|
|
15
23
|
getOverlayPageHost,
|
|
16
24
|
registerOverlayDismissRequester,
|
|
17
25
|
scheduleOverlayInertSync,
|
|
26
|
+
subscribeOverlayPageHost,
|
|
18
27
|
} from './web/overlayLayers';
|
|
19
28
|
import { useKeyboardShift } from './web/useKeyboardShift';
|
|
20
29
|
import { useSheetDrag } from './web/useSheetDrag';
|
|
@@ -48,6 +57,7 @@ interface IWebOverlayEntryProps {
|
|
|
48
57
|
/** Closed but kept mounted: hidden and inert. */
|
|
49
58
|
parked?: boolean;
|
|
50
59
|
stackOrder: number;
|
|
60
|
+
levelOrder: number;
|
|
51
61
|
presented: boolean;
|
|
52
62
|
animation: IResolvedOverlayAnimation;
|
|
53
63
|
blocking: boolean;
|
|
@@ -69,6 +79,7 @@ function WebOverlayEntry({
|
|
|
69
79
|
entryId,
|
|
70
80
|
parked = false,
|
|
71
81
|
stackOrder,
|
|
82
|
+
levelOrder,
|
|
72
83
|
presented,
|
|
73
84
|
animation,
|
|
74
85
|
blocking,
|
|
@@ -92,10 +103,22 @@ function WebOverlayEntry({
|
|
|
92
103
|
const keyboardLayerRef = useRef<HTMLDivElement>(null);
|
|
93
104
|
const backdropRef = useRef<HTMLDivElement>(null);
|
|
94
105
|
const directionRef = useRef<'in' | 'out' | undefined>(undefined);
|
|
106
|
+
const runningAnimationsRef = useRef<Animation[]>([]);
|
|
95
107
|
const restoreFocusRef = useRef<HTMLElement | null>(null);
|
|
96
108
|
const callbacksRef = useRef({ onPresented, onDismissed });
|
|
97
109
|
callbacksRef.current = { onPresented, onDismissed };
|
|
98
110
|
|
|
111
|
+
useLayoutEffect(() => {
|
|
112
|
+
if (!isSheet && sheetRef.current) {
|
|
113
|
+
// The host now survives a presentation switch; clear any in-flight pan.
|
|
114
|
+
sheetRef.current.style.removeProperty('transform');
|
|
115
|
+
sheetRef.current.style.removeProperty('transition');
|
|
116
|
+
if (backdropRef.current) {
|
|
117
|
+
backdropRef.current.style.opacity = '1';
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}, [isSheet]);
|
|
121
|
+
|
|
99
122
|
useLayoutEffect(() => {
|
|
100
123
|
const content = contentRef.current;
|
|
101
124
|
if (!content) {
|
|
@@ -108,32 +131,61 @@ function WebOverlayEntry({
|
|
|
108
131
|
const neverEntered = directionRef.current === undefined;
|
|
109
132
|
directionRef.current = direction;
|
|
110
133
|
scheduleOverlayInertSync();
|
|
134
|
+
const interruptedFrames = new Map<HTMLElement, Keyframe>();
|
|
135
|
+
for (const running of runningAnimationsRef.current) {
|
|
136
|
+
const target = (running.effect as KeyframeEffect | null)?.target;
|
|
137
|
+
if (running.playState === 'running' && target instanceof HTMLElement) {
|
|
138
|
+
const style = getComputedStyle(target);
|
|
139
|
+
interruptedFrames.set(target, {
|
|
140
|
+
opacity: style.opacity,
|
|
141
|
+
transform: style.transform,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
running.cancel();
|
|
145
|
+
}
|
|
146
|
+
runningAnimationsRef.current = [];
|
|
111
147
|
if (direction === 'out' && (neverEntered || pannedAwayRef.current)) {
|
|
148
|
+
restoreFocusRef.current?.focus?.({ preventScroll: true });
|
|
149
|
+
restoreFocusRef.current = null;
|
|
112
150
|
callbacksRef.current.onDismissed();
|
|
113
151
|
return;
|
|
114
152
|
}
|
|
115
|
-
if (direction === 'in'
|
|
116
|
-
|
|
117
|
-
|
|
153
|
+
if (direction === 'in') {
|
|
154
|
+
pannedAwayRef.current = false;
|
|
155
|
+
sheetRef.current?.style.removeProperty('transform');
|
|
156
|
+
sheetRef.current?.style.removeProperty('transition');
|
|
157
|
+
if (backdropRef.current) {
|
|
158
|
+
backdropRef.current.style.opacity = '1';
|
|
159
|
+
}
|
|
160
|
+
if (blocking) {
|
|
161
|
+
restoreFocusRef.current = document.activeElement as HTMLElement | null;
|
|
162
|
+
entryRef.current?.focus({ preventScroll: true });
|
|
163
|
+
}
|
|
118
164
|
}
|
|
119
|
-
content.getAnimations().forEach((running) => running.cancel());
|
|
120
|
-
backdropRef.current?.getAnimations().forEach((running) => running.cancel());
|
|
121
165
|
// A sheet is measured as a whole; other presentations by the children
|
|
122
166
|
// of the full-window React root.
|
|
123
167
|
const contentRoot = isSheet
|
|
124
168
|
? content
|
|
125
|
-
: (
|
|
169
|
+
: (sheetRef.current?.firstElementChild as HTMLElement | null) ?? content;
|
|
126
170
|
const transition = direction === 'in' ? animation.enter : animation.exit;
|
|
127
171
|
const running = [
|
|
128
|
-
animateTransition(
|
|
172
|
+
animateTransition(
|
|
173
|
+
content,
|
|
174
|
+
contentRoot,
|
|
175
|
+
transition,
|
|
176
|
+
direction,
|
|
177
|
+
interruptedFrames
|
|
178
|
+
),
|
|
129
179
|
backdropRef.current
|
|
130
180
|
? animateBackdrop(
|
|
131
181
|
backdropRef.current,
|
|
132
182
|
animation.backdrop.motion,
|
|
133
|
-
direction
|
|
183
|
+
direction,
|
|
184
|
+
interruptedFrames
|
|
134
185
|
)
|
|
135
186
|
: undefined,
|
|
136
187
|
].filter((item): item is Animation => !!item);
|
|
188
|
+
runningAnimationsRef.current = running;
|
|
137
189
|
void Promise.all(running.map((item) => item.finished))
|
|
138
190
|
.then(() => {
|
|
139
191
|
if (directionRef.current !== direction) {
|
|
@@ -190,12 +242,79 @@ function WebOverlayEntry({
|
|
|
190
242
|
const showBackdrop = !!backdropColor || blocking;
|
|
191
243
|
const cornerRadius = sheet?.cornerRadius ?? DEFAULT_SHEET_CORNER_RADIUS;
|
|
192
244
|
|
|
245
|
+
const onKeyDownCapture = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
246
|
+
const root = entryRef.current;
|
|
247
|
+
if (
|
|
248
|
+
event.key !== 'Tab' ||
|
|
249
|
+
event.defaultPrevented ||
|
|
250
|
+
event.nativeEvent.isComposing ||
|
|
251
|
+
!root ||
|
|
252
|
+
!presented ||
|
|
253
|
+
parked ||
|
|
254
|
+
overlayStore.getBlockingTop()?.id !== entryId
|
|
255
|
+
) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const tabbable = Array.from(
|
|
259
|
+
root.querySelectorAll<HTMLElement>(
|
|
260
|
+
'a[href], area[href], button, input:not([type="hidden"]), select, textarea, iframe, object, embed, [tabindex], [contenteditable]:not([contenteditable="false"])'
|
|
261
|
+
)
|
|
262
|
+
)
|
|
263
|
+
.filter((node) => {
|
|
264
|
+
if (
|
|
265
|
+
(node.tabIndex < 0 &&
|
|
266
|
+
(node.hasAttribute('tabindex') ||
|
|
267
|
+
!node.hasAttribute('contenteditable'))) ||
|
|
268
|
+
node.matches(':disabled')
|
|
269
|
+
) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
for (
|
|
273
|
+
let ancestor: HTMLElement | null = node;
|
|
274
|
+
ancestor;
|
|
275
|
+
ancestor = ancestor.parentElement
|
|
276
|
+
) {
|
|
277
|
+
const style = getComputedStyle(ancestor);
|
|
278
|
+
if (
|
|
279
|
+
ancestor.inert ||
|
|
280
|
+
ancestor.hasAttribute('inert') ||
|
|
281
|
+
ancestor.hidden ||
|
|
282
|
+
ancestor.getAttribute('aria-hidden') === 'true' ||
|
|
283
|
+
style.display === 'none' ||
|
|
284
|
+
style.visibility === 'hidden'
|
|
285
|
+
) {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return true;
|
|
290
|
+
})
|
|
291
|
+
.sort((a, b) => {
|
|
292
|
+
const aOrder = a.tabIndex > 0 ? a.tabIndex : Infinity;
|
|
293
|
+
const bOrder = b.tabIndex > 0 ? b.tabIndex : Infinity;
|
|
294
|
+
return aOrder === bOrder ? 0 : aOrder - bOrder;
|
|
295
|
+
});
|
|
296
|
+
const active = document.activeElement;
|
|
297
|
+
const first = tabbable[0];
|
|
298
|
+
const last = tabbable[tabbable.length - 1];
|
|
299
|
+
if (
|
|
300
|
+
!first ||
|
|
301
|
+
active === root ||
|
|
302
|
+
!root.contains(active) ||
|
|
303
|
+
active === (event.shiftKey ? first : last)
|
|
304
|
+
) {
|
|
305
|
+
event.preventDefault();
|
|
306
|
+
(event.shiftKey ? last : first)?.focus({ preventScroll: true });
|
|
307
|
+
if (!first) root.focus({ preventScroll: true });
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
193
311
|
return (
|
|
194
312
|
<div
|
|
195
313
|
ref={entryRef}
|
|
196
314
|
{...{ [ENTRY_ATTRIBUTE]: '' }}
|
|
197
315
|
{...(ownerKey ? { [OVERLAY_OWNER_ATTRIBUTE]: ownerKey } : {})}
|
|
198
316
|
data-stack-order={stackOrder}
|
|
317
|
+
data-overlay-level-order={levelOrder}
|
|
199
318
|
data-testid={testID}
|
|
200
319
|
role={blocking ? 'dialog' : undefined}
|
|
201
320
|
aria-modal={blocking || undefined}
|
|
@@ -211,6 +330,7 @@ function WebOverlayEntry({
|
|
|
211
330
|
visibility: parked ? 'hidden' : undefined,
|
|
212
331
|
}}
|
|
213
332
|
{...ISOLATED_EVENT_HANDLERS}
|
|
333
|
+
onKeyDownCapture={onKeyDownCapture}
|
|
214
334
|
>
|
|
215
335
|
{showBackdrop ? (
|
|
216
336
|
<div
|
|
@@ -221,7 +341,7 @@ function WebOverlayEntry({
|
|
|
221
341
|
inset: 0,
|
|
222
342
|
opacity: 0,
|
|
223
343
|
background: backdropColor ?? 'transparent',
|
|
224
|
-
pointerEvents: blocking ? 'auto' : 'none',
|
|
344
|
+
pointerEvents: blocking || dismissOnBackdropPress ? 'auto' : 'none',
|
|
225
345
|
}}
|
|
226
346
|
/>
|
|
227
347
|
) : null}
|
|
@@ -241,48 +361,58 @@ function WebOverlayEntry({
|
|
|
241
361
|
ref={contentRef}
|
|
242
362
|
style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}
|
|
243
363
|
>
|
|
244
|
-
{
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
364
|
+
{/* Keep both hosts and the content slot stable across presentations. */}
|
|
365
|
+
<div
|
|
366
|
+
ref={sheetRef}
|
|
367
|
+
style={
|
|
368
|
+
sheet
|
|
369
|
+
? {
|
|
370
|
+
position: 'absolute',
|
|
371
|
+
left: 0,
|
|
372
|
+
right: 0,
|
|
373
|
+
bottom: 0,
|
|
374
|
+
height: sheet.height,
|
|
375
|
+
maxHeight: sheet.maxHeight ?? DEFAULT_SHEET_MAX_HEIGHT,
|
|
376
|
+
display: 'flex',
|
|
377
|
+
flexDirection: 'column',
|
|
378
|
+
overflow: 'hidden',
|
|
379
|
+
pointerEvents: 'auto',
|
|
380
|
+
touchAction: 'pan-y',
|
|
381
|
+
background: sheet.backgroundColor
|
|
382
|
+
? String(sheet.backgroundColor)
|
|
383
|
+
: undefined,
|
|
384
|
+
borderTopLeftRadius: cornerRadius,
|
|
385
|
+
borderTopRightRadius: cornerRadius,
|
|
386
|
+
}
|
|
387
|
+
: {
|
|
388
|
+
position: 'absolute',
|
|
389
|
+
inset: 0,
|
|
390
|
+
display: 'flex',
|
|
391
|
+
flexDirection: 'column',
|
|
392
|
+
pointerEvents: 'none',
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
>
|
|
396
|
+
{sheet?.showHandle ? (
|
|
397
|
+
<div
|
|
398
|
+
style={{
|
|
399
|
+
alignSelf: 'center',
|
|
400
|
+
width: 36,
|
|
401
|
+
height: 5,
|
|
402
|
+
marginTop: 8,
|
|
403
|
+
borderRadius: 2.5,
|
|
404
|
+
background: 'rgba(0, 0, 0, 0.2)',
|
|
405
|
+
flexShrink: 0,
|
|
406
|
+
}}
|
|
407
|
+
/>
|
|
408
|
+
) : null}
|
|
409
|
+
<View
|
|
410
|
+
pointerEvents={sheet ? undefined : 'box-none'}
|
|
411
|
+
style={sheet ? { flexShrink: 1 } : StyleSheet.absoluteFill}
|
|
265
412
|
>
|
|
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
413
|
{children}
|
|
284
414
|
</View>
|
|
285
|
-
|
|
415
|
+
</div>
|
|
286
416
|
</div>
|
|
287
417
|
</div>
|
|
288
418
|
</div>
|
|
@@ -317,18 +447,29 @@ export function OverlayView(props: IOverlayViewProps) {
|
|
|
317
447
|
// Closed with `keepContentMounted`: the same entry stays in the layer,
|
|
318
448
|
// hidden and inert, so the content keeps its state and can be measured.
|
|
319
449
|
const parked = !mounted || !entry;
|
|
450
|
+
const subscribePageHost = useCallback(
|
|
451
|
+
(listener: () => void) => subscribeOverlayPageHost(hostKey, listener),
|
|
452
|
+
[hostKey]
|
|
453
|
+
);
|
|
454
|
+
const getPageHost = useCallback(() => getOverlayPageHost(hostKey), [hostKey]);
|
|
455
|
+
const pageHost = useSyncExternalStore(
|
|
456
|
+
subscribePageHost,
|
|
457
|
+
getPageHost,
|
|
458
|
+
() => undefined
|
|
459
|
+
);
|
|
320
460
|
if (parked && !keepContentMounted) {
|
|
321
461
|
return null;
|
|
322
462
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
463
|
+
if (scope === 'page' && !pageHost) {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
326
466
|
return createPortal(
|
|
327
467
|
<WebOverlayEntry
|
|
328
468
|
key="entry"
|
|
329
469
|
parked={parked}
|
|
330
470
|
entryId={entry?.id ?? parkedId}
|
|
331
471
|
stackOrder={entry?.seq ?? 0}
|
|
472
|
+
levelOrder={OVERLAY_LEVEL_ORDER[level]}
|
|
332
473
|
presented={!parked && presented}
|
|
333
474
|
animation={animation}
|
|
334
475
|
blocking={blocking ?? isBlockingLevel(level)}
|
|
@@ -342,15 +483,15 @@ export function OverlayView(props: IOverlayViewProps) {
|
|
|
342
483
|
sheet={presentation === 'sheet' ? sheet ?? {} : undefined}
|
|
343
484
|
// Inside a page host all levels share one stacking context.
|
|
344
485
|
zIndex={
|
|
345
|
-
|
|
486
|
+
scope === 'page'
|
|
346
487
|
? OVERLAY_LEVEL_ORDER[level] * 1000 + (entry?.seq ?? 0)
|
|
347
488
|
: entry?.seq ?? 0
|
|
348
489
|
}
|
|
349
|
-
ownerKey={
|
|
490
|
+
ownerKey={scope === 'page' ? ownerKey : undefined}
|
|
350
491
|
testID={testID}
|
|
351
492
|
>
|
|
352
493
|
<OverlayLevelProvider value={level}>{children}</OverlayLevelProvider>
|
|
353
494
|
</WebOverlayEntry>,
|
|
354
|
-
pageHost
|
|
495
|
+
scope === 'page' ? pageHost! : getOverlayLayerRoot(level)
|
|
355
496
|
);
|
|
356
497
|
}
|
|
@@ -69,9 +69,7 @@ export function useOverlayController(
|
|
|
69
69
|
const pageScope = useOverlayPageScope();
|
|
70
70
|
const hostKey = props.hostKey ?? pageScope.hostKey;
|
|
71
71
|
const ownerKey = props.ownerKey ?? pageScope.ownerKey;
|
|
72
|
-
|
|
73
|
-
const scope =
|
|
74
|
-
props.scope === 'page' && hostKey && ownerKey ? 'page' : 'global';
|
|
72
|
+
const scope = props.scope ?? 'global';
|
|
75
73
|
const baseId = useId();
|
|
76
74
|
const cycleRef = useRef(0);
|
|
77
75
|
const [entryId, setEntryId] = useState<string | undefined>();
|
|
@@ -79,6 +77,7 @@ export function useOverlayController(
|
|
|
79
77
|
// still true, so the effect below does not immediately reopen it.
|
|
80
78
|
const closedByStoreRef = useRef(false);
|
|
81
79
|
const propsRef = useLatest(props);
|
|
80
|
+
const previousVisibleRef = useRef(visible);
|
|
82
81
|
|
|
83
82
|
const snapshot = useSyncExternalStore(
|
|
84
83
|
overlayStore.subscribe,
|
|
@@ -95,6 +94,9 @@ export function useOverlayController(
|
|
|
95
94
|
);
|
|
96
95
|
|
|
97
96
|
useEffect(() => {
|
|
97
|
+
const reopening = visible && !previousVisibleRef.current;
|
|
98
|
+
previousVisibleRef.current = visible;
|
|
99
|
+
if (reopening && entryId) overlayStore.reopen(entryId);
|
|
98
100
|
if (!visible) {
|
|
99
101
|
closedByStoreRef.current = false;
|
|
100
102
|
if (entryId) {
|
|
@@ -105,6 +107,11 @@ export function useOverlayController(
|
|
|
105
107
|
if (entryId || closedByStoreRef.current) {
|
|
106
108
|
return;
|
|
107
109
|
}
|
|
110
|
+
// Page context can become ready after the caller first opens the overlay.
|
|
111
|
+
// Keep the request in its page boundary instead of presenting globally.
|
|
112
|
+
if (scope === 'page' && (!hostKey || !ownerKey)) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
108
115
|
cycleRef.current += 1;
|
|
109
116
|
const id = `overlay-view${baseId}${cycleRef.current}`;
|
|
110
117
|
overlayStore.request(
|
|
@@ -134,7 +141,7 @@ export function useOverlayController(
|
|
|
134
141
|
setEntryId(id);
|
|
135
142
|
// Level / strategy changes apply to the next open cycle only.
|
|
136
143
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
137
|
-
}, [visible, entryId]);
|
|
144
|
+
}, [visible, entryId, scope, hostKey, ownerKey]);
|
|
138
145
|
|
|
139
146
|
useEffect(
|
|
140
147
|
() => () => {
|
|
@@ -100,7 +100,8 @@ export function animateTransition(
|
|
|
100
100
|
element: HTMLElement,
|
|
101
101
|
contentRoot: HTMLElement,
|
|
102
102
|
transition: IResolvedOverlayTransition,
|
|
103
|
-
direction: 'in' | 'out'
|
|
103
|
+
direction: 'in' | 'out',
|
|
104
|
+
interruptedFrames?: ReadonlyMap<HTMLElement, Keyframe>
|
|
104
105
|
): Animation | undefined {
|
|
105
106
|
if (transition.type === 'none') {
|
|
106
107
|
return undefined;
|
|
@@ -109,16 +110,32 @@ export function animateTransition(
|
|
|
109
110
|
? { ...transition, type: 'fade', fade: true }
|
|
110
111
|
: transition;
|
|
111
112
|
const extent = measureContentExtent(contentRoot);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
// The React Native root wraps the positioned panel. Animate that panel so its
|
|
114
|
+
// offset parent stays unscaled; multi-child roots retain their shared motion.
|
|
115
|
+
const anchoredContent =
|
|
116
|
+
effective.origin === 'anchor' && contentRoot.childElementCount === 1
|
|
117
|
+
? contentRoot.firstElementChild
|
|
118
|
+
: undefined;
|
|
119
|
+
const target =
|
|
120
|
+
anchoredContent instanceof HTMLElement ? anchoredContent : element;
|
|
121
|
+
const targetRect =
|
|
122
|
+
target === element ? undefined : target.getBoundingClientRect();
|
|
123
|
+
target.style.transformOrigin = `${
|
|
124
|
+
(extent.left + extent.right) / 2 - (targetRect?.left ?? 0)
|
|
125
|
+
}px ${(extent.top + extent.bottom) / 2 - (targetRect?.top ?? 0)}px`;
|
|
115
126
|
const hidden = hiddenKeyframe(effective, extent);
|
|
116
127
|
const visible: Keyframe = {};
|
|
117
128
|
Object.keys(hidden).forEach((key) => {
|
|
118
129
|
visible[key] = VISIBLE_KEYFRAME[key];
|
|
119
130
|
});
|
|
120
|
-
const
|
|
121
|
-
|
|
131
|
+
const sampled = interruptedFrames?.get(target);
|
|
132
|
+
const initial = direction === 'in' ? hidden : visible;
|
|
133
|
+
const start: Keyframe = {};
|
|
134
|
+
Object.keys(initial).forEach((key) => {
|
|
135
|
+
start[key] = sampled?.[key] ?? initial[key];
|
|
136
|
+
});
|
|
137
|
+
const keyframes = [start, direction === 'in' ? visible : hidden];
|
|
138
|
+
return target.animate(keyframes, {
|
|
122
139
|
...toTiming(effective.motion),
|
|
123
140
|
fill: 'forwards',
|
|
124
141
|
});
|
|
@@ -127,11 +144,13 @@ export function animateTransition(
|
|
|
127
144
|
export function animateBackdrop(
|
|
128
145
|
element: HTMLElement,
|
|
129
146
|
motion: IOverlayMotion,
|
|
130
|
-
direction: 'in' | 'out'
|
|
147
|
+
direction: 'in' | 'out',
|
|
148
|
+
interruptedFrames?: ReadonlyMap<HTMLElement, Keyframe>
|
|
131
149
|
): Animation {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
150
|
+
const sampled = interruptedFrames?.get(element)?.opacity;
|
|
151
|
+
const keyframes = [
|
|
152
|
+
{ opacity: sampled ?? (direction === 'in' ? 0 : 1) },
|
|
153
|
+
{ opacity: direction === 'in' ? 1 : 0 },
|
|
154
|
+
];
|
|
136
155
|
return element.animate(keyframes, { ...toTiming(motion), fill: 'forwards' });
|
|
137
156
|
}
|
package/src/web/overlayLayers.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
import { overlayStore } from '../OverlayStore';
|
|
7
7
|
|
|
8
8
|
import type { IOverlayRequestDismissReason } from '../OverlayViewTypes';
|
|
9
|
-
import type { IOverlayLevel } from '../types';
|
|
9
|
+
import type { IOverlayEntry, IOverlayLevel } from '../types';
|
|
10
10
|
|
|
11
11
|
const LAYER_ATTRIBUTE = 'data-onekey-overlay-layer';
|
|
12
12
|
export const ENTRY_ATTRIBUTE = 'data-onekey-overlay-entry';
|
|
@@ -15,6 +15,7 @@ const APP_ROOT_ID = 'root';
|
|
|
15
15
|
|
|
16
16
|
const layerRoots = new Map<IOverlayLevel, HTMLDivElement>();
|
|
17
17
|
const pageHosts = new Map<string, HTMLDivElement>();
|
|
18
|
+
const pageHostListeners = new Map<string, Set<() => void>>();
|
|
18
19
|
const dismissRequesters = new Map<
|
|
19
20
|
string,
|
|
20
21
|
(reason: IOverlayRequestDismissReason) => void
|
|
@@ -68,16 +69,27 @@ function siblingsOutside(host: HTMLElement, root: HTMLElement) {
|
|
|
68
69
|
return siblings;
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
function inertEntriesBelow(container: ParentNode,
|
|
72
|
+
function inertEntriesBelow(container: ParentNode, top?: IOverlayEntry) {
|
|
72
73
|
container
|
|
73
74
|
.querySelectorAll<HTMLElement>(`[${ENTRY_ATTRIBUTE}]`)
|
|
74
75
|
.forEach((node) => {
|
|
75
|
-
|
|
76
|
+
const level = Number(node.dataset.overlayLevelOrder);
|
|
77
|
+
const below =
|
|
78
|
+
!!top &&
|
|
79
|
+
(level < OVERLAY_LEVEL_ORDER[top.level] ||
|
|
80
|
+
(level === OVERLAY_LEVEL_ORDER[top.level] &&
|
|
81
|
+
Number(node.dataset.stackOrder) < top.seq));
|
|
82
|
+
setInert(
|
|
83
|
+
node,
|
|
84
|
+
below ||
|
|
85
|
+
node.style.visibility === 'hidden' ||
|
|
86
|
+
node.getAttribute('aria-hidden') === 'true'
|
|
87
|
+
);
|
|
76
88
|
});
|
|
77
89
|
}
|
|
78
90
|
|
|
79
91
|
/** Everything rendered below the topmost blocking entry becomes inert. */
|
|
80
|
-
function
|
|
92
|
+
export function syncOverlayInert() {
|
|
81
93
|
const top = overlayStore.getBlockingTop();
|
|
82
94
|
const appRoot = document.getElementById(APP_ROOT_ID);
|
|
83
95
|
let nextPageInert = new Set<HTMLElement>();
|
|
@@ -86,7 +98,6 @@ function syncInert() {
|
|
|
86
98
|
if (top && pageHost && appRoot?.contains(pageHost)) {
|
|
87
99
|
setInert(appRoot, false);
|
|
88
100
|
nextPageInert = siblingsOutside(pageHost, appRoot);
|
|
89
|
-
inertEntriesBelow(pageHost, top.seq);
|
|
90
101
|
} else {
|
|
91
102
|
setInert(appRoot, !!top);
|
|
92
103
|
}
|
|
@@ -98,15 +109,23 @@ function syncInert() {
|
|
|
98
109
|
nextPageInert.forEach((element) => setInert(element, true));
|
|
99
110
|
pageInertElements = nextPageInert;
|
|
100
111
|
|
|
112
|
+
for (const [hostKey, host] of pageHosts) {
|
|
113
|
+
inertEntriesBelow(
|
|
114
|
+
host,
|
|
115
|
+
top?.scope === 'page' && top.hostKey === hostKey ? top : undefined
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
101
119
|
for (const [level, root] of layerRoots) {
|
|
102
120
|
// Global layers render above every page overlay.
|
|
103
121
|
const below =
|
|
104
122
|
top?.scope === 'global' &&
|
|
105
123
|
OVERLAY_LEVEL_ORDER[level] < OVERLAY_LEVEL_ORDER[top.level];
|
|
106
124
|
setInert(root, below);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
125
|
+
inertEntriesBelow(
|
|
126
|
+
root,
|
|
127
|
+
top?.scope === 'global' && level === top.level ? top : undefined
|
|
128
|
+
);
|
|
110
129
|
}
|
|
111
130
|
}
|
|
112
131
|
|
|
@@ -131,7 +150,7 @@ function installOverlayWebManager() {
|
|
|
131
150
|
return;
|
|
132
151
|
}
|
|
133
152
|
installed = true;
|
|
134
|
-
overlayStore.subscribe(
|
|
153
|
+
overlayStore.subscribe(syncOverlayInert);
|
|
135
154
|
document.addEventListener('keydown', onKeyDown, true);
|
|
136
155
|
}
|
|
137
156
|
|
|
@@ -159,7 +178,7 @@ export function registerOverlayDismissRequester(
|
|
|
159
178
|
|
|
160
179
|
/** Re-run after entry DOM nodes mount, since the store emits before commit. */
|
|
161
180
|
export function scheduleOverlayInertSync() {
|
|
162
|
-
requestAnimationFrame(
|
|
181
|
+
requestAnimationFrame(syncOverlayInert);
|
|
163
182
|
}
|
|
164
183
|
|
|
165
184
|
export function registerOverlayPageHost(
|
|
@@ -168,13 +187,32 @@ export function registerOverlayPageHost(
|
|
|
168
187
|
): () => void {
|
|
169
188
|
installOverlayWebManager();
|
|
170
189
|
pageHosts.set(hostKey, element);
|
|
190
|
+
pageHostListeners.get(hostKey)?.forEach((listener) => listener());
|
|
171
191
|
return () => {
|
|
172
192
|
if (pageHosts.get(hostKey) === element) {
|
|
173
193
|
pageHosts.delete(hostKey);
|
|
194
|
+
pageHostListeners.get(hostKey)?.forEach((listener) => listener());
|
|
174
195
|
}
|
|
175
196
|
};
|
|
176
197
|
}
|
|
177
198
|
|
|
199
|
+
export function subscribeOverlayPageHost(
|
|
200
|
+
hostKey: string | undefined,
|
|
201
|
+
listener: () => void
|
|
202
|
+
): () => void {
|
|
203
|
+
if (!hostKey) return () => {};
|
|
204
|
+
let listeners = pageHostListeners.get(hostKey);
|
|
205
|
+
if (!listeners) {
|
|
206
|
+
listeners = new Set();
|
|
207
|
+
pageHostListeners.set(hostKey, listeners);
|
|
208
|
+
}
|
|
209
|
+
listeners.add(listener);
|
|
210
|
+
return () => {
|
|
211
|
+
listeners.delete(listener);
|
|
212
|
+
if (listeners.size === 0) pageHostListeners.delete(hostKey);
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
178
216
|
export function getOverlayPageHost(
|
|
179
217
|
hostKey: string | undefined
|
|
180
218
|
): HTMLDivElement | undefined {
|
|
@@ -99,7 +99,7 @@ export function useKeyboardShift({
|
|
|
99
99
|
top = origin + sheet.offsetTop;
|
|
100
100
|
bottom = top + sheet.offsetHeight;
|
|
101
101
|
} else {
|
|
102
|
-
const extent = layoutExtent(contentRef.current);
|
|
102
|
+
const extent = layoutExtent(sheet ?? contentRef.current);
|
|
103
103
|
if (extent) {
|
|
104
104
|
top = origin + extent.top;
|
|
105
105
|
bottom = origin + extent.bottom;
|
package/src/web/useSheetDrag.ts
CHANGED
|
@@ -129,15 +129,36 @@ export function useSheetDrag({
|
|
|
129
129
|
);
|
|
130
130
|
};
|
|
131
131
|
|
|
132
|
+
const onPointerCancel = (event: PointerEvent) => {
|
|
133
|
+
if (!tracking || event.pointerId !== pointerId) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
tracking = false;
|
|
137
|
+
dragging = false;
|
|
138
|
+
velocity = 0;
|
|
139
|
+
if (sheet.hasPointerCapture?.(pointerId)) {
|
|
140
|
+
sheet.releasePointerCapture(pointerId);
|
|
141
|
+
}
|
|
142
|
+
pointerId = -1;
|
|
143
|
+
sheet.style.transform = '';
|
|
144
|
+
sheet.style.transition = '';
|
|
145
|
+
if (backdropRef.current) {
|
|
146
|
+
backdropRef.current.style.opacity = '1';
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
132
150
|
sheet.addEventListener('pointerdown', onPointerDown);
|
|
133
151
|
sheet.addEventListener('pointermove', onPointerMove);
|
|
134
152
|
sheet.addEventListener('pointerup', onPointerUp);
|
|
135
|
-
sheet.addEventListener('pointercancel',
|
|
153
|
+
sheet.addEventListener('pointercancel', onPointerCancel);
|
|
136
154
|
return () => {
|
|
155
|
+
if (sheet.hasPointerCapture?.(pointerId)) {
|
|
156
|
+
sheet.releasePointerCapture(pointerId);
|
|
157
|
+
}
|
|
137
158
|
sheet.removeEventListener('pointerdown', onPointerDown);
|
|
138
159
|
sheet.removeEventListener('pointermove', onPointerMove);
|
|
139
160
|
sheet.removeEventListener('pointerup', onPointerUp);
|
|
140
|
-
sheet.removeEventListener('pointercancel',
|
|
161
|
+
sheet.removeEventListener('pointercancel', onPointerCancel);
|
|
141
162
|
};
|
|
142
163
|
}, [sheetRef, backdropRef, enabled]);
|
|
143
164
|
}
|