@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.
Files changed (33) hide show
  1. package/android/build.gradle +6 -0
  2. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayAccessibilityIsolation.kt +37 -0
  3. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayHost.kt +22 -28
  4. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayPageHostView.kt +21 -0
  5. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayView.kt +7 -2
  6. package/android/src/test/java/com/onekey/nativeoverlay/NativeOverlayAccessibilityIsolationTest.kt +112 -0
  7. package/android/src/test/java/com/onekey/nativeoverlay/NativeOverlayBackTest.kt +122 -0
  8. package/docs/SPEC.md +91 -10
  9. package/lib/module/OverlayPageHost.web.js +19 -6
  10. package/lib/module/OverlayPageHost.web.js.map +1 -1
  11. package/lib/module/OverlayView.web.js +62 -10
  12. package/lib/module/OverlayView.web.js.map +1 -1
  13. package/lib/module/useOverlayController.js +7 -3
  14. package/lib/module/useOverlayController.js.map +1 -1
  15. package/lib/module/web/overlayLayers.js +11 -9
  16. package/lib/module/web/overlayLayers.js.map +1 -1
  17. package/lib/module/web/useKeyboardShift.js +1 -1
  18. package/lib/module/web/useKeyboardShift.js.map +1 -1
  19. package/lib/module/web/useSheetDrag.js +3 -0
  20. package/lib/module/web/useSheetDrag.js.map +1 -1
  21. package/lib/typescript/src/OverlayPageHost.web.d.ts.map +1 -1
  22. package/lib/typescript/src/OverlayView.web.d.ts.map +1 -1
  23. package/lib/typescript/src/useOverlayController.d.ts.map +1 -1
  24. package/lib/typescript/src/web/overlayLayers.d.ts +2 -0
  25. package/lib/typescript/src/web/overlayLayers.d.ts.map +1 -1
  26. package/lib/typescript/src/web/useSheetDrag.d.ts.map +1 -1
  27. package/package.json +2 -1
  28. package/src/OverlayPageHost.web.tsx +24 -9
  29. package/src/OverlayView.web.tsx +134 -41
  30. package/src/useOverlayController.ts +7 -4
  31. package/src/web/overlayLayers.ts +28 -10
  32. package/src/web/useKeyboardShift.ts +1 -1
  33. package/src/web/useSheetDrag.ts +3 -0
@@ -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
- : (content.firstElementChild as HTMLElement | null) ?? content;
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
- {sheet ? (
245
- <div
246
- ref={sheetRef}
247
- style={{
248
- position: 'absolute',
249
- left: 0,
250
- right: 0,
251
- bottom: 0,
252
- height: sheet.height,
253
- maxHeight: sheet.maxHeight ?? DEFAULT_SHEET_MAX_HEIGHT,
254
- display: 'flex',
255
- flexDirection: 'column',
256
- overflow: 'hidden',
257
- pointerEvents: 'auto',
258
- touchAction: 'pan-y',
259
- background: sheet.backgroundColor
260
- ? String(sheet.backgroundColor)
261
- : undefined,
262
- borderTopLeftRadius: cornerRadius,
263
- borderTopRightRadius: cornerRadius,
264
- }}
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
- // Without a host or owner there is nowhere to anchor a page overlay.
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
  () => () => {
@@ -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, seq: number) {
71
+ function inertEntriesBelow(container: ParentNode, top?: IOverlayEntry) {
72
72
  container
73
73
  .querySelectorAll<HTMLElement>(`[${ENTRY_ATTRIBUTE}]`)
74
74
  .forEach((node) => {
75
- setInert(node, Number(node.dataset.stackOrder) < seq);
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 syncInert() {
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
- if (top?.scope === 'global' && level === top.level) {
108
- inertEntriesBelow(root, top.seq);
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(syncInert);
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(syncInert);
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;
@@ -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);