@swmansion/react-native-bottom-sheet 0.10.2 → 0.11.0

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 (27) hide show
  1. package/README.md +100 -25
  2. package/android/src/main/java/com/swmansion/reactnativebottomsheet/BottomSheetPackage.kt +1 -1
  3. package/android/src/main/java/com/swmansion/reactnativebottomsheet/BottomSheetSurfaceView.kt +10 -0
  4. package/android/src/main/java/com/swmansion/reactnativebottomsheet/BottomSheetSurfaceViewManager.kt +27 -0
  5. package/android/src/main/java/com/swmansion/reactnativebottomsheet/BottomSheetView.kt +48 -24
  6. package/common/cpp/react/renderer/components/ReactNativeBottomSheetSpec/ComponentDescriptors.h +4 -0
  7. package/common/cpp/react/renderer/components/ReactNativeBottomSheetSpec/ShadowNodes.h +12 -0
  8. package/ios/BottomSheetComponentView.mm +13 -2
  9. package/ios/BottomSheetContentView.h +2 -0
  10. package/ios/BottomSheetContentView.mm +10 -0
  11. package/ios/BottomSheetHostingView.swift +50 -24
  12. package/ios/BottomSheetSurfaceComponentView.h +13 -0
  13. package/ios/BottomSheetSurfaceComponentView.mm +21 -0
  14. package/lib/module/BottomSheet.js +10 -3
  15. package/lib/module/BottomSheet.js.map +1 -1
  16. package/lib/module/BottomSheetSurfaceNativeComponent.ts +9 -0
  17. package/lib/module/ModalBottomSheet.js.map +1 -1
  18. package/lib/typescript/src/BottomSheet.d.ts +14 -2
  19. package/lib/typescript/src/BottomSheet.d.ts.map +1 -1
  20. package/lib/typescript/src/BottomSheetSurfaceNativeComponent.d.ts +6 -0
  21. package/lib/typescript/src/BottomSheetSurfaceNativeComponent.d.ts.map +1 -0
  22. package/lib/typescript/src/ModalBottomSheet.d.ts.map +1 -1
  23. package/package.json +20 -18
  24. package/react-native.config.js +4 -1
  25. package/src/BottomSheet.tsx +24 -1
  26. package/src/BottomSheetSurfaceNativeComponent.ts +9 -0
  27. package/src/ModalBottomSheet.tsx +4 -2
package/README.md CHANGED
@@ -18,6 +18,47 @@ React Native.
18
18
  - Programmatic‍-‍only detents for snap points unreachable
19
19
  by dragging.
20
20
 
