@lodev09/react-native-true-sheet 4.0.0-beta.1 → 4.0.0-beta.3
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/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +8 -2
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +23 -6
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +9 -2
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +1 -0
- package/android/src/main/java/com/lodev09/truesheet/utils/TouchEventDeduper.kt +31 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp +7 -0
- package/ios/TrueSheetContainerView.h +5 -0
- package/ios/TrueSheetContainerView.mm +4 -0
- package/ios/TrueSheetFooterView.h +6 -0
- package/ios/TrueSheetFooterView.mm +31 -2
- package/ios/TrueSheetView.mm +82 -17
- package/ios/TrueSheetViewController.h +9 -0
- package/ios/TrueSheetViewController.mm +109 -29
- package/lib/module/TrueSheet.js +53 -9
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +11 -9
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/TrueSheetPeek.web.js +3 -3
- package/lib/module/TrueSheetPeek.web.js.map +1 -1
- package/lib/module/mocks/navigation.js +31 -21
- package/lib/module/mocks/navigation.js.map +1 -1
- package/lib/module/mocks/navigationExpoRouter.js +21 -0
- package/lib/module/mocks/navigationExpoRouter.js.map +1 -0
- package/lib/module/mocks/reanimated.js +1 -1
- package/lib/module/mocks/reanimated.js.map +1 -1
- package/lib/module/navigation/{TrueSheetView.js → TrueSheetNavigatorContent.js} +14 -12
- package/lib/module/navigation/TrueSheetNavigatorContent.js.map +1 -0
- package/lib/module/navigation/actions.js +50 -0
- package/lib/module/navigation/actions.js.map +1 -0
- package/lib/module/navigation/{TrueSheetRouter.js → createTrueSheetRouter.js} +20 -24
- package/lib/module/navigation/createTrueSheetRouter.js.map +1 -0
- package/lib/module/navigation/expo-router/index.js +48 -0
- package/lib/module/navigation/expo-router/index.js.map +1 -0
- package/lib/module/navigation/index.js +43 -2
- package/lib/module/navigation/index.js.map +1 -1
- package/lib/module/navigation/navigator.js +10 -0
- package/lib/module/navigation/navigator.js.map +1 -0
- package/lib/module/navigation/screen/ReanimatedTrueSheetScreen.js +2 -2
- package/lib/module/navigation/screen/ReanimatedTrueSheetScreen.js.map +1 -1
- package/lib/module/navigation/screen/TrueSheetScreen.js +2 -2
- package/lib/module/navigation/screen/TrueSheetScreen.js.map +1 -1
- package/lib/module/navigation/screen/useSheetScreenState.js +4 -4
- package/lib/module/navigation/screen/useSheetScreenState.js.map +1 -1
- package/lib/module/navigation/useTrueSheetNavigation.js +1 -1
- package/lib/module/navigation/useTrueSheetNavigation.js.map +1 -1
- package/lib/module/web/dom.js +10 -0
- package/lib/module/web/dom.js.map +1 -0
- package/lib/module/web/vaul/helpers.js +10 -4
- package/lib/module/web/vaul/helpers.js.map +1 -1
- package/lib/typescript/src/TrueSheet.d.ts +2 -0
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheetPeek.web.d.ts.map +1 -1
- package/lib/typescript/src/mocks/navigation.d.ts +5 -1
- package/lib/typescript/src/mocks/navigation.d.ts.map +1 -1
- package/lib/typescript/src/mocks/navigationExpoRouter.d.ts +22 -0
- package/lib/typescript/src/mocks/navigationExpoRouter.d.ts.map +1 -0
- package/lib/typescript/src/mocks/reanimated.d.ts.map +1 -1
- package/lib/typescript/src/navigation/TrueSheetNavigatorContent.d.ts +5 -0
- package/lib/typescript/src/navigation/TrueSheetNavigatorContent.d.ts.map +1 -0
- package/lib/typescript/src/navigation/actions.d.ts +71 -0
- package/lib/typescript/src/navigation/actions.d.ts.map +1 -0
- package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts +14 -0
- package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts.map +1 -0
- package/lib/typescript/src/navigation/expo-router/index.d.ts +37 -0
- package/lib/typescript/src/navigation/expo-router/index.d.ts.map +1 -0
- package/lib/typescript/src/navigation/index.d.ts +42 -2
- package/lib/typescript/src/navigation/index.d.ts.map +1 -1
- package/lib/typescript/src/navigation/navigator.d.ts +7 -0
- package/lib/typescript/src/navigation/navigator.d.ts.map +1 -0
- package/lib/typescript/src/navigation/screen/ReanimatedTrueSheetScreen.d.ts +1 -1
- package/lib/typescript/src/navigation/screen/ReanimatedTrueSheetScreen.d.ts.map +1 -1
- package/lib/typescript/src/navigation/screen/TrueSheetScreen.d.ts +1 -1
- package/lib/typescript/src/navigation/screen/TrueSheetScreen.d.ts.map +1 -1
- package/lib/typescript/src/navigation/screen/types.d.ts +3 -4
- package/lib/typescript/src/navigation/screen/types.d.ts.map +1 -1
- package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts +3 -5
- package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts.map +1 -1
- package/lib/typescript/src/navigation/types.d.ts +24 -0
- package/lib/typescript/src/navigation/types.d.ts.map +1 -1
- package/lib/typescript/src/navigation/useTrueSheetNavigation.d.ts +1 -1
- package/lib/typescript/src/navigation/useTrueSheetNavigation.d.ts.map +1 -1
- package/lib/typescript/src/web/dom.d.ts +7 -0
- package/lib/typescript/src/web/dom.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/helpers.d.ts.map +1 -1
- package/package.json +23 -8
- package/src/TrueSheet.tsx +55 -9
- package/src/TrueSheet.web.tsx +14 -18
- package/src/TrueSheetPeek.web.tsx +4 -3
- package/src/mocks/navigation.ts +32 -25
- package/src/mocks/navigationExpoRouter.ts +40 -0
- package/src/mocks/reanimated.ts +8 -9
- package/src/navigation/{TrueSheetView.tsx → TrueSheetNavigatorContent.tsx} +24 -17
- package/src/navigation/actions.ts +78 -0
- package/src/navigation/createTrueSheetRouter.ts +222 -0
- package/src/navigation/expo-router/index.ts +89 -0
- package/src/navigation/index.ts +69 -2
- package/src/navigation/navigator.ts +18 -0
- package/src/navigation/screen/ReanimatedTrueSheetScreen.tsx +2 -2
- package/src/navigation/screen/TrueSheetScreen.tsx +2 -2
- package/src/navigation/screen/types.ts +4 -6
- package/src/navigation/screen/useSheetScreenState.ts +8 -15
- package/src/navigation/types.ts +32 -0
- package/src/navigation/useTrueSheetNavigation.ts +1 -1
- package/src/web/dom.ts +9 -0
- package/src/web/vaul/helpers.ts +13 -6
- package/lib/module/navigation/TrueSheetRouter.js.map +0 -1
- package/lib/module/navigation/TrueSheetView.js.map +0 -1
- package/lib/module/navigation/createTrueSheetNavigator.js +0 -63
- package/lib/module/navigation/createTrueSheetNavigator.js.map +0 -1
- package/lib/typescript/src/navigation/TrueSheetRouter.d.ts +0 -57
- package/lib/typescript/src/navigation/TrueSheetRouter.d.ts.map +0 -1
- package/lib/typescript/src/navigation/TrueSheetView.d.ts +0 -10
- package/lib/typescript/src/navigation/TrueSheetView.d.ts.map +0 -1
- package/lib/typescript/src/navigation/createTrueSheetNavigator.d.ts +0 -35
- package/lib/typescript/src/navigation/createTrueSheetNavigator.d.ts.map +0 -1
- package/src/navigation/TrueSheetRouter.ts +0 -234
- package/src/navigation/createTrueSheetNavigator.tsx +0 -93
|
@@ -15,6 +15,7 @@ import com.facebook.react.uimanager.ThemedReactContext
|
|
|
15
15
|
import com.facebook.react.uimanager.events.EventDispatcher
|
|
16
16
|
import com.facebook.react.views.view.ReactViewGroup
|
|
17
17
|
import com.lodev09.truesheet.core.TrueSheetCoordinatorLayout
|
|
18
|
+
import com.lodev09.truesheet.utils.TouchEventDeduper
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Delegate interface for footer view size changes and event dispatching
|
|
@@ -91,6 +92,7 @@ class TrueSheetFooterView(private val reactContext: ThemedReactContext) :
|
|
|
91
92
|
|
|
92
93
|
private val jsTouchDispatcher = JSTouchDispatcher(this)
|
|
93
94
|
private var jsPointerDispatcher: JSPointerDispatcher? = null
|
|
95
|
+
private val touchDeduper = TouchEventDeduper()
|
|
94
96
|
|
|
95
97
|
init {
|
|
96
98
|
jsPointerDispatcher = JSPointerDispatcher(this)
|
|
@@ -110,7 +112,9 @@ class TrueSheetFooterView(private val reactContext: ThemedReactContext) :
|
|
|
110
112
|
|
|
111
113
|
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
|
112
114
|
eventDispatcher?.let { dispatcher ->
|
|
113
|
-
|
|
115
|
+
if (touchDeduper.shouldDispatch(event)) {
|
|
116
|
+
jsTouchDispatcher.handleTouchEvent(event, dispatcher, reactContext)
|
|
117
|
+
}
|
|
114
118
|
jsPointerDispatcher?.handleMotionEvent(event, dispatcher, true)
|
|
115
119
|
}
|
|
116
120
|
return super.onInterceptTouchEvent(event)
|
|
@@ -128,7 +132,9 @@ class TrueSheetFooterView(private val reactContext: ThemedReactContext) :
|
|
|
128
132
|
}
|
|
129
133
|
|
|
130
134
|
eventDispatcher?.let { dispatcher ->
|
|
131
|
-
|
|
135
|
+
if (touchDeduper.shouldDispatch(event)) {
|
|
136
|
+
jsTouchDispatcher.handleTouchEvent(event, dispatcher, reactContext)
|
|
137
|
+
}
|
|
132
138
|
jsPointerDispatcher?.handleMotionEvent(event, dispatcher, false)
|
|
133
139
|
}
|
|
134
140
|
super.onTouchEvent(event)
|
|
@@ -101,13 +101,30 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
101
101
|
|
|
102
102
|
override fun onAttachedToWindow() {
|
|
103
103
|
super.onAttachedToWindow()
|
|
104
|
+
presentInitialIfNeeded()
|
|
105
|
+
}
|
|
104
106
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
107
|
+
/**
|
|
108
|
+
* JS holds initialDetentIndex back one commit so effect-driven content (e.g. a
|
|
109
|
+
* navigation footer set via setOptions) commits together with it. The prop then
|
|
110
|
+
* arrives after attach, so the ViewManager triggers this on prop update too.
|
|
111
|
+
* Posted so sibling mounts from the current transaction land before measuring.
|
|
112
|
+
*/
|
|
113
|
+
fun presentInitialIfNeeded() {
|
|
114
|
+
if (initialDetentIndex < 0 || didInitiallyPresent || !isAttachedToWindow) return
|
|
115
|
+
|
|
116
|
+
didInitiallyPresent = true
|
|
117
|
+
post {
|
|
118
|
+
// The view may have been dropped (flag reset) or detached before this
|
|
119
|
+
// ran — roll back so a later attach retries instead of presenting a
|
|
120
|
+
// sheet with no live host.
|
|
121
|
+
if (!didInitiallyPresent) return@post
|
|
122
|
+
if (!isAttachedToWindow) {
|
|
123
|
+
didInitiallyPresent = false
|
|
124
|
+
return@post
|
|
125
|
+
}
|
|
126
|
+
if (!viewController.isPresented) {
|
|
127
|
+
present(initialDetentIndex, initialDetentAnimated) { }
|
|
111
128
|
}
|
|
112
129
|
}
|
|
113
130
|
}
|
|
@@ -39,6 +39,7 @@ import com.lodev09.truesheet.core.TrueSheetKeyboardObserverDelegate
|
|
|
39
39
|
import com.lodev09.truesheet.core.TrueSheetStackManager
|
|
40
40
|
import com.lodev09.truesheet.utils.KeyboardUtils
|
|
41
41
|
import com.lodev09.truesheet.utils.ScreenUtils
|
|
42
|
+
import com.lodev09.truesheet.utils.TouchEventDeduper
|
|
42
43
|
|
|
43
44
|
// =============================================================================
|
|
44
45
|
// MARK: - Data Types & Delegate Protocol
|
|
@@ -199,6 +200,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
199
200
|
// the finger drifts outside the footer's rect mid-gesture.
|
|
200
201
|
private var footerOwnsTouchStream = false
|
|
201
202
|
|
|
203
|
+
private val touchDeduper = TouchEventDeduper()
|
|
204
|
+
|
|
202
205
|
private val eventDispatcher
|
|
203
206
|
get() = delegate?.eventDispatcher
|
|
204
207
|
|
|
@@ -1419,7 +1422,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1419
1422
|
|
|
1420
1423
|
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
|
1421
1424
|
eventDispatcher?.let {
|
|
1422
|
-
|
|
1425
|
+
if (touchDeduper.shouldDispatch(event)) {
|
|
1426
|
+
jsTouchDispatcher.handleTouchEvent(event, it, reactContext)
|
|
1427
|
+
}
|
|
1423
1428
|
jsPointerDispatcher.handleMotionEvent(event, it, true)
|
|
1424
1429
|
}
|
|
1425
1430
|
return super.onInterceptTouchEvent(event)
|
|
@@ -1433,7 +1438,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1433
1438
|
|
|
1434
1439
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
1435
1440
|
eventDispatcher?.let {
|
|
1436
|
-
|
|
1441
|
+
if (touchDeduper.shouldDispatch(event)) {
|
|
1442
|
+
jsTouchDispatcher.handleTouchEvent(event, it, reactContext)
|
|
1443
|
+
}
|
|
1437
1444
|
jsPointerDispatcher.handleMotionEvent(event, it, false)
|
|
1438
1445
|
}
|
|
1439
1446
|
super.onTouchEvent(event)
|
|
@@ -179,6 +179,7 @@ class TrueSheetViewManager :
|
|
|
179
179
|
@ReactProp(name = "initialDetentIndex", defaultInt = -1)
|
|
180
180
|
override fun setInitialDetentIndex(view: TrueSheetView, index: Int) {
|
|
181
181
|
view.initialDetentIndex = index
|
|
182
|
+
view.presentInitialIfNeeded()
|
|
182
183
|
}
|
|
183
184
|
|
|
184
185
|
@ReactProp(name = "initialDetentAnimated", defaultBoolean = true)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
package com.lodev09.truesheet.utils
|
|
2
|
+
|
|
3
|
+
import android.view.MotionEvent
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Guards a nested RootView against dispatching the same [MotionEvent] to JS twice.
|
|
7
|
+
*
|
|
8
|
+
* An event that no child consumes reaches both `onInterceptTouchEvent` and `onTouchEvent`,
|
|
9
|
+
* so dispatching from both starts a second JS touch stream for the same gesture.
|
|
10
|
+
*
|
|
11
|
+
* A plain root can live with that, but the sheet's roots are nested: touches that hit no
|
|
12
|
+
* React child resolve to the root's own tag, and the second stream then finds that view
|
|
13
|
+
* already holding the responder, so React only asks its ancestors - handing the touch to
|
|
14
|
+
* touchables outside the sheet.
|
|
15
|
+
*/
|
|
16
|
+
internal class TouchEventDeduper {
|
|
17
|
+
private var downTime = Long.MIN_VALUE
|
|
18
|
+
private var eventTime = Long.MIN_VALUE
|
|
19
|
+
private var action = -1
|
|
20
|
+
|
|
21
|
+
fun shouldDispatch(event: MotionEvent): Boolean {
|
|
22
|
+
if (event.downTime == downTime && event.eventTime == eventTime && event.action == action) {
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
downTime = event.downTime
|
|
27
|
+
eventTime = event.eventTime
|
|
28
|
+
action = event.action
|
|
29
|
+
return true
|
|
30
|
+
}
|
|
31
|
+
}
|
package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp
CHANGED
|
@@ -25,6 +25,13 @@ void TrueSheetFooterViewShadowNode::adjustLayoutWithState() {
|
|
|
25
25
|
float bottomInset = stateData.bottomInset;
|
|
26
26
|
if (!stateData.initialized && props.autoBottomInset) {
|
|
27
27
|
bottomInset = TrueSheetInsets::bottomSafeArea();
|
|
28
|
+
if (bottomInset != stateData.bottomInset) {
|
|
29
|
+
// Record the seeded value so the mounted view reads exactly the inset
|
|
30
|
+
// baked into this layout — the global may change before mount.
|
|
31
|
+
auto seededData = stateData;
|
|
32
|
+
seededData.bottomInset = bottomInset;
|
|
33
|
+
setStateData(std::move(seededData));
|
|
34
|
+
}
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
// A footer pinned to the sheet's bottom edge owns the bottom safe-area
|
|
@@ -82,6 +82,11 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
82
82
|
*/
|
|
83
83
|
- (CGFloat)footerHeight;
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Returns the bottom inset currently baked into the footer's height
|
|
87
|
+
*/
|
|
88
|
+
- (CGFloat)footerAppliedBottomInset;
|
|
89
|
+
|
|
85
90
|
/**
|
|
86
91
|
* Returns the current height of the peek view within the content
|
|
87
92
|
*/
|
|
@@ -118,6 +118,10 @@ using namespace facebook::react;
|
|
|
118
118
|
return _footerView ? _footerView.frame.size.height : 0;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
- (CGFloat)footerAppliedBottomInset {
|
|
122
|
+
return _footerView ? _footerView.appliedBottomInset : 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
121
125
|
// Distance from the top of the content view to the bottom of the peek view.
|
|
122
126
|
// Includes the peek view's offset within the content (padding, views above it)
|
|
123
127
|
// so the peek detent reveals everything down to the peek content's bottom edge.
|
|
@@ -27,6 +27,12 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
27
27
|
@property (nonatomic, weak, nullable) TrueSheetKeyboardObserver *keyboardObserver;
|
|
28
28
|
@property (nonatomic, weak, nullable) id<TrueSheetFooterViewDelegate> delegate;
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Bottom inset currently baked into the footer's layout height — pairs with
|
|
32
|
+
* the measured frame so detent math can subtract exactly what's baked in.
|
|
33
|
+
*/
|
|
34
|
+
@property (nonatomic, readonly) CGFloat appliedBottomInset;
|
|
35
|
+
|
|
30
36
|
/**
|
|
31
37
|
* Re-applies the footer's keyboard slide using the current keyboard height.
|
|
32
38
|
* No-op when the keyboard is hidden. Used to reflect live `keyboardOffset` changes.
|
|
@@ -25,6 +25,8 @@ using namespace facebook::react;
|
|
|
25
25
|
CGFloat _currentKeyboardOffset;
|
|
26
26
|
CGFloat _bottomInset;
|
|
27
27
|
BOOL _keyboardVisible;
|
|
28
|
+
BOOL _didPushBottomInset;
|
|
29
|
+
CGFloat _appliedBottomInset;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
+ (ComponentDescriptorProvider)componentDescriptorProvider {
|
|
@@ -43,6 +45,8 @@ using namespace facebook::react;
|
|
|
43
45
|
_currentKeyboardOffset = 0;
|
|
44
46
|
_bottomInset = 0;
|
|
45
47
|
_keyboardVisible = NO;
|
|
48
|
+
_didPushBottomInset = NO;
|
|
49
|
+
_appliedBottomInset = 0;
|
|
46
50
|
}
|
|
47
51
|
return self;
|
|
48
52
|
}
|
|
@@ -55,7 +59,11 @@ using namespace facebook::react;
|
|
|
55
59
|
// bottom safe-area inset — the footer owns the sheet's bottom edge, so it
|
|
56
60
|
// absorbs the inset and its background fills it.
|
|
57
61
|
- (void)setBottomInset:(CGFloat)bottomInset {
|
|
58
|
-
|
|
62
|
+
// The first push must go through even when the value matches the ivar
|
|
63
|
+
// default (0) — it marks the state initialized so the shadow node stops
|
|
64
|
+
// seeding from the app-wide precalculated inset, which may belong to
|
|
65
|
+
// another sheet (e.g. a floating sheet whose real inset is 0).
|
|
66
|
+
if (_didPushBottomInset && _bottomInset == bottomInset) {
|
|
59
67
|
return;
|
|
60
68
|
}
|
|
61
69
|
_bottomInset = bottomInset;
|
|
@@ -85,6 +93,14 @@ using namespace facebook::react;
|
|
|
85
93
|
// them an inset stale (same reason Android bridges through
|
|
86
94
|
// TrueSheetStateUpdater).
|
|
87
95
|
_state->updateState(std::move(newState), EventQueue::UpdateMode::unstable_Immediate);
|
|
96
|
+
_didPushBottomInset = YES;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// The inset baked into the current layout by the shadow node (pushed or
|
|
100
|
+
// seeded — see TrueSheetFooterViewShadowNode). Refreshed in
|
|
101
|
+
// updateLayoutMetrics so it always pairs with the measured height.
|
|
102
|
+
- (CGFloat)appliedBottomInset {
|
|
103
|
+
return _appliedBottomInset;
|
|
88
104
|
}
|
|
89
105
|
|
|
90
106
|
// Height the footer occupies above the keyboard — its layout height minus the
|
|
@@ -123,8 +139,19 @@ using namespace facebook::react;
|
|
|
123
139
|
oldLayoutMetrics:(const facebook::react::LayoutMetrics &)oldLayoutMetrics {
|
|
124
140
|
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
|
|
125
141
|
|
|
142
|
+
// State lands before layout metrics within the same mount transaction, so
|
|
143
|
+
// this reflects the inset the shadow node baked into this frame — seeded
|
|
144
|
+
// values are recorded back into the state at layout time (see
|
|
145
|
+
// TrueSheetFooterViewShadowNode).
|
|
146
|
+
CGFloat previousAppliedBottomInset = _appliedBottomInset;
|
|
147
|
+
if (_state) {
|
|
148
|
+
_appliedBottomInset = _state->getData().bottomInset;
|
|
149
|
+
}
|
|
150
|
+
|
|
126
151
|
CGFloat height = layoutMetrics.frame.size.height;
|
|
127
|
-
|
|
152
|
+
// An inset change alone (fixed-height footer: padding moves, frame doesn't)
|
|
153
|
+
// still changes what's baked in — report so the controller re-pairs.
|
|
154
|
+
if (height != _lastHeight || _appliedBottomInset != previousAppliedBottomInset) {
|
|
128
155
|
_lastHeight = height;
|
|
129
156
|
[self.delegate footerViewDidChangeSize:CGSizeMake(layoutMetrics.frame.size.width, height)];
|
|
130
157
|
}
|
|
@@ -143,6 +170,8 @@ using namespace facebook::react;
|
|
|
143
170
|
_currentKeyboardOffset = 0;
|
|
144
171
|
_bottomInset = 0;
|
|
145
172
|
_keyboardVisible = NO;
|
|
173
|
+
_didPushBottomInset = NO;
|
|
174
|
+
_appliedBottomInset = 0;
|
|
146
175
|
}
|
|
147
176
|
|
|
148
177
|
#pragma mark - TrueSheetKeyboardObserverDelegate
|
package/ios/TrueSheetView.mm
CHANGED
|
@@ -110,18 +110,47 @@ using namespace facebook::react;
|
|
|
110
110
|
return;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
[self presentInitialIfNeeded];
|
|
114
|
+
}
|
|
115
115
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
116
|
+
// JS holds initialDetentIndex back one commit so effect-driven content (e.g. a
|
|
117
|
+
// navigation footer set via setOptions) commits together with it — by the time
|
|
118
|
+
// the prop lands the tree is final and the sheet presents at its final height.
|
|
119
|
+
// Called from didMoveToWindow (re-attach) and finalizeUpdates (the prop
|
|
120
|
+
// arriving after attach, which didMoveToWindow won't re-fire for).
|
|
121
|
+
- (void)presentInitialIfNeeded {
|
|
122
|
+
if (_initialDetentIndex < 0 || _didInitiallyPresent)
|
|
123
|
+
return;
|
|
124
|
+
|
|
125
|
+
UIViewController *vc = [self findPresentingViewController];
|
|
126
|
+
|
|
127
|
+
// Only present if the view controller is in the same window and not being dismissed
|
|
128
|
+
if (!vc || vc.view.window != self.window || _controller.isBeingDismissed) {
|
|
129
|
+
// Animate next time when sheet finally moves to the correct window
|
|
130
|
+
_initialDetentAnimated = YES;
|
|
131
|
+
return;
|
|
124
132
|
}
|
|
133
|
+
|
|
134
|
+
_didInitiallyPresent = YES;
|
|
135
|
+
|
|
136
|
+
// Deferred a runloop turn so sibling mounts from the current transaction
|
|
137
|
+
// (e.g. a footer committed alongside the prop) land before measuring.
|
|
138
|
+
__weak __typeof(self) weakSelf = self;
|
|
139
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
140
|
+
__typeof(self) strongSelf = weakSelf;
|
|
141
|
+
if (!strongSelf || strongSelf->_controller.isPresented || strongSelf->_controller.isBeingPresented)
|
|
142
|
+
return;
|
|
143
|
+
|
|
144
|
+
[strongSelf presentAtIndex:strongSelf->_initialDetentIndex
|
|
145
|
+
animated:strongSelf->_initialDetentAnimated
|
|
146
|
+
completion:^(BOOL success, NSError *_Nullable error) {
|
|
147
|
+
// Present can fail if the view detached during this turn —
|
|
148
|
+
// reset so the next attach/prop update retries.
|
|
149
|
+
if (!success) {
|
|
150
|
+
strongSelf->_didInitiallyPresent = NO;
|
|
151
|
+
}
|
|
152
|
+
}];
|
|
153
|
+
});
|
|
125
154
|
}
|
|
126
155
|
|
|
127
156
|
- (void)dealloc {
|
|
@@ -342,6 +371,9 @@ using namespace facebook::react;
|
|
|
342
371
|
_pendingPropsUpdate = YES;
|
|
343
372
|
} else if (_initialDetentIndex >= 0) {
|
|
344
373
|
_pendingLayoutUpdate = NO;
|
|
374
|
+
if (self.window) {
|
|
375
|
+
[self presentInitialIfNeeded];
|
|
376
|
+
}
|
|
345
377
|
}
|
|
346
378
|
}
|
|
347
379
|
|
|
@@ -403,9 +435,8 @@ using namespace facebook::react;
|
|
|
403
435
|
_controller.headerHeight = @(headerHeight);
|
|
404
436
|
}
|
|
405
437
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
_controller.footerHeight = @(footerHeight);
|
|
438
|
+
if ([_containerView footerHeight] > 0) {
|
|
439
|
+
[self syncFooterMetrics];
|
|
409
440
|
}
|
|
410
441
|
|
|
411
442
|
CGFloat peekContentHeight = [_containerView peekContentHeight];
|
|
@@ -480,11 +511,12 @@ using namespace facebook::react;
|
|
|
480
511
|
if (_containerView) {
|
|
481
512
|
_controller.contentHeight = @([_containerView contentHeight]);
|
|
482
513
|
_controller.headerHeight = @([_containerView headerHeight]);
|
|
483
|
-
|
|
514
|
+
[self syncFooterMetrics];
|
|
484
515
|
_controller.peekContentHeight = @([_containerView peekContentHeight]);
|
|
485
516
|
}
|
|
486
517
|
_pendingSizeChange = NO;
|
|
487
518
|
|
|
519
|
+
[self refreshFooterBottomInset];
|
|
488
520
|
[_controller setupSheetDetents];
|
|
489
521
|
[_controller setupActiveDetentWithIndex:index];
|
|
490
522
|
|
|
@@ -583,6 +615,10 @@ using namespace facebook::react;
|
|
|
583
615
|
* Debounced sheet update to handle rapid content/header size changes.
|
|
584
616
|
*/
|
|
585
617
|
- (void)setupSheetDetentsForSizeChange {
|
|
618
|
+
// Keep the footer's absorbed inset in sync with the latest measured heights
|
|
619
|
+
// even before presenting, so the sheet presents at its final size.
|
|
620
|
+
[self refreshFooterBottomInset];
|
|
621
|
+
|
|
586
622
|
// Retargeting the in-flight presentation makes UIKit snap the whole stack
|
|
587
623
|
// (including the sheet behind) instead of animating. Defer to didPresent —
|
|
588
624
|
// presentAtIndex measured synchronously, so this is usually a no-op.
|
|
@@ -610,6 +646,7 @@ using namespace facebook::react;
|
|
|
610
646
|
// Refresh here (not just on peek size events) since the peek's offset
|
|
611
647
|
// within the content can change without its own size changing.
|
|
612
648
|
self->_controller.peekContentHeight = @([self->_containerView peekContentHeight]);
|
|
649
|
+
[self refreshFooterBottomInset];
|
|
613
650
|
[self->_controller setupSheetDetentsForSizeChange];
|
|
614
651
|
});
|
|
615
652
|
}
|
|
@@ -625,7 +662,7 @@ using namespace facebook::react;
|
|
|
625
662
|
}
|
|
626
663
|
|
|
627
664
|
- (void)containerViewFooterDidChangeSize:(CGSize)newSize {
|
|
628
|
-
|
|
665
|
+
[self syncFooterMetrics];
|
|
629
666
|
[self setupSheetDetentsForSizeChange];
|
|
630
667
|
[_controller setupAccessibilityContainer];
|
|
631
668
|
}
|
|
@@ -716,6 +753,10 @@ using namespace facebook::react;
|
|
|
716
753
|
[self updateStateWithSize:size];
|
|
717
754
|
}
|
|
718
755
|
|
|
756
|
+
- (void)viewControllerSafeAreaInsetsDidChange {
|
|
757
|
+
[self refreshFooterBottomInset];
|
|
758
|
+
}
|
|
759
|
+
|
|
719
760
|
- (void)viewControllerWillFocus {
|
|
720
761
|
[TrueSheetFocusEvents emitWillFocus:_eventEmitter];
|
|
721
762
|
}
|
|
@@ -784,13 +825,36 @@ using namespace facebook::react;
|
|
|
784
825
|
_containerView.insetAdjustment = _insetAdjustment;
|
|
785
826
|
_containerView.scrollableOptions = _scrollableOptions;
|
|
786
827
|
_containerView.hasAutoDetent = _hasAutoDetent;
|
|
787
|
-
|
|
828
|
+
[self refreshFooterBottomInset];
|
|
788
829
|
[_containerView setupScrollable];
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// footerHeight and appliedFooterBottomInset must stay paired — detent math
|
|
833
|
+
// subtracts exactly the inset baked into the measured footer height (see
|
|
834
|
+
// autoDetentBottomInset/peekDetentBottomInset).
|
|
835
|
+
- (void)syncFooterMetrics {
|
|
836
|
+
_controller.footerHeight = @([_containerView footerHeight]);
|
|
837
|
+
_controller.appliedFooterBottomInset = [_containerView footerAppliedBottomInset];
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Recomputes the inset the footer absorbs and pushes it down. The inset
|
|
841
|
+
// depends on measured heights and detents (see maxNaturalDetentHeight), so
|
|
842
|
+
// this runs on size and detent changes — not just prop updates.
|
|
843
|
+
- (void)refreshFooterBottomInset {
|
|
844
|
+
if (!_containerView)
|
|
845
|
+
return;
|
|
846
|
+
|
|
847
|
+
// appliedFooterBottomInset is NOT set here — it tracks what's actually
|
|
848
|
+
// baked into the footer's measured height (reported with its layout), not
|
|
849
|
+
// what was just pushed. Setting it at push time desyncs the pair and lets
|
|
850
|
+
// a seeded inset read as natural content height (see maxNaturalDetentHeight).
|
|
851
|
+
CGFloat inset = _controller.footerBottomInset;
|
|
852
|
+
_containerView.footerBottomInset = inset;
|
|
789
853
|
|
|
790
854
|
// Publish the inset so a footer set later (e.g. navigation setOptions) is
|
|
791
855
|
// padded on its very first layout instead of resizing the sheet twice.
|
|
792
856
|
if (_insetAdjustment == TrueSheetViewInsetAdjustment::Automatic) {
|
|
793
|
-
TrueSheetInsets::setBottomSafeArea(
|
|
857
|
+
TrueSheetInsets::setBottomSafeArea(inset);
|
|
794
858
|
}
|
|
795
859
|
}
|
|
796
860
|
|
|
@@ -801,6 +865,7 @@ using namespace facebook::react;
|
|
|
801
865
|
if (_pendingDetents) {
|
|
802
866
|
_controller.detents = _pendingDetents;
|
|
803
867
|
_pendingDetents = nil;
|
|
868
|
+
[self refreshFooterBottomInset];
|
|
804
869
|
}
|
|
805
870
|
|
|
806
871
|
UIView *presenterView = _controller.presentingViewController.view;
|
|
@@ -36,6 +36,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
36
36
|
detent:(CGFloat)detent
|
|
37
37
|
realtime:(BOOL)realtime;
|
|
38
38
|
- (void)viewControllerDidChangeSize:(CGSize)size;
|
|
39
|
+
- (void)viewControllerSafeAreaInsetsDidChange;
|
|
39
40
|
- (void)viewControllerWillFocus;
|
|
40
41
|
- (void)viewControllerDidFocus;
|
|
41
42
|
- (void)viewControllerWillBlur;
|
|
@@ -99,6 +100,14 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
99
100
|
*/
|
|
100
101
|
@property (nonatomic, readonly) CGFloat footerBottomInset;
|
|
101
102
|
|
|
103
|
+
/**
|
|
104
|
+
* The inset currently baked into the footer's measured height, reported
|
|
105
|
+
* alongside its layout (see TrueSheetFooterView.appliedBottomInset). Detent
|
|
106
|
+
* resolution subtracts this exact value so the baked inset is never
|
|
107
|
+
* double-counted.
|
|
108
|
+
*/
|
|
109
|
+
@property (nonatomic, assign) CGFloat appliedFooterBottomInset;
|
|
110
|
+
|
|
102
111
|
- (void)setupAccessibilityContainer;
|
|
103
112
|
- (void)applyActiveDetent;
|
|
104
113
|
- (void)setupActiveDetentWithIndex:(NSInteger)index;
|