@lodev09/react-native-true-sheet 4.0.0-beta.4 → 4.0.0-beta.6

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 (63) hide show
  1. package/README.md +21 -2
  2. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +0 -7
  3. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +0 -36
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +0 -1
  5. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +0 -1
  6. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h +0 -8
  7. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +17 -38
  8. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h +0 -2
  9. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.cpp +1 -2
  10. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h +1 -7
  11. package/ios/TrueSheetContainerView.h +0 -6
  12. package/ios/TrueSheetContainerView.mm +0 -1
  13. package/ios/TrueSheetContentView.h +0 -7
  14. package/ios/TrueSheetContentView.mm +0 -32
  15. package/ios/TrueSheetView.mm +0 -6
  16. package/ios/TrueSheetViewController.mm +35 -27
  17. package/lib/module/TrueSheet.js +10 -5
  18. package/lib/module/TrueSheet.js.map +1 -1
  19. package/lib/module/TrueSheet.web.js.map +1 -1
  20. package/lib/module/mocks/navigationExpoRouter.js.map +1 -1
  21. package/lib/module/navigation/base-types.js +2 -0
  22. package/lib/module/navigation/base-types.js.map +1 -0
  23. package/lib/module/navigation/createTrueSheetRouter.js +0 -9
  24. package/lib/module/navigation/createTrueSheetRouter.js.map +1 -1
  25. package/lib/module/navigation/expo-router/base-types.js +2 -0
  26. package/lib/module/navigation/expo-router/base-types.js.map +1 -0
  27. package/lib/module/navigation/expo-router/index.js +5 -3
  28. package/lib/module/navigation/expo-router/index.js.map +1 -1
  29. package/lib/module/navigation/expo-router/types.js +4 -0
  30. package/lib/module/navigation/expo-router/types.js.map +1 -0
  31. package/lib/module/navigation/index.js.map +1 -1
  32. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  33. package/lib/typescript/src/TrueSheet.types.d.ts +14 -0
  34. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  35. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
  36. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts +3 -3
  37. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts.map +1 -1
  38. package/lib/typescript/src/navigation/base-types.d.ts +9 -0
  39. package/lib/typescript/src/navigation/base-types.d.ts.map +1 -0
  40. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts +2 -13
  41. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts.map +1 -1
  42. package/lib/typescript/src/navigation/expo-router/base-types.d.ts +8 -0
  43. package/lib/typescript/src/navigation/expo-router/base-types.d.ts.map +1 -0
  44. package/lib/typescript/src/navigation/expo-router/index.d.ts +5 -5
  45. package/lib/typescript/src/navigation/expo-router/index.d.ts.map +1 -1
  46. package/lib/typescript/src/navigation/expo-router/types.d.ts +44 -0
  47. package/lib/typescript/src/navigation/expo-router/types.d.ts.map +1 -0
  48. package/lib/typescript/src/navigation/index.d.ts +3 -4
  49. package/lib/typescript/src/navigation/index.d.ts.map +1 -1
  50. package/lib/typescript/src/navigation/types.d.ts +34 -4
  51. package/lib/typescript/src/navigation/types.d.ts.map +1 -1
  52. package/package.json +1 -1
  53. package/src/TrueSheet.tsx +14 -5
  54. package/src/TrueSheet.types.ts +15 -0
  55. package/src/TrueSheet.web.tsx +2 -1
  56. package/src/mocks/navigationExpoRouter.ts +6 -5
  57. package/src/navigation/base-types.ts +20 -0
  58. package/src/navigation/createTrueSheetRouter.ts +5 -33
  59. package/src/navigation/expo-router/base-types.ts +18 -0
  60. package/src/navigation/expo-router/index.ts +20 -19
  61. package/src/navigation/expo-router/types.ts +116 -0
  62. package/src/navigation/index.ts +6 -7
  63. package/src/navigation/types.ts +65 -19
