@lodev09/react-native-true-sheet 3.7.0 → 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 -8
- 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
|
*/
|
|
@@ -109,6 +109,10 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
override fun addView(child: View?, index: Int) {
|
|
112
|
+
if (child is TrueSheetContainerView) {
|
|
113
|
+
viewController.removeSheetSnapshot()
|
|
114
|
+
}
|
|
115
|
+
|
|
112
116
|
viewController.addView(child, index)
|
|
113
117
|
|
|
114
118
|
if (child is TrueSheetContainerView) {
|
|
@@ -128,10 +132,11 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
128
132
|
val child = getChildAt(index)
|
|
129
133
|
if (child is TrueSheetContainerView) {
|
|
130
134
|
child.delegate = null
|
|
135
|
+
viewController.createSheetSnapshot()
|
|
131
136
|
|
|
132
|
-
// Dismiss
|
|
137
|
+
// Dismiss when container is removed
|
|
133
138
|
if (viewController.isPresented) {
|
|
134
|
-
|
|
139
|
+
dismissAll(true) {}
|
|
135
140
|
}
|
|
136
141
|
}
|
|
137
142
|
viewController.removeView(child)
|
|
@@ -157,13 +162,19 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
157
162
|
fun onDropInstance() {
|
|
158
163
|
reactContext.removeLifecycleEventListener(this)
|
|
159
164
|
|
|
160
|
-
viewController.dismiss()
|
|
161
|
-
viewController.delegate = null
|
|
162
|
-
|
|
163
165
|
TrueSheetModule.unregisterView(id)
|
|
164
166
|
TrueSheetStackManager.removeSheet(this)
|
|
165
167
|
|
|
166
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
|
+
}
|
|
167
178
|
}
|
|
168
179
|
|
|
169
180
|
/**
|
|
@@ -310,6 +321,16 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
310
321
|
viewController.dismiss(animated)
|
|
311
322
|
}
|
|
312
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
|
+
|
|
313
334
|
@UiThread
|
|
314
335
|
fun resize(detentIndex: Int, promiseCallback: () -> Unit) {
|
|
315
336
|
if (!viewController.isPresented) {
|
|
@@ -326,12 +347,13 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
326
347
|
* Uses post() to ensure all layout passes complete before reconfiguring.
|
|
327
348
|
*/
|
|
328
349
|
fun updateSheetIfNeeded() {
|
|
329
|
-
if (!viewController.isPresented) return
|
|
330
|
-
if (isSheetUpdatePending) return
|
|
350
|
+
if (!viewController.isPresented || isSheetUpdatePending) return
|
|
331
351
|
|
|
332
352
|
isSheetUpdatePending = true
|
|
333
353
|
viewController.post {
|
|
334
354
|
isSheetUpdatePending = false
|
|
355
|
+
if (viewController.containerView == null) return@post
|
|
356
|
+
|
|
335
357
|
viewController.setupSheetDetentsForSizeChange()
|
|
336
358
|
TrueSheetStackManager.onSheetSizeChanged(this)
|
|
337
359
|
}
|
|
@@ -466,6 +488,10 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
466
488
|
eventDispatcher?.dispatchEvent(BackPressEvent(surfaceId, id))
|
|
467
489
|
}
|
|
468
490
|
|
|
491
|
+
override fun viewControllerDidDetectScreenDisappear() {
|
|
492
|
+
dismissAll(animated = true) {}
|
|
493
|
+
}
|
|
494
|
+
|
|
469
495
|
// ==================== TrueSheetContainerViewDelegate ====================
|
|
470
496
|
|
|
471
497
|
override fun containerViewContentDidChangeSize(width: Int, height: Int) {
|
|
@@ -489,7 +515,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
489
515
|
* of whichever window this view is in - whether that's the activity's window or a
|
|
490
516
|
* Modal's dialog window.
|
|
491
517
|
*/
|
|
492
|
-
|
|
518
|
+
override fun findRootContainerView(): ViewGroup? {
|
|
493
519
|
var current: android.view.ViewParent? = parent
|
|
494
520
|
|
|
495
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
|
package/ios/TrueSheetView.mm
CHANGED
|
@@ -45,6 +45,7 @@ using namespace facebook::react;
|
|
|
45
45
|
TrueSheetViewController *_controller;
|
|
46
46
|
RCTSurfaceTouchHandler *_touchHandler;
|
|
47
47
|
TrueSheetViewShadowNode::ConcreteState::Shared _state;
|
|
48
|
+
UIView *_snapshotView;
|
|
48
49
|
CGSize _lastStateSize;
|
|
49
50
|
NSInteger _initialDetentIndex;
|
|
50
51
|
BOOL _scrollable;
|
|
@@ -69,6 +70,7 @@ using namespace facebook::react;
|
|
|
69
70
|
|
|
70
71
|
_touchHandler = [[RCTSurfaceTouchHandler alloc] init];
|
|
71
72
|
_containerView = nil;
|
|
73
|
+
_snapshotView = nil;
|
|
72
74
|
_lastStateSize = CGSizeZero;
|
|
73
75
|
_initialDetentIndex = -1;
|
|
74
76
|
_initialDetentAnimated = YES;
|
|
@@ -107,6 +109,9 @@ using namespace facebook::react;
|
|
|
107
109
|
_controller.delegate = nil;
|
|
108
110
|
_controller = nil;
|
|
109
111
|
|
|
112
|
+
[_snapshotView removeFromSuperview];
|
|
113
|
+
_snapshotView = nil;
|
|
114
|
+
|
|
110
115
|
[TrueSheetModule unregisterViewWithTag:@(self.tag)];
|
|
111
116
|
}
|
|
112
117
|
|
|
@@ -280,6 +285,11 @@ using namespace facebook::react;
|
|
|
280
285
|
return;
|
|
281
286
|
}
|
|
282
287
|
|
|
288
|
+
if (_snapshotView) {
|
|
289
|
+
[_snapshotView removeFromSuperview];
|
|
290
|
+
_snapshotView = nil;
|
|
291
|
+
}
|
|
292
|
+
|
|
283
293
|
_containerView = (TrueSheetContainerView *)childComponentView;
|
|
284
294
|
_containerView.delegate = self;
|
|
285
295
|
|
|
@@ -308,6 +318,14 @@ using namespace facebook::react;
|
|
|
308
318
|
if (![childComponentView isKindOfClass:[TrueSheetContainerView class]])
|
|
309
319
|
return;
|
|
310
320
|
|
|
321
|
+
UIView *superView = _containerView.superview;
|
|
322
|
+
UIView *snapshot = [_containerView snapshotViewAfterScreenUpdates:NO];
|
|
323
|
+
if (snapshot) {
|
|
324
|
+
snapshot.frame = _containerView.frame;
|
|
325
|
+
[superView insertSubview:snapshot belowSubview:_containerView];
|
|
326
|
+
_snapshotView = snapshot;
|
|
327
|
+
}
|
|
328
|
+
|
|
311
329
|
_containerView.delegate = nil;
|
|
312
330
|
|
|
313
331
|
if (_touchHandler) {
|
|
@@ -396,6 +414,30 @@ using namespace facebook::react;
|
|
|
396
414
|
[self presentAtIndex:index animated:YES completion:completion];
|
|
397
415
|
}
|
|
398
416
|
|
|
417
|
+
- (TrueSheetViewController *)viewController {
|
|
418
|
+
return _controller;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
- (void)dismissAllAnimated:(BOOL)animated completion:(nullable TrueSheetCompletionBlock)completion {
|
|
422
|
+
if (!_controller.isPresented) {
|
|
423
|
+
if (completion) {
|
|
424
|
+
completion(YES, nil);
|
|
425
|
+
}
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
[self viewControllerDidChangePosition:-1 position:_controller.screenHeight detent:0 realtime:NO];
|
|
430
|
+
|
|
431
|
+
// Dismiss from the presenting view controller to dismiss this sheet and all its children
|
|
432
|
+
UIViewController *presenter = _controller.presentingViewController;
|
|
433
|
+
[presenter dismissViewControllerAnimated:animated
|
|
434
|
+
completion:^{
|
|
435
|
+
if (completion) {
|
|
436
|
+
completion(YES, nil);
|
|
437
|
+
}
|
|
438
|
+
}];
|
|
439
|
+
}
|
|
440
|
+
|
|
399
441
|
#pragma mark - TrueSheetContainerViewDelegate
|
|
400
442
|
|
|
401
443
|
/**
|
|
@@ -409,6 +451,9 @@ using namespace facebook::react;
|
|
|
409
451
|
|
|
410
452
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
411
453
|
self->_isSheetUpdatePending = NO;
|
|
454
|
+
if (!self->_containerView)
|
|
455
|
+
return;
|
|
456
|
+
|
|
412
457
|
[self->_controller setupSheetDetentsForSizeChange];
|
|
413
458
|
});
|
|
414
459
|
}
|
|
@@ -499,6 +544,10 @@ using namespace facebook::react;
|
|
|
499
544
|
[TrueSheetFocusEvents emitDidBlur:_eventEmitter];
|
|
500
545
|
}
|
|
501
546
|
|
|
547
|
+
- (void)viewControllerDidDetectScreenDisappear {
|
|
548
|
+
[self dismissAllAnimated:YES completion:nil];
|
|
549
|
+
}
|
|
550
|
+
|
|
502
551
|
#pragma mark - Private Helpers
|
|
503
552
|
|
|
504
553
|
- (UIViewController *)findPresentingViewController {
|