@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.
@@ -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
- private fun isFocusedViewWithinSheet(): Boolean {
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
- hideForModal()
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 rootContainer = sheetView.rootContainerView
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
- if (index <= 0) return
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
- if (index <= 0) return null
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 imm = reactContext.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
17
- reactContext.currentActivity?.currentFocus?.let { focusedView ->
18
- imm?.hideSoftInputFromWindow(focusedView.windowToken, 0)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lodev09/react-native-true-sheet",
3
- "version": "3.7.0-beta.3",
3
+ "version": "3.7.0-beta.5",
4
4
  "description": "The true native bottom sheet experience for your React Native Apps.",
5
5
  "source": "./src/index.ts",
6
6
  "main": "./lib/module/index.js",