@onekeyfe/react-native-native-overlay 6.7.0-12 → 6.7.0-13

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.
@@ -125,7 +125,11 @@ class NativeOverlayView(
125
125
 
126
126
  fun removeReactChild(child: View) {
127
127
  if (contentChild !== child) return
128
- (child.parent as? ViewGroup)?.removeView(child)
128
+ (child.parent as? ViewGroup)?.let { previous ->
129
+ previous.removeOnLayoutChangeListener(contentLayoutListener)
130
+ previous.removeView(child)
131
+ previous.endViewTransition(child)
132
+ }
129
133
  contentChild = null
130
134
  }
131
135
 
@@ -417,6 +421,8 @@ class NativeOverlayView(
417
421
  (child.parent as? ViewGroup)?.let { previous ->
418
422
  previous.removeOnLayoutChangeListener(contentLayoutListener)
419
423
  previous.removeView(child)
424
+ // Android retains a transitioning child's parent until the transition ends.
425
+ previous.endViewTransition(child)
420
426
  }
421
427
  container.addView(child)
422
428
  container.addOnLayoutChangeListener(contentLayoutListener)
@@ -1,8 +1,10 @@
1
1
  package com.onekey.nativeoverlay
2
2
 
3
+ import android.animation.ValueAnimator
3
4
  import android.graphics.Color
4
5
  import android.os.Looper
5
6
  import android.view.View
7
+ import android.view.ViewGroup
6
8
  import androidx.activity.ComponentActivity
7
9
  import androidx.coordinatorlayout.widget.CoordinatorLayout
8
10
  import com.facebook.react.bridge.BridgeReactContext
@@ -11,6 +13,7 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
11
13
  import org.junit.Assert.assertEquals
12
14
  import org.junit.Assert.assertFalse
13
15
  import org.junit.Assert.assertNotSame
16
+ import org.junit.Assert.assertNull
14
17
  import org.junit.Assert.assertSame
15
18
  import org.junit.Assert.assertTrue
16
19
  import org.junit.Test
@@ -67,10 +70,11 @@ class NativeOverlayMotionTest {
67
70
  val reactContext = BridgeReactContext(activity)
68
71
  reactContext.onHostResume(activity)
69
72
  val context = ThemedReactContext(reactContext, activity, "OverlayMotionTest", 1)
73
+ val child = View(context)
70
74
  val overlay = NativeOverlayView(context).apply {
71
75
  presentation = "sheet"
72
76
  sheetHeight = 200.0
73
- addReactChild(View(context))
77
+ addReactChild(child)
74
78
  }
75
79
  activity.setContentView(overlay)
76
80
  var dismissals = 0
@@ -80,13 +84,24 @@ class NativeOverlayMotionTest {
80
84
 
81
85
  val entryField = NativeOverlayView::class.java.getDeclaredField("entry").apply { isAccessible = true }
82
86
  val original = entryField.get(overlay) as NativeOverlayEntryRootView
87
+ Shadows.shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(350))
83
88
  overlay.visible = false
84
89
  overlay.commitConfiguration()
85
90
  assertTrue(original.isClosing)
91
+ val animatorField = NativeOverlayView::class.java.getDeclaredField("contentAnimator").apply { isAccessible = true }
92
+ val exitAnimator = animatorField.get(overlay) as ValueAnimator
93
+ assertTrue(exitAnimator.isStarted)
94
+ assertEquals(292L, exitAnimator.duration)
95
+ exitAnimator.currentPlayTime = exitAnimator.duration / 3
96
+ val alphaBeforeReopen = original.animatedView.alpha
97
+ assertTrue(alphaBeforeReopen > 0f && alphaBeforeReopen < 1f)
98
+ assertTrue(original.isClosing)
86
99
 
87
100
  overlay.visible = true
88
101
  overlay.commitConfiguration()
89
102
  assertSame(original, entryField.get(overlay))
103
+ assertSame(child, original.contentView.getChildAt(0))
104
+ assertEquals(alphaBeforeReopen, original.animatedView.alpha, 0.01f)
90
105
  assertTrue(original.isShownForInput)
91
106
  assertFalse(original.isClosing)
92
107
  assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO, original.contentView.importantForAccessibility)
@@ -98,4 +113,125 @@ class NativeOverlayMotionTest {
98
113
  Shadows.shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(350))
99
114
  assertEquals(1, dismissals)
100
115
  }
116
+
117
+ @Test
118
+ fun finishingExitMovesTransitioningContentBackToStagingView() {
119
+ val activity = Robolectric.buildActivity(ComponentActivity::class.java).setup().visible().get()
120
+ val reactContext = BridgeReactContext(activity)
121
+ reactContext.onHostResume(activity)
122
+ val context = ThemedReactContext(reactContext, activity, "OverlayMotionTest", 1)
123
+ val child = View(context)
124
+ val overlay = NativeOverlayView(context).apply {
125
+ addReactChild(child)
126
+ }
127
+ activity.setContentView(overlay)
128
+ var dismissals = 0
129
+ overlay.onDismissed = { dismissals++ }
130
+
131
+ overlay.visible = true
132
+ overlay.commitConfiguration()
133
+ val entryField = NativeOverlayView::class.java.getDeclaredField("entry").apply { isAccessible = true }
134
+ val entry = entryField.get(overlay) as NativeOverlayEntryRootView
135
+ Shadows.shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(350))
136
+
137
+ entry.contentView.startViewTransition(child)
138
+ overlay.visible = false
139
+ overlay.commitConfiguration()
140
+ Shadows.shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(350))
141
+ assertSame(overlay, child.parent)
142
+ assertEquals(0, entry.contentView.childCount)
143
+ assertEquals(1, overlay.childCount)
144
+ assertSame(child, overlay.getChildAt(0))
145
+ entry.contentView.endViewTransition(child)
146
+ assertSame(overlay, child.parent)
147
+ assertEquals(1, dismissals)
148
+ }
149
+
150
+ @Test
151
+ fun presentingMovesTransitioningContentIntoEntry() {
152
+ val activity = Robolectric.buildActivity(ComponentActivity::class.java).setup().visible().get()
153
+ val reactContext = BridgeReactContext(activity)
154
+ reactContext.onHostResume(activity)
155
+ val context = ThemedReactContext(reactContext, activity, "OverlayMotionTest", 1)
156
+ val child = View(context)
157
+ val overlay = NativeOverlayView(context).apply {
158
+ addReactChild(child)
159
+ }
160
+ activity.setContentView(overlay)
161
+
162
+ overlay.startViewTransition(child)
163
+ overlay.visible = true
164
+ overlay.commitConfiguration()
165
+
166
+ val entryField = NativeOverlayView::class.java.getDeclaredField("entry").apply { isAccessible = true }
167
+ val entry = entryField.get(overlay) as NativeOverlayEntryRootView
168
+ assertSame(entry.contentView, child.parent)
169
+ assertEquals(0, overlay.childCount)
170
+ assertEquals(1, entry.contentView.childCount)
171
+ assertSame(child, entry.contentView.getChildAt(0))
172
+ overlay.endViewTransition(child)
173
+ assertSame(entry.contentView, child.parent)
174
+ }
175
+
176
+ @Test
177
+ fun removingTransitioningStagedContentReleasesItsParentAndLayoutListener() {
178
+ val activity = Robolectric.buildActivity(ComponentActivity::class.java).setup().visible().get()
179
+ val reactContext = BridgeReactContext(activity)
180
+ reactContext.onHostResume(activity)
181
+ val context = ThemedReactContext(reactContext, activity, "OverlayMotionTest", 1)
182
+ val child = View(context)
183
+ val overlay = NativeOverlayView(context).apply { addReactChild(child) }
184
+ activity.setContentView(overlay)
185
+
186
+ overlay.startViewTransition(child)
187
+ overlay.removeReactChild(child)
188
+
189
+ assertNull(child.parent)
190
+ assertEquals(0, overlay.childCount)
191
+ assertEquals(0, overlay.getReactChildCount())
192
+ assertNoContentLayoutListener(overlay, overlay)
193
+ overlay.endViewTransition(child)
194
+ assertNull(child.parent)
195
+ }
196
+
197
+ @Test
198
+ fun replacingTransitioningPresentedContentReleasesTheRemovedChild() {
199
+ val activity = Robolectric.buildActivity(ComponentActivity::class.java).setup().visible().get()
200
+ val reactContext = BridgeReactContext(activity)
201
+ reactContext.onHostResume(activity)
202
+ val context = ThemedReactContext(reactContext, activity, "OverlayMotionTest", 1)
203
+ val child = View(context)
204
+ val overlay = NativeOverlayView(context).apply { addReactChild(child) }
205
+ activity.setContentView(overlay)
206
+ overlay.visible = true
207
+ overlay.commitConfiguration()
208
+ val container = child.parent as ViewGroup
209
+ container.startViewTransition(child)
210
+
211
+ val replacement = View(context)
212
+ overlay.addReactChild(replacement)
213
+
214
+ assertNull(child.parent)
215
+ assertSame(replacement, overlay.getReactChildAt(0))
216
+ assertSame(container, replacement.parent)
217
+ assertEquals(1, container.childCount)
218
+ container.endViewTransition(child)
219
+ assertSame(container, replacement.parent)
220
+
221
+ overlay.removeReactChild(replacement)
222
+ assertNoContentLayoutListener(container, overlay)
223
+ }
224
+
225
+ private fun assertNoContentLayoutListener(container: View, overlay: NativeOverlayView) {
226
+ val listener = NativeOverlayView::class.java.getDeclaredField("contentLayoutListener").apply {
227
+ isAccessible = true
228
+ }.get(overlay)
229
+ val listenerInfo = View::class.java.getDeclaredField("mListenerInfo").apply {
230
+ isAccessible = true
231
+ }.get(container) ?: return
232
+ val layoutListeners = listenerInfo.javaClass.getDeclaredField("mOnLayoutChangeListeners").apply {
233
+ isAccessible = true
234
+ }.get(listenerInfo) as? Collection<*>
235
+ assertFalse(layoutListeners?.contains(listener) == true)
236
+ }
101
237
  }
