@onekeyfe/react-native-native-overlay 3.0.162-alpha.270 → 6.7.0-alpha.335
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/NativeOverlayHost.kt +22 -28
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayPageHostView.kt +21 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayView.kt +7 -2
- package/android/src/test/java/com/onekey/nativeoverlay/NativeOverlayAccessibilityIsolationTest.kt +112 -0
- package/android/src/test/java/com/onekey/nativeoverlay/NativeOverlayBackTest.kt +122 -0
- package/docs/SPEC.md +91 -10
- package/lib/module/OverlayPageHost.web.js +19 -6
- package/lib/module/OverlayPageHost.web.js.map +1 -1
- package/lib/module/OverlayView.web.js +62 -10
- package/lib/module/OverlayView.web.js.map +1 -1
- package/lib/module/useOverlayController.js +7 -3
- package/lib/module/useOverlayController.js.map +1 -1
- package/lib/module/web/overlayLayers.js +11 -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 +3 -0
- package/lib/module/web/useSheetDrag.js.map +1 -1
- package/lib/typescript/src/OverlayPageHost.web.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/overlayLayers.d.ts +2 -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/OverlayView.web.tsx +134 -41
- package/src/useOverlayController.ts +7 -4
- package/src/web/overlayLayers.ts +28 -10
- package/src/web/useKeyboardShift.ts +1 -1
- package/src/web/useSheetDrag.ts +3 -0
package/src/OverlayView.web.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useId, useLayoutEffect, useRef } from 'react';
|
|
2
|
-
import type { ReactNode, SyntheticEvent } from 'react';
|
|
2
|
+
import type { KeyboardEvent, ReactNode, SyntheticEvent } from 'react';
|
|
3
3
|
|
|
4
4
|
import { createPortal } from 'react-dom';
|
|
5
5
|
import { StyleSheet, View } from 'react-native';
|
|
@@ -7,6 +7,7 @@ import { StyleSheet, View } from 'react-native';
|
|
|
7
7
|
import { OverlayLevelProvider } from './OverlayLevelContext';
|
|
8
8
|
import { OVERLAY_LEVEL_ORDER, isBlockingLevel } from './OverlayLevels';
|
|
9
9
|
import { OVERLAY_OWNER_ATTRIBUTE } from './OverlayPageHost.web';
|
|
10
|
+
import { overlayStore } from './OverlayStore';
|
|
10
11
|
import { useOverlayController } from './useOverlayController';
|
|
11
12
|
import { animateBackdrop, animateTransition } from './web/animateTransition';
|
|
12
13
|
import {
|
|
@@ -48,6 +49,7 @@ interface IWebOverlayEntryProps {
|
|
|
48
49
|
/** Closed but kept mounted: hidden and inert. */
|
|
49
50
|
parked?: boolean;
|
|
50
51
|
stackOrder: number;
|
|
52
|
+
levelOrder: number;
|
|
51
53
|
presented: boolean;
|
|
52
54
|
animation: IResolvedOverlayAnimation;
|
|
53
55
|
blocking: boolean;
|
|
@@ -69,6 +71,7 @@ function WebOverlayEntry({
|
|
|
69
71
|
entryId,
|
|
70
72
|
parked = false,
|
|
71
73
|
stackOrder,
|
|
74
|
+
levelOrder,
|
|
72
75
|
presented,
|
|
73
76
|
animation,
|
|
74
77
|
blocking,
|
|
@@ -96,6 +99,17 @@ function WebOverlayEntry({
|
|
|
96
99
|
const callbacksRef = useRef({ onPresented, onDismissed });
|
|
97
100
|
callbacksRef.current = { onPresented, onDismissed };
|
|
98
101
|
|
|
102
|
+
useLayoutEffect(() => {
|
|
103
|
+
if (!isSheet && sheetRef.current) {
|
|
104
|
+
// The host now survives a presentation switch; clear any in-flight pan.
|
|
105
|
+
sheetRef.current.style.removeProperty('transform');
|
|
106
|
+
sheetRef.current.style.removeProperty('transition');
|
|
107
|
+
if (backdropRef.current) {
|
|
108
|
+
backdropRef.current.style.opacity = '1';
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}, [isSheet]);
|
|
112
|
+
|
|
99
113
|
useLayoutEffect(() => {
|
|
100
114
|
const content = contentRef.current;
|
|
101
115
|
if (!content) {
|
|
@@ -122,7 +136,7 @@ function WebOverlayEntry({
|
|
|
122
136
|
// of the full-window React root.
|
|
123
137
|
const contentRoot = isSheet
|
|
124
138
|
? content
|
|
125
|
-
: (
|
|
139
|
+
: (sheetRef.current?.firstElementChild as HTMLElement | null) ?? content;
|
|
126
140
|
const transition = direction === 'in' ? animation.enter : animation.exit;
|
|
127
141
|
const running = [
|
|
128
142
|
animateTransition(content, contentRoot, transition, direction),
|
|
@@ -190,12 +204,79 @@ function WebOverlayEntry({
|
|
|
190
204
|
const showBackdrop = !!backdropColor || blocking;
|
|
191
205
|
const cornerRadius = sheet?.cornerRadius ?? DEFAULT_SHEET_CORNER_RADIUS;
|
|
192
206
|
|
|
207
|
+
const onKeyDownCapture = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
208
|
+
const root = entryRef.current;
|
|
209
|
+
if (
|
|
210
|
+
event.key !== 'Tab' ||
|
|
211
|
+
event.defaultPrevented ||
|
|
212
|
+
event.nativeEvent.isComposing ||
|
|
213
|
+
!root ||
|
|
214
|
+
!presented ||
|
|
215
|
+
parked ||
|
|
216
|
+
overlayStore.getBlockingTop()?.id !== entryId
|
|
217
|
+
) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const tabbable = Array.from(
|
|
221
|
+
root.querySelectorAll<HTMLElement>(
|
|
222
|
+
'a[href], area[href], button, input:not([type="hidden"]), select, textarea, iframe, object, embed, [tabindex], [contenteditable]:not([contenteditable="false"])'
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
.filter((node) => {
|
|
226
|
+
if (
|
|
227
|
+
(node.tabIndex < 0 &&
|
|
228
|
+
(node.hasAttribute('tabindex') ||
|
|
229
|
+
!node.hasAttribute('contenteditable'))) ||
|
|
230
|
+
node.matches(':disabled')
|
|
231
|
+
) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
for (
|
|
235
|
+
let ancestor: HTMLElement | null = node;
|
|
236
|
+
ancestor;
|
|
237
|
+
ancestor = ancestor.parentElement
|
|
238
|
+
) {
|
|
239
|
+
const style = getComputedStyle(ancestor);
|
|
240
|
+
if (
|
|
241
|
+
ancestor.inert ||
|
|
242
|
+
ancestor.hasAttribute('inert') ||
|
|
243
|
+
ancestor.hidden ||
|
|
244
|
+
ancestor.getAttribute('aria-hidden') === 'true' ||
|
|
245
|
+
style.display === 'none' ||
|
|
246
|
+
style.visibility === 'hidden'
|
|
247
|
+
) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return true;
|
|
252
|
+
})
|
|
253
|
+
.sort((a, b) => {
|
|
254
|
+
const aOrder = a.tabIndex > 0 ? a.tabIndex : Infinity;
|
|
255
|
+
const bOrder = b.tabIndex > 0 ? b.tabIndex : Infinity;
|
|
256
|
+
return aOrder === bOrder ? 0 : aOrder - bOrder;
|
|
257
|
+
});
|
|
258
|
+
const active = document.activeElement;
|
|
259
|
+
const first = tabbable[0];
|
|
260
|
+
const last = tabbable[tabbable.length - 1];
|
|
261
|
+
if (
|
|
262
|
+
!first ||
|
|
263
|
+
active === root ||
|
|
264
|
+
!root.contains(active) ||
|
|
265
|
+
active === (event.shiftKey ? first : last)
|
|
266
|
+
) {
|
|
267
|
+
event.preventDefault();
|
|
268
|
+
(event.shiftKey ? last : first)?.focus({ preventScroll: true });
|
|
269
|
+
if (!first) root.focus({ preventScroll: true });
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
193
273
|
return (
|
|
194
274
|
<div
|
|
195
275
|
ref={entryRef}
|
|
196
276
|
{...{ [ENTRY_ATTRIBUTE]: '' }}
|
|
197
277
|
{...(ownerKey ? { [OVERLAY_OWNER_ATTRIBUTE]: ownerKey } : {})}
|
|
198
278
|
data-stack-order={stackOrder}
|
|
279
|
+
data-overlay-level-order={levelOrder}
|
|
199
280
|
data-testid={testID}
|
|
200
281
|
role={blocking ? 'dialog' : undefined}
|
|
201
282
|
aria-modal={blocking || undefined}
|
|
@@ -211,6 +292,7 @@ function WebOverlayEntry({
|
|
|
211
292
|
visibility: parked ? 'hidden' : undefined,
|
|
212
293
|
}}
|
|
213
294
|
{...ISOLATED_EVENT_HANDLERS}
|
|
295
|
+
onKeyDownCapture={onKeyDownCapture}
|
|
214
296
|
>
|
|
215
297
|
{showBackdrop ? (
|
|
216
298
|
<div
|
|
@@ -241,48 +323,58 @@ function WebOverlayEntry({
|
|
|
241
323
|
ref={contentRef}
|
|
242
324
|
style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}
|
|
243
325
|
>
|
|
244
|
-
{
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
326
|
+
{/* Keep both hosts and the content slot stable across presentations. */}
|
|
327
|
+
<div
|
|
328
|
+
ref={sheetRef}
|
|
329
|
+
style={
|
|
330
|
+
sheet
|
|
331
|
+
? {
|
|
332
|
+
position: 'absolute',
|
|
333
|
+
left: 0,
|
|
334
|
+
right: 0,
|
|
335
|
+
bottom: 0,
|
|
336
|
+
height: sheet.height,
|
|
337
|
+
maxHeight: sheet.maxHeight ?? DEFAULT_SHEET_MAX_HEIGHT,
|
|
338
|
+
display: 'flex',
|
|
339
|
+
flexDirection: 'column',
|
|
340
|
+
overflow: 'hidden',
|
|
341
|
+
pointerEvents: 'auto',
|
|
342
|
+
touchAction: 'pan-y',
|
|
343
|
+
background: sheet.backgroundColor
|
|
344
|
+
? String(sheet.backgroundColor)
|
|
345
|
+
: undefined,
|
|
346
|
+
borderTopLeftRadius: cornerRadius,
|
|
347
|
+
borderTopRightRadius: cornerRadius,
|
|
348
|
+
}
|
|
349
|
+
: {
|
|
350
|
+
position: 'absolute',
|
|
351
|
+
inset: 0,
|
|
352
|
+
display: 'flex',
|
|
353
|
+
flexDirection: 'column',
|
|
354
|
+
pointerEvents: 'none',
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
>
|
|
358
|
+
{sheet?.showHandle ? (
|
|
359
|
+
<div
|
|
360
|
+
style={{
|
|
361
|
+
alignSelf: 'center',
|
|
362
|
+
width: 36,
|
|
363
|
+
height: 5,
|
|
364
|
+
marginTop: 8,
|
|
365
|
+
borderRadius: 2.5,
|
|
366
|
+
background: 'rgba(0, 0, 0, 0.2)',
|
|
367
|
+
flexShrink: 0,
|
|
368
|
+
}}
|
|
369
|
+
/>
|
|
370
|
+
) : null}
|
|
371
|
+
<View
|
|
372
|
+
pointerEvents={sheet ? undefined : 'box-none'}
|
|
373
|
+
style={sheet ? { flexShrink: 1 } : StyleSheet.absoluteFill}
|
|
265
374
|
>
|
|
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
375
|
{children}
|
|
284
376
|
</View>
|
|
285
|
-
|
|
377
|
+
</div>
|
|
286
378
|
</div>
|
|
287
379
|
</div>
|
|
288
380
|
</div>
|
|
@@ -329,6 +421,7 @@ export function OverlayView(props: IOverlayViewProps) {
|
|
|
329
421
|
parked={parked}
|
|
330
422
|
entryId={entry?.id ?? parkedId}
|
|
331
423
|
stackOrder={entry?.seq ?? 0}
|
|
424
|
+
levelOrder={OVERLAY_LEVEL_ORDER[level]}
|
|
332
425
|
presented={!parked && presented}
|
|
333
426
|
animation={animation}
|
|
334
427
|
blocking={blocking ?? isBlockingLevel(level)}
|
|
@@ -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>();
|
|
@@ -105,6 +103,11 @@ export function useOverlayController(
|
|
|
105
103
|
if (entryId || closedByStoreRef.current) {
|
|
106
104
|
return;
|
|
107
105
|
}
|
|
106
|
+
// Page context can become ready after the caller first opens the overlay.
|
|
107
|
+
// Keep the request in its page boundary instead of presenting globally.
|
|
108
|
+
if (scope === 'page' && (!hostKey || !ownerKey)) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
108
111
|
cycleRef.current += 1;
|
|
109
112
|
const id = `overlay-view${baseId}${cycleRef.current}`;
|
|
110
113
|
overlayStore.request(
|
|
@@ -134,7 +137,7 @@ export function useOverlayController(
|
|
|
134
137
|
setEntryId(id);
|
|
135
138
|
// Level / strategy changes apply to the next open cycle only.
|
|
136
139
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
137
|
-
}, [visible, entryId]);
|
|
140
|
+
}, [visible, entryId, scope, hostKey, ownerKey]);
|
|
138
141
|
|
|
139
142
|
useEffect(
|
|
140
143
|
() => () => {
|
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';
|
|
@@ -68,16 +68,27 @@ function siblingsOutside(host: HTMLElement, root: HTMLElement) {
|
|
|
68
68
|
return siblings;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
function inertEntriesBelow(container: ParentNode,
|
|
71
|
+
function inertEntriesBelow(container: ParentNode, top?: IOverlayEntry) {
|
|
72
72
|
container
|
|
73
73
|
.querySelectorAll<HTMLElement>(`[${ENTRY_ATTRIBUTE}]`)
|
|
74
74
|
.forEach((node) => {
|
|
75
|
-
|
|
75
|
+
const level = Number(node.dataset.overlayLevelOrder);
|
|
76
|
+
const below =
|
|
77
|
+
!!top &&
|
|
78
|
+
(level < OVERLAY_LEVEL_ORDER[top.level] ||
|
|
79
|
+
(level === OVERLAY_LEVEL_ORDER[top.level] &&
|
|
80
|
+
Number(node.dataset.stackOrder) < top.seq));
|
|
81
|
+
setInert(
|
|
82
|
+
node,
|
|
83
|
+
below ||
|
|
84
|
+
node.style.visibility === 'hidden' ||
|
|
85
|
+
node.getAttribute('aria-hidden') === 'true'
|
|
86
|
+
);
|
|
76
87
|
});
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
/** Everything rendered below the topmost blocking entry becomes inert. */
|
|
80
|
-
function
|
|
91
|
+
export function syncOverlayInert() {
|
|
81
92
|
const top = overlayStore.getBlockingTop();
|
|
82
93
|
const appRoot = document.getElementById(APP_ROOT_ID);
|
|
83
94
|
let nextPageInert = new Set<HTMLElement>();
|
|
@@ -86,7 +97,6 @@ function syncInert() {
|
|
|
86
97
|
if (top && pageHost && appRoot?.contains(pageHost)) {
|
|
87
98
|
setInert(appRoot, false);
|
|
88
99
|
nextPageInert = siblingsOutside(pageHost, appRoot);
|
|
89
|
-
inertEntriesBelow(pageHost, top.seq);
|
|
90
100
|
} else {
|
|
91
101
|
setInert(appRoot, !!top);
|
|
92
102
|
}
|
|
@@ -98,15 +108,23 @@ function syncInert() {
|
|
|
98
108
|
nextPageInert.forEach((element) => setInert(element, true));
|
|
99
109
|
pageInertElements = nextPageInert;
|
|
100
110
|
|
|
111
|
+
for (const [hostKey, host] of pageHosts) {
|
|
112
|
+
inertEntriesBelow(
|
|
113
|
+
host,
|
|
114
|
+
top?.scope === 'page' && top.hostKey === hostKey ? top : undefined
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
101
118
|
for (const [level, root] of layerRoots) {
|
|
102
119
|
// Global layers render above every page overlay.
|
|
103
120
|
const below =
|
|
104
121
|
top?.scope === 'global' &&
|
|
105
122
|
OVERLAY_LEVEL_ORDER[level] < OVERLAY_LEVEL_ORDER[top.level];
|
|
106
123
|
setInert(root, below);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
124
|
+
inertEntriesBelow(
|
|
125
|
+
root,
|
|
126
|
+
top?.scope === 'global' && level === top.level ? top : undefined
|
|
127
|
+
);
|
|
110
128
|
}
|
|
111
129
|
}
|
|
112
130
|
|
|
@@ -131,7 +149,7 @@ function installOverlayWebManager() {
|
|
|
131
149
|
return;
|
|
132
150
|
}
|
|
133
151
|
installed = true;
|
|
134
|
-
overlayStore.subscribe(
|
|
152
|
+
overlayStore.subscribe(syncOverlayInert);
|
|
135
153
|
document.addEventListener('keydown', onKeyDown, true);
|
|
136
154
|
}
|
|
137
155
|
|
|
@@ -159,7 +177,7 @@ export function registerOverlayDismissRequester(
|
|
|
159
177
|
|
|
160
178
|
/** Re-run after entry DOM nodes mount, since the store emits before commit. */
|
|
161
179
|
export function scheduleOverlayInertSync() {
|
|
162
|
-
requestAnimationFrame(
|
|
180
|
+
requestAnimationFrame(syncOverlayInert);
|
|
163
181
|
}
|
|
164
182
|
|
|
165
183
|
export function registerOverlayPageHost(
|
|
@@ -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
|
@@ -134,6 +134,9 @@ export function useSheetDrag({
|
|
|
134
134
|
sheet.addEventListener('pointerup', onPointerUp);
|
|
135
135
|
sheet.addEventListener('pointercancel', onPointerUp);
|
|
136
136
|
return () => {
|
|
137
|
+
if (sheet.hasPointerCapture?.(pointerId)) {
|
|
138
|
+
sheet.releasePointerCapture(pointerId);
|
|
139
|
+
}
|
|
137
140
|
sheet.removeEventListener('pointerdown', onPointerDown);
|
|
138
141
|
sheet.removeEventListener('pointermove', onPointerMove);
|
|
139
142
|
sheet.removeEventListener('pointerup', onPointerUp);
|