@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.
Files changed (48) 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/NativeOverlayAnimation.kt +5 -10
  4. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayEntryRootView.kt +23 -2
  5. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayHost.kt +31 -32
  6. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayPageHostView.kt +21 -0
  7. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayView.kt +10 -3
  8. package/android/src/test/java/com/onekey/nativeoverlay/NativeOverlayAccessibilityIsolationTest.kt +112 -0
  9. package/android/src/test/java/com/onekey/nativeoverlay/NativeOverlayBackTest.kt +295 -0
  10. package/android/src/test/java/com/onekey/nativeoverlay/NativeOverlayMotionTest.kt +101 -0
  11. package/docs/SPEC.md +198 -30
  12. package/ios/NativeOverlayContainerView.swift +20 -7
  13. package/ios/NativeOverlaySheetController.swift +37 -1
  14. package/lib/module/OverlayPageHost.web.js +19 -6
  15. package/lib/module/OverlayPageHost.web.js.map +1 -1
  16. package/lib/module/OverlayStore.js +13 -1
  17. package/lib/module/OverlayStore.js.map +1 -1
  18. package/lib/module/OverlayView.web.js +107 -25
  19. package/lib/module/OverlayView.web.js.map +1 -1
  20. package/lib/module/useOverlayController.js +11 -3
  21. package/lib/module/useOverlayController.js.map +1 -1
  22. package/lib/module/web/animateTransition.js +20 -12
  23. package/lib/module/web/animateTransition.js.map +1 -1
  24. package/lib/module/web/overlayLayers.js +27 -9
  25. package/lib/module/web/overlayLayers.js.map +1 -1
  26. package/lib/module/web/useKeyboardShift.js +1 -1
  27. package/lib/module/web/useKeyboardShift.js.map +1 -1
  28. package/lib/module/web/useSheetDrag.js +22 -2
  29. package/lib/module/web/useSheetDrag.js.map +1 -1
  30. package/lib/typescript/src/OverlayPageHost.web.d.ts.map +1 -1
  31. package/lib/typescript/src/OverlayStore.d.ts +2 -0
  32. package/lib/typescript/src/OverlayStore.d.ts.map +1 -1
  33. package/lib/typescript/src/OverlayView.web.d.ts.map +1 -1
  34. package/lib/typescript/src/useOverlayController.d.ts.map +1 -1
  35. package/lib/typescript/src/web/animateTransition.d.ts +2 -2
  36. package/lib/typescript/src/web/animateTransition.d.ts.map +1 -1
  37. package/lib/typescript/src/web/overlayLayers.d.ts +3 -0
  38. package/lib/typescript/src/web/overlayLayers.d.ts.map +1 -1
  39. package/lib/typescript/src/web/useSheetDrag.d.ts.map +1 -1
  40. package/package.json +2 -1
  41. package/src/OverlayPageHost.web.tsx +24 -9
  42. package/src/OverlayStore.ts +13 -1
  43. package/src/OverlayView.web.tsx +197 -56
  44. package/src/useOverlayController.ts +11 -4
  45. package/src/web/animateTransition.ts +30 -11
  46. package/src/web/overlayLayers.ts +48 -10
  47. package/src/web/useKeyboardShift.ts +1 -1
  48. package/src/web/useSheetDrag.ts +23 -2
