@onekeyfe/react-native-native-overlay 3.0.162-alpha.270 → 6.7.0-8

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 (45) 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 +27 -30
  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 +157 -0
  8. package/docs/SPEC.md +148 -26
  9. package/ios/NativeOverlayContainerView.swift +20 -7
  10. package/ios/NativeOverlaySheetController.swift +35 -1
  11. package/lib/module/OverlayPageHost.web.js +19 -6
  12. package/lib/module/OverlayPageHost.web.js.map +1 -1
  13. package/lib/module/OverlayStore.js +13 -1
  14. package/lib/module/OverlayStore.js.map +1 -1
  15. package/lib/module/OverlayView.web.js +107 -25
  16. package/lib/module/OverlayView.web.js.map +1 -1
  17. package/lib/module/useOverlayController.js +11 -3
  18. package/lib/module/useOverlayController.js.map +1 -1
  19. package/lib/module/web/animateTransition.js +20 -12
  20. package/lib/module/web/animateTransition.js.map +1 -1
  21. package/lib/module/web/overlayLayers.js +27 -9
  22. package/lib/module/web/overlayLayers.js.map +1 -1
  23. package/lib/module/web/useKeyboardShift.js +1 -1
  24. package/lib/module/web/useKeyboardShift.js.map +1 -1
  25. package/lib/module/web/useSheetDrag.js +22 -2
  26. package/lib/module/web/useSheetDrag.js.map +1 -1
  27. package/lib/typescript/src/OverlayPageHost.web.d.ts.map +1 -1
  28. package/lib/typescript/src/OverlayStore.d.ts +2 -0
  29. package/lib/typescript/src/OverlayStore.d.ts.map +1 -1
  30. package/lib/typescript/src/OverlayView.web.d.ts.map +1 -1
  31. package/lib/typescript/src/useOverlayController.d.ts.map +1 -1
  32. package/lib/typescript/src/web/animateTransition.d.ts +2 -2
  33. package/lib/typescript/src/web/animateTransition.d.ts.map +1 -1
  34. package/lib/typescript/src/web/overlayLayers.d.ts +3 -0
  35. package/lib/typescript/src/web/overlayLayers.d.ts.map +1 -1
  36. package/lib/typescript/src/web/useSheetDrag.d.ts.map +1 -1
  37. package/package.json +2 -1
  38. package/src/OverlayPageHost.web.tsx +24 -9
  39. package/src/OverlayStore.ts +13 -1
  40. package/src/OverlayView.web.tsx +197 -56
  41. package/src/useOverlayController.ts +11 -4
  42. package/src/web/animateTransition.ts +30 -11
  43. package/src/web/overlayLayers.ts +48 -10
  44. package/src/web/useKeyboardShift.ts +1 -1
  45. package/src/web/useSheetDrag.ts +23 -2
@@ -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>();
@@ -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
- element.style.transformOrigin = `${(extent.left + extent.right) / 2}px ${
113
- (extent.top + extent.bottom) / 2
114
- }px`;
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 keyframes = direction === 'in' ? [hidden, visible] : [visible, hidden];
121
- return element.animate(keyframes, {
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 keyframes =
133
- direction === 'in'
134
- ? [{ opacity: 0 }, { opacity: 1 }]
135
- : [{ opacity: 1 }, { opacity: 0 }];
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
  }
@@ -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, seq: number) {
72
+ function inertEntriesBelow(container: ParentNode, top?: IOverlayEntry) {
72
73
  container
73
74
  .querySelectorAll<HTMLElement>(`[${ENTRY_ATTRIBUTE}]`)
74
75
  .forEach((node) => {
75
- setInert(node, Number(node.dataset.stackOrder) < seq);
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 syncInert() {
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
- if (top?.scope === 'global' && level === top.level) {
108
- inertEntriesBelow(root, top.seq);
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(syncInert);
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(syncInert);
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;
@@ -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', onPointerUp);
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', onPointerUp);
161
+ sheet.removeEventListener('pointercancel', onPointerCancel);
141
162
  };
142
163
  }, [sheetRef, backdropRef, enabled]);
143
164
  }