package/docs/SPEC.md CHANGED
@@ -41,14 +41,15 @@ attribution in source comments.
41
41
 
42
42
  ### Release boundary
43
43
 
44
- The `6.7.0-11` batch adds the Catalyst compile-time sheet background guard
45
- and restores the npm-packaged Gopenpgp framework links. The incomplete
46
- `6.7.0-10` candidate is superseded.
47
- All 43 publishable workspace manifests and exact internal pins use the same
48
- version, as required by the release validator. Publish the synchronized batch
49
- through `package-publish` with `npm_dist_tag=latest`, verify the registry
50
- versions, and refresh the App dependency and native locks before acceptance.
51
- The source PR remains open for review.
44
+ The earlier `3.0.162-alpha.272` release remains immutable. This branch is
45
+ prepared at `6.7.0-9`: the repository's release validator requires all 43
46
+ publishable workspace manifests and exact internal pins to use the same
47
+ version. Publish only `@onekeyfe/react-native-native-overlay` through the
48
+ manual `package-publish` workflow with `npm_dist_tag=latest` and
49
+ `only_workspace=@onekeyfe/react-native-native-overlay`; verify that exact
50
+ registry tarball before the App pins it and refreshes its dependency and native
51
+ locks. The other workspace version changes are release metadata, not a request
52
+ to publish their packages in this run. Leave the source PR open for review.
52
53
 
