@lodev09/react-native-true-sheet 3.7.0-beta.2 → 3.7.0-beta.4
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 +5 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +11 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +22 -2
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetStackManager.kt +10 -0
- 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
|
@@ -5,6 +5,7 @@ import android.view.MotionEvent
|
|
|
5
5
|
import android.view.View
|
|
6
6
|
import com.facebook.react.uimanager.JSPointerDispatcher
|
|
7
7
|
import com.facebook.react.uimanager.JSTouchDispatcher
|
|
8
|
+
import com.facebook.react.uimanager.PointerEvents
|
|
8
9
|
import com.facebook.react.uimanager.RootView
|
|
9
10
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
10
11
|
import com.facebook.react.uimanager.events.EventDispatcher
|
|
@@ -66,6 +67,10 @@ class TrueSheetFooterView(private val reactContext: ThemedReactContext) :
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
70
|
+
if (pointerEvents == PointerEvents.NONE || pointerEvents == PointerEvents.BOX_NONE) {
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
73
|
+
|
|
69
74
|
eventDispatcher?.let { dispatcher ->
|
|
70
75
|
jsTouchDispatcher.handleTouchEvent(event, dispatcher, reactContext)
|
|
71
76
|
jsPointerDispatcher?.handleMotionEvent(event, dispatcher, false)
|
|
@@ -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,16 @@ 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
|
+
// Only dismiss keyboard if the focused view is within a parent sheet (iOS-like behavior)
|
|
281
|
+
val parentSheet = TrueSheetStackManager.getTopmostSheet()
|
|
282
|
+
if (KeyboardUtils.isKeyboardVisible(reactContext) && parentSheet?.viewController?.isFocusedViewWithinSheet() == true) {
|
|
283
|
+
parentSheet.viewController.saveFocusedView()
|
|
284
|
+
KeyboardUtils.dismiss(this) {
|
|
285
|
+
post { present(detentIndex, animated, promiseCallback) }
|
|
286
|
+
}
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
|
|
279
290
|
// Attach coordinator to the root container
|
|
280
291
|
rootContainerView = findRootContainerView()
|
|
281
292
|
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
|
|
@@ -248,7 +249,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
248
249
|
private val isKeyboardTransitioning: Boolean
|
|
249
250
|
get() = keyboardObserver?.isTransitioning ?: false
|
|
250
251
|
|
|
251
|
-
|
|
252
|
+
fun isFocusedViewWithinSheet(): Boolean {
|
|
252
253
|
val sheet = sheetView ?: return false
|
|
253
254
|
return keyboardObserver?.isFocusedViewWithinSheet(sheet) ?: false
|
|
254
255
|
}
|
|
@@ -331,6 +332,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
331
332
|
isPresentAnimating = false
|
|
332
333
|
lastEmittedPositionPx = -1
|
|
333
334
|
detentIndexBeforeKeyboard = -1
|
|
335
|
+
focusedViewBeforeBlur = null
|
|
334
336
|
shouldAnimatePresent = true
|
|
335
337
|
}
|
|
336
338
|
|
|
@@ -513,7 +515,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
513
515
|
reactContext = reactContext,
|
|
514
516
|
onModalPresented = {
|
|
515
517
|
if (isPresented && isSheetVisible && isTopmostSheet) {
|
|
516
|
-
|
|
518
|
+
dismissKeyboard()
|
|
519
|
+
post { hideForModal() }
|
|
517
520
|
}
|
|
518
521
|
},
|
|
519
522
|
onModalWillDismiss = {
|
|
@@ -710,6 +713,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
710
713
|
}
|
|
711
714
|
|
|
712
715
|
private fun finishDismiss() {
|
|
716
|
+
parentSheetView?.viewController?.restoreFocusedView()
|
|
713
717
|
emitDidDismissEvents()
|
|
714
718
|
cleanupSheet()
|
|
715
719
|
}
|
|
@@ -885,6 +889,22 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
885
889
|
return true
|
|
886
890
|
}
|
|
887
891
|
|
|
892
|
+
fun saveFocusedView() {
|
|
893
|
+
focusedViewBeforeBlur = reactContext.currentActivity?.currentFocus
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
fun restoreFocusedView() {
|
|
897
|
+
val viewToFocus = focusedViewBeforeBlur ?: return
|
|
898
|
+
focusedViewBeforeBlur = null
|
|
899
|
+
|
|
900
|
+
if (!viewToFocus.isAttachedToWindow) return
|
|
901
|
+
if (viewToFocus.requestFocus()) {
|
|
902
|
+
viewToFocus.postDelayed({
|
|
903
|
+
KeyboardUtils.show(viewToFocus)
|
|
904
|
+
}, 100)
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
888
908
|
fun setupKeyboardObserver() {
|
|
889
909
|
val coordinator = coordinatorLayout ?: run {
|
|
890
910
|
RNLog.e(reactContext, "TrueSheet: coordinatorLayout is null in setupKeyboardObserver")
|
|
@@ -133,4 +133,14 @@ object TrueSheetStackManager {
|
|
|
133
133
|
return presentedSheetStack.lastOrNull { it.rootContainerView == rootContainer } == sheetView
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Returns the topmost presented sheet, or null if none.
|
|
139
|
+
*/
|
|
140
|
+
@JvmStatic
|
|
141
|
+
fun getTopmostSheet(): TrueSheetView? {
|
|
142
|
+
synchronized(presentedSheetStack) {
|
|
143
|
+
return presentedSheetStack.lastOrNull { it.viewController.isPresented && it.viewController.isSheetVisible }
|
|
144
|
+
}
|
|
145
|
+
}
|
|
136
146
|
}
|
|
@@ -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