21
+ ## How it compares
22
+
23
+ React Native already has strong bottom‍-‍sheet options, but they make
24
+ different tradeoffs. React Native Bottom Sheet gives you composable React Native
25
+ primitives backed by native sheet mechanics: You compose the surface in React,
26
+ while the sheet host, gestures, snapping, and scroll negotiation run in
27
+ native code.
28
+
29
+ [`@gorhom/bottom-sheet`](https://gorhom.dev/react-native-bottom-sheet) is the
30
+ closest match in day‍-‍to‍-‍day functionality: configurable
31
+ detents, dynamic sizing, scrollable coordination, inline sheets, and modal
32
+ presentation. The main difference is the implementation model. React Native
33
+ Bottom Sheet moves the sheet host, gestures, snapping, and scroll negotiation
34
+ into native code, so heavy React rendering and busy JS work are less likely to
35
+ affect drag and snap performance. It also does not require Reanimated or React
36
+ Native Gesture Handler. Because scroll coordination is native, regular React
37
+ Native scrollables work inside the sheet without
38
+ bottom‍-‍sheet‍-‍specific list components or wrapper factories.
39
+
40
+ [Expo UI](https://docs.expo.dev/versions/latest/sdk/ui) sheets,
41
+ [Expo Router form sheets](https://docs.expo.dev/router/advanced/modals/#form-sheet),
42
+ and native modal‍-‍sheet libraries such as
43
+ [True Sheet](https://sheet.lodev09.com) lean into platform presentation APIs.
44
+ That is a good fit when you want a system‍-‍style presented sheet, but
45
+ it also means the platform and presentation system decide more of the behavior.
46
+ React Native Bottom Sheet is built as a lower‍-‍level sheet primitive
47
+ instead: The same native implementation powers both persistent inline sheets and
48
+ modal sheets, you provide the complete sheet surface in React, and detents can
49
+ include app‍-‍level behavior such as programmatic‍-‍only
50
+ snap points.
51
+
52
+ That difference also matters for layering. A platform‍-‍presented sheet
53
+ can disable dimming and allow background interaction, but it is still drawn as a
54
+ presented native sheet over the React Native view hierarchy. `BottomSheet` is
55
+ actually inline: It renders in your screen’s React Native hierarchy and can be
56
+ layered alongside nearby content. When you do need a modal, `ModalBottomSheet`
57
+ is rendered through `BottomSheetProvider`’s portal rather than through a
58
+ separate native window, so global UI such as toasts, menus, floating controls,
59
+ or debug overlays can be arranged above or below it by where you place them
60
+ relative to the provider.
61
+
21
62
  ## Getting started
22
63
 
23
64
  1. Install React Native Bottom Sheet:
@@ -29,7 +70,7 @@ React Native.
29
70
  2. Ensure the peer dependency is installed:
30
71
 
31
72
  ```sh
32
- npm i react-native-safe-area-context@^4.0.0
73
+ npm i react-native-safe-area-context
33
74
  ```
34
75
 
35
76
  3. Wrap your app with `BottomSheetProvider`:
@@ -41,9 +82,9 @@ React Native.
41
82
  ## Usage
42
83
 
43
84
  The library provides two components: `BottomSheet` (inline) and
44
- `ModalBottomSheet` (modal). Both render their children as the sheet content
45
- (including any background) and are controlled via `detents`, `index`,
46
- and `onIndexChange`. Use `onSettle` for
85
+ `ModalBottomSheet` (modal). Both render their children as the sheet content,
86
+ with a `surface` prop for the background behind it, and are controlled via
87
+ `detents`, `index`, and `onIndexChange`. Use `onSettle` for
47
88
  post‍-‍snap observability.
48
89
 
49
90
  ### Inline
@@ -56,14 +97,14 @@ const insets = useSafeAreaInsets();
56
97
  ```
57
98
 
58
99
  ```tsx
59
- <BottomSheet index={index} onIndexChange={setIndex}>
60
- <View
61
- style={{
62
- backgroundColor: 'white',
63
- padding: 16,
64
- paddingBottom: insets.bottom + 16,
65
- }}
66
- >
100
+ <BottomSheet
101
+ index={index}
102
+ onIndexChange={setIndex}
103
+ surface={
104
+ <View style={[StyleSheet.absoluteFill, { backgroundColor: 'white' }]} />
105
+ }
106
+ >
107
+ <View style={{ padding: 16, paddingBottom: insets.bottom + 16 }}>
67
108
  <Text>Sheet content</Text>
68
109
  </View>
69
110
  </BottomSheet>
@@ -80,14 +121,14 @@ const insets = useSafeAreaInsets();
80
121
  ```
81
122
 
82
123
  ```tsx
83
- <ModalBottomSheet index={index} onIndexChange={setIndex}>
84
- <View
85
- style={{
86
- backgroundColor: 'white',
87
- padding: 16,
88
- paddingBottom: insets.bottom + 16,
89
- }}
90
- >
124
+ <ModalBottomSheet
125
+ index={index}
126
+ onIndexChange={setIndex}
127
+ surface={
128
+ <View style={[StyleSheet.absoluteFill, { backgroundColor: 'white' }]} />
129
+ }
130
+ >
131
+ <View style={{ padding: 16, paddingBottom: insets.bottom + 16 }}>
91
132
  <Text>Sheet content</Text>
92
133
  </View>
93
134
  </ModalBottomSheet>
@@ -102,12 +143,39 @@ its&nbsp;color:
102
143
  <ModalBottomSheet
103
144
  index={index}
104
145
  onIndexChange={setIndex}
146
+ surface={/* ... */}
105
147
  scrimColor="rgba(0, 0, 0, 0.3)"
106
148
  >
107
149
  {/* ... */}
108
150
  </ModalBottomSheet>
109
151
  ```
110
152
 
153
+ ### Surface
154
+
155
+ Provide the sheet’s background through the `surface` prop. The library renders
156
+ it behind your content and sizes it natively to cover the whole sheet,
157
+ independently of the content&nbsp;height.
158
+
159
+ Decoupling the surface this way keeps the sheet covered as the content height
160
+ changes. When content shrinks, the sheet animates to its new height without the
161
+ background briefly exposing blank space behind the&nbsp;content.
162
+
163
+ Give the surface a filling style such as `StyleSheet.absoluteFill`. It is
164
+ mounted in a full&zwj;-&zwj;size host, so a surface sized only by its own
165
+ content would collapse and not&nbsp;show.
166
+
167
+ ```tsx
168
+ <BottomSheet // Or `ModalBottomSheet`.
169
+ index={index}
170
+ onIndexChange={setIndex}
171
+ surface={
172
+ <View style={[StyleSheet.absoluteFill, { backgroundColor: 'white' }]} />
173
+ }
174
+ >
175
+ <Text>Sheet content</Text>
176
+ </BottomSheet>
177
+ ```
178
+
111
179
  ### Scrollable negotiation
112
180
 
113
181
  By default, the sheet coordinates vertical gestures with nested scrollables,
@@ -121,6 +189,7 @@ set&nbsp;`disableScrollableNegotiation`:
121
189
  <BottomSheet
122
190
  index={index}
123
191
  onIndexChange={setIndex}
192
+ surface={/* ... */}
124
193
  disableScrollableNegotiation
125
194
  >
126
195
  {/* ... */}
@@ -134,18 +203,20 @@ Detents are the points to which the sheet snaps. Each detent is either a number
134
203
  the available screen height). The default detents are `[0, 'content']`.
135
204
 
136
205
  Sheet children are laid out in a flex container. For a full&zwj;-&zwj;height
137
- sheet, apply `flex: 1` to your sheet surface and use the
138
- `'content'`&nbsp;detent:
206
+ sheet, apply `flex: 1` to your content and use the `'content'`&nbsp;detent.
207
+ `surface` is sized by the library, so `flex: 1` only ever belongs on your
208
+ content, never on the&nbsp;surface:
139
209
 
140
210
  ```tsx
141
211
  <BottomSheet
142
212
  // `detents` defaults to `[0, 'content']`.
143
213
  index={index}
144
214
  onIndexChange={setIndex}
215
+ surface={
216
+ <View style={[StyleSheet.absoluteFill, { backgroundColor: 'white' }]} />
217
+ }
145
218
  >
146
- <View style={{ flex: 1, backgroundColor: 'white' }}>
147
- {/* Full-height sheet content. */}
148
- </View>
219
+ <View style={{ flex: 1 }}>{/* Full-height sheet content. */}</View>
149
220
  </BottomSheet>
150
221
  ```
151
222
 
@@ -168,6 +239,7 @@ const [index, setIndex] = useState(0);
168
239
  detents={[0, 300, 'content']} // Collapsed, 300 px, content height.
169
240
  index={index}
170
241
  onIndexChange={setIndex} // Keep controlled state in sync.
242
+ surface={/* ... */}
171
243
  onSettle={(nextIndex) => {
172
244
  if (nextIndex === 0) console.log('Sheet collapsed.');
173
245
  }}
@@ -190,6 +262,7 @@ drag snapping but can still be targeted via `index`&nbsp;updates.
190
262
  detents={[0, programmatic(300), 'content']}
191
263
  index={index}
192
264
  onIndexChange={setIndex}
265
+ surface={/* ... */}
193
266
  onSettle={(nextIndex) => {
194
267
  console.log(`Settled at ${nextIndex}.`);
195
268
  }}
@@ -207,6 +280,7 @@ pixels from the bottom of the screen to the top of the&nbsp;sheet).
207
280
  <BottomSheet // Or `ModalBottomSheet`.
208
281
  index={index}
209
282
  onIndexChange={setIndex}
283
+ surface={/* ... */}
210
284
  onPositionChange={(position) => {
211
285
  console.log(position);
212
286
  }}
@@ -226,6 +300,7 @@ const position = useSharedValue(0);
226
300
  <BottomSheet
227
301
  index={index}
228
302
  onIndexChange={setIndex}
303
+ surface={/* ... */}
229
304
  onPositionChange={(nextPosition) => {
230
305
  position.value = nextPosition;
231
306
  }}
@@ -10,5 +10,5 @@ class BottomSheetPackage : ReactPackage {
10
10
  emptyList()
11
11
 
12
12
  override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> =
13
- listOf(BottomSheetViewManager())
13
+ listOf(BottomSheetViewManager(), BottomSheetSurfaceViewManager())
14
14
  }
@@ -0,0 +1,10 @@
1
+ package com.swmansion.reactnativebottomsheet
2
+
3
+ import android.content.Context
4
+ import com.facebook.react.views.view.ReactViewGroup
5
+
6
+ // Visual surface that sits behind the sheet content. It carries no behavior of
7
+ // its own; the BottomSheetView identifies it by this type and owns its geometry,
8
+ // laying it out to cover the full sheet so a content shrink never exposes blank
9
+ // space. Its React children provide the appearance only.
10
+ class BottomSheetSurfaceView(context: Context) : ReactViewGroup(context)
@@ -0,0 +1,27 @@
1
+ package com.swmansion.reactnativebottomsheet
2
+
3
+ import com.facebook.react.module.annotations.ReactModule
4
+ import com.facebook.react.uimanager.ThemedReactContext
5
+ import com.facebook.react.uimanager.ViewGroupManager
6
+ import com.facebook.react.uimanager.ViewManagerDelegate
7
+ import com.facebook.react.viewmanagers.BottomSheetSurfaceViewManagerDelegate
8
+ import com.facebook.react.viewmanagers.BottomSheetSurfaceViewManagerInterface
9
+
10
+ @ReactModule(name = BottomSheetSurfaceViewManager.NAME)
11
+ class BottomSheetSurfaceViewManager :
12
+ ViewGroupManager<BottomSheetSurfaceView>(),
13
+ BottomSheetSurfaceViewManagerInterface<BottomSheetSurfaceView> {
14
+
15
+ companion object {
16
+ const val NAME = "BottomSheetSurfaceView"
17
+ }
18
+
19
+ private val delegate = BottomSheetSurfaceViewManagerDelegate(this)
20
+
21
+ override fun getDelegate(): ViewManagerDelegate<BottomSheetSurfaceView> = delegate
22
+
23
+ override fun getName(): String = NAME
24
+
25
+ override fun createViewInstance(context: ThemedReactContext): BottomSheetSurfaceView =
26
+ BottomSheetSurfaceView(context)
27
+ }
@@ -88,6 +88,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
88
88
  private var scrimPinnedFull = false
89
89
  private var maxDetentHeight = Float.NaN
90
90
  private var contentHeightMarker: View? = null
91
+ private var surfaceView: View? = null
91
92
 
92
93
  private val contentHeightMarkerLayoutListener =
93
94
  View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> refreshDetentsFromLayout() }
@@ -166,7 +167,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
166
167
  if (animateIn && isInvalidContentDetentTarget(clampedIndex)) {
167
168
  targetIndex = clampedIndex
168
169
  pendingIndex = clampedIndex
169
- val closedTy = detentSpecs.maxOfOrNull { it.height } ?: h.toFloat()
170
+ val closedTy = resolvedMaxDetentHeight(h)
170
171
  sheetContainer.translationY = closedTy
171
172
  emitPosition()
172
173
  return
@@ -177,7 +178,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
177
178
  targetIndex = clampedIndex
178
179
 
179
180
  if (animateIn) {
180
- val closedTy = detentSpecs.maxOfOrNull { it.height } ?: h.toFloat()
181
+ val closedTy = resolvedMaxDetentHeight(h)
181
182
  sheetContainer.translationY = closedTy
182
183
  emitPosition()
183
184
  snapToIndex(targetIndex, 0f, emitIndexChange = false, emitSettle = true)
@@ -198,18 +199,25 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
198
199
  super.dispatchDraw(canvas)
199
200
  }
200
201
 
201
- private fun layoutSheetChildren() {
202
+ private fun layoutSheetChildren(containerWidth: Int, containerHeight: Int) {
202
203
  for (i in 0 until sheetContainer.childCount) {
203
204
  val child = sheetContainer.getChildAt(i)
204
- child.layout(0, 0, child.measuredWidth, child.measuredHeight)
205
+ if (child === surfaceView) {
206
+ // The surface fills the full container so it always covers the visible
207
+ // sheet (the container is translated to the current sheet position),
208
+ // regardless of how short the content becomes.
209
+ child.layout(0, 0, containerWidth, containerHeight)
210
+ } else {
211
+ child.layout(0, 0, child.measuredWidth, child.measuredHeight)
212
+ }
205
213
  }
206
214
  }
207
215
 
208
216
  private fun layoutSheetContainer(viewWidth: Int, viewHeight: Int) {
209
- val maxHeight = detentSpecs.maxOfOrNull { it.height } ?: resolvedMaxDetentHeight(viewHeight)
217
+ val maxHeight = resolvedMaxDetentHeight(viewHeight)
210
218
  val containerTop = (viewHeight - maxHeight).toInt()
211
219
  sheetContainer.layout(0, containerTop, viewWidth, containerTop + maxHeight.toInt())
212
- layoutSheetChildren()
220
+ layoutSheetChildren(viewWidth, maxHeight.toInt())
213
221
  }
214
222
 
215
223
  // MARK: - Prop setters
@@ -252,6 +260,12 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
252
260
  refreshDetentsFromLayout()
253
261
  }
254
262
 
263
+ // Stable coordinate base for the sheet container. The container is sized to
264
+ // the full available height rather than the tallest detent, so it stays a
265
+ // fixed-size canvas: when content — and thus the `content` detent — shrinks,
266
+ // the container does not collapse underneath the sheet, leaving room to
267
+ // animate the sheet down to its new height. The surface fills this canvas, so
268
+ // the area below the shrunken content stays covered throughout.
255
269
  private fun resolvedMaxDetentHeight(viewHeight: Int = height): Float {
256
270
  val viewHeightPx = viewHeight.toFloat()
257
271
  if (!maxDetentHeight.isFinite() || maxDetentHeight <= 0f) {
@@ -294,7 +308,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
294
308
  return
295
309
  }
296
310
 
297
- val previousMaxHeight = detentSpecs.maxOfOrNull { it.height } ?: resolvedMaxDetentHeight()
311
+ val previousMaxHeight = resolvedMaxDetentHeight()
298
312
  // Whether the scrim is currently fully opaque, i.e. the sheet is settled at
299
313
  // or above the first non-zero detent. If so, a detent resize must not dip
300
314
  // the scrim while the sheet re-anchors to the new geometry.
@@ -308,7 +322,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
308
322
 
309
323
  if (hasLaidOut && !isPanning) {
310
324
  targetIndex = targetIndex.coerceIn(0, detentSpecs.size - 1)
311
- val newMaxHeight = detentSpecs.maxOfOrNull { it.height } ?: resolvedMaxDetentHeight()
325
+ val newMaxHeight = resolvedMaxDetentHeight()
312
326
  val targetTy = translationY(targetIndex)
313
327
  if (activeAnimation != null && isTargetingClosedDetent) {
314
328
  suppressScrimForClosingTarget = true
@@ -337,20 +351,15 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
337
351
  } else {
338
352
  val currentVisibleHeight = previousMaxHeight - sheetContainer.translationY
339
353
  val targetHeight = detentSpecs.getOrNull(targetIndex)?.height ?: 0f
340
- val isContentDetent = rawDetentSpecs.getOrNull(targetIndex)?.kind == DetentKind.CONTENT
341
- val didShrink = targetHeight < currentVisibleHeight - 0.5f
342
- if (isContentDetent && didShrink) {
343
- // Content shrank: snap immediately. Animating here would expose
344
- // blank space below the shrunken content.
345
- sheetContainer.translationY = targetTy
346
- emitPosition()
347
- } else if (kotlin.math.abs(targetHeight - currentVisibleHeight) <= 0.5f) {
354
+ if (kotlin.math.abs(targetHeight - currentVisibleHeight) <= 0.5f) {
348
355
  // No meaningful change.
349
356
  sheetContainer.translationY = targetTy
350
357
  emitPosition()
351
358
  } else {
352
- // Detent value changed (or content grew): re-anchor at the current
353
- // visible height, then animate to the new target.
359
+ // The content detent changed (grew or shrank): re-anchor at the
360
+ // current visible height, then animate to the new target. The
361
+ // surface covers the full sheet, so a shrink no longer exposes
362
+ // blank space.
354
363
  sheetContainer.translationY =
355
364
  (newMaxHeight - currentVisibleHeight).coerceIn(0f, newMaxHeight)
356
365
  scrimPinnedFull = scrimPinnedFull || wasScrimFull
@@ -386,6 +395,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
386
395
  }
387
396
 
388
397
  private fun refreshContentHeightMarker() {
398
+ surfaceView = findSurfaceView()
389
399
  val marker = findContentHeightMarker()
390
400
  if (marker === contentHeightMarker) return
391
401
  contentHeightMarker?.removeOnLayoutChangeListener(contentHeightMarkerLayoutListener)
@@ -393,8 +403,21 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
393
403
  contentHeightMarker?.addOnLayoutChangeListener(contentHeightMarkerLayoutListener)
394
404
  }
395
405
 
406
+ private fun findSurfaceView(): View? {
407
+ for (i in 0 until sheetContainer.childCount) {
408
+ val child = sheetContainer.getChildAt(i)
409
+ if (child is BottomSheetSurfaceView) return child
410
+ }
411
+ return null
412
+ }
413
+
396
414
  private fun findContentHeightMarker(): View? {
397
- val contentView = sheetContainer.getChildAt(0) as? ViewGroup ?: return null
415
+ // The surface is a sibling of the content wrapper; skip it so the marker is
416
+ // always read from the content, never from the surface.
417
+ val contentView =
418
+ (0 until sheetContainer.childCount)
419
+ .map { sheetContainer.getChildAt(it) }
420
+ .firstOrNull { it !== surfaceView } as? ViewGroup ?: return null
398
421
  if (contentView.childCount == 0) return null
399
422
  return contentView.getChildAt(contentView.childCount - 1)
400
423
  }
@@ -402,7 +425,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
402
425
  // MARK: - Snap logic
403
426
 
404
427
  private fun translationY(index: Int): Float {
405
- val maxHeight = detentSpecs.maxOfOrNull { it.height } ?: resolvedMaxDetentHeight()
428
+ val maxHeight = resolvedMaxDetentHeight()
406
429
  val snapHeight = detentSpecs.getOrNull(index)?.height ?: 0f
407
430
  return maxHeight - snapHeight
408
431
  }
@@ -447,7 +470,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
447
470
  }
448
471
 
449
472
  private fun emitPosition() {
450
- val maxHeight = detentSpecs.maxOfOrNull { it.height } ?: resolvedMaxDetentHeight()
473
+ val maxHeight = resolvedMaxDetentHeight()
451
474
  val ty = sheetContainer.translationY
452
475
  val position = maxHeight - ty
453
476
  updateScrim(position)
@@ -464,7 +487,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
464
487
  private var lastShadowOffsetY = Float.NaN
465
488
 
466
489
  private fun updateShadowState(translationY: Float) {
467
- val maxDetentHeight = detentSpecs.maxOfOrNull { it.height } ?: resolvedMaxDetentHeight()
490
+ val maxDetentHeight = resolvedMaxDetentHeight()
468
491
  val containerTop = height.toFloat() - maxDetentHeight
469
492
  val offsetY = ((containerTop + translationY) / density).toDouble()
470
493
  if (offsetY.toFloat() == lastShadowOffsetY) return
@@ -703,7 +726,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
703
726
  v
704
727
  } ?: 0f
705
728
  velocityTracker = null
706
- val maxHeight = detentSpecs.maxOfOrNull { it.height } ?: resolvedMaxDetentHeight()
729
+ val maxHeight = resolvedMaxDetentHeight()
707
730
  val currentHeight = maxHeight - sheetContainer.translationY
708
731
  val index = bestSnapIndex(currentHeight, velocity, panStartingIndex)
709
732
  panStartingIndex = null
@@ -814,6 +837,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
814
837
  velocityTracker = null
815
838
  contentHeightMarker?.removeOnLayoutChangeListener(contentHeightMarkerLayoutListener)
816
839
  contentHeightMarker = null
840
+ surfaceView = null
817
841
  rawDetentSpecs = emptyList()
818
842
  detentSpecs = emptyList()
819
843
  targetIndex = 0
@@ -881,7 +905,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
881
905
  }
882
906
 
883
907
  private fun currentSheetHeight(): Float {
884
- val maxHeight = detentSpecs.maxOfOrNull { it.height } ?: resolvedMaxDetentHeight()
908
+ val maxHeight = resolvedMaxDetentHeight()
885
909
  return maxHeight - sheetContainer.translationY
886
910
  }
887
911
 
@@ -19,4 +19,8 @@ class BottomSheetViewComponentDescriptor final
19
19
  }
20
20
  };
21
21
 
22
+ // The surface needs no custom initial state, so this mirrors the codegen alias.
23
+ using BottomSheetSurfaceViewComponentDescriptor =
24
+ ConcreteComponentDescriptor<BottomSheetSurfaceViewShadowNode>;
25
+
22
26
  } // namespace facebook::react
@@ -5,6 +5,7 @@
5
5
  #include <react/renderer/components/ReactNativeBottomSheetSpec/EventEmitters.h>
6
6
  #include <react/renderer/components/ReactNativeBottomSheetSpec/Props.h>
7
7
  #include <react/renderer/components/view/ConcreteViewShadowNode.h>
8
+ #include <react/renderer/core/StateData.h>
8
9
 
9
10
  namespace facebook::react {
10
11
 
@@ -22,4 +23,15 @@ class JSI_EXPORT BottomSheetViewShadowNode final
22
23
  Point getContentOriginOffset(bool includeTransform) const override;
23
24
  };
24
25
 
26
+ JSI_EXPORT extern const char BottomSheetSurfaceViewComponentName[];
27
+
28
+ // The surface needs no custom shadow-node behavior, so this mirrors the codegen
29
+ // alias exactly (state is the default StateData). It only lives here because the
30
+ // custom header search path shadows the generated ShadowNodes.h.
31
+ using BottomSheetSurfaceViewShadowNode = ConcreteViewShadowNode<
32
+ BottomSheetSurfaceViewComponentName,
33
+ BottomSheetSurfaceViewProps,
34
+ BottomSheetSurfaceViewEventEmitter,
35
+ StateData>;
36
+
25
37
  } // namespace facebook::react
@@ -1,5 +1,6 @@
1
1
  #import "BottomSheetComponentView.h"
2
2
  #import "BottomSheetContentView.h"
3
+ #import "BottomSheetSurfaceComponentView.h"
3
4
  #import "../common/cpp/react/renderer/components/ReactNativeBottomSheetSpec/BottomSheetStateHelper.h"
4
5
  #import "../common/cpp/react/renderer/components/ReactNativeBottomSheetSpec/ComponentDescriptors.h"
5
6
 
@@ -95,12 +96,22 @@ using namespace facebook::react;
95
96
 
96
97
  - (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
97
98
  {
98
- [_sheetView mountChildComponentView:childComponentView atIndex:index];
99
+ // Identify the visual surface by component type so the host can own its
100
+ // geometry. Everything else is treated as content.
101
+ if ([childComponentView isKindOfClass:BottomSheetSurfaceComponentView.class]) {
102
+ [_sheetView mountSurfaceComponentView:childComponentView atIndex:index];
103
+ } else {
104
+ [_sheetView mountChildComponentView:childComponentView atIndex:index];
105
+ }
99
106
  }
100
107
 
101
108
  - (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
102
109
  {
103
- [_sheetView unmountChildComponentView:childComponentView];
110
+ if ([childComponentView isKindOfClass:BottomSheetSurfaceComponentView.class]) {
111
+ [_sheetView unmountSurfaceComponentView:childComponentView];
112
+ } else {
113
+ [_sheetView unmountChildComponentView:childComponentView];
114
+ }
104
115
  }
105
116
 
106
117
  #pragma mark - BottomSheetContentViewDelegate
@@ -26,6 +26,8 @@ NS_ASSUME_NONNULL_BEGIN
26
26
  - (CGFloat)currentContentOffsetY;
27
27
  - (void)mountChildComponentView:(UIView *)childView atIndex:(NSInteger)index;
28
28
  - (void)unmountChildComponentView:(UIView *)childView;
29
+ - (void)mountSurfaceComponentView:(UIView *)surfaceView atIndex:(NSInteger)index;
30
+ - (void)unmountSurfaceComponentView:(UIView *)surfaceView;
29
31
  - (void)resetSheetState;
30
32
 
31
33
  @end
@@ -95,6 +95,16 @@
95
95
  [_impl unmountChildComponentView:childView];
96
96
  }
97
97
 
98
+ - (void)mountSurfaceComponentView:(UIView *)surfaceView atIndex:(NSInteger)index
99
+ {
100
+ [_impl mountSurfaceComponentView:surfaceView atIndex:index];
101
+ }
102
+
103
+ - (void)unmountSurfaceComponentView:(UIView *)surfaceView
104
+ {
105
+ [_impl unmountSurfaceComponentView:surfaceView];
106
+ }
107
+
98
108
  - (void)resetSheetState
99
109
  {
100
110
  [_impl resetSheetState];
@@ -65,6 +65,7 @@ public final class BottomSheetHostingView: UIView {
65
65
  private var panStartingIndex: Int?
66
66
  private var isContentInteractionDisabled = false
67
67
  private var contentHeightMarker: UIView?
68
+ private weak var surfaceView: UIView?
68
69
  private static var markerObservationContext = 0
69
70
 
70
71
  override public init(frame: CGRect) {
@@ -127,10 +128,16 @@ public final class BottomSheetHostingView: UIView {
127
128
 
128
129
  scrimView.frame = bounds
129
130
  refreshDetentsFromLayout()
130
- let maxHeight = maximumResolvedDetentHeight ?? resolvedMaxDetentHeight
131
+ let maxHeight = sheetContainerHeight
131
132
  sheetContainer.bounds = CGRect(x: 0, y: 0, width: bounds.width, height: maxHeight)
132
133
  sheetContainer.center = CGPoint(x: bounds.width / 2, y: bounds.height - maxHeight / 2)
133
134
 
135
+ // The surface fills the full container so it always covers the visible sheet
136
+ // (the container is translated to the current sheet position), regardless of
137
+ // how short the content becomes. Sized from the top via frame — never via
138
+ // anchorPoint.
139
+ surfaceView?.frame = sheetContainer.bounds
140
+
134
141
  if !hasLaidOut && !detentSpecs.isEmpty {
135
142
  let indexToApply = pendingIndex ?? targetIndex
136
143
  let clampedIndex = max(0, min(detentSpecs.count - 1, indexToApply))
@@ -138,7 +145,7 @@ public final class BottomSheetHostingView: UIView {
138
145
  if animateIn, isInvalidContentDetentTarget(clampedIndex) {
139
146
  targetIndex = clampedIndex
140
147
  pendingIndex = clampedIndex
141
- let closedTy = maximumResolvedDetentHeight ?? bounds.height
148
+ let closedTy = sheetContainerHeight
142
149
  sheetContainer.transform = CGAffineTransform(translationX: 0, y: closedTy)
143
150
  emitPosition()
144
151
  return
@@ -149,7 +156,7 @@ public final class BottomSheetHostingView: UIView {
149
156
  targetIndex = clampedIndex
150
157
 
151
158
  if animateIn {
152
- let closedTy = maximumResolvedDetentHeight ?? bounds.height
159
+ let closedTy = sheetContainerHeight
153
160
  sheetContainer.transform = CGAffineTransform(translationX: 0, y: closedTy)
154
161
  emitPosition()
155
162
  snapToIndex(targetIndex, velocity: 0, emitIndexChange: false, emitSettle: true)
@@ -231,6 +238,22 @@ public final class BottomSheetHostingView: UIView {
231
238
  setNeedsLayout()
232
239
  }
233
240
 
241
+ public func mountSurfaceComponentView(_ childView: UIView, atIndex index: Int) {
242
+ surfaceView = childView
243
+ sheetContainer.insertSubview(childView, at: index)
244
+ refreshContentHeightMarker()
245
+ setNeedsLayout()
246
+ }
247
+
248
+ public func unmountSurfaceComponentView(_ childView: UIView) {
249
+ if surfaceView === childView {
250
+ surfaceView = nil
251
+ }
252
+ childView.removeFromSuperview()
253
+ refreshContentHeightMarker()
254
+ setNeedsLayout()
255
+ }
256
+
234
257
  public func resetSheetState() {
235
258
  activeAnimator?.stopAnimation(true)
236
259
  activeAnimator = nil
@@ -244,6 +267,7 @@ public final class BottomSheetHostingView: UIView {
244
267
  panStartingIndex = nil
245
268
  setContentInteractionEnabled(true)
246
269
  stopObservingContentHeightMarker()
270
+ surfaceView = nil
247
271
  sheetContainer.transform = .identity
248
272
  scrimView.alpha = 0
249
273
  scrimView.isHidden = true
@@ -260,7 +284,7 @@ public final class BottomSheetHostingView: UIView {
260
284
  }
261
285
 
262
286
  private func translationY(for index: Int) -> CGFloat {
263
- let maxHeight = maximumResolvedDetentHeight ?? resolvedMaxDetentHeight
287
+ let maxHeight = sheetContainerHeight
264
288
  let snapHeight = detent(at: index).height
265
289
  return maxHeight - snapHeight
266
290
  }
@@ -297,13 +321,13 @@ public final class BottomSheetHostingView: UIView {
297
321
  }
298
322
 
299
323
  private var currentSheetHeight: CGFloat {
300
- let maxHeight = maximumResolvedDetentHeight ?? resolvedMaxDetentHeight
324
+ let maxHeight = sheetContainerHeight
301
325
  let ty = currentTranslationY
302
326
  return maxHeight - ty
303
327
  }
304
328
 
305
329
  public var currentContentOffsetY: CGFloat {
306
- let maxHeight = maximumResolvedDetentHeight ?? resolvedMaxDetentHeight
330
+ let maxHeight = sheetContainerHeight
307
331
  let containerTop = bounds.height - maxHeight
308
332
  let ty = currentTranslationY
309
333
  return containerTop + ty
@@ -314,7 +338,7 @@ public final class BottomSheetHostingView: UIView {
314
338
  }
315
339
 
316
340
  private func emitPosition() {
317
- let maxHeight = maximumResolvedDetentHeight ?? resolvedMaxDetentHeight
341
+ let maxHeight = sheetContainerHeight
318
342
  let ty = currentTranslationY
319
343
  let position = maxHeight - ty
320
344
  updateScrim(forPosition: position)
@@ -414,7 +438,7 @@ public final class BottomSheetHostingView: UIView {
414
438
  }
415
439
 
416
440
  @objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
417
- let maxHeight = maximumResolvedDetentHeight ?? resolvedMaxDetentHeight
441
+ let maxHeight = sheetContainerHeight
418
442
 
419
443
  switch gesture.state {
420
444
  case .began:
@@ -576,8 +600,14 @@ public final class BottomSheetHostingView: UIView {
576
600
  return min(max(0, maxDetentHeight), bounds.height)
577
601
  }
578
602
 
579
- private var maximumResolvedDetentHeight: CGFloat? {
580
- detentSpecs.map(\.height).max()
603
+ /// Stable coordinate base for the sheet container. The container is sized to
604
+ /// the full available height rather than the tallest detent, so it stays a
605
+ /// fixed-size canvas: when content — and thus the `content` detent — shrinks,
606
+ /// the container does not collapse underneath the sheet, leaving room to
607
+ /// animate the sheet down to its new height. The surface fills this canvas, so
608
+ /// the area below the shrunken content stays covered throughout.
609
+ private var sheetContainerHeight: CGFloat {
610
+ resolvedMaxDetentHeight
581
611
  }
582
612
 
583
613
  private func resolveDetentSpecs() -> [DetentSpec]? {
@@ -627,7 +657,7 @@ public final class BottomSheetHostingView: UIView {
627
657
  return
628
658
  }
629
659
 
630
- let previousMaxHeight = maximumResolvedDetentHeight ?? resolvedMaxDetentHeight
660
+ let previousMaxHeight = sheetContainerHeight
631
661
  // Whether the scrim is currently fully opaque, i.e. the sheet is settled at
632
662
  // or above the first non-zero detent. If so, a detent resize must not dip
633
663
  // the scrim while the sheet re-anchors to the new geometry.
@@ -642,7 +672,7 @@ public final class BottomSheetHostingView: UIView {
642
672
 
643
673
  if hasLaidOut, !isPanning {
644
674
  targetIndex = max(0, min(detentSpecs.count - 1, targetIndex))
645
- let newMaxHeight = maximumResolvedDetentHeight ?? resolvedMaxDetentHeight
675
+ let newMaxHeight = sheetContainerHeight
646
676
  let targetTy = translationY(for: targetIndex)
647
677
 
648
678
  if let animator = activeAnimator {
@@ -669,21 +699,14 @@ public final class BottomSheetHostingView: UIView {
669
699
  } else {
670
700
  let currentVisibleHeight = previousMaxHeight - currentTranslationY
671
701
  let targetHeight = detent(at: targetIndex).height
672
- let isContentDetent = rawDetentSpecs.indices.contains(targetIndex)
673
- && rawDetentSpecs[targetIndex].kind == .content
674
- let didShrink = targetHeight < currentVisibleHeight - 0.5
675
- if isContentDetent, didShrink {
676
- // Content shrank: snap immediately. Animating here would expose blank
677
- // space below the shrunken content.
678
- sheetContainer.transform = CGAffineTransform(translationX: 0, y: targetTy)
679
- emitPosition()
680
- } else if abs(targetHeight - currentVisibleHeight) <= 0.5 {
702
+ if abs(targetHeight - currentVisibleHeight) <= 0.5 {
681
703
  // No meaningful change.
682
704
  sheetContainer.transform = CGAffineTransform(translationX: 0, y: targetTy)
683
705
  emitPosition()
684
706
  } else {
685
- // Detent value changed (or content grew): re-anchor at the current
686
- // visible height, then animate to the new target.
707
+ // The content detent changed (grew or shrank): re-anchor at the
708
+ // current visible height, then animate to the new target. The surface
709
+ // covers the full sheet, so a shrink no longer exposes blank space.
687
710
  let startTy = min(max(newMaxHeight - currentVisibleHeight, 0), newMaxHeight)
688
711
  sheetContainer.transform = CGAffineTransform(translationX: 0, y: startTy)
689
712
  scrimPinnedFull = scrimPinnedFull || wasScrimFull
@@ -748,7 +771,10 @@ public final class BottomSheetHostingView: UIView {
748
771
  }
749
772
 
750
773
  private func findContentHeightMarker() -> UIView? {
751
- guard let contentView = sheetContainer.subviews.first else { return nil }
774
+ // The surface is a sibling of the content wrapper; skip it so the marker is
775
+ // always read from the content, never from the surface.
776
+ guard let contentView = sheetContainer.subviews.first(where: { $0 !== surfaceView })
777
+ else { return nil }
752
778
  return contentView.subviews.last
753
779
  }
754
780
 
@@ -0,0 +1,13 @@
1
+ #import <React/RCTViewComponentView.h>
2
+ #import <UIKit/UIKit.h>
3
+
4
+ NS_ASSUME_NONNULL_BEGIN
5
+
6
+ // Visual surface that sits behind the sheet content. It carries no behavior of
7
+ // its own; the bottom sheet host identifies it by this type and owns its
8
+ // geometry. Its React children provide the appearance only.
9
+ @interface BottomSheetSurfaceComponentView : RCTViewComponentView
10
+
11
+ @end
12
+
13
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,21 @@
1
+ #import "BottomSheetSurfaceComponentView.h"
2
+
3
+ #import "../common/cpp/react/renderer/components/ReactNativeBottomSheetSpec/ComponentDescriptors.h"
4
+
5
+ #import <React/RCTFabricComponentsPlugins.h>
6
+
7
+ using namespace facebook::react;
8
+
9
+ @implementation BottomSheetSurfaceComponentView
10
+
11
+ + (ComponentDescriptorProvider)componentDescriptorProvider
12
+ {
13
+ return concreteComponentDescriptorProvider<BottomSheetSurfaceViewComponentDescriptor>();
14
+ }
15
+
16
+ @end
17
+
18
+ Class<RCTComponentViewProtocol> BottomSheetSurfaceViewCls(void)
19
+ {
20
+ return BottomSheetSurfaceComponentView.class;
21
+ }
@@ -3,6 +3,7 @@
3
3
  import { StyleSheet, View, useWindowDimensions } from 'react-native';
4
4
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
5
5
  import BottomSheetNativeComponent from './BottomSheetNativeComponent';
6
+ import BottomSheetSurfaceNativeComponent from './BottomSheetSurfaceNativeComponent';
6
7
  import { Portal } from "./BottomSheetProvider.js";
7
8
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
9
  export { programmatic } from "./bottomSheetUtils.js";
@@ -14,6 +15,7 @@ export { programmatic } from "./bottomSheetUtils.js";
14
15
  /** Native bottom sheet that renders inline within the current screen layout. */
15
16
  export const BottomSheet = ({
16
17
  children,
18
+ surface,
17
19
  style,
18
20
  detents = [0, 'content'],
19
21
  index,
@@ -65,7 +67,7 @@ export const BottomSheet = ({
65
67
  children: /*#__PURE__*/_jsx(View, {
66
68
  pointerEvents: "box-none",
67
69
  style: StyleSheet.absoluteFill,
68
- children: /*#__PURE__*/_jsx(BottomSheetNativeComponent, {
70
+ children: /*#__PURE__*/_jsxs(BottomSheetNativeComponent, {
69
71
  pointerEvents: "box-none",
70
72
  style: [{
71
73
  position: 'absolute',
@@ -87,7 +89,12 @@ export const BottomSheet = ({
87
89
  onIndexChange: handleIndexChange,
88
90
  onSettle: handleSettle,
89
91
  onPositionChange: handlePositionChange,
90
- children: /*#__PURE__*/_jsxs(View, {
92
+ children: [surface != null && /*#__PURE__*/_jsx(BottomSheetSurfaceNativeComponent, {
93
+ collapsable: false,
94
+ pointerEvents: "box-none",
95
+ style: StyleSheet.absoluteFill,
96
+ children: surface
97
+ }), /*#__PURE__*/_jsxs(View, {
91
98
  collapsable: false,
92
99
  style: {
93
100
  flex: 1,
@@ -97,7 +104,7 @@ export const BottomSheet = ({
97
104
  collapsable: false,
98
105
  pointerEvents: "none"
99
106
  })]
100
- })
107
+ })]
101
108
  })
102
109
  })
103
110
  });
@@ -1 +1 @@
1
- {"version":3,"names":["StyleSheet","View","useWindowDimensions","useSafeAreaInsets","BottomSheetNativeComponent","Portal","jsx","_jsx","jsxs","_jsxs","programmatic","BottomSheet","children","style","detents","index","animateIn","onIndexChange","onSettle","onPositionChange","modal","disableScrollableNegotiation","scrimColor","height","windowHeight","insets","maxHeight","top","nativeDetents","map","detent","isDetentProgrammatic","value","resolveDetentValue","kind","Math","max","min","clampedIndex","length","selectedDetentValue","isCollapsed","handleIndexChange","event","nativeEvent","handleSettle","handlePositionChange","position","sheet","absoluteFill","pointerEvents","left","right","bottom","maxDetentHeight","collapsable","flex"],"sourceRoot":"../../src","sources":["BottomSheet.tsx"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["StyleSheet","View","useWindowDimensions","useSafeAreaInsets","BottomSheetNativeComponent","BottomSheetSurfaceNativeComponent","Portal","jsx","_jsx","jsxs","_jsxs","programmatic","BottomSheet","children","surface","style","detents","index","animateIn","onIndexChange","onSettle","onPositionChange","modal","disableScrollableNegotiation","scrimColor","height","windowHeight","insets","maxHeight","top","nativeDetents","map","detent","isDetentProgrammatic","value","resolveDetentValue","kind","Math","max","min","clampedIndex","length","selectedDetentValue","isCollapsed","handleIndexChange","event","nativeEvent","handleSettle","handlePositionChange","position","sheet","absoluteFill","pointerEvents","left","right","bottom","maxDetentHeight","collapsable","flex"],"sourceRoot":"../../src","sources":["BottomSheet.tsx"],"mappings":";;AAEA,SAASA,UAAU,EAAEC,IAAI,EAAEC,mBAAmB,QAAQ,cAAc;AACpE,SAASC,iBAAiB,QAAQ,gCAAgC;AAElE,OAAOC,0BAA0B,MAAM,8BAA8B;AACrE,OAAOC,iCAAiC,MAAM,qCAAqC;AACnF,SAASC,MAAM,QAAQ,0BAAuB;AAAC,SAAAC,GAAA,IAAAC,IAAA,EAAAC,IAAA,IAAAC,KAAA;AAG/C,SAASC,YAAY,QAAQ,uBAAoB;;AAEjD;AACA;AACA;;AA0CA;AACA,OAAO,MAAMC,WAAW,GAAGA,CAAC;EAC1BC,QAAQ;EACRC,OAAO;EACPC,KAAK;EACLC,OAAO,GAAG,CAAC,CAAC,EAAE,SAAS,CAAC;EACxBC,KAAK;EACLC,SAAS,GAAG,IAAI;EAChBC,aAAa;EACbC,QAAQ;EACRC,gBAAgB;EAChBC,KAAK,GAAG,KAAK;EACbC,4BAA4B,GAAG,KAAK;EACpCC;AACgB,CAAC,KAAK;EACtB,MAAM;IAAEC,MAAM,EAAEC;EAAa,CAAC,GAAGxB,mBAAmB,CAAC,CAAC;EACtD,MAAMyB,MAAM,GAAGxB,iBAAiB,CAAC,CAAC;EAClC,MAAMyB,SAAS,GAAGF,YAAY,GAAGC,MAAM,CAACE,GAAG;EAC3C,MAAMC,aAAa,GAAGd,OAAO,CAACe,GAAG,CAAEC,MAAM,IAAK;IAC5C,MAAMrB,YAAY,GAAGsB,oBAAoB,CAACD,MAAM,CAAC;IACjD,MAAME,KAAK,GAAGC,kBAAkB,CAACH,MAAM,CAAC;IAExC,IAAIE,KAAK,KAAK,SAAS,EAAE;MACvB,OAAO;QACLA,KAAK,EAAE,CAAC;QACRE,IAAI,EAAE,SAAS;QACfzB;MACF,CAAC;IACH;IAEA,OAAO;MACLuB,KAAK,EAAEG,IAAI,CAACC,GAAG,CAAC,CAAC,EAAED,IAAI,CAACE,GAAG,CAACL,KAAK,EAAEN,SAAS,CAAC,CAAC;MAC9CQ,IAAI,EAAE,QAAQ;MACdzB;IACF,CAAC;EACH,CAAC,CAAC;EAEF,MAAM6B,YAAY,GAAGH,IAAI,CAACC,GAAG,CAAC,CAAC,EAAED,IAAI,CAACE,GAAG,CAACtB,KAAK,EAAEa,aAAa,CAACW,MAAM,GAAG,CAAC,CAAC,CAAC;EAC3E,MAAMC,mBAAmB,GAAG1B,OAAO,CAACwB,YAAY,CAAC,GAC7CL,kBAAkB,CAACnB,OAAO,CAACwB,YAAY,CAAC,CAAC,GACzC,CAAC;EACL,MAAMG,WAAW,GAAGD,mBAAmB,KAAK,CAAC;EAC7C,MAAME,iBAAiB,GAAIC,KAAyC,IAAK;IACvE1B,aAAa,GAAG0B,KAAK,CAACC,WAAW,CAAC7B,KAAK,CAAC;EAC1C,CAAC;EACD,MAAM8B,YAAY,GAAIF,KAAyC,IAAK;IAClEzB,QAAQ,GAAGyB,KAAK,CAACC,WAAW,CAAC7B,KAAK,CAAC;EACrC,CAAC;EAED,MAAM+B,oBAAoB,GAAIH,KAE7B,IAAK;IACJ,MAAMpB,MAAM,GAAGoB,KAAK,CAACC,WAAW,CAACG,QAAQ;IACzC5B,gBAAgB,GAAGI,MAAM,CAAC;EAC5B,CAAC;EAED,MAAMyB,KAAK,gBACT1C,IAAA,CAACP,IAAI;IACHc,KAAK,EAAEf,UAAU,CAACmD,YAAa;IAC/BC,aAAa,EAAE9B,KAAK,GAAIqB,WAAW,GAAG,MAAM,GAAG,MAAM,GAAI,UAAW;IAAA9B,QAAA,eAEpEL,IAAA,CAACP,IAAI;MAACmD,aAAa,EAAC,UAAU;MAACrC,KAAK,EAAEf,UAAU,CAACmD,YAAa;MAAAtC,QAAA,eAC5DH,KAAA,CAACN,0BAA0B;QACzBgD,aAAa,EAAC,UAAU;QACxBrC,KAAK,EAAE,CACL;UACEkC,QAAQ,EAAE,UAAU;UACpBI,IAAI,EAAE,CAAC;UACPC,KAAK,EAAE,CAAC;UACRC,MAAM,EAAE,CAAC;UACT;UACA;UACA;UACA9B,MAAM,EAAEC;QACV,CAAC,EACDX,KAAK,CACL;QACFC,OAAO,EAAEc,aAAc;QACvB0B,eAAe,EAAE5B,SAAU;QAC3BX,KAAK,EAAEA,KAAM;QACbC,SAAS,EAAEA,SAAU;QACrBI,KAAK,EAAEA,KAAM;QACbC,4BAA4B,EAAEA,4BAA6B;QAC3DC,UAAU,EAAEA,UAAW;QACvBL,aAAa,EAAEyB,iBAAkB;QACjCxB,QAAQ,EAAE2B,YAAa;QACvB1B,gBAAgB,EAAE2B,oBAAqB;QAAAnC,QAAA,GAEtCC,OAAO,IAAI,IAAI,iBACdN,IAAA,CAACH,iCAAiC;UAChCoD,WAAW,EAAE,KAAM;UACnBL,aAAa,EAAC,UAAU;UACxBrC,KAAK,EAAEf,UAAU,CAACmD,YAAa;UAAAtC,QAAA,EAE9BC;QAAO,CACyB,CACpC,eACDJ,KAAA,CAACT,IAAI;UAACwD,WAAW,EAAE,KAAM;UAAC1C,KAAK,EAAE;YAAE2C,IAAI,EAAE,CAAC;YAAE9B;UAAU,CAAE;UAAAf,QAAA,GACrDA,QAAQ,eACTL,IAAA,CAACP,IAAI;YAACwD,WAAW,EAAE,KAAM;YAACL,aAAa,EAAC;UAAM,CAAE,CAAC;QAAA,CAC7C,CAAC;MAAA,CACmB;IAAC,CACzB;EAAC,CACH,CACP;EAED,IAAI9B,KAAK,EAAE;IACT,oBAAOd,IAAA,CAACF,MAAM;MAAAO,QAAA,EAAEqC;IAAK,CAAS,CAAC;EACjC;EAEA,OAAOA,KAAK;AACd,CAAC;AAED,SAASjB,oBAAoBA,CAACD,MAAc,EAAW;EACrD,IAAI,OAAOA,MAAM,KAAK,QAAQ,IAAIA,MAAM,KAAK,IAAI,EAAE;IACjD,OAAOA,MAAM,CAACrB,YAAY,KAAK,IAAI;EACrC;EACA,OAAO,KAAK;AACd;AAEA,SAASwB,kBAAkBA,CAACH,MAAc,EAAE;EAC1C,IAAI,OAAOA,MAAM,KAAK,QAAQ,IAAIA,MAAM,KAAK,IAAI,EAAE;IACjD,OAAOA,MAAM,CAACE,KAAK;EACrB;EACA,OAAOF,MAAM;AACf","ignoreList":[]}
@@ -0,0 +1,9 @@
1
+ import { codegenNativeComponent, type ViewProps } from 'react-native';
2
+
3
+ // The visual surface that sits behind the sheet content. It carries no props of
4
+ // its own: the library identifies it by component type and owns its geometry,
5
+ // sizing it to cover the full sheet so a content shrink never exposes blank
6
+ // space. Its React children provide the visual appearance only.
7
+ export interface NativeProps extends ViewProps {}
8
+
9
+ export default codegenNativeComponent<NativeProps>('BottomSheetSurfaceView');
@@ -1 +1 @@
1
- {"version":3,"names":["BottomSheet","jsx","_jsx","ModalBottomSheet","props","modal"],"sourceRoot":"../../src","sources":["ModalBottomSheet.tsx"],"mappings":";;AAAA,SAASA,WAAW,QAA+B,kBAAe;;AAElE;AAAA,SAAAC,GAAA,IAAAC,IAAA;AAIA;AACA,OAAO,MAAMC,gBAAgB,GAAIC,KAA4B,iBAC3DF,IAAA,CAACF,WAAW;EAAA,GAAKI,KAAK;EAAEC,KAAK;AAAA,CAAE,CAChC","ignoreList":[]}
1
+ {"version":3,"names":["BottomSheet","jsx","_jsx","ModalBottomSheet","props","modal"],"sourceRoot":"../../src","sources":["ModalBottomSheet.tsx"],"mappings":";;AAAA,SAASA,WAAW,QAA+B,kBAAe;;AAElE;AAAA,SAAAC,GAAA,IAAAC,IAAA;AAMA;AACA,OAAO,MAAMC,gBAAgB,GAAIC,KAA4B,iBAC3DF,IAAA,CAACF,WAAW;EAAA,GAAKI,KAAK;EAAEC,KAAK;AAAA,CAAE,CAChC","ignoreList":[]}
@@ -7,8 +7,20 @@ export { programmatic } from './bottomSheetUtils';
7
7
  * Props for the inline bottom-sheet component.
8
8
  */
9
9
  export interface BottomSheetProps {
10
- /** Sheet contents, including any background or scrollable content. */
10
+ /** Sheet contents, including any scrollable content. */
11
11
  children: ReactNode;
12
+ /**
13
+ * Optional visual surface (background) rendered behind the content. The
14
+ * library sizes and positions the surface natively to cover the full sheet,
15
+ * independently of the content, so a shrinking content height never exposes
16
+ * blank space. Put a background View here instead of inside `children` when
17
+ * you want that shrink-safe behavior. When omitted, behavior is unchanged.
18
+ *
19
+ * Give the surface element a filling style such as `StyleSheet.absoluteFill`:
20
+ * it is mounted in a full-size host, so a surface sized only by its own
21
+ * content would collapse and not show.
22
+ */
23
+ surface?: ReactNode;
12
24
  /** Additional style applied to the native sheet host view. */
13
25
  style?: StyleProp<ViewStyle>;
14
26
  /** Snap points for the sheet. Defaults to `[0, 'content']`. */
@@ -35,5 +47,5 @@ export interface BottomSheetProps {
35
47
  scrimColor?: string;
36
48
  }
37
49
  /** Native bottom sheet that renders inline within the current screen layout. */
38
- export declare const BottomSheet: ({ children, style, detents, index, animateIn, onIndexChange, onSettle, onPositionChange, modal, disableScrollableNegotiation, scrimColor, }: BottomSheetProps) => import("react/jsx-runtime").JSX.Element;
50
+ export declare const BottomSheet: ({ children, surface, style, detents, index, animateIn, onIndexChange, onSettle, onPositionChange, modal, disableScrollableNegotiation, scrimColor, }: BottomSheetProps) => import("react/jsx-runtime").JSX.Element;
39
51
  //# sourceMappingURL=BottomSheet.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"BottomSheet.d.ts","sourceRoot":"","sources":["../../../src/BottomSheet.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAMzD,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,oBAAoB,CAAC;AACjD,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,sEAAsE;IACtE,QAAQ,EAAE,SAAS,CAAC;IACpB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,+DAA+D;IAC/D,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,gEAAgE;IAChE,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,uEAAuE;IACvE,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,gDAAgD;IAChD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;OAIG;IACH,4BAA4B,CAAC,EAAE,OAAO,CAAC;IACvC,8CAA8C;IAC9C,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,gFAAgF;AAChF,eAAO,MAAM,WAAW,GAAI,6IAYzB,gBAAgB,4CAwFlB,CAAC"}
1
+ {"version":3,"file":"BottomSheet.d.ts","sourceRoot":"","sources":["../../../src/BottomSheet.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAOzD,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,oBAAoB,CAAC;AACjD,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,wDAAwD;IACxD,QAAQ,EAAE,SAAS,CAAC;IACpB;;;;;;;;;;OAUG;IACH,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,+DAA+D;IAC/D,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,gEAAgE;IAChE,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,uEAAuE;IACvE,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,gDAAgD;IAChD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;OAIG;IACH,4BAA4B,CAAC,EAAE,OAAO,CAAC;IACvC,8CAA8C;IAC9C,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,gFAAgF;AAChF,eAAO,MAAM,WAAW,GAAI,sJAazB,gBAAgB,4CAiGlB,CAAC"}
@@ -0,0 +1,6 @@
1
+ import { type ViewProps } from 'react-native';
2
+ export interface NativeProps extends ViewProps {
3
+ }
4
+ declare const _default: import("react-native/types_generated/Libraries/Utilities/codegenNativeComponent").NativeComponentType<NativeProps>;
5
+ export default _default;
6
+ //# sourceMappingURL=BottomSheetSurfaceNativeComponent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BottomSheetSurfaceNativeComponent.d.ts","sourceRoot":"","sources":["../../../src/BottomSheetSurfaceNativeComponent.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAMtE,MAAM,WAAW,WAAY,SAAQ,SAAS;CAAG;;AAEjD,wBAA6E"}
@@ -1 +1 @@
1
- {"version":3,"file":"ModalBottomSheet.d.ts","sourceRoot":"","sources":["../../../src/ModalBottomSheet.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEnE,qFAAqF;AACrF,MAAM,WAAW,qBACf,SAAQ,IAAI,CAAC,gBAAgB,EAAE,OAAO,CAAC;CAAG;AAE5C,gEAAgE;AAChE,eAAO,MAAM,gBAAgB,GAAI,OAAO,qBAAqB,4CAE5D,CAAC"}
1
+ {"version":3,"file":"ModalBottomSheet.d.ts","sourceRoot":"","sources":["../../../src/ModalBottomSheet.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEnE,qFAAqF;AACrF,MAAM,WAAW,qBAAsB,SAAQ,IAAI,CACjD,gBAAgB,EAChB,OAAO,CACR;CAAG;AAEJ,gEAAgE;AAChE,eAAO,MAAM,gBAAgB,GAAI,OAAO,qBAAqB,4CAE5D,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swmansion/react-native-bottom-sheet",
3
- "version": "0.10.2",
3
+ "version": "0.11.0",
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",
@@ -33,7 +33,7 @@
33
33
  "!**/.*"
34
34
  ],
35
35
  "scripts": {
36
- "example": "yarn workspace @swmansion/react-native-bottom-sheet-example",
36
+ "example": "bun run --filter @swmansion/react-native-bottom-sheet-example",
37
37
  "clean": "del-cli lib",
38
38
  "prepare": "lefthook install && bob build",
39
39
  "typecheck": "tsc",
@@ -58,23 +58,23 @@
58
58
  "registry": "https://registry.npmjs.org"
59
59
  },
60
60
  "devDependencies": {
61
- "@eslint/compat": "1.4.1",
62
- "@eslint/eslintrc": "3.3.3",
63
- "@eslint/js": "9.39.2",
64
- "@react-native/babel-preset": "0.83.0",
65
- "@react-native/eslint-config": "0.83.0",
66
- "@types/react": "19.2.14",
61
+ "@eslint/compat": "2.1.0",
62
+ "@eslint/eslintrc": "3.3.5",
63
+ "@eslint/js": "9.39.4",
64
+ "@react-native/babel-preset": "0.85.3",
65
+ "@react-native/eslint-config": "0.85.3",
66
+ "@types/react": "19.2.15",
67
67
  "babel-plugin-react-compiler": "1.0.0",
68
68
  "del-cli": "6.0.0",
69
- "eslint": "9.39.2",
69
+ "eslint": "9.39.4",
70
70
  "eslint-config-prettier": "10.1.8",
71
- "eslint-plugin-prettier": "5.5.5",
72
- "lefthook": "2.1.6",
73
- "prettier": "2.8.8",
74
- "react": "19.1.0",
75
- "react-native": "0.81.5",
76
- "react-native-builder-bob": "0.40.18",
77
- "react-native-safe-area-context": "5.6.2",
71
+ "eslint-plugin-prettier": "5.5.6",
72
+ "lefthook": "2.1.9",
73
+ "prettier": "3.8.3",
74
+ "react": "19.2.3",
75
+ "react-native": "0.85.3",
76
+ "react-native-builder-bob": "0.41.0",
77
+ "react-native-safe-area-context": "5.7.0",
78
78
  "typescript": "5.9.3"
79
79
  },
80
80
  "peerDependencies": {
@@ -85,7 +85,7 @@
85
85
  "workspaces": [
86
86
  "example"
87
87
  ],
88
- "packageManager": "yarn@4.11.0",
88
+ "packageManager": "bun@1.3.14",
89
89
  "react-native-builder-bob": {
90
90
  "source": "src",
91
91
  "output": "lib",
@@ -110,7 +110,8 @@
110
110
  "jsSrcsDir": "src",
111
111
  "ios": {
112
112
  "componentProvider": {
113
- "BottomSheetView": "BottomSheetComponentView"
113
+ "BottomSheetView": "BottomSheetComponentView",
114
+ "BottomSheetSurfaceView": "BottomSheetSurfaceComponentView"
114
115
  }
115
116
  },
116
117
  "android": {
@@ -118,6 +119,7 @@
118
119
  }
119
120
  },
120
121
  "prettier": {
122
+ "proseWrap": "always",
121
123
  "quoteProps": "consistent",
122
124
  "singleQuote": true,
123
125
  "tabWidth": 2,
@@ -3,7 +3,10 @@ module.exports = {
3
3
  platforms: {
4
4
  android: {
5
5
  libraryName: 'ReactNativeBottomSheetSpec',
6
- componentDescriptors: ['BottomSheetViewComponentDescriptor'],
6
+ componentDescriptors: [
7
+ 'BottomSheetViewComponentDescriptor',
8
+ 'BottomSheetSurfaceViewComponentDescriptor',
9
+ ],
7
10
  cmakeListsPath: 'src/main/jni/CMakeLists.txt',
8
11
  },
9
12
  },
@@ -4,6 +4,7 @@ import { StyleSheet, View, useWindowDimensions } from 'react-native';
4
4
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
5
5
 
6
6
  import BottomSheetNativeComponent from './BottomSheetNativeComponent';
7
+ import BottomSheetSurfaceNativeComponent from './BottomSheetSurfaceNativeComponent';
7
8
  import { Portal } from './BottomSheetProvider';
8
9
  import { type Detent } from './bottomSheetUtils';
9
10
  export type { Detent, DetentValue } from './bottomSheetUtils';
@@ -13,8 +14,20 @@ export { programmatic } from './bottomSheetUtils';
13
14
  * Props for the inline bottom-sheet component.
14
15
  */
15
16
  export interface BottomSheetProps {
16
- /** Sheet contents, including any background or scrollable content. */
17
+ /** Sheet contents, including any scrollable content. */
17
18
  children: ReactNode;
19
+ /**
20
+ * Optional visual surface (background) rendered behind the content. The
21
+ * library sizes and positions the surface natively to cover the full sheet,
22
+ * independently of the content, so a shrinking content height never exposes
23
+ * blank space. Put a background View here instead of inside `children` when
24
+ * you want that shrink-safe behavior. When omitted, behavior is unchanged.
25
+ *
26
+ * Give the surface element a filling style such as `StyleSheet.absoluteFill`:
27
+ * it is mounted in a full-size host, so a surface sized only by its own
28
+ * content would collapse and not show.
29
+ */
30
+ surface?: ReactNode;
18
31
  /** Additional style applied to the native sheet host view. */
19
32
  style?: StyleProp<ViewStyle>;
20
33
  /** Snap points for the sheet. Defaults to `[0, 'content']`. */
@@ -44,6 +57,7 @@ export interface BottomSheetProps {
44
57
  /** Native bottom sheet that renders inline within the current screen layout. */
45
58
  export const BottomSheet = ({
46
59
  children,
60
+ surface,
47
61
  style,
48
62
  detents = [0, 'content'],
49
63
  index,
@@ -128,6 +142,15 @@ export const BottomSheet = ({
128
142
  onSettle={handleSettle}
129
143
  onPositionChange={handlePositionChange}
130
144
  >
145
+ {surface != null && (
146
+ <BottomSheetSurfaceNativeComponent
147
+ collapsable={false}
148
+ pointerEvents="box-none"
149
+ style={StyleSheet.absoluteFill}
150
+ >
151
+ {surface}
152
+ </BottomSheetSurfaceNativeComponent>
153
+ )}
131
154
  <View collapsable={false} style={{ flex: 1, maxHeight }}>
132
155
  {children}
133
156
  <View collapsable={false} pointerEvents="none" />
@@ -0,0 +1,9 @@
1
+ import { codegenNativeComponent, type ViewProps } from 'react-native';
2
+
3
+ // The visual surface that sits behind the sheet content. It carries no props of
4
+ // its own: the library identifies it by component type and owns its geometry,
5
+ // sizing it to cover the full sheet so a content shrink never exposes blank
6
+ // space. Its React children provide the visual appearance only.
7
+ export interface NativeProps extends ViewProps {}
8
+
9
+ export default codegenNativeComponent<NativeProps>('BottomSheetSurfaceView');
@@ -1,8 +1,10 @@
1
1
  import { BottomSheet, type BottomSheetProps } from './BottomSheet';
2
2
 
3
3
  /** Props for the modal bottom-sheet variant rendered through the provider portal. */
4
- export interface ModalBottomSheetProps
5
- extends Omit<BottomSheetProps, 'modal'> {}
4
+ export interface ModalBottomSheetProps extends Omit<
5
+ BottomSheetProps,
6
+ 'modal'
7
+ > {}
6
8
 
7
9
  /** Bottom sheet presented above the current UI with a scrim. */
8
10
  export const ModalBottomSheet = (props: ModalBottomSheetProps) => (