package/README.md CHANGED
@@ -22,6 +22,23 @@ The true native bottom sheet experience for your React Native Apps. 💩
22
22
  * 🧭 **React Navigation** - Built-in [sheet navigator](https://sheet.lodev09.com/guides/navigation) for seamless navigation integration
23
23
  * 🌐 **Web Support** - Full [web support](https://sheet.lodev09.com/guides/web) out of the box
24
24
 
25
+ ## 🎉 What's New in v4
26
+
27
+ > [!TIP]
28
+ > ✨ **v4 is in beta!** It rewrites the layout engine — the sheet now lays out synchronously per detent, so the container is sized to the sheet's visible height and tracks it in realtime while dragging.
29
+
30
+ * 📏 **Synchronous layout** - Content resizes in the same frame as the sheet, no more lag
31
+ * 📐 **`auto` detent with scrollables** - The sheet now sizes itself to a `ScrollView`/`FlatList`'s content height
32
+ * 📜 **`scrollableRef`** - Pass a `ScrollView`/`FlatList` ref to wire nested scrolling, keyboard insets, and `auto` detent sizing
33
+ * 🦶 **Relative footers** - Footers take up space below the content by default
34
+ * 🧭 **Expo Router** - First-class support via the `Sheet` layout from `/navigation/expo-router`
35
+
36
+ ```sh
37
+ npx expo install @lodev09/react-native-true-sheet@beta
38
+ ```
39
+
40
+ See the [Migration Guide](docs/docs/migration.mdx) to upgrade. **v3 is still supported** for React Native 0.81+ and remains the `latest` release on npm.
41
+
25
42
  ## Installation
26
43
 
27
44
  > [!IMPORTANT]
@@ -30,7 +47,7 @@ The true native bottom sheet experience for your React Native Apps. 💩
30
47
 
31
48
  ### Prerequisites
32
49
 
33
- - React Native 0.82+
50
+ - React Native 0.82+ (0.81+ on v3)
34
51
  - New Architecture enabled
35
52
  - Xcode 26.1+
36
53
 
@@ -55,6 +72,8 @@ yarn add @lodev09/react-native-true-sheet
55
72
  cd ios && pod install
56
73
  ```
57
74
 
75
+ These install v3. For v4, append `@beta` — see [🎉 What's New in v4](#-whats-new-in-v4).
76
+
58
77
  ## Documentation
59
78
 
60
79
  - [Example](example)
@@ -63,7 +82,7 @@ cd ios && pod install
63
82
  - [React Navigation](https://sheet.lodev09.com/guides/navigation)
64
83
  - [Troubleshooting](https://sheet.lodev09.com/troubleshooting)
65
84
  - [Testing with Jest](https://sheet.lodev09.com/guides/jest)
66
- - [Migrating to v3](https://sheet.lodev09.com/migration)
85
+ - [Migrating to v3](https://sheet.lodev09.com/migration-v3)
67
86
 
68
87
  ## Usage
69
88
 
@@ -95,12 +95,6 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
95
95
  contentView?.bottomInset = value
96
96
  }
97
97
 
98
- var hasAutoDetent = false
99
- set(value) {
100
- field = value
101
- contentView?.hasAutoDetent = value
102
- }
103
-
104
98
  override val eventDispatcher: EventDispatcher?
105
99
  get() = delegate?.eventDispatcher
106
100
 
@@ -131,7 +125,6 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
131
125
  child.scrollableOptions = scrollableOptions
132
126
  child.scrollableHandle = scrollableHandle
133
127
  child.bottomInset = scrollableBottomInset
134
- child.hasAutoDetent = hasAutoDetent
135
128
  contentView = child
136
129
 
137
130
  // State lands before the view is attached, so pull the current value
@@ -8,9 +8,7 @@ import android.view.View
8
8
  import android.view.ViewGroup
9
9
  import android.widget.EditText
10
10
  import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
11
- import com.facebook.react.bridge.WritableNativeMap
12
11
  import com.facebook.react.uimanager.PixelUtil.dpToPx
13
- import com.facebook.react.uimanager.StateWrapper
14
12
  import com.facebook.react.uimanager.ThemedReactContext
15
13
  import com.facebook.react.views.view.ReactViewGroup
16
14
  import com.lodev09.truesheet.core.TrueSheetKeyboardObserver
@@ -57,11 +55,9 @@ interface TrueSheetContentViewDelegate {
57
55
  @SuppressLint("ViewConstructor")
58
56
  class TrueSheetContentView(private val reactContext: ThemedReactContext) : ReactViewGroup(reactContext) {
59
57
  var delegate: TrueSheetContentViewDelegate? = null
60
- var stateWrapper: StateWrapper? = null
61
58
 
62
59
  private var detectedScrollView: ViewGroup? = null
63
60
  private var originalScrollViewPaddingBottom: Int = 0
64
- private var scrollableBounded = false
65
61
 
66
62
  /**
67
63
  * Content height measured unconstrained by the shadow node — the height the
@@ -141,20 +137,6 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
141
137
  clearScrollable()
142
138
  }
143
139
 
144
- /**
145
- * Whether the sheet has an `auto` detent. Deriving the sheet height from the
146
- * scroll content is circular with natural layout, so the detected ScrollView's
147
- * viewport is force-bounded to the container only in this case.
148
- */
149
- var hasAutoDetent = false
150
- set(value) {
151
- if (field == value) return
152
- field = value
153
- if (detectedScrollView != null) {
154
- setScrollableBounded(value)
155
- }
156
- }
157
-
158
140
  override fun addView(child: View?, index: Int) {
159
141
  super.addView(child, index)
160
142
  checkScrollViewChanged()
@@ -171,22 +153,6 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
171
153
  }
172
154
  }
173
155
 
174
- /**
175
- * Tells the shadow node to fill the container (flexGrow/flexShrink) so the
176
- * detected ScrollView's viewport is bounded to the visible space. Only applied
177
- * for auto detents — otherwise content lays out naturally like a regular view.
178
- */
179
- private fun setScrollableBounded(bounded: Boolean) {
180
- if (scrollableBounded == bounded) return
181
- scrollableBounded = bounded
182
-
183
- stateWrapper?.let {
184
- val newState = WritableNativeMap()
185
- newState.putBoolean("scrollableBounded", bounded)
186
- it.updateState(newState)
187
- }
188
- }
189
-
190
156
  fun setupScrollable() {
191
157
  // Check if the detected scroll view is still valid (still in view hierarchy)
192
158
  if (detectedScrollView != null && detectedScrollView?.isDescendantOf(this) == false) {
@@ -216,7 +182,6 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
216
182
  scrollView.addOnLayoutChangeListener(scrollableLayoutListener)
217
183
  scrollView.getChildAt(0)?.addOnLayoutChangeListener(scrollableLayoutListener)
218
184
 
219
- setScrollableBounded(hasAutoDetent)
220
185
  updateContentInset()
221
186
 
222
187
  // If keyboard is currently showing, re-apply the keyboard inset to the new ScrollView
@@ -274,7 +239,6 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
274
239
  detectedScrollView?.isNestedScrollingEnabled = false
275
240
  (detectedScrollView?.parent as? SwipeRefreshLayout)?.isNestedScrollingEnabled = true
276
241
  setScrollViewPaddingBottom(originalScrollViewPaddingBottom)
277
- setScrollableBounded(false)
278
242
  detectedScrollView = null
279
243
  originalScrollViewPaddingBottom = 0
280
244
  baseBottomInset = 0
@@ -20,7 +20,6 @@ class TrueSheetContentViewManager : ViewGroupManager<TrueSheetContentView>() {
20
20
  override fun createViewInstance(reactContext: ThemedReactContext): TrueSheetContentView = TrueSheetContentView(reactContext)
21
21
 
22
22
  override fun updateState(view: TrueSheetContentView, props: ReactStylesDiffMap?, stateWrapper: StateWrapper?): Any? {
23
- view.stateWrapper = stateWrapper
24
23
  stateWrapper?.stateData?.let { data ->
25
24
  if (data.hasKey("naturalHeight")) {
26
25
  view.updateNaturalHeight(data.getDouble("naturalHeight"))
@@ -328,7 +328,6 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
328
328
  it.scrollableOptions = viewController.scrollableOptions
329
329
  it.scrollableHandle = viewController.scrollableHandle
330
330
  it.scrollableBottomInset = viewController.contentBottomInset
331
- it.hasAutoDetent = viewController.detents.contains(-1.0)
332
331
  it.footerBottomInset = viewController.contentBottomInset
333
332
  it.absoluteFooter = viewController.absoluteFooter
334
333
  it.setupScrollable()
@@ -11,14 +11,6 @@ namespace facebook::react {
11
11
  class TrueSheetContentViewComponentDescriptor final
12
12
  : public ConcreteComponentDescriptor<TrueSheetContentViewShadowNode> {
13
13
  using ConcreteComponentDescriptor::ConcreteComponentDescriptor;
14
-
15
- void adopt(ShadowNode &shadowNode) const override {
16
- auto &concreteShadowNode =
17
- static_cast<TrueSheetContentViewShadowNode &>(shadowNode);
18
- concreteShadowNode.adjustLayoutWithState();
19
-
20
- ConcreteComponentDescriptor::adopt(shadowNode);
21
- }
22
14
  };
23
15
 
24
16
  } // namespace facebook::react
@@ -16,49 +16,28 @@ extern const char TrueSheetContentViewComponentName[] = "TrueSheetContentView";
16
16
  // clone — guard so the measurement pass doesn't measure again recursively.
17
17
  static thread_local bool gIsMeasuringNaturalHeight = false;
18
18
 
19
- void TrueSheetContentViewShadowNode::adjustLayoutWithState() {
20
- ensureUnsealed();
21
-
22
- auto state = std::static_pointer_cast<
23
- const TrueSheetContentViewShadowNode::ConcreteState>(getState());
24
- auto stateData = state->getData();
25
-
26
- auto &props = getConcreteProps();
27
-
28
- // When a ScrollView is detected under an auto detent, fill the container so
29
- // descendant flex layouts (flex:1 wrappers, the ScrollView itself) resolve
30
- // against a definite height and the viewport is bounded — natural layout is
31
- // circular there since the sheet height derives from the content's natural
32
- // height instead (see updateNaturalHeightIfNeeded). For fixed detents,
33
- // content lays out naturally like a regular view.
34
- auto flexGrow = stateData.scrollableBounded
35
- ? FloatOptional{1.0f}
36
- : props.yogaStyle.flexGrow();
37
- auto flexShrink = stateData.scrollableBounded
38
- ? FloatOptional{1.0f}
39
- : props.yogaStyle.flexShrink();
40
-
41
- if (yogaNode_.style().flexGrow() != flexGrow ||
42
- yogaNode_.style().flexShrink() != flexShrink) {
43
- yoga::Style adjustedStyle = props.yogaStyle;
44
- adjustedStyle.setFlexGrow(flexGrow);
45
- adjustedStyle.setFlexShrink(flexShrink);
46
- yogaNode_.setStyle(adjustedStyle);
47
- yogaNode_.setDirty(true);
48
- }
49
- }
50
-
51
19
  void TrueSheetContentViewShadowNode::layout(LayoutContext layoutContext) {
52
20
  ConcreteViewShadowNode::layout(layoutContext);
53
21
  updateNaturalHeightIfNeeded(layoutContext);
54
22
  }
55
23
 
24
+ // The content's committed height follows the container instead of its own
25
+ // content when it can grow/shrink along the main axis or uses a percent
26
+ // height. Reporting that height as the natural height would echo the sheet's
27
+ // own size back as a content size change — e.g. flex: 1 content tracking a
28
+ // drag frame by frame.
29
+ static bool isHeightContainerDerived(const yoga::Style &style) {
30
+ return style.flexGrow().unwrapOrDefault(0) > 0 ||
31
+ style.flexShrink().unwrapOrDefault(0) > 0 ||
32
+ style.dimension(Dimension::Height).isPercent();
33
+ }
34
+
56
35
  // The natural height is the height the content wants when unbounded — the
57
- // source for the auto detent. Once the container bounds the content
58
- // (scrollableBounded), the committed layout no longer reflects it, so lay out
59
- // a clone of the subtree with an unconstrained height. Yoga respects the
60
- // user's styles: an explicit-height ScrollView keeps its height while a
61
- // flexible one expands to its content — no ScrollView discovery involved.
36
+ // source for the auto detent. When the content's height derives from the
37
+ // container, the committed layout doesn't reflect it lay out a clone of the
38
+ // subtree with an unconstrained height instead. Yoga respects the user's
39
+ // styles: an explicit-height ScrollView keeps its height while a flexible one
40
+ // expands to its content — no ScrollView discovery involved.
62
41
  void TrueSheetContentViewShadowNode::updateNaturalHeightIfNeeded(
63
42
  const LayoutContext &layoutContext) {
64
43
  if (gIsMeasuringNaturalHeight) {
@@ -69,7 +48,7 @@ void TrueSheetContentViewShadowNode::updateNaturalHeightIfNeeded(
69
48
  auto size = getLayoutMetrics().frame.size;
70
49
 
71
50
  Float naturalHeight = size.height;
72
- if (stateData.scrollableBounded) {
51
+ if (isHeightContainerDerived(getConcreteProps().yogaStyle)) {
73
52
  auto constraints = LayoutConstraints{};
74
53
  constraints.minimumSize = {size.width, 0};
75
54
  constraints.maximumSize = {size.width, std::numeric_limits<Float>::infinity()};
@@ -22,8 +22,6 @@ class JSI_EXPORT TrueSheetContentViewShadowNode final
22
22
  using ConcreteViewShadowNode::ConcreteViewShadowNode;
23
23
 
24
24
  public:
25
- void adjustLayoutWithState();
26
-
27
25
  #pragma mark - LayoutableShadowNode
28
26
 
29
27
  void layout(LayoutContext layoutContext) override;
@@ -4,8 +4,7 @@ namespace facebook::react {
4
4
 
5
5
  #ifdef ANDROID
6
6
  folly::dynamic TrueSheetContentViewState::getDynamic() const {
7
- return folly::dynamic::object("scrollableBounded", scrollableBounded)(
8
- "naturalHeight", naturalHeight);
7
+ return folly::dynamic::object("naturalHeight", naturalHeight);
9
8
  }
10
9
  #endif
11
10
 
@@ -12,8 +12,6 @@ namespace facebook::react {
12
12
 
13
13
  /*
14
14
  * State for <TrueSheetContentView> component.
15
- * `scrollableBounded` is set from native when a ScrollView is detected so the
16
- * shadow node bounds the content to the container via flexShrink.
17
15
  * `naturalHeight` is measured by the shadow node during layout — the height
18
16
  * the content wants when unbounded (see TrueSheetContentViewShadowNode).
19
17
  */
@@ -25,13 +23,9 @@ class TrueSheetContentViewState final {
25
23
  TrueSheetContentViewState(
26
24
  TrueSheetContentViewState const &previousState,
27
25
  folly::dynamic data)
28
- : scrollableBounded(
29
- data.getDefault("scrollableBounded", previousState.scrollableBounded)
30
- .getBool()),
31
- naturalHeight(previousState.naturalHeight) {}
26
+ : naturalHeight(previousState.naturalHeight) {}
32
27
  #endif
33
28
 
34
- bool scrollableBounded{false};
35
29
  Float naturalHeight{0};
36
30
 
37
31
  #ifdef ANDROID
@@ -63,12 +63,6 @@ NS_ASSUME_NONNULL_BEGIN
63
63
  */
64
64
  @property (nonatomic, assign) BOOL contentInsetAdjustment;
65
65
 
66
- /**
67
- * Whether the sheet has an `auto` detent — bounds the detected scrollable's
68
- * viewport to the container so the sheet height can derive from its content size
69
- */
70
- @property (nonatomic, assign) BOOL hasAutoDetent;
71
-
72
66
  /**
73
67
  * Bottom safe-area inset the footer absorbs as padding — the footer
74
68
  * owns the sheet's bottom edge, so its background fills the inset
@@ -157,7 +157,6 @@ using namespace facebook::react;
157
157
  if (_contentView) {
158
158
  _contentView.scrollableHandle = _scrollableHandle;
159
159
  _contentView.contentInsetAdjustment = _contentInsetAdjustment;
160
- _contentView.hasAutoDetent = _hasAutoDetent;
161
160
  [_contentView setupScrollable];
162
161
  [_contentView applyScrollEdgeEffects:_scrollableOptions];
163
162
  if (@available(iOS 26.0, *)) {
@@ -48,13 +48,6 @@ NS_ASSUME_NONNULL_BEGIN
48
48
  */
49
49
  @property (nonatomic, weak, nullable) TrueSheetFooterView *footerView;
50
50
 
51
- /**
52
- * Whether the sheet has an `auto` detent. Deriving the sheet height from the
53
- * scroll content is circular with natural layout, so the detected ScrollView's
54
- * viewport is force-bounded to the container only in this case.
55
- */
56
- @property (nonatomic, assign) BOOL hasAutoDetent;
57
-
58
51
  /**
59
52
  * Content height measured unconstrained by the shadow node — the height the
60
53
  * content wants regardless of container bounds (see
@@ -29,7 +29,6 @@ using namespace facebook::react;
29
29
  CGFloat _lastReportedNaturalHeight;
30
30
  CGFloat _appliedKeyboardOffset;
31
31
  BOOL _observingTextChanges;
32
- BOOL _scrollableBounded;
33
32
  UIScrollViewContentInsetAdjustmentBehavior _originalInsetAdjustmentBehavior;
34
33
  BOOL _appliedContentInsetAdjustment;
35
34
  }
@@ -60,22 +59,6 @@ using namespace facebook::react;
60
59
  }
61
60
  }
62
61
 
63
- // Tells the shadow node to fill the container (flexGrow/flexShrink) so the
64
- // detected ScrollView's viewport is bounded to the visible space. Only applied
65
- // for auto detents — otherwise content lays out naturally like a regular view.
66
- - (void)setScrollableBounded:(BOOL)bounded {
67
- if (_scrollableBounded == bounded) {
68
- return;
69
- }
70
- _scrollableBounded = bounded;
71
-
72
- if (_state) {
73
- auto newState = _state->getData();
74
- newState.scrollableBounded = bounded;
75
- _state->updateState(std::move(newState));
76
- }
77
- }
78
-
79
62
  - (void)setScrollableHandle:(NSInteger)scrollableHandle {
80
63
  if (_scrollableHandle == scrollableHandle) {
81
64
  return;
@@ -112,17 +95,6 @@ using namespace facebook::react;
112
95
  }
113
96
  }
114
97
 
115
- - (void)setHasAutoDetent:(BOOL)hasAutoDetent {
116
- if (_hasAutoDetent == hasAutoDetent) {
117
- return;
118
- }
119
- _hasAutoDetent = hasAutoDetent;
120
-
121
- if (_detectedScrollView) {
122
- [self setScrollableBounded:hasAutoDetent];
123
- }
124
- }
125
-
126
98
  #pragma mark - Text Change Observing
127
99
 
128
100
  // The notification is app-wide (object:nil), so only observe while the keyboard is up
@@ -207,7 +179,6 @@ using namespace facebook::react;
207
179
  _detectedScrollView.scrollView.contentInsetAdjustmentBehavior = _originalInsetAdjustmentBehavior;
208
180
  _appliedContentInsetAdjustment = NO;
209
181
  }
210
- [self setScrollableBounded:NO];
211
182
  _detectedScrollView = nil;
212
183
  }
213
184
 
@@ -229,7 +200,6 @@ using namespace facebook::react;
229
200
  _detectedScrollView = scrollView;
230
201
 
231
202
  [self applyContentInsetAdjustment];
232
- [self setScrollableBounded:_hasAutoDetent];
233
203
 
234
204
  // If keyboard is currently showing, re-apply the keyboard inset to the new ScrollView
235
205
  CGFloat keyboardHeight = _keyboardObserver ? _keyboardObserver.currentHeight : 0;
@@ -443,10 +413,8 @@ using namespace facebook::react;
443
413
  [self stopObservingTextChanges];
444
414
  [self clearScrollable];
445
415
  _state.reset();
446
- _scrollableBounded = NO;
447
416
  _scrollableHandle = 0;
448
417
  _contentInsetAdjustment = NO;
449
- _hasAutoDetent = NO;
450
418
  _lastReportedNaturalHeight = 0;
451
419
  }
452
420
 
@@ -54,7 +54,6 @@ using namespace facebook::react;
54
54
  TrueSheetViewInsetAdjustment _insetAdjustment;
55
55
  ScrollableOptions *_scrollableOptions;
56
56
  NSInteger _scrollableHandle;
57
- BOOL _hasAutoDetent;
58
57
  BOOL _initialDetentAnimated;
59
58
  BOOL _isSheetUpdatePending;
60
59
  BOOL _pendingLayoutUpdate;
@@ -193,12 +192,8 @@ using namespace facebook::react;
193
192
 
194
193
  // Detents (-1 represents "auto")
195
194
  NSMutableArray *detents = [NSMutableArray new];
196
- _hasAutoDetent = NO;
197
195
  for (const auto &detent : newProps.detents) {
198
196
  [detents addObject:@(detent)];
199
- if (detent == -1) {
200
- _hasAutoDetent = YES;
201
- }
202
197
  }
203
198
 
204
199
  if (oldProps) {
@@ -833,7 +828,6 @@ using namespace facebook::react;
833
828
  _containerView.scrollableHandle = _scrollableHandle;
834
829
  _containerView.contentInsetAdjustment = (_scrollableOptions ? _scrollableOptions.contentInsetAdjustment : YES) &&
835
830
  _insetAdjustment == TrueSheetViewInsetAdjustment::Automatic;
836
- _containerView.hasAutoDetent = _hasAutoDetent;
837
831
  [self refreshFooterBottomInset];
838
832
  [_containerView setupScrollable];
839
833
  }
@@ -639,24 +639,26 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
639
639
  return;
640
640
  }
641
641
 
642
- if (_pendingContentSizeChange || _pendingDetentsChange) {
642
+ // Mid-drag the frame tracks the finger — never settle or learn from it;
643
+ // pending changes keep their flags and settle on the first post-drag layout.
644
+ if ((_pendingContentSizeChange || _pendingDetentsChange) && !_isDragging) {
643
645
  _pendingContentSizeChange = NO;
644
646
  _pendingDetentsChange = NO;
645
647
  [self settleAtDetentIndex:self.currentDetentIndex debug:@"layout"];
646
- } else if (!_isDragging) {
647
- // Drags emit from the pan handler. A child on top moves this sheet two
648
- // ways: an animated collapse/restore step (presenting/dismissing) — emit
649
- // the target non-realtime so JS animates to it — or direct per-frame
650
- // re-layouts while the child drags, which stay realtime.
648
+ } else {
649
+ // A child on top moves this sheet two ways: an animated collapse/restore
650
+ // step (presenting/dismissing) — emit the target non-realtime so JS
651
+ // animates to it — or direct per-frame re-layouts while the child drags,
652
+ // which stay realtime. Mid-drag no child is on top, so drags emit realtime.
651
653
  BOOL realtime = self.presentedViewController == nil || !self.isPresentedViewAnimating;
652
654
 
653
655
  // A layout pass at rest can nudge the frame by a sub-pixel amount
654
656
  // (pixel-grid snapping after present), leaving the interpolated index
655
657
  // slightly off the whole number. Re-learn when the frame is effectively
656
658
  // at the current detent so the drift is absorbed instead of emitted.
657
- // The tight threshold keeps real movement (e.g. scroll-driven expansion)
658
- // from being absorbed.
659
- if (self.presentedViewController == nil && !self.isPresentedViewAnimating) {
659
+ // The tight threshold keeps real movement (e.g. scroll-driven expansion
660
+ // or a drag hovering near the detent) from being absorbed.
661
+ if (!_isDragging && self.presentedViewController == nil && !self.isPresentedViewAnimating) {
660
662
  NSInteger index = self.currentDetentIndex;
661
663
  CGFloat expectedHeight = [_detentCalculator resolvedHeightForIndex:index];
662
664
  CGFloat actualHeight = self.screenHeight - self.currentPosition;
@@ -780,7 +782,14 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
780
782
  _isDragging = YES;
781
783
  break;
782
784
  case UIGestureRecognizerStateChanged:
783
- [self emitChangePositionDelegateWithPosition:self.currentPosition realtime:YES debug:@"drag change"];
785
+ // Layout owns the emit while the sheet resizes with the finger (a
786
+ // layout pass is already queued), and the transition tick owns it
787
+ // during an interactive dismiss. Translation-only movement (e.g.
788
+ // dragging down to the first detent) triggers neither — emit here as
789
+ // the backup.
790
+ if (!_isTransitioning && !self.presentedView.layer.needsLayout && !self.viewIfLoaded.layer.needsLayout) {
791
+ [self emitChangePositionDelegateWithPosition:self.currentPosition realtime:YES debug:@"drag change"];
792
+ }
784
793
  break;
785
794
  case UIGestureRecognizerStateEnded:
786
795
  case UIGestureRecognizerStateCancelled: {
@@ -865,27 +874,26 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
865
874
  }
866
875
 
867
876
  - (void)handleTransitionTick {
868
- // Interactive dismiss phase (finger down) — the pan handler emits.
869
- if (!_isDragging) {
870
- CGFloat position = self.livePosition;
871
-
872
- if (self.isBeingDismissed) {
873
- // Emit only once the dismiss is committed: on release UIKit moves the
874
- // model frame off-screen, while a cancelled drag rewinds it back to the
875
- // detent (isBeingDismissed stays YES during the rewind).
876
- if (self.currentPosition >= self.screenHeight) {
877
- [self emitWillDismissEvents];
878
- }
877
+ // Also covers the interactive dismiss phase (finger down) — layout passes
878
+ // are skipped while transitioning, so the tick owns position there too.
879
+ CGFloat position = self.livePosition;
879
880
 
880
- // Hide blur at the end of dismiss to prevent UIVisualEffectView
881
- // from causing a flicker/flash at the bottom edge of the sheet.
882
- if (self.screenHeight - position < 1) {
883
- _blurView.alpha = 0;
884
- }
881
+ if (self.isBeingDismissed) {
882
+ // Emit only once the dismiss is committed: on release UIKit moves the
883
+ // model frame off-screen, while a cancelled drag rewinds it back to the
884
+ // detent (isBeingDismissed stays YES during the rewind).
885
+ if (self.currentPosition >= self.screenHeight) {
886
+ [self emitWillDismissEvents];
885
887
  }
886
888
 
887
- [self emitChangePositionDelegateWithPosition:position realtime:YES debug:@"transition"];
889
+ // Hide blur at the end of dismiss to prevent UIVisualEffectView
890
+ // from causing a flicker/flash at the bottom edge of the sheet.
891
+ if (self.screenHeight - position < 1) {
892
+ _blurView.alpha = 0;
893
+ }
888
894
  }
895
+
896
+ [self emitChangePositionDelegateWithPosition:position realtime:YES debug:@"transition"];
889
897
  }
890
898
 
891
899
  /**
@@ -69,8 +69,8 @@ export class TrueSheet extends PureComponent {
69
69
  this.nativeRef = /*#__PURE__*/createRef();
70
70
  this.validateDetents();
71
71
 
72
- // Lazy load by default, except when initialDetentIndex is set (for auto-presentation)
73
- const shouldRenderImmediately = props.initialDetentIndex !== undefined && props.initialDetentIndex >= 0;
72
+ // Lazy load by default, except when auto-presenting or when lazy is disabled
73
+ const shouldRenderImmediately = props.initialDetentIndex !== undefined && props.initialDetentIndex >= 0 || props.lazy === false;
74
74
  this.state = {
75
75
  shouldRenderNativeView: shouldRenderImmediately,
76
76
  initialPresentReady: false,
@@ -262,9 +262,8 @@ export class TrueSheet extends PureComponent {
262
262
  this.backHandlerSubscription?.remove();
263
263
  this.backHandlerSubscription = null;
264
264
 
265
- // Clean up native view after dismiss for lazy loading.
266
- // Skip unmount if a present is in progress to avoid race condition.
267
- if (!this.isPresenting) {
265
+ // Non-lazy content stays mounted; otherwise clean it up unless another presentation is active.
266
+ if (!this.isPresenting && this.props.lazy !== false) {
268
267
  this.setState({
269
268
  shouldRenderNativeView: false
270
269
  });
@@ -379,6 +378,11 @@ export class TrueSheet extends PureComponent {
379
378
  componentDidUpdate(prevProps) {
380
379
  this.registerInstance();
381
380
  this.updateScrollableHandle();
381
+ if (prevProps.lazy !== false && this.props.lazy === false && !this.state.shouldRenderNativeView) {
382
+ this.setState({
383
+ shouldRenderNativeView: true
384
+ });
385
+ }
382
386
 
383
387
  // Validate when detents prop changes
384
388
  if (prevProps.detents !== this.props.detents) {
@@ -435,6 +439,7 @@ export class TrueSheet extends PureComponent {
435
439
  insetAdjustment = 'automatic',
436
440
  ...rest
437
441
  } = this.props;
442
+ delete rest.lazy;
438
443
 
439
444
  // Trim to max 3 detents and clamp fractions
440
445
  const resolvedDetents = detents.slice(0, 3).map(detent => {