@swmansion/react-native-bottom-sheet 0.9.0-next.3 → 0.9.1-next.1
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/README.md +25 -1
- package/android/src/main/java/com/swmansion/reactnativebottomsheet/BottomSheetView.kt +161 -79
- package/android/src/main/java/com/swmansion/reactnativebottomsheet/BottomSheetViewManager.kt +28 -28
- package/ios/RNSBottomSheetHostingView.swift +59 -20
- package/lib/module/BottomSheetProvider.js +8 -9
- package/lib/module/BottomSheetProvider.js.map +1 -1
- package/lib/typescript/src/BottomSheetProvider.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/BottomSheetProvider.tsx +16 -11
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ React Native.
|
|
|
10
10
|
## Highlights
|
|
11
11
|
|
|
12
12
|
- Native implementation for optimal performance.
|
|
13
|
+
- Both inline and modal sheet components.
|
|
13
14
|
- Bring your own sheet surface.
|
|
14
15
|
- Dynamic, content‍-‍based sizing out of the box.
|
|
15
16
|
- Automatic handling of vertically scrollable children.
|
|
@@ -70,7 +71,8 @@ const insets = useSafeAreaInsets();
|
|
|
70
71
|
|
|
71
72
|
### Modal
|
|
72
73
|
|
|
73
|
-
`ModalBottomSheet` renders above other content with
|
|
74
|
+
`ModalBottomSheet` renders above other content with an optional scrim
|
|
75
|
+
(transparent by default).
|
|
74
76
|
|
|
75
77
|
```tsx
|
|
76
78
|
const [index, setIndex] = useState(0);
|
|
@@ -106,6 +108,25 @@ its color:
|
|
|
106
108
|
</ModalBottomSheet>
|
|
107
109
|
```
|
|
108
110
|
|
|
111
|
+
### Scrollable negotiation
|
|
112
|
+
|
|
113
|
+
By default, the sheet coordinates vertical gestures with nested scrollables,
|
|
114
|
+
such as `ScrollView` and `FlatList`.
|
|
115
|
+
|
|
116
|
+
If you want gestures that start inside a nested scrollable to stay with that
|
|
117
|
+
scrollable even when it cannot scroll any further,
|
|
118
|
+
set `disableScrollableNegotiation`:
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
<BottomSheet
|
|
122
|
+
index={index}
|
|
123
|
+
onIndexChange={setIndex}
|
|
124
|
+
disableScrollableNegotiation
|
|
125
|
+
>
|
|
126
|
+
{/* ... */}
|
|
127
|
+
</BottomSheet>
|
|
128
|
+
```
|
|
129
|
+
|
|
109
130
|
### Detents and index
|
|
110
131
|
|
|
111
132
|
Detents are the points to which the sheet snaps. Each detent is either a number
|
|
@@ -139,6 +160,9 @@ const [index, setIndex] = useState(0);
|
|
|
139
160
|
</BottomSheet>
|
|
140
161
|
```
|
|
141
162
|
|
|
163
|
+
Detents can also change over time. When you update `detents`, the sheet keeps
|
|
164
|
+
the current index and animates to the updated detent height when needed.
|
|
165
|
+
|
|
142
166
|
#### Programmatic-only detents
|
|
143
167
|
|
|
144
168
|
If you want a detent to be reachable only via code (not by dragging), use the
|
|
@@ -17,8 +17,8 @@ import androidx.dynamicanimation.animation.SpringForce
|
|
|
17
17
|
import com.facebook.react.bridge.Arguments
|
|
18
18
|
import com.facebook.react.uimanager.PointerEvents
|
|
19
19
|
import com.facebook.react.uimanager.StateWrapper
|
|
20
|
-
import com.facebook.react.views.view.ReactViewGroup
|
|
21
20
|
import com.facebook.react.uimanager.events.NativeGestureUtil
|
|
21
|
+
import com.facebook.react.views.view.ReactViewGroup
|
|
22
22
|
import kotlin.math.abs
|
|
23
23
|
|
|
24
24
|
private enum class DetentKind {
|
|
@@ -27,11 +27,14 @@ private enum class DetentKind {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
private data class RawDetentSpec(val value: Float, val kind: DetentKind, val programmatic: Boolean)
|
|
30
|
+
|
|
30
31
|
private data class DetentSpec(val height: Float, val programmatic: Boolean)
|
|
31
32
|
|
|
32
33
|
interface BottomSheetViewListener {
|
|
33
34
|
fun onIndexChange(index: Int)
|
|
35
|
+
|
|
34
36
|
fun onSettle(index: Int)
|
|
37
|
+
|
|
35
38
|
fun onPositionChange(position: Double)
|
|
36
39
|
}
|
|
37
40
|
|
|
@@ -54,6 +57,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
54
57
|
updateInteractionState()
|
|
55
58
|
updateScrim()
|
|
56
59
|
}
|
|
60
|
+
|
|
57
61
|
var disableScrollableNegotiation: Boolean = false
|
|
58
62
|
private var pendingIndex: Int? = null
|
|
59
63
|
private var hasLaidOut = false
|
|
@@ -76,15 +80,15 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
76
80
|
private var lastTouchY = 0f
|
|
77
81
|
private var activePointerId = MotionEvent.INVALID_POINTER_ID
|
|
78
82
|
private var scrimPressed = false
|
|
83
|
+
private var scrimTouchActive = false
|
|
79
84
|
private var scrimColor = Color.TRANSPARENT
|
|
80
85
|
private var scrimProgress = 0f
|
|
86
|
+
private var suppressScrimForClosingTarget = false
|
|
81
87
|
private var maxDetentHeight = Float.NaN
|
|
82
88
|
private var contentHeightMarker: View? = null
|
|
83
89
|
|
|
84
90
|
private val contentHeightMarkerLayoutListener =
|
|
85
|
-
View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
|
86
|
-
refreshDetentsFromLayout()
|
|
87
|
-
}
|
|
91
|
+
View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> refreshDetentsFromLayout() }
|
|
88
92
|
|
|
89
93
|
init {
|
|
90
94
|
clipChildren = false
|
|
@@ -143,10 +147,10 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
143
147
|
|
|
144
148
|
// MARK: - Layout
|
|
145
149
|
|
|
146
|
-
override fun onLayout(changed: Boolean,
|
|
147
|
-
super.onLayout(changed,
|
|
148
|
-
val w =
|
|
149
|
-
val h =
|
|
150
|
+
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
151
|
+
super.onLayout(changed, left, top, right, bottom)
|
|
152
|
+
val w = right - left
|
|
153
|
+
val h = bottom - top
|
|
150
154
|
if (w <= 0 || h <= 0) return
|
|
151
155
|
|
|
152
156
|
refreshContentHeightMarker()
|
|
@@ -154,10 +158,21 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
154
158
|
layoutSheetContainer(w, h)
|
|
155
159
|
|
|
156
160
|
if (!hasLaidOut && detentSpecs.isNotEmpty()) {
|
|
157
|
-
hasLaidOut = true
|
|
158
161
|
val indexToApply = pendingIndex ?: targetIndex
|
|
162
|
+
val clampedIndex = indexToApply.coerceIn(0, detentSpecs.size - 1)
|
|
163
|
+
|
|
164
|
+
if (animateIn && isInvalidContentDetentTarget(clampedIndex)) {
|
|
165
|
+
targetIndex = clampedIndex
|
|
166
|
+
pendingIndex = clampedIndex
|
|
167
|
+
val closedTy = detentSpecs.maxOfOrNull { it.height } ?: h.toFloat()
|
|
168
|
+
sheetContainer.translationY = closedTy
|
|
169
|
+
emitPosition()
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
hasLaidOut = true
|
|
159
174
|
pendingIndex = null
|
|
160
|
-
targetIndex =
|
|
175
|
+
targetIndex = clampedIndex
|
|
161
176
|
|
|
162
177
|
if (animateIn) {
|
|
163
178
|
val closedTy = detentSpecs.maxOfOrNull { it.height } ?: h.toFloat()
|
|
@@ -198,16 +213,17 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
198
213
|
// MARK: - Prop setters
|
|
199
214
|
|
|
200
215
|
fun setDetents(raw: List<Map<String, Any>>) {
|
|
201
|
-
rawDetentSpecs =
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
"
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
216
|
+
rawDetentSpecs =
|
|
217
|
+
raw.mapNotNull { dict ->
|
|
218
|
+
val value = (dict["value"] as? Number)?.toDouble() ?: return@mapNotNull null
|
|
219
|
+
val kind =
|
|
220
|
+
when ((dict["kind"] as? String)?.lowercase()) {
|
|
221
|
+
"content" -> DetentKind.CONTENT
|
|
222
|
+
else -> DetentKind.POINTS
|
|
223
|
+
}
|
|
224
|
+
val programmatic = dict["programmatic"] as? Boolean ?: false
|
|
225
|
+
RawDetentSpec(value = (value * density).toFloat(), kind = kind, programmatic = programmatic)
|
|
226
|
+
}
|
|
211
227
|
refreshDetentsFromLayout()
|
|
212
228
|
}
|
|
213
229
|
|
|
@@ -244,7 +260,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
244
260
|
|
|
245
261
|
private fun resolveDetentSpecs(): List<DetentSpec> {
|
|
246
262
|
val maxHeight = resolvedMaxDetentHeight()
|
|
247
|
-
val contentHeight =
|
|
263
|
+
val contentHeight = validContentHeight().takeIf { it.isFinite() } ?: maxHeight
|
|
248
264
|
return rawDetentSpecs.map { spec ->
|
|
249
265
|
val height =
|
|
250
266
|
when (spec.kind) {
|
|
@@ -256,6 +272,11 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
256
272
|
}
|
|
257
273
|
|
|
258
274
|
private fun refreshDetentsFromLayout() {
|
|
275
|
+
if (hasLaidOut && isInvalidContentDetentTarget(targetIndex)) {
|
|
276
|
+
updateScrim()
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
|
|
259
280
|
val resolvedDetents = resolveDetentSpecs()
|
|
260
281
|
if (resolvedDetents == detentSpecs) {
|
|
261
282
|
updateScrim()
|
|
@@ -268,6 +289,10 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
268
289
|
|
|
269
290
|
if (hasLaidOut && !isPanning) {
|
|
270
291
|
targetIndex = targetIndex.coerceIn(0, detentSpecs.size - 1)
|
|
292
|
+
if (activeAnimation != null && isTargetingClosedDetent) {
|
|
293
|
+
suppressScrimForClosingTarget = true
|
|
294
|
+
hideScrim()
|
|
295
|
+
}
|
|
271
296
|
if (activeAnimation != null) {
|
|
272
297
|
val currentTy = sheetContainer.translationY
|
|
273
298
|
val shouldEmitSettle = activeAnimationEmitsSettle
|
|
@@ -301,6 +326,15 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
301
326
|
return marker.top.toFloat()
|
|
302
327
|
}
|
|
303
328
|
|
|
329
|
+
private fun validContentHeight(): Float {
|
|
330
|
+
return currentContentHeight().takeIf { it.isFinite() && it > 0f } ?: Float.NaN
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private fun isInvalidContentDetentTarget(index: Int): Boolean {
|
|
334
|
+
return rawDetentSpecs.getOrNull(index)?.kind == DetentKind.CONTENT &&
|
|
335
|
+
!validContentHeight().isFinite()
|
|
336
|
+
}
|
|
337
|
+
|
|
304
338
|
private fun refreshContentHeightMarker() {
|
|
305
339
|
val marker = findContentHeightMarker()
|
|
306
340
|
if (marker === contentHeightMarker) return
|
|
@@ -323,12 +357,21 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
323
357
|
return maxHeight - snapHeight
|
|
324
358
|
}
|
|
325
359
|
|
|
360
|
+
private val minDetentTranslationY: Float
|
|
361
|
+
get() = detentSpecs.indices.minOfOrNull(::translationY) ?: 0f
|
|
362
|
+
|
|
363
|
+
private val maxDetentTranslationY: Float
|
|
364
|
+
get() = detentSpecs.indices.maxOfOrNull(::translationY) ?: 0f
|
|
365
|
+
|
|
326
366
|
private val closedIndex: Int?
|
|
327
367
|
get() = detentSpecs.indexOfFirst { it.height == 0f }.takeIf { it >= 0 }
|
|
328
368
|
|
|
329
369
|
private val firstNonZeroDetentHeight: Float
|
|
330
370
|
get() = detentSpecs.firstOrNull { it.height > 0f }?.height ?: 0f
|
|
331
371
|
|
|
372
|
+
private val isTargetingClosedDetent: Boolean
|
|
373
|
+
get() = closedIndex?.let { targetIndex == it } == true
|
|
374
|
+
|
|
332
375
|
private val draggableMinTy: Float
|
|
333
376
|
get() {
|
|
334
377
|
val highestIndex = detentSpecs.indices.lastOrNull { !detentSpecs[it].programmatic } ?: 0
|
|
@@ -377,12 +420,13 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
377
420
|
|
|
378
421
|
private fun startChoreographer() {
|
|
379
422
|
if (choreographerCallback != null) return
|
|
380
|
-
val callback =
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
423
|
+
val callback =
|
|
424
|
+
object : Choreographer.FrameCallback {
|
|
425
|
+
override fun doFrame(frameTimeNanos: Long) {
|
|
426
|
+
emitPosition()
|
|
427
|
+
choreographerCallback?.let { Choreographer.getInstance().postFrameCallback(it) }
|
|
428
|
+
}
|
|
384
429
|
}
|
|
385
|
-
}
|
|
386
430
|
choreographerCallback = callback
|
|
387
431
|
Choreographer.getInstance().postFrameCallback(callback)
|
|
388
432
|
}
|
|
@@ -402,30 +446,42 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
402
446
|
) {
|
|
403
447
|
if (index < 0 || index >= detentSpecs.size) return
|
|
404
448
|
targetIndex = index
|
|
449
|
+
if (!isTargetingClosedDetent) {
|
|
450
|
+
suppressScrimForClosingTarget = false
|
|
451
|
+
}
|
|
405
452
|
|
|
406
453
|
val targetTy = translationY(index)
|
|
407
454
|
activeAnimationEmitsSettle = emitSettle
|
|
408
455
|
activeAnimation?.cancel()
|
|
409
456
|
|
|
410
|
-
val spring =
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
457
|
+
val spring =
|
|
458
|
+
SpringAnimation(sheetContainer, DynamicAnimation.TRANSLATION_Y, targetTy).apply {
|
|
459
|
+
spring =
|
|
460
|
+
SpringForce(targetTy).apply {
|
|
461
|
+
dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY
|
|
462
|
+
stiffness = SpringForce.STIFFNESS_MEDIUM
|
|
463
|
+
}
|
|
464
|
+
setMinValue(minDetentTranslationY)
|
|
465
|
+
setMaxValue(maxDetentTranslationY)
|
|
466
|
+
setStartVelocity(velocity)
|
|
467
|
+
addEndListener { _, canceled, _, _ ->
|
|
468
|
+
if (canceled) {
|
|
469
|
+
return@addEndListener
|
|
470
|
+
}
|
|
471
|
+
stopChoreographer()
|
|
472
|
+
activeAnimation = null
|
|
473
|
+
activeAnimationEmitsSettle = false
|
|
474
|
+
suppressScrimForClosingTarget = false
|
|
475
|
+
if (closedIndex == index) {
|
|
476
|
+
sheetContainer.translationY = translationY(index)
|
|
477
|
+
hideScrim()
|
|
478
|
+
}
|
|
479
|
+
emitPosition()
|
|
480
|
+
updateInteractionState()
|
|
481
|
+
if (emitIndexChange) listener?.onIndexChange(index)
|
|
482
|
+
if (emitSettle) listener?.onSettle(index)
|
|
419
483
|
}
|
|
420
|
-
stopChoreographer()
|
|
421
|
-
emitPosition()
|
|
422
|
-
activeAnimation = null
|
|
423
|
-
activeAnimationEmitsSettle = false
|
|
424
|
-
updateInteractionState()
|
|
425
|
-
if (emitIndexChange) listener?.onIndexChange(index)
|
|
426
|
-
if (emitSettle) listener?.onSettle(index)
|
|
427
484
|
}
|
|
428
|
-
}
|
|
429
485
|
|
|
430
486
|
activeAnimation = spring
|
|
431
487
|
startChoreographer()
|
|
@@ -440,11 +496,13 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
440
496
|
|
|
441
497
|
if (velocity < -flickThreshold) {
|
|
442
498
|
return draggable.firstOrNull { it.value.height > currentHeight }?.index
|
|
443
|
-
?: draggable.lastOrNull()?.index
|
|
499
|
+
?: draggable.lastOrNull()?.index
|
|
500
|
+
?: targetIndex
|
|
444
501
|
}
|
|
445
502
|
if (velocity > flickThreshold) {
|
|
446
503
|
return draggable.lastOrNull { it.value.height < currentHeight }?.index
|
|
447
|
-
?: draggable.firstOrNull()?.index
|
|
504
|
+
?: draggable.firstOrNull()?.index
|
|
505
|
+
?: targetIndex
|
|
448
506
|
}
|
|
449
507
|
|
|
450
508
|
return draggable.minByOrNull { abs(it.value.height - currentHeight) }?.index ?: targetIndex
|
|
@@ -452,32 +510,34 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
452
510
|
|
|
453
511
|
// MARK: - Touch handling
|
|
454
512
|
|
|
455
|
-
override fun onInterceptTouchEvent(
|
|
513
|
+
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
|
456
514
|
val sheetTop = sheetContainer.top + sheetContainer.translationY
|
|
457
|
-
if (
|
|
515
|
+
if (event.actionMasked == MotionEvent.ACTION_DOWN && event.y < sheetTop) {
|
|
458
516
|
if (isScrimVisible()) {
|
|
459
|
-
initialTouchX =
|
|
460
|
-
initialTouchY =
|
|
461
|
-
lastTouchY =
|
|
517
|
+
initialTouchX = event.x
|
|
518
|
+
initialTouchY = event.y
|
|
519
|
+
lastTouchY = event.y
|
|
520
|
+
activePointerId = event.getPointerId(0)
|
|
462
521
|
scrimPressed = true
|
|
522
|
+
scrimTouchActive = true
|
|
463
523
|
return true
|
|
464
524
|
}
|
|
465
525
|
return false
|
|
466
526
|
}
|
|
467
527
|
|
|
468
|
-
when (
|
|
528
|
+
when (event.actionMasked) {
|
|
469
529
|
MotionEvent.ACTION_DOWN -> {
|
|
470
|
-
initialTouchX =
|
|
471
|
-
initialTouchY =
|
|
472
|
-
lastTouchY =
|
|
473
|
-
activePointerId =
|
|
530
|
+
initialTouchX = event.x
|
|
531
|
+
initialTouchY = event.y
|
|
532
|
+
lastTouchY = event.y
|
|
533
|
+
activePointerId = event.getPointerId(0)
|
|
474
534
|
}
|
|
475
535
|
MotionEvent.ACTION_MOVE -> {
|
|
476
536
|
if (activePointerId == MotionEvent.INVALID_POINTER_ID) return false
|
|
477
|
-
val pointerIndex =
|
|
537
|
+
val pointerIndex = event.findPointerIndex(activePointerId)
|
|
478
538
|
if (pointerIndex < 0) return false
|
|
479
|
-
val x =
|
|
480
|
-
val y =
|
|
539
|
+
val x = event.getX(pointerIndex)
|
|
540
|
+
val y = event.getY(pointerIndex)
|
|
481
541
|
val dx = x - initialTouchX
|
|
482
542
|
val dy = y - initialTouchY
|
|
483
543
|
|
|
@@ -492,29 +552,31 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
492
552
|
// processes events at the root view level before onInterceptTouchEvent
|
|
493
553
|
// runs, so without this the JS side never sees a cancel and Pressable
|
|
494
554
|
// would still fire onPress.
|
|
495
|
-
NativeGestureUtil.notifyNativeGestureStarted(this,
|
|
555
|
+
NativeGestureUtil.notifyNativeGestureStarted(this, event)
|
|
496
556
|
return true
|
|
497
557
|
}
|
|
498
558
|
if (dy > 0 && isScrollViewAtTop()) {
|
|
499
559
|
lastTouchY = y
|
|
500
560
|
requestDisallowInterceptTouchEvent(false)
|
|
501
|
-
NativeGestureUtil.notifyNativeGestureStarted(this,
|
|
561
|
+
NativeGestureUtil.notifyNativeGestureStarted(this, event)
|
|
502
562
|
return true
|
|
503
563
|
}
|
|
504
564
|
}
|
|
505
565
|
}
|
|
506
|
-
MotionEvent.ACTION_UP,
|
|
566
|
+
MotionEvent.ACTION_UP,
|
|
567
|
+
MotionEvent.ACTION_CANCEL -> {
|
|
507
568
|
initialTouchX = 0f
|
|
508
569
|
initialTouchY = 0f
|
|
509
570
|
activePointerId = MotionEvent.INVALID_POINTER_ID
|
|
510
571
|
scrimPressed = false
|
|
572
|
+
scrimTouchActive = false
|
|
511
573
|
}
|
|
512
574
|
}
|
|
513
575
|
return false
|
|
514
576
|
}
|
|
515
577
|
|
|
516
578
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
517
|
-
if (
|
|
579
|
+
if (scrimTouchActive) {
|
|
518
580
|
when (event.actionMasked) {
|
|
519
581
|
MotionEvent.ACTION_MOVE -> {
|
|
520
582
|
val sheetTop = sheetContainer.top + sheetContainer.translationY
|
|
@@ -525,14 +587,22 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
525
587
|
}
|
|
526
588
|
MotionEvent.ACTION_UP -> {
|
|
527
589
|
val closeIndex = closedIndex
|
|
590
|
+
val shouldDismiss = scrimPressed && isScrimVisible()
|
|
528
591
|
scrimPressed = false
|
|
529
|
-
|
|
592
|
+
scrimTouchActive = false
|
|
593
|
+
activePointerId = MotionEvent.INVALID_POINTER_ID
|
|
594
|
+
if (shouldDismiss && closeIndex != null) {
|
|
530
595
|
snapToIndex(closeIndex, 0f)
|
|
531
596
|
}
|
|
532
597
|
return true
|
|
533
598
|
}
|
|
534
599
|
MotionEvent.ACTION_CANCEL -> {
|
|
535
600
|
scrimPressed = false
|
|
601
|
+
scrimTouchActive = false
|
|
602
|
+
activePointerId = MotionEvent.INVALID_POINTER_ID
|
|
603
|
+
return true
|
|
604
|
+
}
|
|
605
|
+
MotionEvent.ACTION_POINTER_UP -> {
|
|
536
606
|
return true
|
|
537
607
|
}
|
|
538
608
|
}
|
|
@@ -553,15 +623,17 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
553
623
|
emitPosition()
|
|
554
624
|
return true
|
|
555
625
|
}
|
|
556
|
-
MotionEvent.ACTION_UP,
|
|
626
|
+
MotionEvent.ACTION_UP,
|
|
627
|
+
MotionEvent.ACTION_CANCEL -> {
|
|
557
628
|
isPanning = false
|
|
558
629
|
activePointerId = MotionEvent.INVALID_POINTER_ID
|
|
559
|
-
val velocity =
|
|
560
|
-
tracker
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
630
|
+
val velocity =
|
|
631
|
+
velocityTracker?.let { tracker ->
|
|
632
|
+
tracker.computeCurrentVelocity(1000)
|
|
633
|
+
val v = tracker.yVelocity
|
|
634
|
+
tracker.recycle()
|
|
635
|
+
v
|
|
636
|
+
} ?: 0f
|
|
565
637
|
velocityTracker = null
|
|
566
638
|
val maxHeight = detentSpecs.maxOfOrNull { it.height } ?: resolvedMaxDetentHeight()
|
|
567
639
|
val currentHeight = maxHeight - sheetContainer.translationY
|
|
@@ -616,9 +688,9 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
616
688
|
val containerY = initialTouchY - sheetContainer.top - sheetContainer.translationY
|
|
617
689
|
if (
|
|
618
690
|
containerX < 0f ||
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
691
|
+
containerX >= sheetContainer.width ||
|
|
692
|
+
containerY < 0f ||
|
|
693
|
+
containerY >= sheetContainer.height
|
|
622
694
|
) {
|
|
623
695
|
return null
|
|
624
696
|
}
|
|
@@ -636,7 +708,9 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
636
708
|
if (childX < 0f || childX >= child.width || childY < 0f || childY >= child.height) {
|
|
637
709
|
continue
|
|
638
710
|
}
|
|
639
|
-
findScrollableAtPoint(child, childX, childY)?.let {
|
|
711
|
+
findScrollableAtPoint(child, childX, childY)?.let {
|
|
712
|
+
return it
|
|
713
|
+
}
|
|
640
714
|
}
|
|
641
715
|
}
|
|
642
716
|
|
|
@@ -680,8 +754,10 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
680
754
|
lastTouchY = 0f
|
|
681
755
|
activePointerId = MotionEvent.INVALID_POINTER_ID
|
|
682
756
|
scrimPressed = false
|
|
757
|
+
scrimTouchActive = false
|
|
683
758
|
sheetContainer.translationY = 0f
|
|
684
759
|
scrimProgress = 0f
|
|
760
|
+
suppressScrimForClosingTarget = false
|
|
685
761
|
sheetContainer.removeAllViews()
|
|
686
762
|
stateWrapper = null
|
|
687
763
|
lastShadowOffsetY = Float.NaN
|
|
@@ -696,15 +772,21 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
696
772
|
|
|
697
773
|
// When settled at the closed detent, dynamic content updates can briefly
|
|
698
774
|
// produce stale non-zero positions. Keep scrim hidden in this state.
|
|
699
|
-
if (
|
|
700
|
-
|
|
701
|
-
|
|
775
|
+
if (
|
|
776
|
+
(isTargetingClosedDetent && activeAnimation == null && !isPanning) ||
|
|
777
|
+
(suppressScrimForClosingTarget && isTargetingClosedDetent)
|
|
778
|
+
) {
|
|
779
|
+
hideScrim()
|
|
702
780
|
return
|
|
703
781
|
}
|
|
704
782
|
|
|
705
783
|
val threshold = firstNonZeroDetentHeight
|
|
706
|
-
scrimProgress =
|
|
707
|
-
|
|
784
|
+
scrimProgress = if (threshold <= 0f) 0f else (position / threshold).coerceIn(0f, 1f)
|
|
785
|
+
invalidate()
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
private fun hideScrim() {
|
|
789
|
+
scrimProgress = 0f
|
|
708
790
|
invalidate()
|
|
709
791
|
}
|
|
710
792
|
|
package/android/src/main/java/com/swmansion/reactnativebottomsheet/BottomSheetViewManager.kt
CHANGED
|
@@ -14,8 +14,7 @@ import com.facebook.react.viewmanagers.BottomSheetViewManagerInterface
|
|
|
14
14
|
|
|
15
15
|
@ReactModule(name = BottomSheetViewManager.NAME)
|
|
16
16
|
class BottomSheetViewManager :
|
|
17
|
-
ViewGroupManager<BottomSheetView>(),
|
|
18
|
-
BottomSheetViewManagerInterface<BottomSheetView> {
|
|
17
|
+
ViewGroupManager<BottomSheetView>(), BottomSheetViewManagerInterface<BottomSheetView> {
|
|
19
18
|
|
|
20
19
|
companion object {
|
|
21
20
|
const val NAME = "BottomSheetView"
|
|
@@ -29,37 +28,37 @@ class BottomSheetViewManager :
|
|
|
29
28
|
|
|
30
29
|
override fun createViewInstance(context: ThemedReactContext): BottomSheetView {
|
|
31
30
|
val view = BottomSheetView(context)
|
|
32
|
-
view.listener =
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
view.listener =
|
|
32
|
+
object : BottomSheetViewListener {
|
|
33
|
+
override fun onIndexChange(index: Int) {
|
|
34
|
+
val event =
|
|
35
|
+
com.facebook.react.bridge.Arguments.createMap().apply { putInt("index", index) }
|
|
36
|
+
val reactContext = view.context as? ThemedReactContext ?: return
|
|
37
|
+
reactContext
|
|
38
|
+
.getJSModule(com.facebook.react.uimanager.events.RCTEventEmitter::class.java)
|
|
39
|
+
.receiveEvent(view.id, "topIndexChange", event)
|
|
36
40
|
}
|
|
37
|
-
val reactContext = view.context as? ThemedReactContext ?: return
|
|
38
|
-
reactContext
|
|
39
|
-
.getJSModule(com.facebook.react.uimanager.events.RCTEventEmitter::class.java)
|
|
40
|
-
.receiveEvent(view.id, "topIndexChange", event)
|
|
41
|
-
}
|
|
42
41
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
override fun onSettle(index: Int) {
|
|
43
|
+
val event =
|
|
44
|
+
com.facebook.react.bridge.Arguments.createMap().apply { putInt("index", index) }
|
|
45
|
+
val reactContext = view.context as? ThemedReactContext ?: return
|
|
46
|
+
reactContext
|
|
47
|
+
.getJSModule(com.facebook.react.uimanager.events.RCTEventEmitter::class.java)
|
|
48
|
+
.receiveEvent(view.id, "topSettle", event)
|
|
46
49
|
}
|
|
47
|
-
val reactContext = view.context as? ThemedReactContext ?: return
|
|
48
|
-
reactContext
|
|
49
|
-
.getJSModule(com.facebook.react.uimanager.events.RCTEventEmitter::class.java)
|
|
50
|
-
.receiveEvent(view.id, "topSettle", event)
|
|
51
|
-
}
|
|
52
50
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
override fun onPositionChange(position: Double) {
|
|
52
|
+
val event =
|
|
53
|
+
com.facebook.react.bridge.Arguments.createMap().apply {
|
|
54
|
+
putDouble("position", position)
|
|
55
|
+
}
|
|
56
|
+
val reactContext = view.context as? ThemedReactContext ?: return
|
|
57
|
+
reactContext
|
|
58
|
+
.getJSModule(com.facebook.react.uimanager.events.RCTEventEmitter::class.java)
|
|
59
|
+
.receiveEvent(view.id, "topPositionChange", event)
|
|
56
60
|
}
|
|
57
|
-
val reactContext = view.context as? ThemedReactContext ?: return
|
|
58
|
-
reactContext
|
|
59
|
-
.getJSModule(com.facebook.react.uimanager.events.RCTEventEmitter::class.java)
|
|
60
|
-
.receiveEvent(view.id, "topPositionChange", event)
|
|
61
61
|
}
|
|
62
|
-
}
|
|
63
62
|
return view
|
|
64
63
|
}
|
|
65
64
|
|
|
@@ -69,7 +68,8 @@ class BottomSheetViewManager :
|
|
|
69
68
|
|
|
70
69
|
override fun getChildCount(parent: BottomSheetView): Int = parent.sheetChildCount
|
|
71
70
|
|
|
72
|
-
override fun getChildAt(parent: BottomSheetView, index: Int): View? =
|
|
71
|
+
override fun getChildAt(parent: BottomSheetView, index: Int): View? =
|
|
72
|
+
parent.getSheetChildAt(index)
|
|
73
73
|
|
|
74
74
|
override fun removeViewAt(parent: BottomSheetView, index: Int) {
|
|
75
75
|
parent.removeSheetChildAt(index)
|
|
@@ -28,12 +28,15 @@ public final class RNSBottomSheetHostingView: UIView {
|
|
|
28
28
|
public var modal: Bool = false {
|
|
29
29
|
didSet { updateScrim() }
|
|
30
30
|
}
|
|
31
|
+
|
|
31
32
|
public var scrimColor: UIColor? = .clear {
|
|
32
33
|
didSet { scrimView.backgroundColor = scrimColor }
|
|
33
34
|
}
|
|
35
|
+
|
|
34
36
|
public var maxDetentHeight: CGFloat = .nan {
|
|
35
37
|
didSet { refreshDetentsFromLayout() }
|
|
36
38
|
}
|
|
39
|
+
|
|
37
40
|
public var disableScrollableNegotiation: Bool = false
|
|
38
41
|
|
|
39
42
|
private var rawDetentSpecs: [RawDetentSpec] = []
|
|
@@ -59,7 +62,7 @@ public final class RNSBottomSheetHostingView: UIView {
|
|
|
59
62
|
private var isContentInteractionDisabled = false
|
|
60
63
|
private weak var contentHeightMarker: UIView?
|
|
61
64
|
|
|
62
|
-
public
|
|
65
|
+
override public init(frame: CGRect) {
|
|
63
66
|
super.init(frame: frame)
|
|
64
67
|
backgroundColor = .clear
|
|
65
68
|
clipsToBounds = false
|
|
@@ -84,19 +87,20 @@ public final class RNSBottomSheetHostingView: UIView {
|
|
|
84
87
|
sheetContainer.addGestureRecognizer(panGesture)
|
|
85
88
|
}
|
|
86
89
|
|
|
87
|
-
|
|
90
|
+
@available(*, unavailable)
|
|
91
|
+
public required init?(coder _: NSCoder) {
|
|
88
92
|
fatalError("init(coder:) has not been implemented")
|
|
89
93
|
}
|
|
90
94
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
/// RCTSurfaceTouchHandler dispatches touch events to JS independently of the
|
|
96
|
+
/// pan gesture (it fires in touchesBegan: regardless of its recognizer state).
|
|
97
|
+
/// We cache it here and toggle isEnabled in handlePan(.began) to force a
|
|
98
|
+
/// touchesCancelled dispatch to JS, preventing Pressable from firing onPress
|
|
99
|
+
/// during a sheet drag. This is the iOS equivalent of Android's
|
|
100
|
+
/// NativeGestureUtil.notifyNativeGestureStarted.
|
|
97
101
|
private weak var surfaceTouchHandler: UIGestureRecognizer?
|
|
98
102
|
|
|
99
|
-
public
|
|
103
|
+
override public func didMoveToWindow() {
|
|
100
104
|
super.didMoveToWindow()
|
|
101
105
|
surfaceTouchHandler = nil
|
|
102
106
|
guard window != nil else { return }
|
|
@@ -112,7 +116,7 @@ public final class RNSBottomSheetHostingView: UIView {
|
|
|
112
116
|
}
|
|
113
117
|
}
|
|
114
118
|
|
|
115
|
-
public
|
|
119
|
+
override public func layoutSubviews() {
|
|
116
120
|
super.layoutSubviews()
|
|
117
121
|
guard bounds.width > 0, bounds.height > 0 else { return }
|
|
118
122
|
|
|
@@ -123,10 +127,21 @@ public final class RNSBottomSheetHostingView: UIView {
|
|
|
123
127
|
sheetContainer.center = CGPoint(x: bounds.width / 2, y: bounds.height - maxHeight / 2)
|
|
124
128
|
|
|
125
129
|
if !hasLaidOut && !detentSpecs.isEmpty {
|
|
126
|
-
hasLaidOut = true
|
|
127
130
|
let indexToApply = pendingIndex ?? targetIndex
|
|
131
|
+
let clampedIndex = max(0, min(detentSpecs.count - 1, indexToApply))
|
|
132
|
+
|
|
133
|
+
if animateIn && isInvalidContentDetentTarget(clampedIndex) {
|
|
134
|
+
targetIndex = clampedIndex
|
|
135
|
+
pendingIndex = clampedIndex
|
|
136
|
+
let closedTy = maximumResolvedDetentHeight ?? bounds.height
|
|
137
|
+
sheetContainer.transform = CGAffineTransform(translationX: 0, y: closedTy)
|
|
138
|
+
emitPosition()
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
hasLaidOut = true
|
|
128
143
|
pendingIndex = nil
|
|
129
|
-
targetIndex =
|
|
144
|
+
targetIndex = clampedIndex
|
|
130
145
|
|
|
131
146
|
if animateIn {
|
|
132
147
|
let closedTy = maximumResolvedDetentHeight ?? bounds.height
|
|
@@ -152,7 +167,7 @@ public final class RNSBottomSheetHostingView: UIView {
|
|
|
152
167
|
return sheetContainer.frame
|
|
153
168
|
}
|
|
154
169
|
|
|
155
|
-
public
|
|
170
|
+
override public func point(inside point: CGPoint, with _: UIEvent?) -> Bool {
|
|
156
171
|
if presentedSheetFrame.contains(point) {
|
|
157
172
|
return true
|
|
158
173
|
}
|
|
@@ -160,10 +175,10 @@ public final class RNSBottomSheetHostingView: UIView {
|
|
|
160
175
|
return isScrimVisible && bounds.contains(point)
|
|
161
176
|
}
|
|
162
177
|
|
|
163
|
-
public
|
|
178
|
+
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
164
179
|
guard self.point(inside: point, with: event) else { return nil }
|
|
165
180
|
|
|
166
|
-
if isScrimVisible
|
|
181
|
+
if isScrimVisible, !presentedSheetFrame.contains(point) {
|
|
167
182
|
let scrimPoint = convert(point, to: scrimView)
|
|
168
183
|
return scrimView.hitTest(scrimPoint, with: event)
|
|
169
184
|
}
|
|
@@ -475,7 +490,7 @@ public final class RNSBottomSheetHostingView: UIView {
|
|
|
475
490
|
return nil
|
|
476
491
|
}
|
|
477
492
|
|
|
478
|
-
public
|
|
493
|
+
override public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
479
494
|
guard gestureRecognizer === panGesture else { return true }
|
|
480
495
|
|
|
481
496
|
let velocity = panGesture.velocity(in: self)
|
|
@@ -525,7 +540,7 @@ public final class RNSBottomSheetHostingView: UIView {
|
|
|
525
540
|
|
|
526
541
|
private func resolveDetentSpecs() -> [DetentSpec] {
|
|
527
542
|
let maxHeight = resolvedMaxDetentHeight
|
|
528
|
-
let contentHeight =
|
|
543
|
+
let contentHeight = validContentHeight.map { min($0, maxHeight) } ?? maxHeight
|
|
529
544
|
return rawDetentSpecs.map { spec in
|
|
530
545
|
let height: CGFloat
|
|
531
546
|
switch spec.kind {
|
|
@@ -540,6 +555,11 @@ public final class RNSBottomSheetHostingView: UIView {
|
|
|
540
555
|
|
|
541
556
|
private func refreshDetentsFromLayout() {
|
|
542
557
|
refreshContentHeightMarker()
|
|
558
|
+
if hasLaidOut, isInvalidContentDetentTarget(targetIndex) {
|
|
559
|
+
updateScrim()
|
|
560
|
+
return
|
|
561
|
+
}
|
|
562
|
+
|
|
543
563
|
let resolvedDetents = resolveDetentSpecs()
|
|
544
564
|
guard resolvedDetents != detentSpecs else {
|
|
545
565
|
updateScrim()
|
|
@@ -552,7 +572,7 @@ public final class RNSBottomSheetHostingView: UIView {
|
|
|
552
572
|
return
|
|
553
573
|
}
|
|
554
574
|
|
|
555
|
-
if hasLaidOut
|
|
575
|
+
if hasLaidOut, !isPanning {
|
|
556
576
|
targetIndex = max(0, min(detentSpecs.count - 1, targetIndex))
|
|
557
577
|
|
|
558
578
|
if let animator = activeAnimator {
|
|
@@ -594,6 +614,25 @@ public final class RNSBottomSheetHostingView: UIView {
|
|
|
594
614
|
guard let marker = contentHeightMarker else { return nil }
|
|
595
615
|
return marker.frame.minY.isFinite ? marker.frame.minY : nil
|
|
596
616
|
}
|
|
617
|
+
|
|
618
|
+
private var validContentHeight: CGFloat? {
|
|
619
|
+
guard let height = currentContentHeight, height.isFinite, height > 0 else {
|
|
620
|
+
return nil
|
|
621
|
+
}
|
|
622
|
+
return height
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
private func isInvalidContentDetentTarget(_ index: Int) -> Bool {
|
|
626
|
+
guard rawDetentSpecs.indices.contains(index) else {
|
|
627
|
+
return false
|
|
628
|
+
}
|
|
629
|
+
switch rawDetentSpecs[index].kind {
|
|
630
|
+
case .points:
|
|
631
|
+
return false
|
|
632
|
+
case .content:
|
|
633
|
+
return validContentHeight == nil
|
|
634
|
+
}
|
|
635
|
+
}
|
|
597
636
|
}
|
|
598
637
|
|
|
599
638
|
extension RNSBottomSheetHostingView: UIGestureRecognizerDelegate {
|
|
@@ -606,8 +645,8 @@ extension RNSBottomSheetHostingView: UIGestureRecognizerDelegate {
|
|
|
606
645
|
}
|
|
607
646
|
|
|
608
647
|
public func gestureRecognizer(
|
|
609
|
-
_
|
|
610
|
-
shouldRecognizeSimultaneouslyWith
|
|
648
|
+
_: UIGestureRecognizer,
|
|
649
|
+
shouldRecognizeSimultaneouslyWith _: UIGestureRecognizer
|
|
611
650
|
) -> Bool {
|
|
612
651
|
return false
|
|
613
652
|
}
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import { createContext, useContext,
|
|
3
|
+
import { createContext, useContext, useId, useLayoutEffect, useState, useSyncExternalStore } from 'react';
|
|
4
4
|
import { StyleSheet, View } from 'react-native';
|
|
5
5
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
6
6
|
const PortalContext = /*#__PURE__*/createContext(null);
|
|
7
7
|
const PortalHost = () => {
|
|
8
8
|
const context = useContext(PortalContext);
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
return context.subscribe(forceRender);
|
|
12
|
-
}, [context]);
|
|
13
|
-
return Array.from(context.getPortals().entries()).map(([key, element]) => /*#__PURE__*/_jsx(View, {
|
|
9
|
+
const portals = useSyncExternalStore(context.subscribe, context.getSnapshot, context.getSnapshot);
|
|
10
|
+
return portals.map(([key, element]) => /*#__PURE__*/_jsx(View, {
|
|
14
11
|
style: StyleSheet.absoluteFill,
|
|
15
12
|
pointerEvents: "box-none",
|
|
16
13
|
children: element
|
|
@@ -24,7 +21,9 @@ export const BottomSheetProvider = ({
|
|
|
24
21
|
const [context] = useState(() => {
|
|
25
22
|
const portals = new Map();
|
|
26
23
|
const subscribers = new Set();
|
|
24
|
+
let snapshot = [];
|
|
27
25
|
const notify = () => {
|
|
26
|
+
snapshot = Array.from(portals.entries());
|
|
28
27
|
subscribers.forEach(subscriber => subscriber());
|
|
29
28
|
};
|
|
30
29
|
return {
|
|
@@ -42,7 +41,7 @@ export const BottomSheetProvider = ({
|
|
|
42
41
|
subscribers.delete(callback);
|
|
43
42
|
};
|
|
44
43
|
},
|
|
45
|
-
|
|
44
|
+
getSnapshot: () => snapshot
|
|
46
45
|
};
|
|
47
46
|
});
|
|
48
47
|
return /*#__PURE__*/_jsxs(PortalContext.Provider, {
|
|
@@ -62,10 +61,10 @@ export const Portal = ({
|
|
|
62
61
|
removePortal
|
|
63
62
|
} = context;
|
|
64
63
|
const id = useId();
|
|
65
|
-
|
|
64
|
+
useLayoutEffect(() => {
|
|
66
65
|
addPortal(id, children);
|
|
67
66
|
}, [id, children, addPortal]);
|
|
68
|
-
|
|
67
|
+
useLayoutEffect(() => {
|
|
69
68
|
return () => {
|
|
70
69
|
removePortal(id);
|
|
71
70
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["createContext","useContext","
|
|
1
|
+
{"version":3,"names":["createContext","useContext","useId","useLayoutEffect","useState","useSyncExternalStore","StyleSheet","View","jsx","_jsx","jsxs","_jsxs","PortalContext","PortalHost","context","portals","subscribe","getSnapshot","map","key","element","style","absoluteFill","pointerEvents","children","BottomSheetProvider","Map","subscribers","Set","snapshot","notify","Array","from","entries","forEach","subscriber","addPortal","set","removePortal","delete","callback","add","Provider","value","Portal","Error","id"],"sourceRoot":"../../src","sources":["BottomSheetProvider.tsx"],"mappings":"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","ignoreList":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BottomSheetProvider.d.ts","sourceRoot":"","sources":["../../../src/BottomSheetProvider.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"BottomSheetProvider.d.ts","sourceRoot":"","sources":["../../../src/BottomSheetProvider.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AA6BvC,iEAAiE;AACjE,eAAO,MAAM,mBAAmB,GAAI,cAAc;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,4CAkCxE,CAAC;AAEF,eAAO,MAAM,MAAM,GAAI,cAAc;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,SAkB3D,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swmansion/react-native-bottom-sheet",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1-next.1",
|
|
4
4
|
"description": "Provides bottom-sheet components for React Native.",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"scripts": {
|
|
36
36
|
"example": "yarn workspace @swmansion/react-native-bottom-sheet-example",
|
|
37
37
|
"clean": "del-cli lib",
|
|
38
|
-
"prepare": "bob build",
|
|
38
|
+
"prepare": "lefthook install && bob build",
|
|
39
39
|
"typecheck": "tsc",
|
|
40
40
|
"lint": "eslint \"**/*.{js,ts,tsx}\""
|
|
41
41
|
},
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
"eslint": "9.39.2",
|
|
70
70
|
"eslint-config-prettier": "10.1.8",
|
|
71
71
|
"eslint-plugin-prettier": "5.5.5",
|
|
72
|
+
"lefthook": "2.1.6",
|
|
72
73
|
"prettier": "2.8.8",
|
|
73
74
|
"react": "19.1.0",
|
|
74
75
|
"react-native": "0.81.5",
|
|
@@ -1,31 +1,34 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createContext,
|
|
3
3
|
useContext,
|
|
4
|
-
useEffect,
|
|
5
4
|
useId,
|
|
6
|
-
|
|
5
|
+
useLayoutEffect,
|
|
7
6
|
useState,
|
|
7
|
+
useSyncExternalStore,
|
|
8
8
|
} from 'react';
|
|
9
9
|
import type { ReactNode } from 'react';
|
|
10
10
|
import { StyleSheet, View } from 'react-native';
|
|
11
11
|
|
|
12
|
+
type PortalSnapshot = Array<[string, ReactNode]>;
|
|
13
|
+
|
|
12
14
|
interface PortalContextType {
|
|
13
15
|
addPortal: (key: string, element: ReactNode) => void;
|
|
14
16
|
removePortal: (key: string) => void;
|
|
15
17
|
subscribe: (callback: () => void) => () => void;
|
|
16
|
-
|
|
18
|
+
getSnapshot: () => PortalSnapshot;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
const PortalContext = createContext<PortalContextType | null>(null);
|
|
20
22
|
|
|
21
23
|
const PortalHost = () => {
|
|
22
24
|
const context = useContext(PortalContext)!;
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
const portals = useSyncExternalStore(
|
|
26
|
+
context.subscribe,
|
|
27
|
+
context.getSnapshot,
|
|
28
|
+
context.getSnapshot
|
|
29
|
+
);
|
|
27
30
|
|
|
28
|
-
return
|
|
31
|
+
return portals.map(([key, element]) => (
|
|
29
32
|
<View key={key} style={StyleSheet.absoluteFill} pointerEvents="box-none">
|
|
30
33
|
{element}
|
|
31
34
|
</View>
|
|
@@ -37,7 +40,9 @@ export const BottomSheetProvider = ({ children }: { children: ReactNode }) => {
|
|
|
37
40
|
const [context] = useState<PortalContextType>(() => {
|
|
38
41
|
const portals = new Map<string, ReactNode>();
|
|
39
42
|
const subscribers = new Set<() => void>();
|
|
43
|
+
let snapshot: PortalSnapshot = [];
|
|
40
44
|
const notify = () => {
|
|
45
|
+
snapshot = Array.from(portals.entries());
|
|
41
46
|
subscribers.forEach((subscriber) => subscriber());
|
|
42
47
|
};
|
|
43
48
|
return {
|
|
@@ -55,7 +60,7 @@ export const BottomSheetProvider = ({ children }: { children: ReactNode }) => {
|
|
|
55
60
|
subscribers.delete(callback);
|
|
56
61
|
};
|
|
57
62
|
},
|
|
58
|
-
|
|
63
|
+
getSnapshot: () => snapshot,
|
|
59
64
|
};
|
|
60
65
|
});
|
|
61
66
|
|
|
@@ -76,10 +81,10 @@ export const Portal = ({ children }: { children: ReactNode }) => {
|
|
|
76
81
|
const { addPortal, removePortal } = context;
|
|
77
82
|
const id = useId();
|
|
78
83
|
|
|
79
|
-
|
|
84
|
+
useLayoutEffect(() => {
|
|
80
85
|
addPortal(id, children);
|
|
81
86
|
}, [id, children, addPortal]);
|
|
82
|
-
|
|
87
|
+
useLayoutEffect(() => {
|
|
83
88
|
return () => {
|
|
84
89
|
removePortal(id);
|
|
85
90
|
};
|