@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.
- package/README.md +21 -2
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +0 -7
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +0 -36
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +0 -1
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +0 -1
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h +0 -8
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +17 -38
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h +0 -2
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.cpp +1 -2
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h +1 -7
- package/ios/TrueSheetContainerView.h +0 -6
- package/ios/TrueSheetContainerView.mm +0 -1
- package/ios/TrueSheetContentView.h +0 -7
- package/ios/TrueSheetContentView.mm +0 -32
- package/ios/TrueSheetView.mm +0 -6
- package/ios/TrueSheetViewController.mm +35 -27
- package/lib/module/TrueSheet.js +10 -5
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/mocks/navigationExpoRouter.js.map +1 -1
- package/lib/module/navigation/base-types.js +2 -0
- package/lib/module/navigation/base-types.js.map +1 -0
- package/lib/module/navigation/createTrueSheetRouter.js +0 -9
- package/lib/module/navigation/createTrueSheetRouter.js.map +1 -1
- package/lib/module/navigation/expo-router/base-types.js +2 -0
- package/lib/module/navigation/expo-router/base-types.js.map +1 -0
- package/lib/module/navigation/expo-router/index.js +5 -3
- package/lib/module/navigation/expo-router/index.js.map +1 -1
- package/lib/module/navigation/expo-router/types.js +4 -0
- package/lib/module/navigation/expo-router/types.js.map +1 -0
- package/lib/module/navigation/index.js.map +1 -1
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +14 -0
- package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
- package/lib/typescript/src/mocks/navigationExpoRouter.d.ts +3 -3
- package/lib/typescript/src/mocks/navigationExpoRouter.d.ts.map +1 -1
- package/lib/typescript/src/navigation/base-types.d.ts +9 -0
- package/lib/typescript/src/navigation/base-types.d.ts.map +1 -0
- package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts +2 -13
- package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts.map +1 -1
- package/lib/typescript/src/navigation/expo-router/base-types.d.ts +8 -0
- package/lib/typescript/src/navigation/expo-router/base-types.d.ts.map +1 -0
- package/lib/typescript/src/navigation/expo-router/index.d.ts +5 -5
- package/lib/typescript/src/navigation/expo-router/index.d.ts.map +1 -1
- package/lib/typescript/src/navigation/expo-router/types.d.ts +44 -0
- package/lib/typescript/src/navigation/expo-router/types.d.ts.map +1 -0
- package/lib/typescript/src/navigation/index.d.ts +3 -4
- package/lib/typescript/src/navigation/index.d.ts.map +1 -1
- package/lib/typescript/src/navigation/types.d.ts +34 -4
- package/lib/typescript/src/navigation/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/TrueSheet.tsx +14 -5
- package/src/TrueSheet.types.ts +15 -0
- package/src/TrueSheet.web.tsx +2 -1
- package/src/mocks/navigationExpoRouter.ts +6 -5
- package/src/navigation/base-types.ts +20 -0
- package/src/navigation/createTrueSheetRouter.ts +5 -33
- package/src/navigation/expo-router/base-types.ts +18 -0
- package/src/navigation/expo-router/index.ts +20 -19
- package/src/navigation/expo-router/types.ts +116 -0
- package/src/navigation/index.ts +6 -7
- 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()
|
package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h
CHANGED
|
@@ -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
|
package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp
CHANGED
|
@@ -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.
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
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 (
|
|
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()};
|
|
@@ -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("
|
|
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
|
-
:
|
|
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
|
|
package/ios/TrueSheetView.mm
CHANGED
|
@@ -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
|
-
|
|
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
|
|
647
|
-
//
|
|
648
|
-
//
|
|
649
|
-
//
|
|
650
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
869
|
-
|
|
870
|
-
|
|
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
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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
|
-
|
|
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
|
/**
|
package/lib/module/TrueSheet.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
266
|
-
|
|
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 => {
|