53
54
  ## 5.1 Purpose, scope, and non-goals
54
55
 
@@ -231,6 +232,15 @@ request ─► queued ─► active ─► (enter anim) presented
231
232
  - Content stays mounted until `finalize`. This replaces today's hardcoded
232
233
  timers: the Dialog 300 ms portal destroy, the ColorPicker 300 ms, the
233
234
  ActionList and Select 150 ms waits, and the DialogLoading 50 ms delays.
235
+ - On Android, moving the same React child between its hidden staging view and
236
+ the entry host must release an in-progress transition held by the previous
237
+ parent before the new parent adds it. Completing that previous transition
238
+ later must not detach the child from its new parent. Animated closing moves
239
+ the child back after exit completion; reopening during exit keeps the same
240
+ child and entry without reporting a completed dismissal. Fabric removal or
241
+ replacement of the React child must also release the old container's layout
242
+ listener and in-progress transition, leaving the removed child without a
243
+ parent; a later transition completion must not affect replacement content.
234
244
  - A queued entry is promoted only after the previous entry in its lane is
235
245
  finalized, so exit and enter never overlap. `replace` is the exception: the
236
246
  new entry enters while the replaced one exits.
@@ -299,14 +309,6 @@ their existing values and does not publish a release.
299
309
 
300
310
  ## 5.6 Platform contract
301
311
 
302
- ### Mac Catalyst sheet background
303
-
304
- Implemented; Catalyst build and runtime verification pending. Global sheets
305
- retain their configured surface color on Mac Catalyst and MUST NOT call
306
- `UISheetPresentationController.backgroundEffect` or the matching detent API,
307
- which are unavailable there. iOS 26.1 and later retain their existing color
308
- effect. Presentation, sizing, motion, and lifecycle contracts are unchanged.
309
-
310
312
  ### Web keyboard focus containment
311
313
 
312
314
  The topmost active, unsuspended blocking Web entry MUST contain sequential
@@ -414,6 +416,11 @@ Automated (P0):
414
416
  blockers, input readiness, canceled key-up, global/page precedence,
415
417
  suspension and blocking-only accessibility isolation. It also checks live
416
418
  host reuse, Activity isolation and rebuilding after a cleared weak value.
419
+ - `NativeOverlayMotionTest` exercises the production Android view's transition
420
+ reparenting in both directions, including the exit-completion staging path,
421
+ old-parent transition completion after the move, and reopen during exit.
422
+ It also covers Fabric removal of staged content and replacement of presented
423
+ content during a transition, including layout-listener cleanup.
417
424
  - `src/__tests__/pageIsolation.web.test.ts` mounts a real React portal in a
418
425
  DOM host and verifies late suspension, level ordering and restoration after
419
426
  the blocking entry is removed.
@@ -183,13 +183,11 @@ final class NativeOverlaySheetController: UIViewController,
183
183
  ) { [weak self] context in
184
184
  min(max(self?.style.height ?? 1, 1), context.maximumDetentValue)
185
185
  }
186
- #if !targetEnvironment(macCatalyst)
187
186
  if #available(iOS 26.1, *) {
188
187
  let effect = UIColorEffect(color: style.backgroundColor)
189
188
  sheet.backgroundEffect = effect
190
189
  detent.backgroundEffect = effect
191
190
  }
192
- #endif
193
191
  sheet.detents = [detent]
194
192
  sheet.selectedDetentIdentifier = detentIdentifier
195
193
  sheet.largestUndimmedDetentIdentifier = detentIdentifier
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-native-overlay",
3
- "version": "6.7.0-12",
3
+ "version": "6.7.0-13",
4
4
  "description": "Native and Web overlay hosts with ordered levels, page scope and Fabric measurement",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",