@lodev09/react-native-true-sheet 4.0.0-beta.24 → 4.0.0-beta.25
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 +1 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +13 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +11 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +89 -9
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +100 -24
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +13 -10
- package/android/src/main/java/com/lodev09/truesheet/core/RNScreensEventObserver.kt +31 -6
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +3 -8
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +14 -1
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +11 -17
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetGrabberView.kt +2 -4
- package/android/src/main/java/com/lodev09/truesheet/utils/ColorUtils.kt +19 -0
- package/ios/TrueSheetModule.mm +11 -2
- package/ios/TrueSheetView.h +2 -0
- package/ios/TrueSheetView.mm +53 -7
- package/ios/TrueSheetViewController.h +2 -0
- package/ios/TrueSheetViewController.mm +158 -4
- package/ios/core/RNScreensEventObserver.mm +18 -1
- package/ios/core/TrueSheetDimView.h +27 -0
- package/ios/core/TrueSheetDimView.mm +41 -0
- package/ios/core/TrueSheetKeyboardObserver.mm +11 -8
- package/lib/module/TrueSheet.js +88 -17
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +150 -37
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/TrueSheetProvider.web.js +13 -9
- package/lib/module/TrueSheetProvider.web.js.map +1 -1
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +2 -0
- package/lib/module/scrollable.js +28 -0
- package/lib/module/scrollable.js.map +1 -0
- package/lib/module/web/keyboard.js +69 -0
- package/lib/module/web/keyboard.js.map +1 -0
- package/lib/module/web/layout.js +1 -1
- package/lib/module/web/layout.js.map +1 -1
- package/lib/module/web/vaul/context.js.map +1 -1
- package/lib/module/web/vaul/index.js +18 -8
- package/lib/module/web/vaul/index.js.map +1 -1
- package/lib/module/web/vaul/use-snap-points.js +15 -10
- package/lib/module/web/vaul/use-snap-points.js.map +1 -1
- package/lib/typescript/src/TrueSheet.d.ts +14 -1
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +22 -2
- 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/TrueSheetProvider.web.d.ts +5 -1
- package/lib/typescript/src/TrueSheetProvider.web.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +2 -0
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/navigation/types.d.ts +1 -1
- package/lib/typescript/src/navigation/types.d.ts.map +1 -1
- package/lib/typescript/src/scrollable.d.ts +2 -0
- package/lib/typescript/src/scrollable.d.ts.map +1 -0
- package/lib/typescript/src/web/keyboard.d.ts +15 -0
- package/lib/typescript/src/web/keyboard.d.ts.map +1 -0
- package/lib/typescript/src/web/layout.d.ts +1 -0
- package/lib/typescript/src/web/layout.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/context.d.ts +2 -1
- package/lib/typescript/src/web/vaul/context.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/index.d.ts +8 -1
- package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/types.d.ts +9 -0
- package/lib/typescript/src/web/vaul/types.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/use-snap-points.d.ts +3 -2
- package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/TrueSheet.tsx +91 -14
- package/src/TrueSheet.types.ts +26 -2
- package/src/TrueSheet.web.tsx +170 -41
- package/src/TrueSheetProvider.web.tsx +22 -13
- package/src/fabric/TrueSheetViewNativeComponent.ts +2 -0
- package/src/navigation/types.ts +2 -0
- package/src/scrollable.ts +25 -0
- package/src/web/keyboard.ts +79 -0
- package/src/web/layout.ts +1 -1
- package/src/web/vaul/context.ts +2 -1
- package/src/web/vaul/index.tsx +20 -5
- package/src/web/vaul/types.ts +10 -0
- package/src/web/vaul/use-snap-points.ts +19 -9
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
package com.lodev09.truesheet.core
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
|
-
import android.content.res.Configuration
|
|
5
4
|
import android.graphics.Color
|
|
6
5
|
import android.graphics.Outline
|
|
7
6
|
import android.graphics.Rect
|
|
@@ -20,6 +19,7 @@ import com.facebook.react.uimanager.ThemedReactContext
|
|
|
20
19
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
21
20
|
import com.lodev09.truesheet.TrueSheetPlacement
|
|
22
21
|
import com.lodev09.truesheet.utils.ScreenUtils
|
|
22
|
+
import com.lodev09.truesheet.utils.isDarkMode
|
|
23
23
|
|
|
24
24
|
interface TrueSheetBottomSheetViewDelegate {
|
|
25
25
|
val isTopmostSheet: Boolean
|
|
@@ -186,13 +186,8 @@ class TrueSheetBottomSheetView(private val reactContext: ThemedReactContext) : F
|
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
fun getDefaultBackgroundColor(): Int
|
|
190
|
-
|
|
191
|
-
reactContext.resources.configuration.uiMode and
|
|
192
|
-
Configuration.UI_MODE_NIGHT_MASK
|
|
193
|
-
) == Configuration.UI_MODE_NIGHT_YES
|
|
194
|
-
return if (isNight) COLOR_SURFACE_CONTAINER_LOW_DARK else COLOR_SURFACE_CONTAINER_LOW_LIGHT
|
|
195
|
-
}
|
|
189
|
+
fun getDefaultBackgroundColor(): Int =
|
|
190
|
+
if (reactContext.isDarkMode) COLOR_SURFACE_CONTAINER_LOW_DARK else COLOR_SURFACE_CONTAINER_LOW_LIGHT
|
|
196
191
|
|
|
197
192
|
fun setupElevation() {
|
|
198
193
|
val value = delegate?.sheetElevation ?: DEFAULT_ELEVATION
|
|
@@ -28,6 +28,9 @@ interface TrueSheetCoordinatorLayoutDelegate {
|
|
|
28
28
|
* didn't follow, `keyboardWasVisible` whether the keyboard was up when it began.
|
|
29
29
|
*/
|
|
30
30
|
fun coordinatorLayoutDidPullDown(overdragPx: Int, keyboardWasVisible: Boolean)
|
|
31
|
+
|
|
32
|
+
// A touch that began inside the sheet ended, after the behavior handled the release
|
|
33
|
+
fun coordinatorLayoutDidEndTouch()
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
/**
|
|
@@ -86,6 +89,10 @@ class TrueSheetCoordinatorLayout(context: Context) :
|
|
|
86
89
|
private var pullOverdrag = 0f
|
|
87
90
|
private var pullKeyboardVisible = false
|
|
88
91
|
|
|
92
|
+
// A touch that began inside the sheet is still down
|
|
93
|
+
val isTouchingSheet: Boolean
|
|
94
|
+
get() = pullTracking
|
|
95
|
+
|
|
89
96
|
init {
|
|
90
97
|
layoutParams = LayoutParams(
|
|
91
98
|
LayoutParams.MATCH_PARENT,
|
|
@@ -150,10 +157,16 @@ class TrueSheetCoordinatorLayout(context: Context) :
|
|
|
150
157
|
if (dy > dx && pullOverdrag > PULL_DOWN_THRESHOLD_DP.dpToPx()) {
|
|
151
158
|
delegate?.coordinatorLayoutDidPullDown(pullOverdrag.toInt(), pullKeyboardVisible)
|
|
152
159
|
}
|
|
160
|
+
delegate?.coordinatorLayoutDidEndTouch()
|
|
153
161
|
return handled
|
|
154
162
|
}
|
|
155
163
|
|
|
156
|
-
MotionEvent.ACTION_CANCEL -> pullTracking
|
|
164
|
+
MotionEvent.ACTION_CANCEL -> if (pullTracking) {
|
|
165
|
+
pullTracking = false
|
|
166
|
+
val handled = super.dispatchTouchEvent(ev)
|
|
167
|
+
delegate?.coordinatorLayoutDidEndTouch()
|
|
168
|
+
return handled
|
|
169
|
+
}
|
|
157
170
|
}
|
|
158
171
|
return super.dispatchTouchEvent(ev)
|
|
159
172
|
}
|
|
@@ -34,10 +34,6 @@ class TrueSheetDimView(private val reactContext: ThemedReactContext) :
|
|
|
34
34
|
View(reactContext),
|
|
35
35
|
ReactPointerEventsView {
|
|
36
36
|
|
|
37
|
-
companion object {
|
|
38
|
-
private const val MAX_ALPHA = 0.5f
|
|
39
|
-
}
|
|
40
|
-
|
|
41
37
|
var delegate: TrueSheetDimViewDelegate? = null
|
|
42
38
|
|
|
43
39
|
private var targetView: ViewGroup? = null
|
|
@@ -48,6 +44,10 @@ class TrueSheetDimView(private val reactContext: ThemedReactContext) :
|
|
|
48
44
|
private val blockGestures: Boolean
|
|
49
45
|
get() = alpha > 0f
|
|
50
46
|
|
|
47
|
+
fun setDimColor(color: Int) {
|
|
48
|
+
setBackgroundColor(color)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
51
|
init {
|
|
52
52
|
layoutParams = ViewGroup.LayoutParams(
|
|
53
53
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
@@ -109,30 +109,24 @@ class TrueSheetDimView(private val reactContext: ThemedReactContext) :
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
// =============================================================================
|
|
112
|
-
// MARK: -
|
|
112
|
+
// MARK: - Progress Calculation
|
|
113
113
|
// =============================================================================
|
|
114
114
|
|
|
115
|
-
|
|
115
|
+
/**
|
|
116
|
+
* How far the sheet is into its dimmed range, from 0 (undimmed) to 1 (fully dimmed).
|
|
117
|
+
*/
|
|
118
|
+
fun calculateProgress(sheetTop: Int, dimmedDetentIndex: Int, getSheetTopForDetentIndex: (Int) -> Int): Float {
|
|
116
119
|
val realHeight = ScreenUtils.getRealScreenHeight(reactContext)
|
|
117
120
|
val dimmedDetentTop = getSheetTopForDetentIndex(dimmedDetentIndex)
|
|
118
121
|
val belowDimmedTop = if (dimmedDetentIndex > 0) getSheetTopForDetentIndex(dimmedDetentIndex - 1) else realHeight
|
|
119
122
|
|
|
120
123
|
return when {
|
|
121
|
-
sheetTop <= dimmedDetentTop ->
|
|
122
|
-
|
|
124
|
+
sheetTop <= dimmedDetentTop -> 1f
|
|
123
125
|
sheetTop >= belowDimmedTop -> 0f
|
|
124
|
-
|
|
125
|
-
else -> {
|
|
126
|
-
val progress = 1f - (sheetTop - dimmedDetentTop).toFloat() / (belowDimmedTop - dimmedDetentTop)
|
|
127
|
-
(progress * MAX_ALPHA).coerceIn(0f, MAX_ALPHA)
|
|
128
|
-
}
|
|
126
|
+
else -> (1f - (sheetTop - dimmedDetentTop).toFloat() / (belowDimmedTop - dimmedDetentTop)).coerceIn(0f, 1f)
|
|
129
127
|
}
|
|
130
128
|
}
|
|
131
129
|
|
|
132
|
-
fun interpolateAlpha(sheetTop: Int, dimmedDetentIndex: Int, getSheetTopForDetentIndex: (Int) -> Int) {
|
|
133
|
-
alpha = calculateAlpha(sheetTop, dimmedDetentIndex, getSheetTopForDetentIndex)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
130
|
// =============================================================================
|
|
137
131
|
// MARK: - Touch Handling
|
|
138
132
|
// =============================================================================
|
|
@@ -2,7 +2,6 @@ package com.lodev09.truesheet.core
|
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
4
|
import android.content.Context
|
|
5
|
-
import android.content.res.Configuration
|
|
6
5
|
import android.graphics.Color
|
|
7
6
|
import android.graphics.drawable.GradientDrawable
|
|
8
7
|
import android.os.Bundle
|
|
@@ -13,6 +12,7 @@ import android.widget.FrameLayout
|
|
|
13
12
|
import androidx.core.graphics.ColorUtils
|
|
14
13
|
import androidx.core.view.ViewCompat
|
|
15
14
|
import com.facebook.react.uimanager.PixelUtil.dpToPx
|
|
15
|
+
import com.lodev09.truesheet.utils.isDarkMode
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Options for customizing the grabber appearance.
|
|
@@ -165,9 +165,7 @@ class TrueSheetGrabberView(
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
private fun getAdaptiveColor(baseColor: Int? = null): Int {
|
|
168
|
-
val
|
|
169
|
-
val isDarkMode = nightMode == Configuration.UI_MODE_NIGHT_YES
|
|
170
|
-
val modeColor = if (isDarkMode) Color.WHITE else Color.BLACK
|
|
168
|
+
val modeColor = if (context.isDarkMode) Color.WHITE else Color.BLACK
|
|
171
169
|
|
|
172
170
|
return if (baseColor != null) {
|
|
173
171
|
// Blend user color with mode color for adaptive effect
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
package com.lodev09.truesheet.utils
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.res.Configuration
|
|
5
|
+
import com.facebook.react.bridge.ColorPropConverter
|
|
6
|
+
import com.facebook.react.bridge.ReadableMap
|
|
7
|
+
import com.facebook.react.bridge.ReadableType
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolves a processed color (number or `PlatformColor` map) at [key], or null when missing.
|
|
11
|
+
*/
|
|
12
|
+
fun ReadableMap.getColorOrNull(key: String, context: Context): Int? {
|
|
13
|
+
if (!hasKey(key) || isNull(key)) return null
|
|
14
|
+
val value = if (getType(key) == ReadableType.Map) getMap(key) else getDouble(key)
|
|
15
|
+
return ColorPropConverter.getColor(value, context)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
val Context.isDarkMode: Boolean
|
|
19
|
+
get() = (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
|
package/ios/TrueSheetModule.mm
CHANGED
|
@@ -149,11 +149,17 @@ RCT_EXPORT_MODULE(TrueSheetModule)
|
|
|
149
149
|
- (void)dismissAll:(BOOL)animated resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
150
150
|
RCTExecuteOnMainQueue(^{
|
|
151
151
|
@synchronized(viewRegistry) {
|
|
152
|
+
// Drop presents still waiting on another sheet's dismissal
|
|
153
|
+
for (TrueSheetView *view in viewRegistry.objectEnumerator) {
|
|
154
|
+
[view cancelPendingPresent];
|
|
155
|
+
}
|
|
156
|
+
|
|
152
157
|
// Find the root presented sheet (one without a parent TrueSheet)
|
|
153
158
|
TrueSheetView *rootSheet = nil;
|
|
154
159
|
|
|
155
160
|
for (TrueSheetView *view in viewRegistry.objectEnumerator) {
|
|
156
|
-
|
|
161
|
+
// A sheet still animating in counts too — dismissAnimated waits for it to land
|
|
162
|
+
if (!view.viewController.isPresented && !view.viewController.isBeingPresented) {
|
|
157
163
|
continue;
|
|
158
164
|
}
|
|
159
165
|
|
|
@@ -171,7 +177,10 @@ RCT_EXPORT_MODULE(TrueSheetModule)
|
|
|
171
177
|
return;
|
|
172
178
|
}
|
|
173
179
|
|
|
174
|
-
|
|
180
|
+
// Mid-present, the deferred dismiss tracks its own position like a regular dismiss
|
|
181
|
+
if (rootSheet.viewController.isPresented) {
|
|
182
|
+
[rootSheet emitDismissedPosition];
|
|
183
|
+
}
|
|
175
184
|
[rootSheet dismissAnimated:animated
|
|
176
185
|
completion:^(BOOL success, NSError *_Nullable error) {
|
|
177
186
|
if (success) {
|
package/ios/TrueSheetView.h
CHANGED
|
@@ -34,6 +34,8 @@ typedef void (^TrueSheetCompletionBlock)(BOOL success, NSError *_Nullable error)
|
|
|
34
34
|
|
|
35
35
|
- (void)emitDismissedPosition;
|
|
36
36
|
|
|
37
|
+
- (BOOL)cancelPendingPresent;
|
|
38
|
+
|
|
37
39
|
- (void)resizeToIndex:(NSInteger)index completion:(nullable TrueSheetCompletionBlock)completion;
|
|
38
40
|
|
|
39
41
|
- (void)dismissStackAnimated:(BOOL)animated completion:(nullable TrueSheetCompletionBlock)completion;
|
package/ios/TrueSheetView.mm
CHANGED
|
@@ -317,6 +317,8 @@ using namespace facebook::react;
|
|
|
317
317
|
_controller.dismissible = newProps.dismissible;
|
|
318
318
|
_controller.draggable = newProps.draggable;
|
|
319
319
|
_controller.dimmed = newProps.dimmed;
|
|
320
|
+
_controller.dimOpacity = newProps.dimOpacity < 0 ? nil : @(newProps.dimOpacity);
|
|
321
|
+
_controller.dimColor = RCTUIColorFromSharedColor(newProps.dimColor);
|
|
320
322
|
|
|
321
323
|
if (newProps.dimmedDetentIndex >= 0) {
|
|
322
324
|
_controller.dimmedDetentIndex = @(newProps.dimmedDetentIndex);
|
|
@@ -419,7 +421,8 @@ using namespace facebook::react;
|
|
|
419
421
|
[self setupScrollable];
|
|
420
422
|
|
|
421
423
|
if (_controller.isPresented) {
|
|
422
|
-
|
|
424
|
+
_pendingPropsUpdate = YES;
|
|
425
|
+
[self setupSheetDetentsForSizeChange];
|
|
423
426
|
} else if (_controller.isBeingPresented) {
|
|
424
427
|
_pendingPropsUpdate = YES;
|
|
425
428
|
} else if (_initialDetentIndex >= 0) {
|
|
@@ -630,6 +633,8 @@ using namespace facebook::react;
|
|
|
630
633
|
_controller.peekContentHeight = @([_containerView peekContentHeight]);
|
|
631
634
|
}
|
|
632
635
|
_pendingSizeChange = NO;
|
|
636
|
+
// Props queued while the previous presentation dismissed — applied above.
|
|
637
|
+
_pendingPropsUpdate = NO;
|
|
633
638
|
|
|
634
639
|
[self refreshFooterBottomInset];
|
|
635
640
|
[_controller setupSheetDetents];
|
|
@@ -719,6 +724,20 @@ using namespace facebook::react;
|
|
|
719
724
|
return;
|
|
720
725
|
}
|
|
721
726
|
|
|
727
|
+
// Still animating in (isPresented flips at viewDidAppear): dismiss once the presentation lands
|
|
728
|
+
id<UIViewControllerTransitionCoordinator> presentCoordinator =
|
|
729
|
+
_controller.isBeingPresented ? _controller.transitionCoordinator : nil;
|
|
730
|
+
if (presentCoordinator) {
|
|
731
|
+
__weak __typeof(self) weakSelf = self;
|
|
732
|
+
[presentCoordinator animateAlongsideTransition:nil
|
|
733
|
+
completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
|
|
734
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
735
|
+
[weakSelf dismissAnimated:animated completion:completion];
|
|
736
|
+
});
|
|
737
|
+
}];
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
|
|
722
741
|
if (_controller.isBeingDismissed || !_controller.isPresented) {
|
|
723
742
|
RCTLogWarn(@"TrueSheet: sheet is already dismissed. No need to dismiss it again.");
|
|
724
743
|
|
|
@@ -768,7 +787,8 @@ using namespace facebook::react;
|
|
|
768
787
|
#pragma mark - TrueSheetContainerViewDelegate
|
|
769
788
|
|
|
770
789
|
/**
|
|
771
|
-
*
|
|
790
|
+
* Coalesces prop and size changes outside Fabric's mount transaction so
|
|
791
|
+
* the footer's layout can mount inside UIKit's resize animation.
|
|
772
792
|
*/
|
|
773
793
|
- (void)setupSheetDetentsForSizeChange {
|
|
774
794
|
// Keep the footer's absorbed inset in sync with the latest measured heights
|
|
@@ -784,26 +804,52 @@ using namespace facebook::react;
|
|
|
784
804
|
}
|
|
785
805
|
|
|
786
806
|
// Not presented: presentAtIndex measures at present time.
|
|
787
|
-
|
|
788
|
-
// presentation stack, visibly glitching the sheet behind.
|
|
789
|
-
if (!_controller.isPresented || _controller.isBeingDismissed)
|
|
807
|
+
if (!_controller.isPresented || _isSheetUpdatePending)
|
|
790
808
|
return;
|
|
791
809
|
|
|
792
|
-
|
|
810
|
+
// Dismissing: the sheet is going away — touching detents perturbs the
|
|
811
|
+
// presentation stack, visibly glitching the sheet behind. An interactive
|
|
812
|
+
// dismiss can still cancel, so retry once the transition resolves.
|
|
813
|
+
if (_controller.isBeingDismissed) {
|
|
814
|
+
id<UIViewControllerTransitionCoordinator> coordinator = _controller.transitionCoordinator;
|
|
815
|
+
if (coordinator) {
|
|
816
|
+
_isSheetUpdatePending = YES;
|
|
817
|
+
|
|
818
|
+
__weak __typeof(self) weakSelf = self;
|
|
819
|
+
[coordinator animateAlongsideTransition:nil
|
|
820
|
+
completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
|
|
821
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
822
|
+
__typeof(self) strongSelf = weakSelf;
|
|
823
|
+
if (!strongSelf)
|
|
824
|
+
return;
|
|
825
|
+
|
|
826
|
+
strongSelf->_isSheetUpdatePending = NO;
|
|
827
|
+
[strongSelf setupSheetDetentsForSizeChange];
|
|
828
|
+
});
|
|
829
|
+
}];
|
|
830
|
+
}
|
|
793
831
|
return;
|
|
832
|
+
}
|
|
794
833
|
|
|
795
834
|
_isSheetUpdatePending = YES;
|
|
796
835
|
|
|
797
836
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
798
837
|
self->_isSheetUpdatePending = NO;
|
|
799
|
-
if (!self->_containerView || self->_controller.isBeingDismissed)
|
|
838
|
+
if (!self->_containerView || !self->_controller.isPresented || self->_controller.isBeingDismissed)
|
|
800
839
|
return;
|
|
801
840
|
|
|
802
841
|
// Refresh here (not just on peek size events) since the peek's offset
|
|
803
842
|
// within the content can change without its own size changing.
|
|
804
843
|
self->_controller.peekContentHeight = @([self->_containerView peekContentHeight]);
|
|
805
844
|
[self refreshFooterBottomInset];
|
|
845
|
+
|
|
846
|
+
// Size first: the props rebuild refreshes the heights itself, hiding the
|
|
847
|
+
// change from the controller and skipping its post-resize settle.
|
|
806
848
|
[self->_controller setupSheetDetentsForSizeChange];
|
|
849
|
+
if (self->_pendingPropsUpdate) {
|
|
850
|
+
self->_pendingPropsUpdate = NO;
|
|
851
|
+
[self applySheetPropsUpdate];
|
|
852
|
+
}
|
|
807
853
|
});
|
|
808
854
|
}
|
|
809
855
|
|
|
@@ -78,6 +78,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
78
78
|
@property (nonatomic, assign) facebook::react::TrueSheetViewAccessibilityOptionsStruct accessibilityOptions;
|
|
79
79
|
@property (nonatomic, assign) BOOL draggable;
|
|
80
80
|
@property (nonatomic, assign) BOOL dimmed;
|
|
81
|
+
@property (nonatomic, strong, nullable) UIColor *dimColor;
|
|
82
|
+
@property (nonatomic, strong, nullable) NSNumber *dimOpacity;
|
|
81
83
|
@property (nonatomic, strong, nullable) NSNumber *dimmedDetentIndex;
|
|
82
84
|
@property (nonatomic, assign) facebook::react::TrueSheetViewBackgroundBlur backgroundBlur;
|
|
83
85
|
@property (nonatomic, assign) facebook::react::TrueSheetViewPresentation presentation;
|
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
#import "TrueSheetContentView.h"
|
|
12
12
|
#import "core/TrueSheetBlurView.h"
|
|
13
13
|
#import "core/TrueSheetDetentCalculator.h"
|
|
14
|
+
#import "core/TrueSheetDimView.h"
|
|
14
15
|
#import "core/TrueSheetGrabberView.h"
|
|
15
16
|
#import "utils/BlurUtil.h"
|
|
16
17
|
#import "utils/GestureUtil.h"
|
|
17
18
|
#import "utils/PlatformUtil.h"
|
|
19
|
+
#import "utils/UIView+FirstResponder.h"
|
|
18
20
|
#import "utils/WindowUtil.h"
|
|
19
21
|
|
|
20
22
|
#import <React/RCTLog.h>
|
|
@@ -81,6 +83,8 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
81
83
|
// coordinator — started alongside the transition, stopped in its completion.
|
|
82
84
|
CADisplayLink *_transitionLink;
|
|
83
85
|
BOOL _isTransitioning;
|
|
86
|
+
// Where the sheet rests during the transition: the target on present, the start on dismiss
|
|
87
|
+
CGFloat _transitionRestPosition;
|
|
84
88
|
|
|
85
89
|
BOOL _pendingContentSizeChange;
|
|
86
90
|
BOOL _pendingDetentsChange;
|
|
@@ -91,6 +95,7 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
91
95
|
// for the next present (beats the float-threshold guess for re-presents).
|
|
92
96
|
BOOL _hasObservedBottomInset;
|
|
93
97
|
CGFloat _observedBottomInset;
|
|
98
|
+
BOOL _isRefreshingSafeArea;
|
|
94
99
|
|
|
95
100
|
BOOL _isInteractiveDismiss;
|
|
96
101
|
CGFloat _interactiveStartPosition;
|
|
@@ -99,6 +104,7 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
99
104
|
CADisplayLink *_interactivePositionLink;
|
|
100
105
|
|
|
101
106
|
__weak TrueSheetViewController *_parentSheetController;
|
|
107
|
+
__weak UIView *_presenterFirstResponder;
|
|
102
108
|
|
|
103
109
|
UIView *_anchorView;
|
|
104
110
|
|
|
@@ -107,6 +113,7 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
107
113
|
TrueSheetBlurView *_blurView;
|
|
108
114
|
TrueSheetGrabberView *_grabberView;
|
|
109
115
|
TrueSheetDetentCalculator *_detentCalculator;
|
|
116
|
+
TrueSheetDimView *_dimView;
|
|
110
117
|
}
|
|
111
118
|
|
|
112
119
|
#pragma mark - Initialization
|
|
@@ -126,6 +133,8 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
126
133
|
_footerAvoidsKeyboard = YES;
|
|
127
134
|
_dismissible = YES;
|
|
128
135
|
_dimmed = YES;
|
|
136
|
+
_dimColor = nil;
|
|
137
|
+
_dimOpacity = nil;
|
|
129
138
|
_dimmedDetentIndex = @(0);
|
|
130
139
|
_presentation = facebook::react::TrueSheetViewPresentation::Page;
|
|
131
140
|
_lastEmittedPositionState = (TrueSheetPositionState){0, 0, 0};
|
|
@@ -316,6 +325,12 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
316
325
|
// wrong leaves the safe-area region unfilled (gap) or double-padded.
|
|
317
326
|
UIView *view = self.viewIfLoaded;
|
|
318
327
|
if (view.window) {
|
|
328
|
+
// Mid-present, hold the expected inset over a stale zero so the footer
|
|
329
|
+
// doesn't shrink and regrow — viewDidAppear forces the real value.
|
|
330
|
+
if (!_isPresented && [self hasStaleBottomSafeArea]) {
|
|
331
|
+
return [self bottomSafeAreaForHeight:view.bounds.size.height];
|
|
332
|
+
}
|
|
333
|
+
|
|
319
334
|
_hasObservedBottomInset = YES;
|
|
320
335
|
_observedBottomInset = view.safeAreaInsets.bottom;
|
|
321
336
|
return _observedBottomInset;
|
|
@@ -430,6 +445,67 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
430
445
|
[_grabberView addToView:self.view];
|
|
431
446
|
}
|
|
432
447
|
|
|
448
|
+
#pragma mark - Custom Dimming
|
|
449
|
+
|
|
450
|
+
// UISheetPresentationController has no API for the dim's color or opacity, so a custom
|
|
451
|
+
// color/opacity turns the native dim off and fades in our own view behind the sheet.
|
|
452
|
+
- (BOOL)useCustomDimming {
|
|
453
|
+
return self.dimmed && (self.dimColor != nil || self.dimOpacity != nil);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
- (BOOL)isDimmedAtEveryDetent {
|
|
457
|
+
return self.dimmed && [self.dimmedDetentIndex integerValue] == 0;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Like the system dim, fades in between the detent below `dimmedDetentIndex` and the dimmed detent.
|
|
461
|
+
- (CGFloat)dimAlphaForIndex:(CGFloat)index {
|
|
462
|
+
if (![self useCustomDimming]) {
|
|
463
|
+
return 0;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
CGFloat progress = index - [self.dimmedDetentIndex integerValue] + 1;
|
|
467
|
+
return fmax(0, fmin(1, progress));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Present and dismiss fade the dim over the whole travel below the resting position.
|
|
471
|
+
- (CGFloat)dimAlphaForPosition:(CGFloat)position index:(CGFloat)index {
|
|
472
|
+
if (!_isTransitioning || position <= _transitionRestPosition) {
|
|
473
|
+
return [self dimAlphaForIndex:index];
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
CGFloat travel = self.screenHeight - _transitionRestPosition;
|
|
477
|
+
CGFloat progress = travel > 0 ? fmax(0, (self.screenHeight - position) / travel) : 0;
|
|
478
|
+
return [self dimAlphaForIndex:[self interpolatedIndexForPosition:_transitionRestPosition]] * progress;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
- (void)setupDimView {
|
|
482
|
+
UIView *container = self.presentationController.containerView;
|
|
483
|
+
if (!container || (!_dimView && ![self useCustomDimming])) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (!_dimView) {
|
|
488
|
+
_dimView = [[TrueSheetDimView alloc] init];
|
|
489
|
+
|
|
490
|
+
__weak __typeof(self) weakSelf = self;
|
|
491
|
+
_dimView.onTap = ^{
|
|
492
|
+
[weakSelf handleDimViewTap];
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Keep opacity in the color so a transparent dim still receives touches.
|
|
497
|
+
UIColor *color = self.dimColor ?: [UIColor blackColor];
|
|
498
|
+
CGFloat opacity = self.dimOpacity ? [self.dimOpacity doubleValue] : 0.5;
|
|
499
|
+
_dimView.backgroundColor = [color colorWithAlphaComponent:CGColorGetAlpha(color.CGColor) * opacity];
|
|
500
|
+
[_dimView addToView:container];
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
- (void)handleDimViewTap {
|
|
504
|
+
if (self.dismissible) {
|
|
505
|
+
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
433
509
|
- (void)viewWillAppear:(BOOL)animated {
|
|
434
510
|
[super viewWillAppear:animated];
|
|
435
511
|
|
|
@@ -443,6 +519,17 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
443
519
|
[_parentSheetController setAccessibilityContentElement:self.accessibilityContentView ?: self.view];
|
|
444
520
|
}
|
|
445
521
|
|
|
522
|
+
[self setupDimView];
|
|
523
|
+
|
|
524
|
+
// UIKit hides the presenter's keyboard under a sheet dimmed at every detent and
|
|
525
|
+
// brings it back on dismiss, but custom dimming leaves the native sheet undimmed.
|
|
526
|
+
if ([self useCustomDimming] && [self isDimmedAtEveryDetent]) {
|
|
527
|
+
UIView *firstResponder = [presenter.view findFirstResponder];
|
|
528
|
+
if ([firstResponder resignFirstResponder]) {
|
|
529
|
+
_presenterFirstResponder = firstResponder;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
446
533
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
447
534
|
NSInteger index = self.currentDetentIndex;
|
|
448
535
|
CGFloat position = self.currentPosition;
|
|
@@ -475,11 +562,41 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
475
562
|
|
|
476
563
|
[self setupGestureRecognizer];
|
|
477
564
|
_isPresented = YES;
|
|
565
|
+
[self refreshStaleBottomSafeArea];
|
|
478
566
|
}
|
|
479
567
|
|
|
480
568
|
[self setupAccessibilityContainer];
|
|
481
569
|
}
|
|
482
570
|
|
|
571
|
+
/**
|
|
572
|
+
* A present that overlaps another transition (e.g. re-presenting while a
|
|
573
|
+
* native stack pops) can leave the root view's bottom safe area at zero, and
|
|
574
|
+
* UIKit doesn't recompute it until the sheet's frame next changes. Suspect a
|
|
575
|
+
* zero the presented height says shouldn't be there — a small floating sheet
|
|
576
|
+
* legitimately has none.
|
|
577
|
+
*/
|
|
578
|
+
- (BOOL)hasStaleBottomSafeArea {
|
|
579
|
+
UIView *view = self.viewIfLoaded;
|
|
580
|
+
return view.window && view.safeAreaInsets.bottom <= 0 && [self bottomSafeAreaForHeight:view.bounds.size.height] > 0;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Toggling additionalSafeAreaInsets forces UIKit to recompute the safe area.
|
|
584
|
+
// The toggle's intermediate value isn't reported, so the footer only sees the
|
|
585
|
+
// settled one.
|
|
586
|
+
- (void)refreshStaleBottomSafeArea {
|
|
587
|
+
if (![self hasStaleBottomSafeArea]) {
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
_isRefreshingSafeArea = YES;
|
|
592
|
+
UIEdgeInsets insets = self.additionalSafeAreaInsets;
|
|
593
|
+
self.additionalSafeAreaInsets = UIEdgeInsetsMake(insets.top, insets.left, insets.bottom + 1, insets.right);
|
|
594
|
+
self.additionalSafeAreaInsets = insets;
|
|
595
|
+
_isRefreshingSafeArea = NO;
|
|
596
|
+
|
|
597
|
+
[self.delegate viewControllerSafeAreaInsetsDidChange];
|
|
598
|
+
}
|
|
599
|
+
|
|
483
600
|
- (void)setupAccessibilityContainer {
|
|
484
601
|
UIView *contentView = self.accessibilityContentView;
|
|
485
602
|
if (!contentView) {
|
|
@@ -656,6 +773,13 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
656
773
|
[self.delegate viewControllerWillBlur];
|
|
657
774
|
[self.delegate viewControllerWillDismiss];
|
|
658
775
|
[_parentSheetController.delegate viewControllerWillFocus];
|
|
776
|
+
|
|
777
|
+
// Restored once the dismiss commits, like UIKit's own keyboard preservation.
|
|
778
|
+
UIView *firstResponder = _presenterFirstResponder;
|
|
779
|
+
_presenterFirstResponder = nil;
|
|
780
|
+
if (!self.presentingViewController.isBeingDismissed) {
|
|
781
|
+
[firstResponder becomeFirstResponder];
|
|
782
|
+
}
|
|
659
783
|
}
|
|
660
784
|
}
|
|
661
785
|
|
|
@@ -666,6 +790,9 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
666
790
|
_isWillDismissEmitted = NO;
|
|
667
791
|
_hasObservedBottomInset = NO;
|
|
668
792
|
|
|
793
|
+
[_dimView removeFromSuperview];
|
|
794
|
+
_dimView = nil;
|
|
795
|
+
|
|
669
796
|
[_anchorView removeFromSuperview];
|
|
670
797
|
_anchorView = nil;
|
|
671
798
|
|
|
@@ -684,6 +811,15 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
684
811
|
[self restoreWindowAccessibilityElements];
|
|
685
812
|
[self setSheetAccessibilityElementsHidden:YES];
|
|
686
813
|
|
|
814
|
+
// Dismissing with a child on top only animates the child out — this sheet
|
|
815
|
+
// doesn't move and isn't part of that transition, so fade its dim over it.
|
|
816
|
+
if (self.isBeingDismissed && self.presentedViewController) {
|
|
817
|
+
[UIView animateWithDuration:self.transitionCoordinator.transitionDuration
|
|
818
|
+
animations:^{
|
|
819
|
+
self->_dimView.alpha = 0;
|
|
820
|
+
}];
|
|
821
|
+
}
|
|
822
|
+
|
|
687
823
|
// Dispatch to allow pan gesture to set _isDragging before checking;
|
|
688
824
|
// the transition tracker emits when the sheet is transitioning to dismiss
|
|
689
825
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
@@ -779,7 +915,7 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
779
915
|
|
|
780
916
|
// Mid-dismiss the frame moves off-screen and reports transient insets —
|
|
781
917
|
// don't let them pollute the observed value (matches viewDidLayoutSubviews).
|
|
782
|
-
if (!self.isBeingDismissed) {
|
|
918
|
+
if (!self.isBeingDismissed && !_isRefreshingSafeArea) {
|
|
783
919
|
[self.delegate viewControllerSafeAreaInsetsDidChange];
|
|
784
920
|
}
|
|
785
921
|
}
|
|
@@ -928,6 +1064,7 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
928
1064
|
}
|
|
929
1065
|
|
|
930
1066
|
_isTransitioning = YES;
|
|
1067
|
+
_transitionRestPosition = self.isBeingDismissed ? self.livePosition : self.currentPosition;
|
|
931
1068
|
|
|
932
1069
|
// Learn the resolver-vs-actual offset before emitting transition positions so
|
|
933
1070
|
// the interpolated index lands exactly on the target detent. The presented
|
|
@@ -963,10 +1100,13 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
963
1100
|
// Settle after a present, cancelled dismiss, or detent-snap transition.
|
|
964
1101
|
// Delayed because the presentedView frame isn't final until UIKit
|
|
965
1102
|
// completes its layout pass after the transition animation — learning
|
|
966
|
-
// here absorbs any sub-pixel drift since the earlier learns.
|
|
1103
|
+
// here absorbs any sub-pixel drift since the earlier learns. A dismiss
|
|
1104
|
+
// cancelled mid-drag leaves the finger down, so the drag end settles instead.
|
|
967
1105
|
if (strongSelf->_isPresented && !strongSelf.isBeingDismissed) {
|
|
968
1106
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
969
|
-
|
|
1107
|
+
if (!strongSelf->_isDragging) {
|
|
1108
|
+
[strongSelf settleAtDetentIndex:strongSelf.currentDetentIndex debug:@"transition end"];
|
|
1109
|
+
}
|
|
970
1110
|
});
|
|
971
1111
|
}
|
|
972
1112
|
}];
|
|
@@ -1145,6 +1285,11 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1145
1285
|
.index = [self interpolatedIndexForPosition:position],
|
|
1146
1286
|
};
|
|
1147
1287
|
|
|
1288
|
+
// A child on top only pushes this sheet back, its detent stays.
|
|
1289
|
+
if (!presented) {
|
|
1290
|
+
_dimView.alpha = [self dimAlphaForPosition:position index:state.index];
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1148
1293
|
// Settle (non-realtime) emits are authoritative and bypass the dedupe — a
|
|
1149
1294
|
// learn right before them can correct the interpolated index by less than
|
|
1150
1295
|
// the tolerance, and the corrected value must still reach JS.
|
|
@@ -1263,7 +1408,9 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1263
1408
|
|
|
1264
1409
|
sheet.detents = detents;
|
|
1265
1410
|
|
|
1266
|
-
if (
|
|
1411
|
+
if ([self useCustomDimming]) {
|
|
1412
|
+
sheet.largestUndimmedDetentIdentifier = sheet.detents.lastObject.identifier;
|
|
1413
|
+
} else if ([self isDimmedAtEveryDetent]) {
|
|
1267
1414
|
sheet.largestUndimmedDetentIdentifier = nil;
|
|
1268
1415
|
} else {
|
|
1269
1416
|
sheet.largestUndimmedDetentIdentifier = UISheetPresentationControllerDetentIdentifierLarge;
|
|
@@ -1702,6 +1849,13 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1702
1849
|
|
|
1703
1850
|
[self setupBackground];
|
|
1704
1851
|
[self setupGrabber];
|
|
1852
|
+
|
|
1853
|
+
// Dim props changed while presented. A hidden custom dim stays until dismiss;
|
|
1854
|
+
// at alpha 0 it no longer takes touches.
|
|
1855
|
+
if (_isPresented) {
|
|
1856
|
+
[self setupDimView];
|
|
1857
|
+
_dimView.alpha = [self dimAlphaForIndex:self.currentDetentIndex];
|
|
1858
|
+
}
|
|
1705
1859
|
}
|
|
1706
1860
|
|
|
1707
1861
|
#pragma mark - UISheetPresentationControllerDelegate
|
|
@@ -272,7 +272,24 @@ using namespace facebook::react;
|
|
|
272
272
|
|
|
273
273
|
// Dismiss if presenter is no longer top of nav stack (pushed/popped)
|
|
274
274
|
// Skip if still top (e.g. modal dismiss - sheet dismisses naturally)
|
|
275
|
-
|
|
275
|
+
if (navController.topViewController != _presenterScreenController) {
|
|
276
|
+
return YES;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// A presenter inside a screen container (e.g. a bottom tab) stays the top of
|
|
280
|
+
// that container while an enclosing stack pushes a screen over it.
|
|
281
|
+
for (UINavigationController *outerNav = navController.navigationController; outerNav;
|
|
282
|
+
outerNav = outerNav.navigationController) {
|
|
283
|
+
UIViewController *ancestor = _presenterScreenController;
|
|
284
|
+
while (ancestor && ancestor != outerNav.topViewController) {
|
|
285
|
+
ancestor = ancestor.parentViewController;
|
|
286
|
+
}
|
|
287
|
+
if (!ancestor) {
|
|
288
|
+
return YES;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return NO;
|
|
276
293
|
}
|
|
277
294
|
|
|
278
295
|
@end
|