@swmansion/react-native-bottom-sheet 0.9.5 → 0.10.0-next.2

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.
@@ -260,12 +260,20 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
260
260
 
261
261
  private fun resolveDetentSpecs(): List<DetentSpec> {
262
262
  val maxHeight = resolvedMaxDetentHeight()
263
- val contentHeight =
264
- validContentHeight().takeIf { it.isFinite() }?.coerceAtMost(maxHeight) ?: maxHeight
265
- return rawDetentSpecs.map { spec ->
263
+ val measuredContentHeight =
264
+ validContentHeight().takeIf { maxHeight > 0f && it.isFinite() }?.coerceAtMost(maxHeight)
265
+ val contentHeight = measuredContentHeight ?: maxHeight
266
+ return rawDetentSpecs.mapIndexed { index, spec ->
266
267
  val height =
267
268
  when (spec.kind) {
268
- DetentKind.POINTS -> spec.value.coerceAtMost(contentHeight)
269
+ DetentKind.POINTS -> {
270
+ if (measuredContentHeight != null && spec.value > contentHeight) {
271
+ throw IllegalArgumentException(
272
+ "Invalid bottom sheet detent at index $index: fixed detent ${spec.value / density} exceeds measured content height ${contentHeight / density}."
273
+ )
274
+ }
275
+ spec.value
276
+ }
269
277
  DetentKind.CONTENT -> contentHeight
270
278
  }.coerceIn(0f, maxHeight)
271
279
  DetentSpec(height = height, programmatic = spec.programmatic)
@@ -284,12 +292,15 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
284
292
  return
285
293
  }
286
294
 
295
+ val previousMaxHeight = detentSpecs.maxOfOrNull { it.height } ?: resolvedMaxDetentHeight()
287
296
  detentSpecs = resolvedDetents
288
297
  if (width > 0 && height > 0 && detentSpecs.isNotEmpty()) {
289
298
  layoutSheetContainer(width, height)
290
299
 
291
300
  if (hasLaidOut && !isPanning) {
292
301
  targetIndex = targetIndex.coerceIn(0, detentSpecs.size - 1)
302
+ val newMaxHeight = detentSpecs.maxOfOrNull { it.height } ?: resolvedMaxDetentHeight()
303
+ val targetTy = translationY(targetIndex)
293
304
  if (activeAnimation != null && isTargetingClosedDetent) {
294
305
  suppressScrimForClosingTarget = true
295
306
  hideScrim()
@@ -301,17 +312,26 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
301
312
  activeAnimation = null
302
313
  activeAnimationEmitsSettle = false
303
314
  stopChoreographer()
304
- sheetContainer.translationY =
305
- currentTy.coerceIn(0f, detentSpecs.maxOfOrNull { it.height } ?: currentTy)
315
+ // Re-anchor the in-flight position to the new container height so the
316
+ // sheet surface keeps the same on-screen height across the resize.
317
+ val visibleHeight = previousMaxHeight - currentTy
318
+ sheetContainer.translationY = (newMaxHeight - visibleHeight).coerceIn(0f, newMaxHeight)
306
319
  emitPosition()
307
320
  snapToIndex(targetIndex, 0f, emitIndexChange = false, emitSettle = shouldEmitSettle)
308
321
  } else {
309
- val targetTy = translationY(targetIndex)
310
- val currentTy = sheetContainer.translationY
311
- if (abs(targetTy - currentTy) <= 0.5f) {
322
+ val currentVisibleHeight = previousMaxHeight - sheetContainer.translationY
323
+ val targetHeight = detentSpecs.getOrNull(targetIndex)?.height ?: 0f
324
+ if (targetHeight <= currentVisibleHeight + 0.5f) {
325
+ // Content shrank (or is unchanged): snap immediately. Animating here
326
+ // would expose blank space below the shrunken content.
312
327
  sheetContainer.translationY = targetTy
313
328
  emitPosition()
314
329
  } else {
330
+ // Content grew: re-anchor at the current visible height, then animate
331
+ // up to the taller detent.
332
+ sheetContainer.translationY =
333
+ (newMaxHeight - currentVisibleHeight).coerceIn(0f, newMaxHeight)
334
+ emitPosition()
315
335
  snapToIndex(targetIndex, 0f, emitIndexChange = false, emitSettle = false)
316
336
  }
317
337
  }
@@ -60,7 +60,8 @@ public final class RNSBottomSheetHostingView: UIView {
60
60
  private var hasLaidOut = false
61
61
  private var isPanning = false
62
62
  private var isContentInteractionDisabled = false
63
- private weak var contentHeightMarker: UIView?
63
+ private var contentHeightMarker: UIView?
64
+ private static var markerObservationContext = 0
64
65
 
65
66
  override public init(frame: CGRect) {
66
67
  super.init(frame: frame)
@@ -237,7 +238,7 @@ public final class RNSBottomSheetHostingView: UIView {
237
238
  hasLaidOut = false
238
239
  isPanning = false
239
240
  setContentInteractionEnabled(true)
240
- contentHeightMarker = nil
241
+ stopObservingContentHeightMarker()
241
242
  sheetContainer.transform = .identity
242
243
  scrimView.alpha = 0
243
244
  scrimView.isHidden = true
@@ -541,12 +542,21 @@ public final class RNSBottomSheetHostingView: UIView {
541
542
 
542
543
  private func resolveDetentSpecs() -> [DetentSpec] {
543
544
  let maxHeight = resolvedMaxDetentHeight
544
- let contentHeight = validContentHeight.map { min($0, maxHeight) } ?? maxHeight
545
- return rawDetentSpecs.map { spec in
545
+ let measuredContentHeight = maxHeight > 0 ? validContentHeight.map { min($0, maxHeight) } : nil
546
+ let contentHeight = measuredContentHeight ?? maxHeight
547
+ return rawDetentSpecs.enumerated().map { index, spec in
546
548
  let height: CGFloat
547
549
  switch spec.kind {
548
550
  case .points:
549
- height = min(spec.value, contentHeight)
551
+ if measuredContentHeight != nil, spec.value > contentHeight {
552
+ NSException(
553
+ name: NSExceptionName.invalidArgumentException,
554
+ reason:
555
+ "Invalid bottom sheet detent at index \(index): fixed detent \(spec.value) exceeds measured content height \(contentHeight).",
556
+ userInfo: nil
557
+ ).raise()
558
+ }
559
+ height = spec.value
550
560
  case .content:
551
561
  height = contentHeight
552
562
  }
@@ -567,6 +577,7 @@ public final class RNSBottomSheetHostingView: UIView {
567
577
  return
568
578
  }
569
579
 
580
+ let previousMaxHeight = maximumResolvedDetentHeight ?? resolvedMaxDetentHeight
570
581
  detentSpecs = resolvedDetents
571
582
 
572
583
  guard bounds.width > 0, bounds.height > 0, !detentSpecs.isEmpty else {
@@ -575,6 +586,8 @@ public final class RNSBottomSheetHostingView: UIView {
575
586
 
576
587
  if hasLaidOut, !isPanning {
577
588
  targetIndex = max(0, min(detentSpecs.count - 1, targetIndex))
589
+ let newMaxHeight = maximumResolvedDetentHeight ?? resolvedMaxDetentHeight
590
+ let targetTy = translationY(for: targetIndex)
578
591
 
579
592
  if let animator = activeAnimator {
580
593
  stopDisplayLink()
@@ -583,19 +596,27 @@ public final class RNSBottomSheetHostingView: UIView {
583
596
  animator.stopAnimation(true)
584
597
  activeAnimator = nil
585
598
  activeAnimatorEmitsSettle = false
586
- sheetContainer.transform = CGAffineTransform(
587
- translationX: 0,
588
- y: min(max(visualTy, 0), maximumResolvedDetentHeight ?? visualTy)
589
- )
599
+ // Re-anchor the in-flight position to the new container height so the
600
+ // sheet surface keeps the same on-screen height across the resize.
601
+ let visibleHeight = previousMaxHeight - visualTy
602
+ let reanchoredTy = min(max(newMaxHeight - visibleHeight, 0), newMaxHeight)
603
+ sheetContainer.transform = CGAffineTransform(translationX: 0, y: reanchoredTy)
590
604
  emitPosition()
591
605
  snapToIndex(targetIndex, velocity: 0, emitIndexChange: false, emitSettle: shouldEmitSettle)
592
606
  } else {
593
- let targetTy = translationY(for: targetIndex)
594
- let currentTy = currentTranslationY
595
- if abs(targetTy - currentTy) <= 0.5 {
607
+ let currentVisibleHeight = previousMaxHeight - currentTranslationY
608
+ let targetHeight = detent(at: targetIndex).height
609
+ if targetHeight <= currentVisibleHeight + 0.5 {
610
+ // Content shrank (or is unchanged): snap immediately. Animating here
611
+ // would expose blank space below the shrunken content.
596
612
  sheetContainer.transform = CGAffineTransform(translationX: 0, y: targetTy)
597
613
  emitPosition()
598
614
  } else {
615
+ // Content grew: re-anchor at the current visible height, then animate
616
+ // up to the taller detent.
617
+ let startTy = min(max(newMaxHeight - currentVisibleHeight, 0), newMaxHeight)
618
+ sheetContainer.transform = CGAffineTransform(translationX: 0, y: startTy)
619
+ emitPosition()
599
620
  snapToIndex(targetIndex, velocity: 0, emitIndexChange: false, emitSettle: false)
600
621
  }
601
622
  }
@@ -603,7 +624,50 @@ public final class RNSBottomSheetHostingView: UIView {
603
624
  }
604
625
 
605
626
  private func refreshContentHeightMarker() {
606
- contentHeightMarker = findContentHeightMarker()
627
+ let marker = findContentHeightMarker()
628
+ guard marker !== contentHeightMarker else { return }
629
+ stopObservingContentHeightMarker()
630
+ contentHeightMarker = marker
631
+ if let marker {
632
+ // The marker's frame is updated by React Native when content above it
633
+ // resizes; observe its layer so we can re-resolve detents immediately
634
+ // instead of waiting for an unrelated layout pass. This is the iOS
635
+ // counterpart to Android's OnLayoutChangeListener on the marker.
636
+ marker.layer.addObserver(
637
+ self, forKeyPath: "position", options: [], context: &Self.markerObservationContext
638
+ )
639
+ marker.layer.addObserver(
640
+ self, forKeyPath: "bounds", options: [], context: &Self.markerObservationContext
641
+ )
642
+ }
643
+ }
644
+
645
+ private func stopObservingContentHeightMarker() {
646
+ guard let marker = contentHeightMarker else { return }
647
+ marker.layer.removeObserver(
648
+ self, forKeyPath: "position", context: &Self.markerObservationContext
649
+ )
650
+ marker.layer.removeObserver(
651
+ self, forKeyPath: "bounds", context: &Self.markerObservationContext
652
+ )
653
+ contentHeightMarker = nil
654
+ }
655
+
656
+ override public func observeValue(
657
+ forKeyPath keyPath: String?,
658
+ of object: Any?,
659
+ change: [NSKeyValueChangeKey: Any]?,
660
+ context: UnsafeMutableRawPointer?
661
+ ) {
662
+ if context == &Self.markerObservationContext {
663
+ refreshDetentsFromLayout()
664
+ } else {
665
+ super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
666
+ }
667
+ }
668
+
669
+ deinit {
670
+ stopObservingContentHeightMarker()
607
671
  }
608
672
 
609
673
  private func findContentHeightMarker() -> UIView? {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swmansion/react-native-bottom-sheet",
3
- "version": "0.9.5",
3
+ "version": "0.10.0-next.2",
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",