@@ -0,0 +1,295 @@
1
+ package com.onekey.nativeoverlay
2
+
3
+ import android.view.KeyEvent
4
+ import android.view.MotionEvent
5
+ import android.view.View
6
+ import android.widget.FrameLayout
7
+ import androidx.activity.ComponentActivity
8
+ import com.facebook.react.bridge.BridgeReactContext
9
+ import com.facebook.react.uimanager.PointerEvents
10
+ import com.facebook.react.uimanager.ThemedReactContext
11
+ import com.facebook.react.uimanager.TouchTargetHelper
12
+ import com.facebook.react.views.view.ReactViewGroup
13
+ import java.lang.ref.WeakReference
14
+ import java.util.WeakHashMap
15
+ import org.junit.Assert.assertEquals
16
+ import org.junit.Assert.assertFalse
17
+ import org.junit.Assert.assertNotSame
18
+ import org.junit.Assert.assertSame
19
+ import org.junit.Assert.assertTrue
20
+ import org.junit.Before
21
+ import org.junit.Test
22
+ import org.junit.runner.RunWith
23
+ import org.robolectric.Robolectric
24
+ import org.robolectric.RobolectricTestRunner
25
+ import org.robolectric.annotation.Config
26
+
27
+ @RunWith(RobolectricTestRunner::class)
28
+ @Config(sdk = [34])
29
+ class NativeOverlayBackTest {
30
+ private lateinit var activity: ComponentActivity
31
+ private lateinit var context: ThemedReactContext
32
+ private lateinit var host: NativeOverlayHost
33
+ private val dismissals = mutableListOf<String>()
34
+
35
+ @Before
36
+ fun setUp() {
37
+ activity = Robolectric.buildActivity(ComponentActivity::class.java).setup().visible().get()
38
+ val reactContext = BridgeReactContext(activity)
39
+ reactContext.onHostResume(activity)
40
+ context = ThemedReactContext(reactContext, activity, "OverlayBackTest", 1)
41
+ host = NativeOverlayHost.of(activity)
42
+ }
43
+
44
+ private fun entry(name: String, blocking: Boolean, dismissible: Boolean, order: Int = 0) =
45
+ NativeOverlayEntryRootView(context).apply {
46
+ this.blocking = blocking
47
+ dismissOnBackPress = dismissible
48
+ stackOrder = order
49
+ isShownForInput = true
50
+ onRequestDismiss = { dismissals.add("$name:$it") }
51
+ }
52
+
53
+ private fun back(action: Int = KeyEvent.ACTION_UP, flags: Int = 0) =
54
+ host.handleBackKey(KeyEvent(0L, 0L, action, KeyEvent.KEYCODE_BACK, 0, 0, 0, 0, flags))
55
+
56
+ @Test
57
+ fun dismissibleNonBlockingGlobalEntryWinsOverLowerBlocker() {
58
+ host.attach(entry("lower", true, true), NativeOverlayLevel.MODAL)
59
+ host.attach(entry("top", false, true), NativeOverlayLevel.TOAST)
60
+ assertTrue(back(KeyEvent.ACTION_DOWN))
61
+ assertEquals(emptyList<String>(), dismissals)
62
+ assertTrue(back())
63
+ assertEquals(listOf("top:back"), dismissals)
64
+ }
65
+
66
+ @Test
67
+ fun dispatcherHandlesNonBlockingEntryAndKeepsPageAccessible() {
68
+ val page = View(activity).apply { importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES }
69
+ activity.setContentView(page)
70
+ val top = entry("top", false, true)
71
+ host.attach(top, NativeOverlayLevel.MODAL)
72
+ activity.onBackPressedDispatcher.onBackPressed()
73
+ assertEquals(listOf("top:back"), dismissals)
74
+ assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_YES, page.importantForAccessibility)
75
+ host.detach(top)
76
+ assertFalse(back())
77
+ }
78
+
79
+ @Test
80
+ fun nonDismissibleBlockerConsumesBackWithoutClosingLowerEntry() {
81
+ host.attach(entry("lower", false, true), NativeOverlayLevel.MODAL)
82
+ host.attach(entry("lock", true, false), NativeOverlayLevel.LOCK)
83
+ assertTrue(back())
84
+ assertEquals(emptyList<String>(), dismissals)
85
+ }
86
+
87
+ @Test
88
+ fun passiveOrNotInputReadyEntriesDoNotTakeBack() {
89
+ host.attach(entry("passive", false, false), NativeOverlayLevel.TOAST)
90
+ host.attach(entry("hidden", true, true).apply { isShownForInput = false }, NativeOverlayLevel.LOCK)
91
+ assertFalse(back())
92
+ assertEquals(emptyList<String>(), dismissals)
93
+ }
94
+
95
+ @Test
96
+ fun canceledKeyUpDoesNotDismiss() {
97
+ host.attach(entry("top", false, true), NativeOverlayLevel.MODAL)
98
+ assertTrue(back(flags = KeyEvent.FLAG_CANCELED))
99
+ assertEquals(emptyList<String>(), dismissals)
100
+ }
101
+
102
+ @Test
103
+ fun dismissiblePageEntryWinsOverLowerPageBlockerAndSuspensionSkipsIt() {
104
+ val pageHost = NativeOverlayPageHostView(context).apply { hostKey = "page-back-test" }
105
+ activity.setContentView(pageHost)
106
+ val lower = entry("lower", true, true).apply { ownerKey = "owner" }
107
+ val top = entry("top", false, true, 1).apply { ownerKey = "owner" }
108
+ pageHost.attach(lower)
109
+ pageHost.attach(top)
110
+ assertTrue(back())
111
+ assertEquals(listOf("top:back"), dismissals)
112
+ pageHost.setSuspendedOwners("owner")
113
+ assertFalse(back())
114
+ pageHost.invalidateHost()
115
+ }
116
+
117
+ @Test
118
+ fun globalEntryTakesBackBeforePageEntryAndDismissalRestoresPageTarget() {
119
+ val pageHost = NativeOverlayPageHostView(context).apply { hostKey = "page-global-test" }
120
+ activity.setContentView(FrameLayout(activity).apply { addView(pageHost) })
121
+ pageHost.attach(entry("page", false, true))
122
+ val global = entry("global", false, true)
123
+ host.attach(global, NativeOverlayLevel.MODAL)
124
+ assertTrue(back())
125
+ host.detach(global)
126
+ assertTrue(back())
127
+ assertEquals(listOf("global:back", "page:back"), dismissals)
128
+ pageHost.invalidateHost()
129
+ }
130
+
131
+ @Test
132
+ fun closingGlobalBlockerKeepsTouchBackAndAccessibilityOwnership() {
133
+ var pageTouches = 0
134
+ val page = View(activity).apply {
135
+ setOnTouchListener { _, _ ->
136
+ pageTouches += 1
137
+ true
138
+ }
139
+ }
140
+ activity.setContentView(page)
141
+ val lower = entry("lower", true, true)
142
+ val top = entry("top", true, true, 1)
143
+ host.attach(lower, NativeOverlayLevel.MODAL)
144
+ host.attach(top, NativeOverlayLevel.MODAL)
145
+ top.isClosing = true
146
+ host.onEntriesChanged()
147
+
148
+ assertEquals(PointerEvents.BOX_ONLY, top.pointerEvents)
149
+ assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS, top.contentView.importantForAccessibility)
150
+ assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS, page.importantForAccessibility)
151
+ assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS, lower.importantForAccessibility)
152
+ assertTrue(back())
153
+ assertEquals(emptyList<String>(), dismissals)
154
+
155
+ val content = activity.findViewById<FrameLayout>(android.R.id.content)
156
+ val size = View.MeasureSpec.makeMeasureSpec(300, View.MeasureSpec.EXACTLY)
157
+ content.measure(size, size)
158
+ content.layout(0, 0, 300, 300)
159
+ val down = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 150f, 150f, 0)
160
+ try {
161
+ assertTrue(content.dispatchTouchEvent(down))
162
+ assertEquals(0, pageTouches)
163
+ } finally {
164
+ down.recycle()
165
+ }
166
+
167
+ top.isClosing = false
168
+ host.onEntriesChanged()
169
+ assertEquals(PointerEvents.AUTO, top.pointerEvents)
170
+ assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO, top.contentView.importantForAccessibility)
171
+ assertTrue(back())
172
+ assertEquals(listOf("top:back"), dismissals)
173
+ }
174
+
175
+ @Test
176
+ fun closingPageBlockerStillTakesBack() {
177
+ val pageHost = NativeOverlayPageHostView(context).apply { hostKey = "closing-page" }
178
+ activity.setContentView(pageHost)
179
+ val top = entry("page", true, true)
180
+ pageHost.attach(top)
181
+ top.isClosing = true
182
+ host.onEntriesChanged()
183
+
184
+ assertEquals(PointerEvents.BOX_ONLY, top.pointerEvents)
185
+ assertTrue(back())
186
+ assertEquals(emptyList<String>(), dismissals)
187
+ top.isClosing = false
188
+ host.onEntriesChanged()
189
+ assertEquals(PointerEvents.AUTO, top.pointerEvents)
190
+ assertTrue(back())
191
+ assertEquals(listOf("page:back"), dismissals)
192
+ pageHost.invalidateHost()
193
+ }
194
+
195
+ @Test
196
+ fun pageEntryHitTestingKeepsClosingContentAndLowerPageSeparate() {
197
+ val root = ReactViewGroup(context).apply { id = 101 }
198
+ val below = ReactViewGroup(context).apply { id = 102 }
199
+ val child = ReactViewGroup(context).apply { id = 103 }
200
+ val pageHost = NativeOverlayPageHostView(context).apply { hostKey = "page-hit-test" }
201
+ root.addView(below)
202
+ root.addView(pageHost)
203
+ activity.setContentView(root)
204
+ val overlay = entry("page", true, true)
205
+ overlay.contentView.addView(child)
206
+ pageHost.attach(overlay)
207
+
208
+ val size = View.MeasureSpec.makeMeasureSpec(300, View.MeasureSpec.EXACTLY)
209
+ root.measure(size, size)
210
+ root.layout(0, 0, 300, 300)
211
+ below.layout(0, 0, 300, 300)
212
+ pageHost.layout(0, 0, 300, 300)
213
+ child.layout(0, 0, 300, 300)
214
+
215
+ fun target() = TouchTargetHelper.findTargetTagForTouch(150f, 150f, root)
216
+ assertEquals(child.id, target())
217
+ overlay.isClosing = true
218
+ assertEquals(root.id, target())
219
+ overlay.updateBlocking(false)
220
+ assertEquals(below.id, target())
221
+ overlay.updateBlocking(true)
222
+ overlay.isClosing = false
223
+ assertEquals(child.id, target())
224
+ pageHost.invalidateHost()
225
+ }
226
+
227
+ @Test
228
+ fun reopeningCoveredClosingEntryRestoresItsContentAccessibility() {
229
+ activity.setContentView(View(activity))
230
+ val first = entry("first", true, true)
231
+ val second = entry("second", true, true)
232
+ host.attach(first, NativeOverlayLevel.MODAL)
233
+ first.isClosing = true
234
+ host.onEntriesChanged()
235
+ host.attach(second, NativeOverlayLevel.SECURE)
236
+
237
+ first.isClosing = false
238
+ host.onEntriesChanged()
239
+ assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS, first.importantForAccessibility)
240
+ host.detach(second)
241
+ assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO, first.importantForAccessibility)
242
+ assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO, first.contentView.importantForAccessibility)
243
+ }
244
+
245
+ @Test
246
+ fun closingNonBlockingEntryPassesTouchAndBackToLowerBlocker() {
247
+ val lower = entry("lower", true, true)
248
+ val toast = entry("toast", false, true)
249
+ host.attach(lower, NativeOverlayLevel.MODAL)
250
+ host.attach(toast, NativeOverlayLevel.TOAST)
251
+ toast.isClosing = true
252
+ host.onEntriesChanged()
253
+
254
+ assertEquals(PointerEvents.NONE, toast.pointerEvents)
255
+ val down = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 150f, 150f, 0)
256
+ try {
257
+ assertFalse(toast.dispatchTouchEvent(down))
258
+ } finally {
259
+ down.recycle()
260
+ }
261
+ assertTrue(back())
262
+ assertEquals(listOf("lower:back"), dismissals)
263
+ }
264
+
265
+ @Test
266
+ fun hostCacheReusesLiveHostAndSeparatesActivities() {
267
+ assertSame(host, NativeOverlayHost.of(activity))
268
+ val otherActivity = Robolectric.buildActivity(ComponentActivity::class.java).setup().get()
269
+ assertNotSame(host, NativeOverlayHost.of(otherActivity))
270
+ }
271
+
272
+ @Test
273
+ fun hostCacheRebuildsAfterWeakValueIsCleared() {
274
+ val field = NativeOverlayHost::class.java.getDeclaredField("hosts").apply {
275
+ isAccessible = true
276
+ }
277
+ val cache = field.get(null) as WeakHashMap<*, *>
278
+ (cache[activity] as WeakReference<*>).clear()
279
+ val replacement = NativeOverlayHost.of(activity)
280
+ assertNotSame(host, replacement)
281
+ assertSame(replacement, NativeOverlayHost.of(activity))
282
+ }
283
+
284
+ @Test
285
+ fun pageOnlyBackInterceptorRetainsItsHost() {
286
+ val pageHost = NativeOverlayPageHostView(context).apply { hostKey = "page-only" }
287
+ activity.setContentView(pageHost)
288
+ pageHost.attach(entry("page", false, true))
289
+ host.installBackInterceptor()
290
+ assertTrue(activity.window.callback.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK)))
291
+ assertEquals(listOf("page:back"), dismissals)
292
+ assertSame(host, NativeOverlayHost.of(activity))
293
+ pageHost.invalidateHost()
294
+ }
295
+ }
@@ -0,0 +1,101 @@
1
+ package com.onekey.nativeoverlay
2
+
3
+ import android.graphics.Color
4
+ import android.os.Looper
5
+ import android.view.View
6
+ import androidx.activity.ComponentActivity
7
+ import androidx.coordinatorlayout.widget.CoordinatorLayout
8
+ import com.facebook.react.bridge.BridgeReactContext
9
+ import com.facebook.react.uimanager.ThemedReactContext
10
+ import com.google.android.material.bottomsheet.BottomSheetBehavior
11
+ import org.junit.Assert.assertEquals
12
+ import org.junit.Assert.assertFalse
13
+ import org.junit.Assert.assertNotSame
14
+ import org.junit.Assert.assertSame
15
+ import org.junit.Assert.assertTrue
16
+ import org.junit.Test
17
+ import org.junit.runner.RunWith
18
+ import org.robolectric.Robolectric
19
+ import org.robolectric.RobolectricTestRunner
20
+ import org.robolectric.Shadows
21
+ import org.robolectric.annotation.Config
22
+ import java.time.Duration
23
+
24
+ @RunWith(RobolectricTestRunner::class)
25
+ @Config(sdk = [34])
26
+ class NativeOverlayMotionTest {
27
+ @Test
28
+ fun sheetAnimationMovesCoordinatorWithoutTakingMaterialsSheetTranslation() {
29
+ val activity = Robolectric.buildActivity(ComponentActivity::class.java).setup().get()
30
+ val reactContext = BridgeReactContext(activity)
31
+ reactContext.onHostResume(activity)
32
+ val context = ThemedReactContext(reactContext, activity, "OverlayMotionTest", 1)
33
+ val entry = NativeOverlayEntryRootView(context)
34
+ assertSame(entry.contentView, entry.animatedView)
35
+
36
+ entry.configureSheet(500, 16f, Color.WHITE, showHandle = true, draggable = true)
37
+ val sheet = entry.sheetView!!
38
+ val animated = entry.animatedView
39
+ assertTrue(animated is CoordinatorLayout)
40
+ assertSame(animated, sheet.parent)
41
+ assertNotSame(sheet, animated)
42
+ assertTrue((sheet.layoutParams as CoordinatorLayout.LayoutParams).behavior is BottomSheetBehavior<*>)
43
+
44
+ animated.translationY = 42f
45
+ assertEquals(0f, sheet.translationY)
46
+ sheet.translationY = 7f
47
+ assertEquals(42f, animated.translationY)
48
+ assertEquals(7f, sheet.translationY)
49
+ }
50
+
51
+ @Test
52
+ fun quickMatchesMeasuredCriticalResponseWithoutChangingUnderdampedResponse() {
53
+ val quick = OverlayMotion.QUICK
54
+ assertEquals(292L, quick.durationMs())
55
+ assertEquals(0.8238140348, SpringInterpolator.progressAt(quick, 0.1), 0.000001)
56
+ assertEquals(0.9990003945, SpringInterpolator.progressAt(quick, 0.292), 0.000001)
57
+
58
+ val underdamped = OverlayMotion.Spring(1.0, 100.0, 5.0)
59
+ assertEquals(0.3929451508, SpringInterpolator.progressAt(underdamped, 0.1), 0.000001)
60
+ val timing = OverlayMotion.Timing(250L, 0.25f, 0.1f, 0.25f, 1f)
61
+ assertEquals(250L, timing.durationMs())
62
+ }
63
+
64
+ @Test
65
+ fun reopeningTheSameEntryDuringExitRetargetsItsAnimation() {
66
+ val activity = Robolectric.buildActivity(ComponentActivity::class.java).setup().visible().get()
67
+ val reactContext = BridgeReactContext(activity)
68
+ reactContext.onHostResume(activity)
69
+ val context = ThemedReactContext(reactContext, activity, "OverlayMotionTest", 1)
70
+ val overlay = NativeOverlayView(context).apply {
71
+ presentation = "sheet"
72
+ sheetHeight = 200.0
73
+ addReactChild(View(context))
74
+ }
75
+ activity.setContentView(overlay)
76
+ var dismissals = 0
77
+ overlay.onDismissed = { dismissals++ }
78
+ overlay.visible = true
79
+ overlay.commitConfiguration()
80
+
81
+ val entryField = NativeOverlayView::class.java.getDeclaredField("entry").apply { isAccessible = true }
82
+ val original = entryField.get(overlay) as NativeOverlayEntryRootView
83
+ overlay.visible = false
84
+ overlay.commitConfiguration()
85
+ assertTrue(original.isClosing)
86
+
87
+ overlay.visible = true
88
+ overlay.commitConfiguration()
89
+ assertSame(original, entryField.get(overlay))
90
+ assertTrue(original.isShownForInput)
91
+ assertFalse(original.isClosing)
92
+ assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO, original.contentView.importantForAccessibility)
93
+ Shadows.shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(350))
94
+ assertEquals(0, dismissals)
95
+
96
+ overlay.visible = false
97
+ overlay.commitConfiguration()
98
+ Shadows.shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(350))
99
+ assertEquals(1, dismissals)
100
+ }
101
+ }