react-native-nitro-modal 0.1.5 → 0.3.0-rc
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/README.md +65 -11
- package/android/src/main/java/com/margelo/nitro/nitromodal/BottomSheetPresenter.kt +335 -34
- package/android/src/main/java/com/margelo/nitro/nitromodal/HybridNitroModal.kt +63 -22
- package/android/src/main/java/com/margelo/nitro/nitromodal/ModalConfig.kt +31 -1
- package/android/src/main/java/com/margelo/nitro/nitromodal/ModalContentRoot.kt +48 -5
- package/android/src/main/java/com/margelo/nitro/nitromodal/ModalPresenter.kt +63 -4
- package/android/src/main/java/com/margelo/nitro/nitromodal/NitroModalHostView.kt +68 -8
- package/ios/HybridNitroModal.swift +111 -20
- package/ios/ModalConfig.swift +44 -2
- package/ios/NitroModalHostView.swift +73 -8
- package/ios/SheetView.swift +575 -0
- package/ios/SheetViewController.swift +19 -450
- package/lib/module/NitroModal.js +39 -7
- package/lib/module/NitroModal.js.map +1 -1
- package/lib/module/NitroModalView.js +4 -3
- package/lib/module/NitroModalView.js.map +1 -1
- package/lib/module/NitroModalView.native.js +3 -0
- package/lib/module/NitroModalView.native.js.map +1 -1
- package/lib/module/NitroModalView.web.js +159 -0
- package/lib/module/NitroModalView.web.js.map +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/web/AnimatedValue.js +126 -0
- package/lib/module/web/AnimatedValue.js.map +1 -0
- package/lib/module/web/ModalConfig.js +30 -0
- package/lib/module/web/ModalConfig.js.map +1 -0
- package/lib/module/web/ModalController.js +306 -0
- package/lib/module/web/ModalController.js.map +1 -0
- package/lib/module/web/ModalPresenter.js +280 -0
- package/lib/module/web/ModalPresenter.js.map +1 -0
- package/lib/module/web/PopupPresenter.js +117 -0
- package/lib/module/web/PopupPresenter.js.map +1 -0
- package/lib/module/web/SheetPresenter.js +494 -0
- package/lib/module/web/SheetPresenter.js.map +1 -0
- package/lib/module/web/dom.js +138 -0
- package/lib/module/web/dom.js.map +1 -0
- package/lib/typescript/src/NitroModal.d.ts +35 -4
- package/lib/typescript/src/NitroModal.d.ts.map +1 -1
- package/lib/typescript/src/NitroModal.nitro.d.ts +29 -2
- package/lib/typescript/src/NitroModal.nitro.d.ts.map +1 -1
- package/lib/typescript/src/NitroModalView.d.ts +2 -1
- package/lib/typescript/src/NitroModalView.d.ts.map +1 -1
- package/lib/typescript/src/NitroModalView.native.d.ts +1 -0
- package/lib/typescript/src/NitroModalView.native.d.ts.map +1 -1
- package/lib/typescript/src/NitroModalView.web.d.ts +6 -0
- package/lib/typescript/src/NitroModalView.web.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +2 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/web/AnimatedValue.d.ts +49 -0
- package/lib/typescript/src/web/AnimatedValue.d.ts.map +1 -0
- package/lib/typescript/src/web/ModalConfig.d.ts +30 -0
- package/lib/typescript/src/web/ModalConfig.d.ts.map +1 -0
- package/lib/typescript/src/web/ModalController.d.ts +70 -0
- package/lib/typescript/src/web/ModalController.d.ts.map +1 -0
- package/lib/typescript/src/web/ModalPresenter.d.ts +107 -0
- package/lib/typescript/src/web/ModalPresenter.d.ts.map +1 -0
- package/lib/typescript/src/web/PopupPresenter.d.ts +20 -0
- package/lib/typescript/src/web/PopupPresenter.d.ts.map +1 -0
- package/lib/typescript/src/web/SheetPresenter.d.ts +65 -0
- package/lib/typescript/src/web/SheetPresenter.d.ts.map +1 -0
- package/lib/typescript/src/web/dom.d.ts +42 -0
- package/lib/typescript/src/web/dom.d.ts.map +1 -0
- package/nitrogen/generated/android/c++/JHybridNitroModalSpec.cpp +60 -6
- package/nitrogen/generated/android/c++/JHybridNitroModalSpec.hpp +10 -2
- package/nitrogen/generated/android/c++/JNamedSheetDetent.hpp +64 -0
- package/nitrogen/generated/android/c++/JSheetDetent.cpp +26 -0
- package/nitrogen/generated/android/c++/JSheetDetent.hpp +42 -37
- package/nitrogen/generated/android/c++/views/JHybridNitroModalStateUpdater.cpp +20 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/HybridNitroModalSpec.kt +40 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/NamedSheetDetent.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/SheetDetent.kt +47 -10
- package/nitrogen/generated/android/nitromodal+autolinking.cmake +1 -0
- package/nitrogen/generated/ios/NitroModal-Swift-Cxx-Bridge.hpp +53 -8
- package/nitrogen/generated/ios/NitroModal-Swift-Cxx-Umbrella.hpp +4 -3
- package/nitrogen/generated/ios/c++/HybridNitroModalSpecSwift.hpp +32 -5
- package/nitrogen/generated/ios/c++/views/HybridNitroModalComponent.mm +24 -0
- package/nitrogen/generated/ios/swift/HybridNitroModalSpec.swift +4 -0
- package/nitrogen/generated/ios/swift/HybridNitroModalSpec_cxx.swift +110 -5
- package/nitrogen/generated/ios/swift/NamedSheetDetent.swift +48 -0
- package/nitrogen/generated/ios/swift/SheetDetent.swift +15 -33
- package/nitrogen/generated/shared/c++/HybridNitroModalSpec.cpp +8 -0
- package/nitrogen/generated/shared/c++/HybridNitroModalSpec.hpp +14 -5
- package/nitrogen/generated/shared/c++/{SheetDetent.hpp → NamedSheetDetent.hpp} +17 -17
- package/nitrogen/generated/shared/c++/views/HybridNitroModalComponent.cpp +9 -1
- package/nitrogen/generated/shared/c++/views/HybridNitroModalComponent.hpp +15 -2
- package/nitrogen/generated/shared/json/NitroModalConfig.json +4 -0
- package/package.json +3 -2
- package/src/NitroModal.nitro.ts +30 -2
- package/src/NitroModal.tsx +71 -8
- package/src/NitroModalView.native.tsx +4 -0
- package/src/NitroModalView.tsx +9 -4
- package/src/NitroModalView.web.tsx +180 -0
- package/src/index.tsx +2 -0
- package/src/web/AnimatedValue.ts +193 -0
- package/src/web/ModalConfig.ts +67 -0
- package/src/web/ModalController.ts +338 -0
- package/src/web/ModalPresenter.ts +333 -0
- package/src/web/PopupPresenter.ts +129 -0
- package/src/web/SheetPresenter.ts +647 -0
- package/src/web/dom.ts +171 -0
|
@@ -4,10 +4,35 @@ import android.content.Context
|
|
|
4
4
|
import android.content.res.Configuration
|
|
5
5
|
import android.graphics.Color
|
|
6
6
|
|
|
7
|
+
/** A resting height of a `bottomSheet`. */
|
|
8
|
+
internal sealed interface Detent {
|
|
9
|
+
data object Small : Detent
|
|
10
|
+
data object Medium : Detent
|
|
11
|
+
data object Large : Detent
|
|
12
|
+
data object FitContent : Detent
|
|
13
|
+
|
|
14
|
+
/** Content height in dp, above the bottom system inset. */
|
|
15
|
+
data class Height(val dp: Float) : Detent
|
|
16
|
+
|
|
17
|
+
companion object {
|
|
18
|
+
fun from(detent: SheetDetent): Detent = when (detent) {
|
|
19
|
+
is SheetDetent.First -> when (detent.value) {
|
|
20
|
+
NamedSheetDetent.SMALL -> Small
|
|
21
|
+
NamedSheetDetent.MEDIUM -> Medium
|
|
22
|
+
NamedSheetDetent.LARGE -> Large
|
|
23
|
+
NamedSheetDetent.FITCONTENT -> FitContent
|
|
24
|
+
}
|
|
25
|
+
is SheetDetent.Second -> if (detent.value.isFinite()) Height(detent.value.toFloat()) else Large
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
7
30
|
/** Snapshot of the React props that shape a presentation. */
|
|
8
31
|
internal data class ModalConfig(
|
|
9
32
|
val mode: ModalMode = ModalMode.BOTTOMSHEET,
|
|
10
|
-
|
|
33
|
+
/** An inline `bottomSheet` (see [isInline]), as opposed to a modal one. */
|
|
34
|
+
val inlinePresentation: Boolean = false,
|
|
35
|
+
val detents: List<Detent> = listOf(Detent.FitContent),
|
|
11
36
|
val initialDetentIndex: Int = 0,
|
|
12
37
|
val backdropColor: Int = Color.BLACK,
|
|
13
38
|
val backdropOpacity: Float = 0.4f,
|
|
@@ -23,7 +48,12 @@ internal data class ModalConfig(
|
|
|
23
48
|
val contentBackgroundColor: Int? = null,
|
|
24
49
|
val keyboardBehavior: KeyboardBehavior = KeyboardBehavior.PAN,
|
|
25
50
|
val popupAnimation: PopupAnimation = PopupAnimation.SCALE,
|
|
51
|
+
val pullToRefreshEnabled: Boolean = false,
|
|
26
52
|
) {
|
|
53
|
+
/** A `bottomSheet` living in the host view's bounds. Popups are always modal. */
|
|
54
|
+
val isInline: Boolean
|
|
55
|
+
get() = inlinePresentation && mode == ModalMode.BOTTOMSHEET
|
|
56
|
+
|
|
27
57
|
val clampedInitialDetentIndex: Int
|
|
28
58
|
get() = initialDetentIndex.coerceIn(0, (detents.size - 1).coerceAtLeast(0))
|
|
29
59
|
|
|
@@ -8,6 +8,7 @@ import com.facebook.react.config.ReactFeatureFlags
|
|
|
8
8
|
import com.facebook.react.uimanager.JSPointerDispatcher
|
|
9
9
|
import com.facebook.react.uimanager.JSTouchDispatcher
|
|
10
10
|
import com.facebook.react.uimanager.RootView
|
|
11
|
+
import com.facebook.react.uimanager.RootViewUtil
|
|
11
12
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
12
13
|
import com.facebook.react.uimanager.events.EventDispatcher
|
|
13
14
|
import com.facebook.react.views.view.ReactViewGroup
|
|
@@ -19,6 +20,9 @@ import com.facebook.react.views.view.ReactViewGroup
|
|
|
19
20
|
* root view. Like React Native's own `Modal`, this view is a [RootView] that
|
|
20
21
|
* forwards touch and pointer events to JS itself. Children are laid out by
|
|
21
22
|
* Fabric; [ReactViewGroup.onLayout] is a no-op.
|
|
23
|
+
*
|
|
24
|
+
* An inline sheet keeps this view inside the React root view, which already
|
|
25
|
+
* forwards its touches: then it only passes native gestures on to that root.
|
|
22
26
|
*/
|
|
23
27
|
@SuppressLint("ViewConstructor")
|
|
24
28
|
internal class ModalContentRoot(private val reactContext: ThemedReactContext) :
|
|
@@ -29,40 +33,68 @@ internal class ModalContentRoot(private val reactContext: ThemedReactContext) :
|
|
|
29
33
|
/** Called when a child's laid-out bounds change. */
|
|
30
34
|
var onContentSizeChange: (() -> Unit)? = null
|
|
31
35
|
|
|
36
|
+
/** Hosted by an inline sheet, inside the React root view. */
|
|
37
|
+
var isInline: Boolean = false
|
|
38
|
+
set(value) {
|
|
39
|
+
if (field == value) return
|
|
40
|
+
field = value
|
|
41
|
+
updateReactOrigin()
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
private val touchDispatcher = JSTouchDispatcher(this)
|
|
33
45
|
private val pointerDispatcher: JSPointerDispatcher? =
|
|
34
46
|
if (ReactFeatureFlags.dispatchPointerEvents) JSPointerDispatcher(this) else null
|
|
35
47
|
|
|
48
|
+
/** Dispatches touches to JS itself: only outside the React root view. */
|
|
49
|
+
private val activeEventDispatcher: EventDispatcher?
|
|
50
|
+
get() = if (isInline) null else eventDispatcher
|
|
51
|
+
|
|
36
52
|
private val childLayoutListener =
|
|
37
53
|
OnLayoutChangeListener { _, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
|
|
38
54
|
if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) {
|
|
55
|
+
updateReactOrigin()
|
|
39
56
|
onContentSizeChange?.invoke()
|
|
40
57
|
}
|
|
41
58
|
}
|
|
42
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Where React placed the children, in px. Inline, React lays the content
|
|
62
|
+
* out at the sheet's resting top so that `measure()` matches the screen;
|
|
63
|
+
* scrolling this view by the same amount cancels that offset out on screen.
|
|
64
|
+
*/
|
|
65
|
+
private val reactOriginY: Int
|
|
66
|
+
get() = if (isInline) (0 until childCount).minOfOrNull { getChildAt(it).top } ?: 0 else 0
|
|
67
|
+
|
|
43
68
|
/** Bounding size of the React children, in px. */
|
|
44
69
|
val contentWidth: Int
|
|
45
70
|
get() = (0 until childCount).maxOfOrNull { getChildAt(it).right } ?: 0
|
|
46
71
|
|
|
47
72
|
val contentHeight: Int
|
|
48
|
-
get() = (0 until childCount).maxOfOrNull { getChildAt(it).bottom } ?: 0
|
|
73
|
+
get() = ((0 until childCount).maxOfOrNull { getChildAt(it).bottom } ?: 0) - reactOriginY
|
|
49
74
|
|
|
50
75
|
override fun onViewAdded(child: View) {
|
|
51
76
|
super.onViewAdded(child)
|
|
52
77
|
child.addOnLayoutChangeListener(childLayoutListener)
|
|
78
|
+
updateReactOrigin()
|
|
53
79
|
onContentSizeChange?.invoke()
|
|
54
80
|
}
|
|
55
81
|
|
|
56
82
|
override fun onViewRemoved(child: View) {
|
|
57
83
|
super.onViewRemoved(child)
|
|
58
84
|
child.removeOnLayoutChangeListener(childLayoutListener)
|
|
85
|
+
updateReactOrigin()
|
|
59
86
|
onContentSizeChange?.invoke()
|
|
60
87
|
}
|
|
61
88
|
|
|
89
|
+
private fun updateReactOrigin() {
|
|
90
|
+
val originY = reactOriginY
|
|
91
|
+
if (scrollY != originY) scrollTo(0, originY)
|
|
92
|
+
}
|
|
93
|
+
|
|
62
94
|
// region Touch forwarding (mirrors ReactModalHostView.DialogRootViewGroup)
|
|
63
95
|
|
|
64
96
|
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
|
65
|
-
|
|
97
|
+
activeEventDispatcher?.let {
|
|
66
98
|
touchDispatcher.handleTouchEvent(event, it, reactContext)
|
|
67
99
|
pointerDispatcher?.handleMotionEvent(event, it, true)
|
|
68
100
|
}
|
|
@@ -71,7 +103,7 @@ internal class ModalContentRoot(private val reactContext: ThemedReactContext) :
|
|
|
71
103
|
|
|
72
104
|
@SuppressLint("ClickableViewAccessibility")
|
|
73
105
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
74
|
-
|
|
106
|
+
activeEventDispatcher?.let {
|
|
75
107
|
touchDispatcher.handleTouchEvent(event, it, reactContext)
|
|
76
108
|
pointerDispatcher?.handleMotionEvent(event, it, false)
|
|
77
109
|
}
|
|
@@ -81,17 +113,21 @@ internal class ModalContentRoot(private val reactContext: ThemedReactContext) :
|
|
|
81
113
|
}
|
|
82
114
|
|
|
83
115
|
override fun onInterceptHoverEvent(event: MotionEvent): Boolean {
|
|
84
|
-
|
|
116
|
+
activeEventDispatcher?.let { pointerDispatcher?.handleMotionEvent(event, it, true) }
|
|
85
117
|
return super.onInterceptHoverEvent(event)
|
|
86
118
|
}
|
|
87
119
|
|
|
88
120
|
override fun onHoverEvent(event: MotionEvent): Boolean {
|
|
89
|
-
|
|
121
|
+
activeEventDispatcher?.let { pointerDispatcher?.handleMotionEvent(event, it, false) }
|
|
90
122
|
return super.onHoverEvent(event)
|
|
91
123
|
}
|
|
92
124
|
|
|
93
125
|
@OptIn(UnstableReactNativeAPI::class)
|
|
94
126
|
override fun onChildStartedNativeGesture(childView: View?, ev: MotionEvent) {
|
|
127
|
+
if (isInline) {
|
|
128
|
+
outerRootView()?.onChildStartedNativeGesture(childView, ev)
|
|
129
|
+
return
|
|
130
|
+
}
|
|
95
131
|
eventDispatcher?.let {
|
|
96
132
|
touchDispatcher.onChildStartedNativeGesture(ev, it, reactContext)
|
|
97
133
|
pointerDispatcher?.onChildStartedNativeGesture(childView, ev, it)
|
|
@@ -99,10 +135,17 @@ internal class ModalContentRoot(private val reactContext: ThemedReactContext) :
|
|
|
99
135
|
}
|
|
100
136
|
|
|
101
137
|
override fun onChildEndedNativeGesture(childView: View, ev: MotionEvent) {
|
|
138
|
+
if (isInline) {
|
|
139
|
+
outerRootView()?.onChildEndedNativeGesture(childView, ev)
|
|
140
|
+
return
|
|
141
|
+
}
|
|
102
142
|
eventDispatcher?.let { touchDispatcher.onChildEndedNativeGesture(ev, it) }
|
|
103
143
|
pointerDispatcher?.onChildEndedNativeGesture()
|
|
104
144
|
}
|
|
105
145
|
|
|
146
|
+
/** The React root view an inline sheet lives in. */
|
|
147
|
+
private fun outerRootView(): RootView? = (parent as? View)?.let { RootViewUtil.getRootView(it) }
|
|
148
|
+
|
|
106
149
|
override fun handleException(t: Throwable) {
|
|
107
150
|
reactContext.reactApplicationContext.handleException(RuntimeException(t))
|
|
108
151
|
}
|
|
@@ -22,6 +22,10 @@ import com.facebook.react.uimanager.ThemedReactContext
|
|
|
22
22
|
* drive the enter/exit animations; this class handles the window, edge-to-edge
|
|
23
23
|
* insets, keyboard tracking, back handling and teardown.
|
|
24
24
|
*
|
|
25
|
+
* With an [inlineHost], there is no window: the layout fills that view, in
|
|
26
|
+
* the screen's own hierarchy, and the keyboard and back button are left to
|
|
27
|
+
* the screen.
|
|
28
|
+
*
|
|
25
29
|
* Transitions are interruptible like native ones: dismissing mid-enter turns
|
|
26
30
|
* the enter around, and [cancelDismiss] turns an exit back around. Once an
|
|
27
31
|
* exit starts the window stops taking input, as a dismissed dialog would.
|
|
@@ -34,6 +38,7 @@ internal abstract class ModalPresenter(
|
|
|
34
38
|
protected val contentRoot: ModalContentRoot,
|
|
35
39
|
config: ModalConfig,
|
|
36
40
|
protected val listener: Listener,
|
|
41
|
+
private val inlineHost: NitroModalHostView? = null,
|
|
37
42
|
) {
|
|
38
43
|
interface Listener {
|
|
39
44
|
/** An enter transition settled: the first one, or one that reversed an exit. */
|
|
@@ -45,12 +50,20 @@ internal abstract class ModalPresenter(
|
|
|
45
50
|
fun onDetentChange(index: Int)
|
|
46
51
|
/** Area available to the content, in px. */
|
|
47
52
|
fun onContentAreaChange(width: Int, height: Int)
|
|
53
|
+
/** The user pulled the sheet past its lowest detent and let go. */
|
|
54
|
+
fun onPullToRefresh()
|
|
55
|
+
/** Inline sheets: the sheet's top at rest, in the host view, in px. */
|
|
56
|
+
fun onRestingTopChange(top: Int)
|
|
48
57
|
}
|
|
49
58
|
|
|
50
59
|
protected var config: ModalConfig = config
|
|
51
60
|
private set
|
|
52
61
|
|
|
53
|
-
|
|
62
|
+
/** Lives in [inlineHost] rather than in a window of its own. */
|
|
63
|
+
protected val isInline: Boolean
|
|
64
|
+
get() = inlineHost != null
|
|
65
|
+
|
|
66
|
+
protected val dialog by lazy { ComponentDialog(activity, R.style.NitroModal_Dialog) }
|
|
54
67
|
protected val backdrop: View = BackdropView(activity)
|
|
55
68
|
|
|
56
69
|
/** System bars + display cutout. */
|
|
@@ -119,6 +132,9 @@ internal abstract class ModalPresenter(
|
|
|
119
132
|
|
|
120
133
|
open fun snapToDetent(index: Int) = Unit
|
|
121
134
|
|
|
135
|
+
/** Runs once, right before the layout is torn down. */
|
|
136
|
+
protected open fun onTeardown() = Unit
|
|
137
|
+
|
|
122
138
|
protected open fun onBackGestureStarted(event: BackEventCompat) = Unit
|
|
123
139
|
protected open fun onBackGestureProgressed(event: BackEventCompat) = Unit
|
|
124
140
|
protected open fun onBackGestureCancelled() = Unit
|
|
@@ -130,6 +146,13 @@ internal abstract class ModalPresenter(
|
|
|
130
146
|
fun show() {
|
|
131
147
|
(contentRoot.parent as? ViewGroup)?.removeView(contentRoot)
|
|
132
148
|
val root = createLayout()
|
|
149
|
+
inlineHost?.let { host ->
|
|
150
|
+
host.addInlineRoot(root)
|
|
151
|
+
updateInlineInsets()
|
|
152
|
+
entering = true
|
|
153
|
+
animateIn()
|
|
154
|
+
return
|
|
155
|
+
}
|
|
133
156
|
backdrop.setBackgroundColor(config.effectiveBackdropColor)
|
|
134
157
|
backdrop.alpha = 0f
|
|
135
158
|
backdrop.setOnClickListener { if (!isDismissing) listener.onBackdropPress() }
|
|
@@ -179,10 +202,15 @@ internal abstract class ModalPresenter(
|
|
|
179
202
|
if (previous == newConfig) return
|
|
180
203
|
config = newConfig
|
|
181
204
|
backdrop.setBackgroundColor(config.effectiveBackdropColor)
|
|
182
|
-
if (previous.backdropBlurRadius != newConfig.backdropBlurRadius) applyBlur()
|
|
205
|
+
if (!isInline && previous.backdropBlurRadius != newConfig.backdropBlurRadius) applyBlur()
|
|
183
206
|
onConfigChanged(previous)
|
|
184
207
|
}
|
|
185
208
|
|
|
209
|
+
/** The inline host moved or resized. */
|
|
210
|
+
fun onHostLayoutChanged() {
|
|
211
|
+
if (isInline && !finished) updateInlineInsets()
|
|
212
|
+
}
|
|
213
|
+
|
|
186
214
|
/**
|
|
187
215
|
* Animated dismissal, also from mid-enter. [Listener.onDismissed] follows
|
|
188
216
|
* when it completes, unless [cancelDismiss] turns it around first.
|
|
@@ -207,7 +235,7 @@ internal abstract class ModalPresenter(
|
|
|
207
235
|
if (!isDismissing || finished || !canReverseDismiss) return false
|
|
208
236
|
isDismissing = false
|
|
209
237
|
dismissReason = DismissReason.PROGRAMMATIC
|
|
210
|
-
dialog.window?.clearFlags(INPUT_RELEASE_FLAGS)
|
|
238
|
+
if (!isInline) dialog.window?.clearFlags(INPUT_RELEASE_FLAGS)
|
|
211
239
|
entering = true
|
|
212
240
|
animateIn()
|
|
213
241
|
return true
|
|
@@ -257,6 +285,7 @@ internal abstract class ModalPresenter(
|
|
|
257
285
|
* then would cut the keyboard's hide animation short.
|
|
258
286
|
*/
|
|
259
287
|
private fun releaseInput() {
|
|
288
|
+
if (isInline) return
|
|
260
289
|
val window = dialog.window ?: return
|
|
261
290
|
val keyboardVisible = imeAnimating || imeHeight > 0
|
|
262
291
|
window.addFlags(
|
|
@@ -273,6 +302,12 @@ internal abstract class ModalPresenter(
|
|
|
273
302
|
|
|
274
303
|
private fun teardown() {
|
|
275
304
|
backdrop.animate().cancel()
|
|
305
|
+
onTeardown()
|
|
306
|
+
if (inlineHost != null) {
|
|
307
|
+
(contentRoot.parent as? ViewGroup)?.removeView(contentRoot)
|
|
308
|
+
inlineHost.removeAllViews()
|
|
309
|
+
return
|
|
310
|
+
}
|
|
276
311
|
dialog.window?.let { reactContext.onExtraWindowDestroy(it) }
|
|
277
312
|
if (dialog.isShowing && !activity.isFinishing && !activity.isDestroyed) {
|
|
278
313
|
try {
|
|
@@ -285,11 +320,35 @@ internal abstract class ModalPresenter(
|
|
|
285
320
|
}
|
|
286
321
|
|
|
287
322
|
private fun hideKeyboard() {
|
|
288
|
-
|
|
323
|
+
if (isInline && !contentRoot.hasFocus()) return
|
|
324
|
+
val token = (if (isInline) contentRoot.windowToken else dialog.window?.decorView?.windowToken) ?: return
|
|
289
325
|
(activity.getSystemService(Activity.INPUT_METHOD_SERVICE) as? InputMethodManager)
|
|
290
326
|
?.hideSoftInputFromWindow(token, 0)
|
|
291
327
|
}
|
|
292
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Inline: the system bars and cutout overlapping the host view, so the
|
|
331
|
+
* sheet keeps its content clear of them like a modal sheet does.
|
|
332
|
+
*/
|
|
333
|
+
private fun updateInlineInsets() {
|
|
334
|
+
val host = inlineHost ?: return
|
|
335
|
+
val window = ViewCompat.getRootWindowInsets(host)
|
|
336
|
+
?.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout())
|
|
337
|
+
?: Insets.NONE
|
|
338
|
+
val location = IntArray(2)
|
|
339
|
+
host.getLocationInWindow(location)
|
|
340
|
+
val root = host.rootView
|
|
341
|
+
val next = Insets.of(
|
|
342
|
+
(window.left - location[0]).coerceIn(0, host.width),
|
|
343
|
+
(window.top - location[1]).coerceIn(0, host.height),
|
|
344
|
+
(location[0] + host.width - (root.width - window.right)).coerceIn(0, host.width),
|
|
345
|
+
(location[1] + host.height - (root.height - window.bottom)).coerceIn(0, host.height),
|
|
346
|
+
)
|
|
347
|
+
if (next == systemInsets) return
|
|
348
|
+
systemInsets = next
|
|
349
|
+
onInsetsChanged()
|
|
350
|
+
}
|
|
351
|
+
|
|
293
352
|
private fun applyBlur() {
|
|
294
353
|
val window = dialog.window ?: return
|
|
295
354
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return
|
|
@@ -4,21 +4,51 @@ import android.annotation.SuppressLint
|
|
|
4
4
|
import android.view.View
|
|
5
5
|
import android.view.ViewGroup
|
|
6
6
|
import android.view.accessibility.AccessibilityEvent
|
|
7
|
+
import com.facebook.react.uimanager.PointerEvents
|
|
8
|
+
import com.facebook.react.uimanager.ReactPointerEventsView
|
|
7
9
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
|
-
* The view Nitro mounts into the React tree.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
12
|
+
* The view Nitro mounts into the React tree. React children added to it (via
|
|
13
|
+
* [NitroModalViewManager]) are hosted by [contentRoot].
|
|
14
|
+
*
|
|
15
|
+
* Modal: an invisible placeholder; [contentRoot] travels into the modal
|
|
16
|
+
* window. Inline: the area the sheet lives in. Its only child is the sheet's
|
|
17
|
+
* layout, which it lays out itself, and only touches on the sheet hit it.
|
|
13
18
|
*/
|
|
14
19
|
@SuppressLint("ViewConstructor")
|
|
15
|
-
class NitroModalHostView(context: ThemedReactContext) : ViewGroup(context) {
|
|
20
|
+
class NitroModalHostView(context: ThemedReactContext) : ViewGroup(context), ReactPointerEventsView {
|
|
16
21
|
internal val contentRoot = ModalContentRoot(context)
|
|
17
22
|
internal var onAttachedChange: ((attached: Boolean) -> Unit)? = null
|
|
18
23
|
|
|
24
|
+
/** The size or position changed. */
|
|
25
|
+
internal var onLayoutChange: (() -> Unit)? = null
|
|
26
|
+
|
|
27
|
+
internal var isInline: Boolean = false
|
|
28
|
+
set(value) {
|
|
29
|
+
if (field == value) return
|
|
30
|
+
field = value
|
|
31
|
+
contentRoot.isInline = value
|
|
32
|
+
}
|
|
33
|
+
|
|
19
34
|
internal val reactChildCount: Int
|
|
20
35
|
get() = contentRoot.childCount
|
|
21
36
|
|
|
37
|
+
// Inline: touches that miss the sheet go to the views behind it.
|
|
38
|
+
override val pointerEvents: PointerEvents
|
|
39
|
+
get() = if (isInline) PointerEvents.BOX_NONE else PointerEvents.AUTO
|
|
40
|
+
|
|
41
|
+
private var layoutPending = false
|
|
42
|
+
|
|
43
|
+
private val measureAndLayout = Runnable {
|
|
44
|
+
layoutPending = false
|
|
45
|
+
measure(
|
|
46
|
+
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
47
|
+
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY),
|
|
48
|
+
)
|
|
49
|
+
layout(left, top, right, bottom)
|
|
50
|
+
}
|
|
51
|
+
|
|
22
52
|
internal fun addReactChild(child: View, index: Int) = contentRoot.addView(child, index)
|
|
23
53
|
|
|
24
54
|
internal fun getReactChildAt(index: Int): View? = contentRoot.getChildAt(index)
|
|
@@ -31,8 +61,31 @@ class NitroModalHostView(context: ThemedReactContext) : ViewGroup(context) {
|
|
|
31
61
|
contentRoot.id = id
|
|
32
62
|
}
|
|
33
63
|
|
|
64
|
+
override fun requestLayout() {
|
|
65
|
+
super.requestLayout()
|
|
66
|
+
// React Native views drop layout requests; the inline sheet's layout
|
|
67
|
+
// (CoordinatorLayout, BottomSheetBehavior) relies on them. Also called
|
|
68
|
+
// from the super constructor, before any child exists.
|
|
69
|
+
if (childCount > 0 && !layoutPending) {
|
|
70
|
+
layoutPending = true
|
|
71
|
+
post(measureAndLayout)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
34
75
|
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
|
|
35
|
-
// Laid out by Fabric
|
|
76
|
+
// Laid out by Fabric. A modal's children live in the modal window; an
|
|
77
|
+
// inline sheet's layout fills this view.
|
|
78
|
+
val width = r - l
|
|
79
|
+
val height = b - t
|
|
80
|
+
for (i in 0 until childCount) {
|
|
81
|
+
val child = getChildAt(i)
|
|
82
|
+
child.measure(
|
|
83
|
+
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
84
|
+
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY),
|
|
85
|
+
)
|
|
86
|
+
child.layout(0, 0, width, height)
|
|
87
|
+
}
|
|
88
|
+
if (changed) onLayoutChange?.invoke()
|
|
36
89
|
}
|
|
37
90
|
|
|
38
91
|
override fun onAttachedToWindow() {
|
|
@@ -45,8 +98,15 @@ class NitroModalHostView(context: ThemedReactContext) : ViewGroup(context) {
|
|
|
45
98
|
onAttachedChange?.invoke(false)
|
|
46
99
|
}
|
|
47
100
|
|
|
48
|
-
//
|
|
49
|
-
override fun addChildrenForAccessibility(outChildren: ArrayList<View>)
|
|
101
|
+
// A modal's content is handled for accessibility inside the modal window.
|
|
102
|
+
override fun addChildrenForAccessibility(outChildren: ArrayList<View>) {
|
|
103
|
+
if (isInline) super.addChildrenForAccessibility(outChildren)
|
|
104
|
+
}
|
|
50
105
|
|
|
51
|
-
override fun dispatchPopulateAccessibilityEvent(event: AccessibilityEvent): Boolean =
|
|
106
|
+
override fun dispatchPopulateAccessibilityEvent(event: AccessibilityEvent): Boolean =
|
|
107
|
+
isInline && super.dispatchPopulateAccessibilityEvent(event)
|
|
108
|
+
|
|
109
|
+
internal fun addInlineRoot(root: ViewGroup) {
|
|
110
|
+
addView(root, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
|
|
111
|
+
}
|
|
52
112
|
}
|