@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.
@@ -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
- private fun isFocusedViewWithinSheet(): Boolean {
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
- hideForModal()
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 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.2",
3
+ "version": "3.7.0-beta.4",
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",