@lodev09/react-native-true-sheet 3.7.0-beta.5 → 3.7.1
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/TrueSheetModule.kt +25 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +34 -16
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +60 -3
- package/android/src/main/java/com/lodev09/truesheet/core/RNScreensFragmentObserver.kt +26 -1
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +32 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetGrabberView.kt +26 -10
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetKeyboardObserver.kt +2 -2
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetStackManager.kt +26 -3
- package/ios/TrueSheetModule.mm +39 -0
- package/ios/TrueSheetView.h +4 -0
- package/ios/TrueSheetView.mm +49 -0
- package/ios/TrueSheetViewController.h +16 -0
- package/ios/TrueSheetViewController.mm +16 -0
- package/lib/module/TrueSheet.js +10 -0
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +34 -7
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/TrueSheetProvider.js +2 -1
- package/lib/module/TrueSheetProvider.js.map +1 -1
- package/lib/module/TrueSheetProvider.web.js +24 -1
- package/lib/module/TrueSheetProvider.web.js.map +1 -1
- package/lib/module/mocks/index.js +3 -1
- package/lib/module/mocks/index.js.map +1 -1
- package/lib/module/specs/NativeTrueSheetModule.js.map +1 -1
- package/lib/typescript/src/TrueSheet.d.ts +7 -0
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +4 -0
- package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.web.d.ts +9 -1
- package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheetProvider.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheetProvider.web.d.ts +5 -0
- package/lib/typescript/src/TrueSheetProvider.web.d.ts.map +1 -1
- package/lib/typescript/src/mocks/index.d.ts +1 -0
- package/lib/typescript/src/mocks/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/NativeTrueSheetModule.d.ts +6 -0
- package/lib/typescript/src/specs/NativeTrueSheetModule.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/TrueSheet.tsx +10 -0
- package/src/TrueSheet.types.ts +4 -0
- package/src/TrueSheet.web.tsx +50 -6
- package/src/TrueSheetProvider.tsx +1 -0
- package/src/TrueSheetProvider.web.tsx +26 -0
- package/src/mocks/index.ts +2 -0
- package/src/specs/NativeTrueSheetModule.ts +7 -0
|
@@ -95,6 +95,31 @@ class TrueSheetModule(reactContext: ReactApplicationContext) :
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Dismiss all presented sheets by dismissing from the bottom of the stack
|
|
100
|
+
*
|
|
101
|
+
* @param animated Whether to animate the dismissals
|
|
102
|
+
* @param promise Promise that resolves when all sheets are dismissed
|
|
103
|
+
*/
|
|
104
|
+
@ReactMethod
|
|
105
|
+
fun dismissAll(animated: Boolean, promise: Promise) {
|
|
106
|
+
Handler(Looper.getMainLooper()).post {
|
|
107
|
+
try {
|
|
108
|
+
val rootSheet = TrueSheetStackManager.getRootSheet()
|
|
109
|
+
if (rootSheet == null) {
|
|
110
|
+
promise.resolve(null)
|
|
111
|
+
return@post
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
rootSheet.dismissAll(animated) {
|
|
115
|
+
promise.resolve(null)
|
|
116
|
+
}
|
|
117
|
+
} catch (e: Exception) {
|
|
118
|
+
promise.reject("OPERATION_FAILED", "Failed to dismiss all sheets: ${e.message}", e)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
98
123
|
/**
|
|
99
124
|
* Helper method to get TrueSheetView by tag and execute closure
|
|
100
125
|
*/
|
|
@@ -39,10 +39,6 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
39
39
|
// ==================== Properties ====================
|
|
40
40
|
|
|
41
41
|
internal val viewController: TrueSheetViewController = TrueSheetViewController(reactContext)
|
|
42
|
-
|
|
43
|
-
private val containerView: TrueSheetContainerView?
|
|
44
|
-
get() = viewController.getChildAt(0) as? TrueSheetContainerView
|
|
45
|
-
|
|
46
42
|
override var eventDispatcher: EventDispatcher? = null
|
|
47
43
|
|
|
48
44
|
// Initial present configuration (set by ViewManager before mount)
|
|
@@ -81,10 +77,6 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
81
77
|
|
|
82
78
|
// ==================== ReactViewGroup Overrides ====================
|
|
83
79
|
|
|
84
|
-
override fun dispatchProvideStructure(structure: ViewStructure) {
|
|
85
|
-
super.dispatchProvideStructure(structure)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
80
|
override fun onLayout(
|
|
89
81
|
changed: Boolean,
|
|
90
82
|
left: Int,
|
|
@@ -117,6 +109,10 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
117
109
|
}
|
|
118
110
|
|
|
119
111
|
override fun addView(child: View?, index: Int) {
|
|
112
|
+
if (child is TrueSheetContainerView) {
|
|
113
|
+
viewController.removeSheetSnapshot()
|
|
114
|
+
}
|
|
115
|
+
|
|
120
116
|
viewController.addView(child, index)
|
|
121
117
|
|
|
122
118
|
if (child is TrueSheetContainerView) {
|
|
@@ -136,10 +132,11 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
136
132
|
val child = getChildAt(index)
|
|
137
133
|
if (child is TrueSheetContainerView) {
|
|
138
134
|
child.delegate = null
|
|
135
|
+
viewController.createSheetSnapshot()
|
|
139
136
|
|
|
140
|
-
// Dismiss
|
|
137
|
+
// Dismiss when container is removed
|
|
141
138
|
if (viewController.isPresented) {
|
|
142
|
-
|
|
139
|
+
dismissAll(true) {}
|
|
143
140
|
}
|
|
144
141
|
}
|
|
145
142
|
viewController.removeView(child)
|
|
@@ -165,13 +162,19 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
165
162
|
fun onDropInstance() {
|
|
166
163
|
reactContext.removeLifecycleEventListener(this)
|
|
167
164
|
|
|
168
|
-
viewController.dismiss()
|
|
169
|
-
viewController.delegate = null
|
|
170
|
-
|
|
171
165
|
TrueSheetModule.unregisterView(id)
|
|
172
166
|
TrueSheetStackManager.removeSheet(this)
|
|
173
167
|
|
|
174
168
|
didInitiallyPresent = false
|
|
169
|
+
|
|
170
|
+
if (viewController.isPresented) {
|
|
171
|
+
viewController.dismissPromise = {
|
|
172
|
+
viewController.delegate = null
|
|
173
|
+
}
|
|
174
|
+
viewController.dismiss()
|
|
175
|
+
} else {
|
|
176
|
+
viewController.delegate = null
|
|
177
|
+
}
|
|
175
178
|
}
|
|
176
179
|
|
|
177
180
|
/**
|
|
@@ -318,6 +321,16 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
318
321
|
viewController.dismiss(animated)
|
|
319
322
|
}
|
|
320
323
|
|
|
324
|
+
@UiThread
|
|
325
|
+
fun dismissAll(animated: Boolean = true, promiseCallback: () -> Unit) {
|
|
326
|
+
// Dismiss all sheets above first
|
|
327
|
+
dismiss(animated) {}
|
|
328
|
+
|
|
329
|
+
// Then dismiss itself
|
|
330
|
+
viewController.dismissPromise = promiseCallback
|
|
331
|
+
viewController.dismiss(animated)
|
|
332
|
+
}
|
|
333
|
+
|
|
321
334
|
@UiThread
|
|
322
335
|
fun resize(detentIndex: Int, promiseCallback: () -> Unit) {
|
|
323
336
|
if (!viewController.isPresented) {
|
|
@@ -334,12 +347,13 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
334
347
|
* Uses post() to ensure all layout passes complete before reconfiguring.
|
|
335
348
|
*/
|
|
336
349
|
fun updateSheetIfNeeded() {
|
|
337
|
-
if (!viewController.isPresented) return
|
|
338
|
-
if (isSheetUpdatePending) return
|
|
350
|
+
if (!viewController.isPresented || isSheetUpdatePending) return
|
|
339
351
|
|
|
340
352
|
isSheetUpdatePending = true
|
|
341
353
|
viewController.post {
|
|
342
354
|
isSheetUpdatePending = false
|
|
355
|
+
if (viewController.containerView == null) return@post
|
|
356
|
+
|
|
343
357
|
viewController.setupSheetDetentsForSizeChange()
|
|
344
358
|
TrueSheetStackManager.onSheetSizeChanged(this)
|
|
345
359
|
}
|
|
@@ -474,6 +488,10 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
474
488
|
eventDispatcher?.dispatchEvent(BackPressEvent(surfaceId, id))
|
|
475
489
|
}
|
|
476
490
|
|
|
491
|
+
override fun viewControllerDidDetectScreenDisappear() {
|
|
492
|
+
dismissAll(animated = true) {}
|
|
493
|
+
}
|
|
494
|
+
|
|
477
495
|
// ==================== TrueSheetContainerViewDelegate ====================
|
|
478
496
|
|
|
479
497
|
override fun containerViewContentDidChangeSize(width: Int, height: Int) {
|
|
@@ -497,7 +515,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
497
515
|
* of whichever window this view is in - whether that's the activity's window or a
|
|
498
516
|
* Modal's dialog window.
|
|
499
517
|
*/
|
|
500
|
-
|
|
518
|
+
override fun findRootContainerView(): ViewGroup? {
|
|
501
519
|
var current: android.view.ViewParent? = parent
|
|
502
520
|
|
|
503
521
|
while (current != null) {
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
package com.lodev09.truesheet
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
|
+
import android.graphics.Bitmap
|
|
5
|
+
import android.graphics.Canvas
|
|
4
6
|
import android.os.Build
|
|
5
7
|
import android.view.MotionEvent
|
|
6
8
|
import android.view.View
|
|
7
9
|
import android.view.ViewGroup
|
|
8
10
|
import android.view.accessibility.AccessibilityNodeInfo
|
|
11
|
+
import android.widget.ImageView
|
|
9
12
|
import androidx.activity.OnBackPressedCallback
|
|
10
13
|
import androidx.appcompat.app.AppCompatActivity
|
|
11
14
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
15
|
+
import androidx.core.graphics.createBitmap
|
|
12
16
|
import androidx.core.view.isNotEmpty
|
|
13
17
|
import androidx.core.view.isVisible
|
|
14
18
|
import com.facebook.react.R
|
|
@@ -46,6 +50,7 @@ data class DetentInfo(val index: Int, val position: Float)
|
|
|
46
50
|
|
|
47
51
|
interface TrueSheetViewControllerDelegate {
|
|
48
52
|
val eventDispatcher: EventDispatcher?
|
|
53
|
+
fun findRootContainerView(): ViewGroup?
|
|
49
54
|
fun viewControllerWillPresent(index: Int, position: Float, detent: Float)
|
|
50
55
|
fun viewControllerDidPresent(index: Int, position: Float, detent: Float)
|
|
51
56
|
fun viewControllerWillDismiss()
|
|
@@ -61,6 +66,7 @@ interface TrueSheetViewControllerDelegate {
|
|
|
61
66
|
fun viewControllerWillBlur()
|
|
62
67
|
fun viewControllerDidBlur()
|
|
63
68
|
fun viewControllerDidBackPress()
|
|
69
|
+
fun viewControllerDidDetectScreenDisappear()
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
// =============================================================================
|
|
@@ -130,7 +136,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
130
136
|
|
|
131
137
|
private var interactionState: InteractionState = InteractionState.Idle
|
|
132
138
|
private var isDismissing = false
|
|
133
|
-
|
|
139
|
+
internal var wasHiddenByModal = false
|
|
134
140
|
private var shouldAnimatePresent = false
|
|
135
141
|
private var isPresentAnimating = false
|
|
136
142
|
|
|
@@ -213,7 +219,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
213
219
|
private val behavior: BottomSheetBehavior<TrueSheetBottomSheetView>?
|
|
214
220
|
get() = sheetView?.behavior
|
|
215
221
|
|
|
216
|
-
|
|
222
|
+
internal val containerView: TrueSheetContainerView?
|
|
217
223
|
get() = if (this.isNotEmpty()) getChildAt(0) as? TrueSheetContainerView else null
|
|
218
224
|
|
|
219
225
|
// Screen Measurements
|
|
@@ -320,6 +326,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
320
326
|
parentDimView?.detach()
|
|
321
327
|
parentDimView = null
|
|
322
328
|
|
|
329
|
+
// Cleanup snapshot
|
|
330
|
+
removeSheetSnapshot()
|
|
331
|
+
|
|
323
332
|
// Detach content from sheet
|
|
324
333
|
sheetView?.removeView(this)
|
|
325
334
|
|
|
@@ -338,6 +347,34 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
338
347
|
shouldAnimatePresent = true
|
|
339
348
|
}
|
|
340
349
|
|
|
350
|
+
private var snapshotView: View? = null
|
|
351
|
+
|
|
352
|
+
fun createSheetSnapshot() {
|
|
353
|
+
if (!isPresented) return
|
|
354
|
+
val sheet = sheetView ?: return
|
|
355
|
+
if (sheet.width <= 0 || sheet.height <= 0) return
|
|
356
|
+
|
|
357
|
+
val bitmap = createBitmap(sheet.width, sheet.height)
|
|
358
|
+
val canvas = Canvas(bitmap)
|
|
359
|
+
sheet.draw(canvas)
|
|
360
|
+
|
|
361
|
+
val snapshot = ImageView(reactContext).apply {
|
|
362
|
+
setImageBitmap(bitmap)
|
|
363
|
+
layoutParams = LayoutParams(sheet.width, sheet.height)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
sheet.addView(snapshot, 0)
|
|
367
|
+
snapshotView = snapshot
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
fun removeSheetSnapshot() {
|
|
371
|
+
snapshotView?.let {
|
|
372
|
+
(it as? ImageView)?.setImageDrawable(null)
|
|
373
|
+
sheetView?.removeView(it)
|
|
374
|
+
}
|
|
375
|
+
snapshotView = null
|
|
376
|
+
}
|
|
377
|
+
|
|
341
378
|
// =============================================================================
|
|
342
379
|
// MARK: - Back Button Handling
|
|
343
380
|
// =============================================================================
|
|
@@ -405,6 +442,19 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
405
442
|
dismissOrCollapseToLowest()
|
|
406
443
|
}
|
|
407
444
|
|
|
445
|
+
// =============================================================================
|
|
446
|
+
// MARK: - TrueSheetBottomSheetViewDelegate - Grabber Tap
|
|
447
|
+
// =============================================================================
|
|
448
|
+
|
|
449
|
+
override fun bottomSheetViewDidTapGrabber() {
|
|
450
|
+
val nextIndex = (currentDetentIndex + 1) % detents.size
|
|
451
|
+
if (nextIndex == 0 && detents.size == 1 && dismissible) {
|
|
452
|
+
dismiss(animated = true)
|
|
453
|
+
} else {
|
|
454
|
+
setStateForDetentIndex(nextIndex)
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
408
458
|
// =============================================================================
|
|
409
459
|
// MARK: - BottomSheetCallback
|
|
410
460
|
// =============================================================================
|
|
@@ -534,6 +584,12 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
534
584
|
post { parent.showAfterModal() }
|
|
535
585
|
}
|
|
536
586
|
}
|
|
587
|
+
},
|
|
588
|
+
onNonModalScreenPushed = {
|
|
589
|
+
// Only handle on root sheet (no parent) to trigger dismissAll
|
|
590
|
+
if (isPresented && isSheetVisible && parentSheetView == null) {
|
|
591
|
+
delegate?.viewControllerDidDetectScreenDisappear()
|
|
592
|
+
}
|
|
537
593
|
}
|
|
538
594
|
)
|
|
539
595
|
rnScreensObserver?.start()
|
|
@@ -892,7 +948,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
892
948
|
}
|
|
893
949
|
|
|
894
950
|
fun saveFocusedView() {
|
|
895
|
-
focusedViewBeforeBlur =
|
|
951
|
+
focusedViewBeforeBlur = delegate?.findRootContainerView()?.findFocus()
|
|
952
|
+
?: reactContext.currentActivity?.currentFocus
|
|
896
953
|
}
|
|
897
954
|
|
|
898
955
|
fun restoreFocusedView() {
|
|
@@ -18,7 +18,8 @@ class RNScreensFragmentObserver(
|
|
|
18
18
|
private val reactContext: ReactContext,
|
|
19
19
|
private val onModalPresented: () -> Unit,
|
|
20
20
|
private val onModalWillDismiss: () -> Unit,
|
|
21
|
-
private val onModalDidDismiss: () -> Unit
|
|
21
|
+
private val onModalDidDismiss: () -> Unit,
|
|
22
|
+
private val onNonModalScreenPushed: () -> Unit
|
|
22
23
|
) {
|
|
23
24
|
private var fragmentLifecycleCallback: FragmentManager.FragmentLifecycleCallbacks? = null
|
|
24
25
|
private var activityLifecycleObserver: DefaultLifecycleObserver? = null
|
|
@@ -61,6 +62,9 @@ class RNScreensFragmentObserver(
|
|
|
61
62
|
if (activeModalFragments.size == 1) {
|
|
62
63
|
onModalPresented()
|
|
63
64
|
}
|
|
65
|
+
} else if (activeModalFragments.isEmpty() && isNonModalScreenFragment(f)) {
|
|
66
|
+
// Only trigger non-modal push when no modals are active
|
|
67
|
+
onNonModalScreenPushed()
|
|
64
68
|
}
|
|
65
69
|
}
|
|
66
70
|
|
|
@@ -145,6 +149,27 @@ class RNScreensFragmentObserver(
|
|
|
145
149
|
*/
|
|
146
150
|
private fun isScreensFragment(fragment: Fragment): Boolean = fragment.javaClass.name.startsWith(RN_SCREENS_PACKAGE)
|
|
147
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Check if fragment is a non-modal screen (regular push presentation).
|
|
154
|
+
*/
|
|
155
|
+
private fun isNonModalScreenFragment(fragment: Fragment): Boolean {
|
|
156
|
+
if (!isScreensFragment(fragment)) return false
|
|
157
|
+
// ScreenModalFragment is always a modal
|
|
158
|
+
if (fragment.javaClass.name.contains("ScreenModalFragment")) return false
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
val getScreenMethod = fragment.javaClass.getMethod("getScreen")
|
|
162
|
+
val screen = getScreenMethod.invoke(fragment) ?: return false
|
|
163
|
+
|
|
164
|
+
val getStackPresentationMethod = screen.javaClass.getMethod("getStackPresentation")
|
|
165
|
+
val stackPresentation = getStackPresentationMethod.invoke(screen) ?: return false
|
|
166
|
+
|
|
167
|
+
return stackPresentation.toString() == "PUSH"
|
|
168
|
+
} catch (e: Exception) {
|
|
169
|
+
return false
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
148
173
|
/**
|
|
149
174
|
* Check if fragment is a react-native-screens modal (fullScreenModal, transparentModal, or formSheet).
|
|
150
175
|
* Uses reflection to check the fragment's screen.stackPresentation property.
|
|
@@ -3,10 +3,13 @@ package com.lodev09.truesheet.core
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
4
|
import android.graphics.Color
|
|
5
5
|
import android.graphics.Outline
|
|
6
|
+
import android.graphics.Rect
|
|
6
7
|
import android.graphics.drawable.ShapeDrawable
|
|
7
8
|
import android.graphics.drawable.shapes.RoundRectShape
|
|
8
9
|
import android.util.TypedValue
|
|
10
|
+
import android.view.GestureDetector
|
|
9
11
|
import android.view.Gravity
|
|
12
|
+
import android.view.MotionEvent
|
|
10
13
|
import android.view.View
|
|
11
14
|
import android.view.ViewOutlineProvider
|
|
12
15
|
import android.widget.FrameLayout
|
|
@@ -22,6 +25,7 @@ interface TrueSheetBottomSheetViewDelegate {
|
|
|
22
25
|
val sheetBackgroundColor: Int?
|
|
23
26
|
val grabber: Boolean
|
|
24
27
|
val grabberOptions: GrabberOptions?
|
|
28
|
+
fun bottomSheetViewDidTapGrabber()
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
/**
|
|
@@ -162,4 +166,32 @@ class TrueSheetBottomSheetView(private val reactContext: ThemedReactContext) : F
|
|
|
162
166
|
|
|
163
167
|
addView(grabberView, 0)
|
|
164
168
|
}
|
|
169
|
+
|
|
170
|
+
// =============================================================================
|
|
171
|
+
// MARK: - Grabber Tap Detection
|
|
172
|
+
// =============================================================================
|
|
173
|
+
|
|
174
|
+
private val grabberTapDetector = GestureDetector(
|
|
175
|
+
context,
|
|
176
|
+
object : GestureDetector.SimpleOnGestureListener() {
|
|
177
|
+
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
|
178
|
+
delegate?.bottomSheetViewDidTapGrabber()
|
|
179
|
+
return true
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
|
|
185
|
+
val grabber = findViewWithTag<View>(GRABBER_TAG)
|
|
186
|
+
if (grabber != null) {
|
|
187
|
+
val rect = Rect()
|
|
188
|
+
grabber.getHitRect(rect)
|
|
189
|
+
if (rect.contains(ev.x.toInt(), ev.y.toInt())) {
|
|
190
|
+
if (grabberTapDetector.onTouchEvent(ev)) {
|
|
191
|
+
return true
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return super.dispatchTouchEvent(ev)
|
|
196
|
+
}
|
|
165
197
|
}
|
|
@@ -25,16 +25,18 @@ data class GrabberOptions(
|
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Native grabber (drag handle) view for the bottom sheet.
|
|
28
|
-
* Displays a small pill-shaped indicator at the top of the sheet.
|
|
28
|
+
* Displays a small pill-shaped indicator at the top of the sheet with a tappable hitbox.
|
|
29
29
|
*/
|
|
30
30
|
@SuppressLint("ViewConstructor")
|
|
31
|
-
class TrueSheetGrabberView(context: Context, private val options: GrabberOptions? = null) :
|
|
31
|
+
class TrueSheetGrabberView(context: Context, private val options: GrabberOptions? = null) : FrameLayout(context) {
|
|
32
32
|
|
|
33
33
|
companion object {
|
|
34
34
|
private const val DEFAULT_WIDTH = 32f // dp
|
|
35
35
|
private const val DEFAULT_HEIGHT = 4f // dp
|
|
36
36
|
private const val DEFAULT_TOP_MARGIN = 16f // dp
|
|
37
37
|
private const val DEFAULT_ALPHA = 0.4f
|
|
38
|
+
private const val HITBOX_PADDING_HORIZONTAL = 16f // dp
|
|
39
|
+
private const val HITBOX_PADDING_VERTICAL = 16f // dp
|
|
38
40
|
private val DEFAULT_COLOR = Color.argb((DEFAULT_ALPHA * 255).toInt(), 73, 69, 79) // #49454F @ 40%
|
|
39
41
|
}
|
|
40
42
|
|
|
@@ -57,19 +59,33 @@ class TrueSheetGrabberView(context: Context, private val options: GrabberOptions
|
|
|
57
59
|
get() = if (isAdaptive) getAdaptiveColor(options?.color) else options?.color ?: DEFAULT_COLOR
|
|
58
60
|
|
|
59
61
|
init {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
val hitboxWidth = grabberWidth + (HITBOX_PADDING_HORIZONTAL * 2)
|
|
63
|
+
val hitboxHeight = grabberHeight + (HITBOX_PADDING_VERTICAL * 2)
|
|
64
|
+
|
|
65
|
+
layoutParams = LayoutParams(
|
|
66
|
+
hitboxWidth.dpToPx().toInt(),
|
|
67
|
+
hitboxHeight.dpToPx().toInt()
|
|
63
68
|
).apply {
|
|
64
69
|
gravity = Gravity.CENTER_HORIZONTAL or Gravity.TOP
|
|
65
|
-
topMargin = grabberTopMargin.dpToPx().toInt()
|
|
70
|
+
topMargin = (grabberTopMargin - HITBOX_PADDING_VERTICAL).dpToPx().toInt()
|
|
66
71
|
}
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
// The visible pill centered inside the hitbox
|
|
74
|
+
val pillView = View(context).apply {
|
|
75
|
+
layoutParams = LayoutParams(
|
|
76
|
+
grabberWidth.dpToPx().toInt(),
|
|
77
|
+
grabberHeight.dpToPx().toInt()
|
|
78
|
+
).apply {
|
|
79
|
+
gravity = Gravity.CENTER
|
|
80
|
+
}
|
|
81
|
+
background = GradientDrawable().apply {
|
|
82
|
+
shape = GradientDrawable.RECTANGLE
|
|
83
|
+
cornerRadius = grabberCornerRadius.dpToPx()
|
|
84
|
+
setColor(grabberColor)
|
|
85
|
+
}
|
|
72
86
|
}
|
|
87
|
+
|
|
88
|
+
addView(pillView)
|
|
73
89
|
}
|
|
74
90
|
|
|
75
91
|
private fun getAdaptiveColor(baseColor: Int? = null): Int {
|
|
@@ -35,7 +35,7 @@ class TrueSheetKeyboardObserver(private val targetView: View, private val reactC
|
|
|
35
35
|
private set
|
|
36
36
|
|
|
37
37
|
fun isFocusedViewWithinSheet(sheetView: View): Boolean {
|
|
38
|
-
val focusedView =
|
|
38
|
+
val focusedView = targetView.rootView?.findFocus() ?: return false
|
|
39
39
|
var current: View? = focusedView
|
|
40
40
|
while (current != null && current !== targetView) {
|
|
41
41
|
if (current === sheetView) return true
|
|
@@ -131,7 +131,7 @@ class TrueSheetKeyboardObserver(private val targetView: View, private val reactC
|
|
|
131
131
|
// Ensure we don't add duplicate listeners
|
|
132
132
|
if (globalLayoutListener != null) return
|
|
133
133
|
|
|
134
|
-
val rootView =
|
|
134
|
+
val rootView = targetView.rootView ?: return
|
|
135
135
|
activityRootView = rootView
|
|
136
136
|
|
|
137
137
|
globalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener {
|
|
@@ -18,16 +18,16 @@ object TrueSheetStackManager {
|
|
|
18
18
|
*/
|
|
19
19
|
private fun getParentSheetAt(index: Int, rootContainer: ViewGroup?): TrueSheetView? {
|
|
20
20
|
if (index <= 0) return null
|
|
21
|
-
return presentedSheetStack[index - 1].takeIf { it.rootContainerView == rootContainer }
|
|
21
|
+
return presentedSheetStack[index - 1].takeIf { !it.viewController.wasHiddenByModal && it.rootContainerView == rootContainer }
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* Returns the topmost presented and visible sheet.
|
|
25
|
+
* Returns the topmost presented and visible sheet that is not hidden by modal.
|
|
26
26
|
* Must be called within synchronized block.
|
|
27
27
|
*/
|
|
28
28
|
private fun findTopmostSheet(): TrueSheetView? =
|
|
29
29
|
presentedSheetStack.lastOrNull {
|
|
30
|
-
it.viewController.isPresented && it.viewController.isSheetVisible
|
|
30
|
+
it.viewController.isPresented && it.viewController.isSheetVisible && !it.viewController.wasHiddenByModal
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
@@ -151,4 +151,27 @@ object TrueSheetStackManager {
|
|
|
151
151
|
return findTopmostSheet()
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Returns the root presented sheet for dismissAll.
|
|
157
|
+
* Starts from the topmost sheet and walks up to find the root of its stack.
|
|
158
|
+
* Stops at modal boundary (parent hidden by modal) or when there's no parent.
|
|
159
|
+
*/
|
|
160
|
+
@JvmStatic
|
|
161
|
+
fun getRootSheet(): TrueSheetView? {
|
|
162
|
+
synchronized(presentedSheetStack) {
|
|
163
|
+
val topmost = presentedSheetStack.lastOrNull { it.viewController.isPresented } ?: return null
|
|
164
|
+
|
|
165
|
+
var current: TrueSheetView = topmost
|
|
166
|
+
while (true) {
|
|
167
|
+
val parent = current.viewController.parentSheetView ?: return current
|
|
168
|
+
|
|
169
|
+
if (parent.viewController.wasHiddenByModal) {
|
|
170
|
+
return current
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
current = parent
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
154
177
|
}
|
package/ios/TrueSheetModule.mm
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
#import "TrueSheetModule.h"
|
|
15
15
|
#import <React/RCTUtils.h>
|
|
16
16
|
#import "TrueSheetView.h"
|
|
17
|
+
#import "TrueSheetViewController.h"
|
|
17
18
|
|
|
18
19
|
#import <TrueSheetSpec/TrueSheetSpec.h>
|
|
19
20
|
|
|
@@ -115,6 +116,44 @@ RCT_EXPORT_MODULE(TrueSheetModule)
|
|
|
115
116
|
});
|
|
116
117
|
}
|
|
117
118
|
|
|
119
|
+
- (void)dismissAll:(BOOL)animated resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
120
|
+
RCTExecuteOnMainQueue(^{
|
|
121
|
+
@synchronized(viewRegistry) {
|
|
122
|
+
// Find the root presented sheet (one without a parent TrueSheet)
|
|
123
|
+
TrueSheetView *rootSheet = nil;
|
|
124
|
+
|
|
125
|
+
for (TrueSheetView *view in viewRegistry.allValues) {
|
|
126
|
+
if (!view.viewController.isPresented) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
UIViewController *presenter = view.viewController.presentingViewController;
|
|
131
|
+
BOOL hasParentSheet = [presenter isKindOfClass:[TrueSheetViewController class]];
|
|
132
|
+
|
|
133
|
+
if (!hasParentSheet) {
|
|
134
|
+
rootSheet = view;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!rootSheet) {
|
|
140
|
+
resolve(nil);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
[rootSheet
|
|
145
|
+
dismissAllAnimated:animated
|
|
146
|
+
completion:^(BOOL success, NSError *_Nullable error) {
|
|
147
|
+
if (success) {
|
|
148
|
+
resolve(nil);
|
|
149
|
+
} else {
|
|
150
|
+
reject(@"DISMISS_FAILED", error.localizedDescription ?: @"Failed to dismiss sheets", error);
|
|
151
|
+
}
|
|
152
|
+
}];
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
118
157
|
#pragma mark - Helper Methods
|
|
119
158
|
|
|
120
159
|
+ (nullable TrueSheetView *)getTrueSheetViewByTag:(NSNumber *)reactTag {
|
package/ios/TrueSheetView.h
CHANGED
|
@@ -23,6 +23,8 @@ typedef void (^TrueSheetCompletionBlock)(BOOL success, NSError *_Nullable error)
|
|
|
23
23
|
|
|
24
24
|
@interface TrueSheetView : RCTViewComponentView
|
|
25
25
|
|
|
26
|
+
@property (nonatomic, readonly) TrueSheetViewController *viewController;
|
|
27
|
+
|
|
26
28
|
// TurboModule methods
|
|
27
29
|
- (void)presentAtIndex:(NSInteger)index
|
|
28
30
|
animated:(BOOL)animated
|
|
@@ -32,6 +34,8 @@ typedef void (^TrueSheetCompletionBlock)(BOOL success, NSError *_Nullable error)
|
|
|
32
34
|
|
|
33
35
|
- (void)resizeToIndex:(NSInteger)index completion:(nullable TrueSheetCompletionBlock)completion;
|
|
34
36
|
|
|
37
|
+
- (void)dismissAllAnimated:(BOOL)animated completion:(nullable TrueSheetCompletionBlock)completion;
|
|
38
|
+
|
|
35
39
|
@end
|
|
36
40
|
|
|
37
41
|
NS_ASSUME_NONNULL_END
|