@swmansion/react-native-bottom-sheet 0.9.0 → 0.9.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 +26 -2
- package/android/src/main/java/com/swmansion/reactnativebottomsheet/BottomSheetView.kt +74 -27
- package/ios/RNSBottomSheetHostingView.swift +38 -3
- 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 +1 -1
- 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
|
|
@@ -208,6 +232,6 @@ product‍—‍[hire us](https://swmansion.com/contact/projects?utm_so
|
|
|
208
232
|
|
|
209
233
|
## Sponsored by [Gobi Maps](https://www.gobimaps.com)
|
|
210
234
|
|
|
211
|
-
|
|
235
|
+
The best of your city, all in one map.
|
|
212
236
|
|
|
213
237
|
[<img src="gobi.png" height="80" />](https://www.gobimaps.com)
|
|
@@ -83,6 +83,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
83
83
|
private var scrimTouchActive = false
|
|
84
84
|
private var scrimColor = Color.TRANSPARENT
|
|
85
85
|
private var scrimProgress = 0f
|
|
86
|
+
private var suppressScrimForClosingTarget = false
|
|
86
87
|
private var maxDetentHeight = Float.NaN
|
|
87
88
|
private var contentHeightMarker: View? = null
|
|
88
89
|
|
|
@@ -146,10 +147,10 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
146
147
|
|
|
147
148
|
// MARK: - Layout
|
|
148
149
|
|
|
149
|
-
override fun onLayout(changed: Boolean,
|
|
150
|
-
super.onLayout(changed,
|
|
151
|
-
val w =
|
|
152
|
-
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
|
|
153
154
|
if (w <= 0 || h <= 0) return
|
|
154
155
|
|
|
155
156
|
refreshContentHeightMarker()
|
|
@@ -157,10 +158,21 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
157
158
|
layoutSheetContainer(w, h)
|
|
158
159
|
|
|
159
160
|
if (!hasLaidOut && detentSpecs.isNotEmpty()) {
|
|
160
|
-
hasLaidOut = true
|
|
161
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
|
|
162
174
|
pendingIndex = null
|
|
163
|
-
targetIndex =
|
|
175
|
+
targetIndex = clampedIndex
|
|
164
176
|
|
|
165
177
|
if (animateIn) {
|
|
166
178
|
val closedTy = detentSpecs.maxOfOrNull { it.height } ?: h.toFloat()
|
|
@@ -248,7 +260,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
248
260
|
|
|
249
261
|
private fun resolveDetentSpecs(): List<DetentSpec> {
|
|
250
262
|
val maxHeight = resolvedMaxDetentHeight()
|
|
251
|
-
val contentHeight =
|
|
263
|
+
val contentHeight = validContentHeight().takeIf { it.isFinite() } ?: maxHeight
|
|
252
264
|
return rawDetentSpecs.map { spec ->
|
|
253
265
|
val height =
|
|
254
266
|
when (spec.kind) {
|
|
@@ -260,6 +272,11 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
260
272
|
}
|
|
261
273
|
|
|
262
274
|
private fun refreshDetentsFromLayout() {
|
|
275
|
+
if (hasLaidOut && isInvalidContentDetentTarget(targetIndex)) {
|
|
276
|
+
updateScrim()
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
|
|
263
280
|
val resolvedDetents = resolveDetentSpecs()
|
|
264
281
|
if (resolvedDetents == detentSpecs) {
|
|
265
282
|
updateScrim()
|
|
@@ -272,6 +289,10 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
272
289
|
|
|
273
290
|
if (hasLaidOut && !isPanning) {
|
|
274
291
|
targetIndex = targetIndex.coerceIn(0, detentSpecs.size - 1)
|
|
292
|
+
if (activeAnimation != null && isTargetingClosedDetent) {
|
|
293
|
+
suppressScrimForClosingTarget = true
|
|
294
|
+
hideScrim()
|
|
295
|
+
}
|
|
275
296
|
if (activeAnimation != null) {
|
|
276
297
|
val currentTy = sheetContainer.translationY
|
|
277
298
|
val shouldEmitSettle = activeAnimationEmitsSettle
|
|
@@ -305,6 +326,15 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
305
326
|
return marker.top.toFloat()
|
|
306
327
|
}
|
|
307
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
|
+
|
|
308
338
|
private fun refreshContentHeightMarker() {
|
|
309
339
|
val marker = findContentHeightMarker()
|
|
310
340
|
if (marker === contentHeightMarker) return
|
|
@@ -339,6 +369,9 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
339
369
|
private val firstNonZeroDetentHeight: Float
|
|
340
370
|
get() = detentSpecs.firstOrNull { it.height > 0f }?.height ?: 0f
|
|
341
371
|
|
|
372
|
+
private val isTargetingClosedDetent: Boolean
|
|
373
|
+
get() = closedIndex?.let { targetIndex == it } == true
|
|
374
|
+
|
|
342
375
|
private val draggableMinTy: Float
|
|
343
376
|
get() {
|
|
344
377
|
val highestIndex = detentSpecs.indices.lastOrNull { !detentSpecs[it].programmatic } ?: 0
|
|
@@ -413,6 +446,9 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
413
446
|
) {
|
|
414
447
|
if (index < 0 || index >= detentSpecs.size) return
|
|
415
448
|
targetIndex = index
|
|
449
|
+
if (!isTargetingClosedDetent) {
|
|
450
|
+
suppressScrimForClosingTarget = false
|
|
451
|
+
}
|
|
416
452
|
|
|
417
453
|
val targetTy = translationY(index)
|
|
418
454
|
activeAnimationEmitsSettle = emitSettle
|
|
@@ -433,9 +469,14 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
433
469
|
return@addEndListener
|
|
434
470
|
}
|
|
435
471
|
stopChoreographer()
|
|
436
|
-
emitPosition()
|
|
437
472
|
activeAnimation = null
|
|
438
473
|
activeAnimationEmitsSettle = false
|
|
474
|
+
suppressScrimForClosingTarget = false
|
|
475
|
+
if (closedIndex == index) {
|
|
476
|
+
sheetContainer.translationY = translationY(index)
|
|
477
|
+
hideScrim()
|
|
478
|
+
}
|
|
479
|
+
emitPosition()
|
|
439
480
|
updateInteractionState()
|
|
440
481
|
if (emitIndexChange) listener?.onIndexChange(index)
|
|
441
482
|
if (emitSettle) listener?.onSettle(index)
|
|
@@ -469,14 +510,14 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
469
510
|
|
|
470
511
|
// MARK: - Touch handling
|
|
471
512
|
|
|
472
|
-
override fun onInterceptTouchEvent(
|
|
513
|
+
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
|
473
514
|
val sheetTop = sheetContainer.top + sheetContainer.translationY
|
|
474
|
-
if (
|
|
515
|
+
if (event.actionMasked == MotionEvent.ACTION_DOWN && event.y < sheetTop) {
|
|
475
516
|
if (isScrimVisible()) {
|
|
476
|
-
initialTouchX =
|
|
477
|
-
initialTouchY =
|
|
478
|
-
lastTouchY =
|
|
479
|
-
activePointerId =
|
|
517
|
+
initialTouchX = event.x
|
|
518
|
+
initialTouchY = event.y
|
|
519
|
+
lastTouchY = event.y
|
|
520
|
+
activePointerId = event.getPointerId(0)
|
|
480
521
|
scrimPressed = true
|
|
481
522
|
scrimTouchActive = true
|
|
482
523
|
return true
|
|
@@ -484,19 +525,19 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
484
525
|
return false
|
|
485
526
|
}
|
|
486
527
|
|
|
487
|
-
when (
|
|
528
|
+
when (event.actionMasked) {
|
|
488
529
|
MotionEvent.ACTION_DOWN -> {
|
|
489
|
-
initialTouchX =
|
|
490
|
-
initialTouchY =
|
|
491
|
-
lastTouchY =
|
|
492
|
-
activePointerId =
|
|
530
|
+
initialTouchX = event.x
|
|
531
|
+
initialTouchY = event.y
|
|
532
|
+
lastTouchY = event.y
|
|
533
|
+
activePointerId = event.getPointerId(0)
|
|
493
534
|
}
|
|
494
535
|
MotionEvent.ACTION_MOVE -> {
|
|
495
536
|
if (activePointerId == MotionEvent.INVALID_POINTER_ID) return false
|
|
496
|
-
val pointerIndex =
|
|
537
|
+
val pointerIndex = event.findPointerIndex(activePointerId)
|
|
497
538
|
if (pointerIndex < 0) return false
|
|
498
|
-
val x =
|
|
499
|
-
val y =
|
|
539
|
+
val x = event.getX(pointerIndex)
|
|
540
|
+
val y = event.getY(pointerIndex)
|
|
500
541
|
val dx = x - initialTouchX
|
|
501
542
|
val dy = y - initialTouchY
|
|
502
543
|
|
|
@@ -511,13 +552,13 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
511
552
|
// processes events at the root view level before onInterceptTouchEvent
|
|
512
553
|
// runs, so without this the JS side never sees a cancel and Pressable
|
|
513
554
|
// would still fire onPress.
|
|
514
|
-
NativeGestureUtil.notifyNativeGestureStarted(this,
|
|
555
|
+
NativeGestureUtil.notifyNativeGestureStarted(this, event)
|
|
515
556
|
return true
|
|
516
557
|
}
|
|
517
558
|
if (dy > 0 && isScrollViewAtTop()) {
|
|
518
559
|
lastTouchY = y
|
|
519
560
|
requestDisallowInterceptTouchEvent(false)
|
|
520
|
-
NativeGestureUtil.notifyNativeGestureStarted(this,
|
|
561
|
+
NativeGestureUtil.notifyNativeGestureStarted(this, event)
|
|
521
562
|
return true
|
|
522
563
|
}
|
|
523
564
|
}
|
|
@@ -716,6 +757,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
716
757
|
scrimTouchActive = false
|
|
717
758
|
sheetContainer.translationY = 0f
|
|
718
759
|
scrimProgress = 0f
|
|
760
|
+
suppressScrimForClosingTarget = false
|
|
719
761
|
sheetContainer.removeAllViews()
|
|
720
762
|
stateWrapper = null
|
|
721
763
|
lastShadowOffsetY = Float.NaN
|
|
@@ -731,10 +773,10 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
731
773
|
// When settled at the closed detent, dynamic content updates can briefly
|
|
732
774
|
// produce stale non-zero positions. Keep scrim hidden in this state.
|
|
733
775
|
if (
|
|
734
|
-
|
|
776
|
+
(isTargetingClosedDetent && activeAnimation == null && !isPanning) ||
|
|
777
|
+
(suppressScrimForClosingTarget && isTargetingClosedDetent)
|
|
735
778
|
) {
|
|
736
|
-
|
|
737
|
-
invalidate()
|
|
779
|
+
hideScrim()
|
|
738
780
|
return
|
|
739
781
|
}
|
|
740
782
|
|
|
@@ -743,6 +785,11 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
743
785
|
invalidate()
|
|
744
786
|
}
|
|
745
787
|
|
|
788
|
+
private fun hideScrim() {
|
|
789
|
+
scrimProgress = 0f
|
|
790
|
+
invalidate()
|
|
791
|
+
}
|
|
792
|
+
|
|
746
793
|
private fun updateInteractionState() {
|
|
747
794
|
pointerEvents =
|
|
748
795
|
if (modal && (activeAnimation != null || isPanning || isScrimVisible())) {
|
|
@@ -127,10 +127,21 @@ public final class RNSBottomSheetHostingView: UIView {
|
|
|
127
127
|
sheetContainer.center = CGPoint(x: bounds.width / 2, y: bounds.height - maxHeight / 2)
|
|
128
128
|
|
|
129
129
|
if !hasLaidOut && !detentSpecs.isEmpty {
|
|
130
|
-
hasLaidOut = true
|
|
131
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
|
|
132
143
|
pendingIndex = nil
|
|
133
|
-
targetIndex =
|
|
144
|
+
targetIndex = clampedIndex
|
|
134
145
|
|
|
135
146
|
if animateIn {
|
|
136
147
|
let closedTy = maximumResolvedDetentHeight ?? bounds.height
|
|
@@ -529,7 +540,7 @@ public final class RNSBottomSheetHostingView: UIView {
|
|
|
529
540
|
|
|
530
541
|
private func resolveDetentSpecs() -> [DetentSpec] {
|
|
531
542
|
let maxHeight = resolvedMaxDetentHeight
|
|
532
|
-
let contentHeight =
|
|
543
|
+
let contentHeight = validContentHeight.map { min($0, maxHeight) } ?? maxHeight
|
|
533
544
|
return rawDetentSpecs.map { spec in
|
|
534
545
|
let height: CGFloat
|
|
535
546
|
switch spec.kind {
|
|
@@ -544,6 +555,11 @@ public final class RNSBottomSheetHostingView: UIView {
|
|
|
544
555
|
|
|
545
556
|
private func refreshDetentsFromLayout() {
|
|
546
557
|
refreshContentHeightMarker()
|
|
558
|
+
if hasLaidOut, isInvalidContentDetentTarget(targetIndex) {
|
|
559
|
+
updateScrim()
|
|
560
|
+
return
|
|
561
|
+
}
|
|
562
|
+
|
|
547
563
|
let resolvedDetents = resolveDetentSpecs()
|
|
548
564
|
guard resolvedDetents != detentSpecs else {
|
|
549
565
|
updateScrim()
|
|
@@ -598,6 +614,25 @@ public final class RNSBottomSheetHostingView: UIView {
|
|
|
598
614
|
guard let marker = contentHeightMarker else { return nil }
|
|
599
615
|
return marker.frame.minY.isFinite ? marker.frame.minY : nil
|
|
600
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
|
+
}
|
|
601
636
|
}
|
|
602
637
|
|
|
603
638
|
extension RNSBottomSheetHostingView: UIGestureRecognizerDelegate {
|
|
@@ -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,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
|
};
|