@lodev09/react-native-true-sheet 3.7.0-beta.3 → 3.7.0-beta.5
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/TrueSheetView.kt +13 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +27 -5
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetStackManager.kt +33 -15
- package/android/src/main/java/com/lodev09/truesheet/utils/KeyboardUtils.kt +58 -3
- package/ios/TrueSheetFooterView.mm +16 -0
- package/ios/utils/UIView+FirstResponder.h +15 -0
- package/ios/utils/UIView+FirstResponder.mm +26 -0
- package/package.json +1 -1
|
@@ -18,6 +18,7 @@ import com.facebook.react.views.view.ReactViewGroup
|
|
|
18
18
|
import com.lodev09.truesheet.core.GrabberOptions
|
|
19
19
|
import com.lodev09.truesheet.core.TrueSheetStackManager
|
|
20
20
|
import com.lodev09.truesheet.events.*
|
|
21
|
+
import com.lodev09.truesheet.utils.KeyboardUtils
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Main TrueSheet host view that manages the sheet and dispatches events to JavaScript.
|
|
@@ -276,6 +277,18 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
276
277
|
@UiThread
|
|
277
278
|
fun present(detentIndex: Int, animated: Boolean = true, promiseCallback: () -> Unit) {
|
|
278
279
|
if (!viewController.isPresented) {
|
|
280
|
+
// Dismiss keyboard if focused view is within a sheet or if target detent will be dimmed
|
|
281
|
+
val parentSheet = TrueSheetStackManager.getTopmostSheet()
|
|
282
|
+
val isFocusedViewWithinSheet = parentSheet?.viewController?.isFocusedViewWithinSheet() == true
|
|
283
|
+
val shouldDismissKeyboard = isFocusedViewWithinSheet || viewController.isDimmedAtDetentIndex(detentIndex)
|
|
284
|
+
if (KeyboardUtils.isKeyboardVisible(reactContext) && shouldDismissKeyboard) {
|
|
285
|
+
viewController.saveFocusedView()
|
|
286
|
+
KeyboardUtils.dismiss(this) {
|
|
287
|
+
post { present(detentIndex, animated, promiseCallback) }
|
|
288
|
+
}
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
|
|
279
292
|
// Attach coordinator to the root container
|
|
280
293
|
rootContainerView = findRootContainerView()
|
|
281
294
|
viewController.coordinatorLayout?.let { rootContainerView?.addView(it) }
|
|
@@ -140,6 +140,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
140
140
|
|
|
141
141
|
// Keyboard State
|
|
142
142
|
private var detentIndexBeforeKeyboard: Int = -1
|
|
143
|
+
private var focusedViewBeforeBlur: View? = null
|
|
143
144
|
|
|
144
145
|
// Promises
|
|
145
146
|
var presentPromise: (() -> Unit)? = null
|
|
@@ -204,9 +205,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
204
205
|
if (isPresented) sheetView?.setupGrabber()
|
|
205
206
|
}
|
|
206
207
|
|
|
207
|
-
val isDimmedAtCurrentDetent: Boolean
|
|
208
|
-
get() = dimmed && currentDetentIndex >= dimmedDetentIndex
|
|
209
|
-
|
|
210
208
|
// =============================================================================
|
|
211
209
|
// MARK: - Computed Properties
|
|
212
210
|
// =============================================================================
|
|
@@ -248,7 +246,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
248
246
|
private val isKeyboardTransitioning: Boolean
|
|
249
247
|
get() = keyboardObserver?.isTransitioning ?: false
|
|
250
248
|
|
|
251
|
-
|
|
249
|
+
fun isFocusedViewWithinSheet(): Boolean {
|
|
252
250
|
val sheet = sheetView ?: return false
|
|
253
251
|
return keyboardObserver?.isFocusedViewWithinSheet(sheet) ?: false
|
|
254
252
|
}
|
|
@@ -287,6 +285,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
287
285
|
private val dimViews: List<TrueSheetDimView>
|
|
288
286
|
get() = listOfNotNull(dimView, parentDimView)
|
|
289
287
|
|
|
288
|
+
val isDimmedAtCurrentDetent: Boolean
|
|
289
|
+
get() = isDimmedAtDetentIndex(currentDetentIndex)
|
|
290
|
+
|
|
291
|
+
fun isDimmedAtDetentIndex(index: Int): Boolean = dimmed && index >= dimmedDetentIndex
|
|
292
|
+
|
|
290
293
|
// =============================================================================
|
|
291
294
|
// MARK: - Sheet Creation & Cleanup
|
|
292
295
|
// =============================================================================
|
|
@@ -331,6 +334,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
331
334
|
isPresentAnimating = false
|
|
332
335
|
lastEmittedPositionPx = -1
|
|
333
336
|
detentIndexBeforeKeyboard = -1
|
|
337
|
+
focusedViewBeforeBlur = null
|
|
334
338
|
shouldAnimatePresent = true
|
|
335
339
|
}
|
|
336
340
|
|
|
@@ -513,7 +517,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
513
517
|
reactContext = reactContext,
|
|
514
518
|
onModalPresented = {
|
|
515
519
|
if (isPresented && isSheetVisible && isTopmostSheet) {
|
|
516
|
-
|
|
520
|
+
dismissKeyboard()
|
|
521
|
+
post { hideForModal() }
|
|
517
522
|
}
|
|
518
523
|
},
|
|
519
524
|
onModalWillDismiss = {
|
|
@@ -710,6 +715,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
710
715
|
}
|
|
711
716
|
|
|
712
717
|
private fun finishDismiss() {
|
|
718
|
+
restoreFocusedView()
|
|
713
719
|
emitDidDismissEvents()
|
|
714
720
|
cleanupSheet()
|
|
715
721
|
}
|
|
@@ -885,6 +891,22 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
885
891
|
return true
|
|
886
892
|
}
|
|
887
893
|
|
|
894
|
+
fun saveFocusedView() {
|
|
895
|
+
focusedViewBeforeBlur = reactContext.currentActivity?.currentFocus
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
fun restoreFocusedView() {
|
|
899
|
+
val viewToFocus = focusedViewBeforeBlur ?: return
|
|
900
|
+
focusedViewBeforeBlur = null
|
|
901
|
+
|
|
902
|
+
if (!viewToFocus.isAttachedToWindow) return
|
|
903
|
+
if (viewToFocus.requestFocus()) {
|
|
904
|
+
viewToFocus.postDelayed({
|
|
905
|
+
KeyboardUtils.show(viewToFocus)
|
|
906
|
+
}, 100)
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
888
910
|
fun setupKeyboardObserver() {
|
|
889
911
|
val coordinator = coordinatorLayout ?: run {
|
|
890
912
|
RNLog.e(reactContext, "TrueSheet: coordinatorLayout is null in setupKeyboardObserver")
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package com.lodev09.truesheet.core
|
|
2
2
|
|
|
3
|
+
import android.view.ViewGroup
|
|
3
4
|
import com.lodev09.truesheet.TrueSheetView
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -10,6 +11,25 @@ object TrueSheetStackManager {
|
|
|
10
11
|
|
|
11
12
|
private val presentedSheetStack = mutableListOf<TrueSheetView>()
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Gets the parent sheet at the given index, if any.
|
|
16
|
+
* Only returns a parent if it's in the same container.
|
|
17
|
+
* Must be called within synchronized block.
|
|
18
|
+
*/
|
|
19
|
+
private fun getParentSheetAt(index: Int, rootContainer: ViewGroup?): TrueSheetView? {
|
|
20
|
+
if (index <= 0) return null
|
|
21
|
+
return presentedSheetStack[index - 1].takeIf { it.rootContainerView == rootContainer }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Returns the topmost presented and visible sheet.
|
|
26
|
+
* Must be called within synchronized block.
|
|
27
|
+
*/
|
|
28
|
+
private fun findTopmostSheet(): TrueSheetView? =
|
|
29
|
+
presentedSheetStack.lastOrNull {
|
|
30
|
+
it.viewController.isPresented && it.viewController.isSheetVisible
|
|
31
|
+
}
|
|
32
|
+
|
|
13
33
|
/**
|
|
14
34
|
* Called when a sheet is about to be presented.
|
|
15
35
|
* Returns the visible parent sheet to stack on, or null if none.
|
|
@@ -18,13 +38,7 @@ object TrueSheetStackManager {
|
|
|
18
38
|
@JvmStatic
|
|
19
39
|
fun onSheetWillPresent(sheetView: TrueSheetView, detentIndex: Int): TrueSheetView? {
|
|
20
40
|
synchronized(presentedSheetStack) {
|
|
21
|
-
val
|
|
22
|
-
val parentSheet = presentedSheetStack.lastOrNull()
|
|
23
|
-
?.takeIf {
|
|
24
|
-
it.viewController.isPresented &&
|
|
25
|
-
it.viewController.isSheetVisible &&
|
|
26
|
-
it.rootContainerView == rootContainer
|
|
27
|
-
}
|
|
41
|
+
val parentSheet = findTopmostSheet()?.takeIf { it.rootContainerView == sheetView.rootContainerView }
|
|
28
42
|
|
|
29
43
|
val childSheetTop = sheetView.viewController.detentCalculator.getSheetTopForDetentIndex(detentIndex)
|
|
30
44
|
parentSheet?.updateTranslationForChild(childSheetTop)
|
|
@@ -60,11 +74,7 @@ object TrueSheetStackManager {
|
|
|
60
74
|
fun onSheetSizeChanged(sheetView: TrueSheetView) {
|
|
61
75
|
synchronized(presentedSheetStack) {
|
|
62
76
|
val index = presentedSheetStack.indexOf(sheetView)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
val rootContainer = sheetView.rootContainerView
|
|
66
|
-
val parentSheet = presentedSheetStack[index - 1]
|
|
67
|
-
.takeIf { it.rootContainerView == rootContainer } ?: return
|
|
77
|
+
val parentSheet = getParentSheetAt(index, sheetView.rootContainerView) ?: return
|
|
68
78
|
|
|
69
79
|
// Post to ensure layout is complete before reading position
|
|
70
80
|
sheetView.viewController.post {
|
|
@@ -117,9 +127,7 @@ object TrueSheetStackManager {
|
|
|
117
127
|
fun getParentSheet(sheetView: TrueSheetView): TrueSheetView? {
|
|
118
128
|
synchronized(presentedSheetStack) {
|
|
119
129
|
val index = presentedSheetStack.indexOf(sheetView)
|
|
120
|
-
|
|
121
|
-
val rootContainer = sheetView.rootContainerView
|
|
122
|
-
return presentedSheetStack[index - 1].takeIf { it.rootContainerView == rootContainer }
|
|
130
|
+
return getParentSheetAt(index, sheetView.rootContainerView)
|
|
123
131
|
}
|
|
124
132
|
}
|
|
125
133
|
|
|
@@ -133,4 +141,14 @@ object TrueSheetStackManager {
|
|
|
133
141
|
return presentedSheetStack.lastOrNull { it.rootContainerView == rootContainer } == sheetView
|
|
134
142
|
}
|
|
135
143
|
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Returns the topmost presented sheet, or null if none.
|
|
147
|
+
*/
|
|
148
|
+
@JvmStatic
|
|
149
|
+
fun getTopmostSheet(): TrueSheetView? {
|
|
150
|
+
synchronized(presentedSheetStack) {
|
|
151
|
+
return findTopmostSheet()
|
|
152
|
+
}
|
|
153
|
+
}
|
|
136
154
|
}
|
|
@@ -1,22 +1,77 @@
|
|
|
1
1
|
package com.lodev09.truesheet.utils
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
+
import android.os.Build
|
|
4
5
|
import android.view.View
|
|
5
6
|
import android.view.inputmethod.InputMethodManager
|
|
6
7
|
import androidx.core.view.ViewCompat
|
|
8
|
+
import androidx.core.view.WindowInsetsAnimationCompat
|
|
7
9
|
import androidx.core.view.WindowInsetsCompat
|
|
8
10
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
9
11
|
|
|
10
12
|
object KeyboardUtils {
|
|
11
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Checks if the soft keyboard is currently visible.
|
|
16
|
+
*/
|
|
17
|
+
fun isKeyboardVisible(reactContext: ThemedReactContext): Boolean {
|
|
18
|
+
val rootView = reactContext.currentActivity?.window?.decorView?.rootView ?: return false
|
|
19
|
+
return isKeyboardVisible(rootView)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private fun isKeyboardVisible(view: View): Boolean {
|
|
23
|
+
val insets = ViewCompat.getRootWindowInsets(view) ?: return false
|
|
24
|
+
return insets.isVisible(WindowInsetsCompat.Type.ime())
|
|
25
|
+
}
|
|
26
|
+
|
|
12
27
|
/**
|
|
13
28
|
* Dismisses the soft keyboard if currently shown.
|
|
14
29
|
*/
|
|
15
30
|
fun dismiss(reactContext: ThemedReactContext) {
|
|
16
|
-
val
|
|
17
|
-
|
|
18
|
-
|
|
31
|
+
val rootView = reactContext.currentActivity?.window?.decorView?.rootView ?: return
|
|
32
|
+
dismiss(rootView, null)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Dismisses the soft keyboard with an optional callback when the animation completes.
|
|
37
|
+
*/
|
|
38
|
+
fun dismiss(view: View, onComplete: (() -> Unit)?) {
|
|
39
|
+
val imm = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
|
40
|
+
val focusedView = view.rootView.findFocus()
|
|
41
|
+
|
|
42
|
+
if (focusedView == null || !isKeyboardVisible(view)) {
|
|
43
|
+
onComplete?.invoke()
|
|
44
|
+
return
|
|
19
45
|
}
|
|
46
|
+
|
|
47
|
+
if (onComplete != null) {
|
|
48
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
49
|
+
ViewCompat.setWindowInsetsAnimationCallback(
|
|
50
|
+
view,
|
|
51
|
+
object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
|
|
52
|
+
override fun onProgress(insets: WindowInsetsCompat, runningAnimations: List<WindowInsetsAnimationCompat>): WindowInsetsCompat =
|
|
53
|
+
insets
|
|
54
|
+
|
|
55
|
+
override fun onEnd(animation: WindowInsetsAnimationCompat) {
|
|
56
|
+
ViewCompat.setWindowInsetsAnimationCallback(view, null)
|
|
57
|
+
onComplete()
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
} else {
|
|
62
|
+
view.postDelayed({ onComplete() }, 120)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
imm?.hideSoftInputFromWindow(focusedView.windowToken, 0)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Shows the soft keyboard for the given view.
|
|
71
|
+
*/
|
|
72
|
+
fun show(view: View) {
|
|
73
|
+
val imm = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
|
74
|
+
imm?.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT)
|
|
20
75
|
}
|
|
21
76
|
|
|
22
77
|
/**
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
#import <react/renderer/components/TrueSheetSpec/RCTComponentViewHelpers.h>
|
|
16
16
|
#import "TrueSheetViewController.h"
|
|
17
17
|
#import "utils/LayoutUtil.h"
|
|
18
|
+
#import "utils/UIView+FirstResponder.h"
|
|
18
19
|
|
|
19
20
|
using namespace facebook::react;
|
|
20
21
|
|
|
@@ -139,6 +140,16 @@ using namespace facebook::react;
|
|
|
139
140
|
return nil;
|
|
140
141
|
}
|
|
141
142
|
|
|
143
|
+
- (BOOL)isFirstResponderWithinSheet {
|
|
144
|
+
TrueSheetViewController *sheetController = [self findSheetViewController];
|
|
145
|
+
if (!sheetController) {
|
|
146
|
+
return NO;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
UIView *firstResponder = [sheetController.view findFirstResponder];
|
|
150
|
+
return firstResponder != nil;
|
|
151
|
+
}
|
|
152
|
+
|
|
142
153
|
- (void)keyboardWillChangeFrame:(NSNotification *)notification {
|
|
143
154
|
if (!_bottomConstraint) {
|
|
144
155
|
return;
|
|
@@ -150,6 +161,11 @@ using namespace facebook::react;
|
|
|
150
161
|
return;
|
|
151
162
|
}
|
|
152
163
|
|
|
164
|
+
// Only respond if the focused view is within this sheet
|
|
165
|
+
if (![self isFirstResponderWithinSheet]) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
153
169
|
NSDictionary *userInfo = notification.userInfo;
|
|
154
170
|
CGRect keyboardFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
|
|
155
171
|
NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Created by Jovanni Lo (@lodev09)
|
|
3
|
+
// Copyright (c) 2024-present. All rights reserved.
|
|
4
|
+
//
|
|
5
|
+
// This source code is licensed under the MIT license found in the
|
|
6
|
+
// LICENSE file in the root directory of this source tree.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
#import <UIKit/UIKit.h>
|
|
10
|
+
|
|
11
|
+
@interface UIView (FirstResponder)
|
|
12
|
+
|
|
13
|
+
- (UIView *)findFirstResponder;
|
|
14
|
+
|
|
15
|
+
@end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Created by Jovanni Lo (@lodev09)
|
|
3
|
+
// Copyright (c) 2024-present. All rights reserved.
|
|
4
|
+
//
|
|
5
|
+
// This source code is licensed under the MIT license found in the
|
|
6
|
+
// LICENSE file in the root directory of this source tree.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
#import "UIView+FirstResponder.h"
|
|
10
|
+
|
|
11
|
+
@implementation UIView (FirstResponder)
|
|
12
|
+
|
|
13
|
+
- (UIView *)findFirstResponder {
|
|
14
|
+
if (self.isFirstResponder) {
|
|
15
|
+
return self;
|
|
16
|
+
}
|
|
17
|
+
for (UIView *subview in self.subviews) {
|
|
18
|
+
UIView *firstResponder = [subview findFirstResponder];
|
|
19
|
+
if (firstResponder) {
|
|
20
|
+
return firstResponder;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return nil;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@end
|
package/package.json
CHANGED