@rn-tools/sheets 0.1.4 → 3.0.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/CHANGELOG.md +8 -0
- package/README.md +18 -7
- package/android/src/main/java/expo/modules/sheets/RNToolsSheetsModule.kt +8 -4
- package/android/src/main/java/expo/modules/sheets/RNToolsSheetsView.kt +110 -76
- package/android/src/main/java/expo/modules/sheets/SheetProps.kt +2 -1
- package/ios/RNToolsSheets.podspec +3 -3
- package/ios/RNToolsSheetsModule.swift +27 -10
- package/ios/RNToolsSheetsView.swift +199 -228
- package/ios/Sources/RNToolsTouchHandlerHelper.h +15 -0
- package/ios/Sources/RNToolsTouchHandlerHelper.mm +31 -0
- package/package.json +6 -13
- package/src/native-sheets-view.tsx +92 -34
- package/ios/Sources/RNTSurfaceTouchHandlerWrapper.h +0 -11
- package/ios/Sources/RNTSurfaceTouchHandlerWrapper.mm +0 -43
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
An expo module for rendering native bottom sheet components in iOS and Android.
|
|
4
4
|
|
|
5
|
-
Uses
|
|
5
|
+
Uses native iOS sheet presentation and Android's BottomSheetDialog to render React Native children in a modal bottom sheet.
|
|
6
6
|
|
|
7
|
+
Supports stacking multiple sheets on top of each other
|
|
7
8
|
|
|
8
9
|
https://github.com/user-attachments/assets/426c77e6-74c6-4748-8010-477267fa9433
|
|
9
10
|
|
|
@@ -52,9 +53,11 @@ export default function App() {
|
|
|
52
53
|
|
|
53
54
|
<BottomSheet
|
|
54
55
|
isOpen={isOpen}
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
setIsOpen={setIsOpen}
|
|
57
|
+
initialIndex={1}
|
|
57
58
|
onStateChange={(event) => console.log({ event })}
|
|
59
|
+
canDismiss={true}
|
|
60
|
+
onDismissPrevented={() => console.log("dismiss prevented")}
|
|
58
61
|
snapPoints={[400, 600, 750]}
|
|
59
62
|
appearanceAndroid={{
|
|
60
63
|
dimAmount: 0,
|
|
@@ -76,16 +79,18 @@ export default function App() {
|
|
|
76
79
|
|
|
77
80
|
## Props
|
|
78
81
|
|
|
79
|
-
- `isOpen /
|
|
82
|
+
- `isOpen / setIsOpen` - Controller props for toggling the sheet open and closed - this is required
|
|
80
83
|
|
|
81
|
-
- `
|
|
84
|
+
- `initialIndex` - will open the bottom sheet to the defined snapPoint index
|
|
82
85
|
|
|
83
86
|
- `onStateChange` - callback to track the internal state of the sheet. The following events are emitted:
|
|
84
87
|
|
|
85
88
|
- { type: "HIDDEN" }
|
|
86
89
|
- { type: "OPEN", payload: { index: number }}
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
|
|
91
|
+
- `canDismiss` - controls whether the user can dismiss the sheet via swipe/back/gesture (default: true)
|
|
92
|
+
|
|
93
|
+
- `onDismissPrevented` - called when a dismiss gesture is blocked by `canDismiss={false}`
|
|
89
94
|
|
|
90
95
|
- `snapPoints` - a list of sizes that the sheet will "snap" to
|
|
91
96
|
|
|
@@ -97,6 +102,12 @@ export default function App() {
|
|
|
97
102
|
|
|
98
103
|
## Caveats
|
|
99
104
|
|
|
105
|
+
- iOS uses an overlay window to present the sheet.
|
|
106
|
+
|
|
107
|
+
- Default appearance values if not provided:
|
|
108
|
+
- iOS: grabber visible, white background, system default corner radius unless set
|
|
109
|
+
- Android: white background, 32dp top corner radius, dim amount 0.56
|
|
110
|
+
|
|
100
111
|
- (Android) can have a maximum of 2 snap points
|
|
101
112
|
|
|
102
113
|
- (Android) use the `nestedScrollEnabled` prop for nested scrollviews
|
|
@@ -31,17 +31,21 @@ class RNToolsSheetsModule : Module() {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
Events("onDismiss", "onStateChange")
|
|
34
|
+
Events("onDismiss", "onStateChange", "onDismissPrevented")
|
|
35
35
|
|
|
36
36
|
Prop("isOpen") { view: RNToolsSheetsView, isOpen: Boolean ->
|
|
37
37
|
view.props.isOpen = isOpen
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
Prop("
|
|
41
|
-
view.props.
|
|
40
|
+
Prop("initialIndex") { view: RNToolsSheetsView, initialIndex: Int ->
|
|
41
|
+
view.props.initialIndex = initialIndex
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
Prop("
|
|
44
|
+
Prop("canDismiss") { view: RNToolsSheetsView, canDismiss: Boolean ->
|
|
45
|
+
view.props.canDismiss = canDismiss
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
Prop("snapPoints") { view: RNToolsSheetsView, snapPoints: List<Double> ->
|
|
45
49
|
view.props.snapPoints = snapPoints.map { view.convertToPx(it) }
|
|
46
50
|
}
|
|
47
51
|
|
|
@@ -13,10 +13,12 @@ import expo.modules.kotlin.AppContext
|
|
|
13
13
|
import expo.modules.kotlin.viewevent.EventDispatcher
|
|
14
14
|
import expo.modules.kotlin.views.ExpoView
|
|
15
15
|
import android.graphics.Color
|
|
16
|
+
import com.facebook.react.bridge.UiThreadUtil
|
|
16
17
|
|
|
17
18
|
class RNToolsSheetsView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
|
|
18
19
|
val onDismiss by EventDispatcher()
|
|
19
20
|
val onStateChange by EventDispatcher()
|
|
21
|
+
val onDismissPrevented by EventDispatcher()
|
|
20
22
|
|
|
21
23
|
var rootViewGroup = SheetRootView(context, appContext)
|
|
22
24
|
private var composeView: ComposeView
|
|
@@ -42,6 +44,15 @@ class RNToolsSheetsView(context: Context, appContext: AppContext) : ExpoView(con
|
|
|
42
44
|
hideSheet()
|
|
43
45
|
}
|
|
44
46
|
}
|
|
47
|
+
|
|
48
|
+
LaunchedEffect(props.canDismiss) {
|
|
49
|
+
UiThreadUtil.runOnUiThread {
|
|
50
|
+
bottomSheetDialog?.apply {
|
|
51
|
+
setCancelable(true)
|
|
52
|
+
behavior.isHideable = props.canDismiss
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
45
56
|
}
|
|
46
57
|
}
|
|
47
58
|
|
|
@@ -54,95 +65,116 @@ class RNToolsSheetsView(context: Context, appContext: AppContext) : ExpoView(con
|
|
|
54
65
|
}
|
|
55
66
|
|
|
56
67
|
private fun hideSheet() {
|
|
57
|
-
|
|
58
|
-
|
|
68
|
+
UiThreadUtil.runOnUiThread {
|
|
69
|
+
bottomSheetDialog?.dismiss()
|
|
70
|
+
bottomSheetDialog = null
|
|
71
|
+
}
|
|
59
72
|
}
|
|
60
73
|
|
|
61
74
|
private fun showSheet() {
|
|
62
|
-
|
|
75
|
+
UiThreadUtil.runOnUiThread {
|
|
76
|
+
(rootViewGroup.parent as? ViewGroup)?.removeView(rootViewGroup)
|
|
63
77
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
78
|
+
val frameLayout = FrameLayout(context).apply {
|
|
79
|
+
layoutParams = LayoutParams(
|
|
80
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
81
|
+
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
82
|
+
)
|
|
69
83
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
val snapPoints = props.snapPoints
|
|
74
|
-
val initialIndex = props.openToIndex
|
|
75
|
-
|
|
76
|
-
val hasTwoSnapPoints = snapPoints.size >= 2
|
|
77
|
-
val peekHeight = if (hasTwoSnapPoints) snapPoints[0] else -1
|
|
78
|
-
val expandedHeight = if (snapPoints.isNotEmpty()) snapPoints.getOrNull(1) ?: snapPoints[0] else -1
|
|
79
|
-
val initialHeight = snapPoints.getOrNull(initialIndex) ?: peekHeight
|
|
84
|
+
addView(rootViewGroup)
|
|
85
|
+
}
|
|
80
86
|
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
val snapPoints = props.snapPoints
|
|
88
|
+
val initialIndex = props.initialIndex
|
|
89
|
+
|
|
90
|
+
val hasTwoSnapPoints = snapPoints.size >= 2
|
|
91
|
+
val peekHeight = if (hasTwoSnapPoints) snapPoints[0] else -1
|
|
92
|
+
val expandedHeight = if (snapPoints.isNotEmpty()) snapPoints.getOrNull(1) ?: snapPoints[0] else -1
|
|
93
|
+
val initialHeight = snapPoints.getOrNull(initialIndex) ?: peekHeight
|
|
94
|
+
|
|
95
|
+
bottomSheetDialog = PreventDismissBottomSheetDialog(
|
|
96
|
+
context = context,
|
|
97
|
+
canDismiss = { props.canDismiss },
|
|
98
|
+
onDismissPrevented = { onDismissPrevented(mapOf()) }
|
|
99
|
+
).apply {
|
|
100
|
+
setCancelable(true)
|
|
101
|
+
setContentView(frameLayout)
|
|
102
|
+
|
|
103
|
+
window?.setDimAmount(props.dimAmount)
|
|
104
|
+
|
|
105
|
+
behavior.isHideable = props.canDismiss
|
|
106
|
+
|
|
107
|
+
window?.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)?.let { bottomSheet ->
|
|
108
|
+
val backgroundColor = props.backgroundColor?.let {
|
|
109
|
+
try {
|
|
110
|
+
Color.parseColor(it)
|
|
111
|
+
} catch (e: IllegalArgumentException) {
|
|
112
|
+
Color.WHITE
|
|
113
|
+
}
|
|
114
|
+
} ?: Color.WHITE
|
|
115
|
+
|
|
116
|
+
val radius = props.cornerRadius ?: 32f
|
|
117
|
+
|
|
118
|
+
val drawable = GradientDrawable().apply {
|
|
119
|
+
setColor(backgroundColor)
|
|
120
|
+
cornerRadii = floatArrayOf(
|
|
121
|
+
radius, radius,
|
|
122
|
+
radius, radius,
|
|
123
|
+
0f, 0f,
|
|
124
|
+
0f, 0f
|
|
125
|
+
)
|
|
126
|
+
}
|
|
83
127
|
|
|
84
|
-
|
|
128
|
+
bottomSheet.background = drawable
|
|
129
|
+
}
|
|
85
130
|
|
|
86
|
-
|
|
131
|
+
val behavior = behavior
|
|
87
132
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
133
|
+
setOnDismissListener {
|
|
134
|
+
onDismiss(mapOf())
|
|
135
|
+
if (behavior.state != BottomSheetBehavior.STATE_HIDDEN) {
|
|
136
|
+
onStateChange(mapOf(
|
|
137
|
+
"type" to "HIDDEN",
|
|
138
|
+
))
|
|
93
139
|
}
|
|
94
|
-
} ?: Color.WHITE
|
|
95
|
-
|
|
96
|
-
val drawable = GradientDrawable().apply {
|
|
97
|
-
setColor(backgroundColor)
|
|
98
|
-
cornerRadius = props.cornerRadius ?: 0f
|
|
99
140
|
}
|
|
100
141
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
val behavior = behavior
|
|
142
|
+
if (peekHeight > 0) {
|
|
143
|
+
behavior.peekHeight = peekHeight
|
|
144
|
+
}
|
|
105
145
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
onStateChange(mapOf(
|
|
110
|
-
"type" to "HIDDEN",
|
|
111
|
-
))
|
|
146
|
+
if (expandedHeight > 0) {
|
|
147
|
+
frameLayout.layoutParams.height = expandedHeight
|
|
148
|
+
frameLayout.requestLayout()
|
|
112
149
|
}
|
|
113
|
-
}
|
|
114
150
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
151
|
+
behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
|
|
152
|
+
override fun onStateChanged(bottomSheet: android.view.View, newState: Int) {
|
|
153
|
+
if (!props.canDismiss && newState == BottomSheetBehavior.STATE_HIDDEN) {
|
|
154
|
+
onDismissPrevented(mapOf())
|
|
155
|
+
}
|
|
118
156
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
frameLayout.requestLayout()
|
|
122
|
-
}
|
|
157
|
+
handleSheetStateChange(newState)
|
|
158
|
+
}
|
|
123
159
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
160
|
+
override fun onSlide(bottomSheet: android.view.View, slideOffset: Float) {
|
|
161
|
+
}
|
|
162
|
+
})
|
|
128
163
|
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
})
|
|
164
|
+
show()
|
|
132
165
|
|
|
133
|
-
|
|
166
|
+
if (initialHeight == peekHeight) {
|
|
167
|
+
behavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
|
168
|
+
} else {
|
|
169
|
+
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
|
170
|
+
}
|
|
134
171
|
|
|
135
|
-
|
|
136
|
-
behavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
|
137
|
-
} else {
|
|
138
|
-
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
|
172
|
+
handleSheetStateChange(behavior.state)
|
|
139
173
|
}
|
|
140
|
-
|
|
141
|
-
handleSheetStateChange(behavior.state)
|
|
142
174
|
}
|
|
143
175
|
}
|
|
144
176
|
|
|
145
|
-
fun convertToPx(height:
|
|
177
|
+
fun convertToPx(height: Double): Int {
|
|
146
178
|
val density = context.resources.displayMetrics.density
|
|
147
179
|
return (height * density).toInt()
|
|
148
180
|
}
|
|
@@ -155,12 +187,6 @@ class RNToolsSheetsView(context: Context, appContext: AppContext) : ExpoView(con
|
|
|
155
187
|
))
|
|
156
188
|
|
|
157
189
|
}
|
|
158
|
-
BottomSheetBehavior.STATE_SETTLING -> {
|
|
159
|
-
onStateChange(mapOf(
|
|
160
|
-
"type" to "SETTLING",
|
|
161
|
-
))
|
|
162
|
-
}
|
|
163
|
-
|
|
164
190
|
BottomSheetBehavior.STATE_COLLAPSED -> {
|
|
165
191
|
onStateChange(mapOf(
|
|
166
192
|
"type" to "OPEN",
|
|
@@ -188,12 +214,20 @@ class RNToolsSheetsView(context: Context, appContext: AppContext) : ExpoView(con
|
|
|
188
214
|
))
|
|
189
215
|
}
|
|
190
216
|
|
|
191
|
-
BottomSheetBehavior.STATE_DRAGGING -> {
|
|
192
|
-
onStateChange(mapOf(
|
|
193
|
-
"type" to "DRAGGING",
|
|
194
|
-
))
|
|
195
|
-
}
|
|
196
217
|
}
|
|
197
218
|
}
|
|
198
219
|
}
|
|
199
220
|
|
|
221
|
+
class PreventDismissBottomSheetDialog(
|
|
222
|
+
context: Context,
|
|
223
|
+
private val canDismiss: () -> Boolean,
|
|
224
|
+
private val onDismissPrevented: () -> Unit
|
|
225
|
+
) : BottomSheetDialog(context) {
|
|
226
|
+
override fun cancel() {
|
|
227
|
+
if (canDismiss()) super.cancel() else onDismissPrevented()
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
override fun onBackPressed() {
|
|
231
|
+
if (canDismiss()) super.onBackPressed() else onDismissPrevented()
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -9,9 +9,10 @@ import expo.modules.kotlin.records.Record
|
|
|
9
9
|
|
|
10
10
|
class SheetProps {
|
|
11
11
|
var isOpen by mutableStateOf(false)
|
|
12
|
-
var
|
|
12
|
+
var initialIndex by mutableIntStateOf(0)
|
|
13
13
|
var snapPoints by mutableStateOf<List<Int>>(emptyList())
|
|
14
14
|
lateinit var rootViewGroup: SheetRootView
|
|
15
|
+
var canDismiss by mutableStateOf(true)
|
|
15
16
|
|
|
16
17
|
// Appearance props
|
|
17
18
|
var dimAmount by mutableStateOf(0.56f)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'json'
|
|
2
2
|
|
|
3
|
+
ENV['RCT_NEW_ARCH_ENABLED'] ||= '1'
|
|
3
4
|
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
|
4
5
|
new_arch_enabled = ENV['RCT_NEW_ARCH_ENABLED'] == '1'
|
|
5
6
|
|
|
@@ -21,12 +22,11 @@ Pod::Spec.new do |s|
|
|
|
21
22
|
s.static_framework = true
|
|
22
23
|
|
|
23
24
|
s.dependency 'ExpoModulesCore'
|
|
24
|
-
s.public_header_files = 'Sources/
|
|
25
|
-
|
|
25
|
+
s.public_header_files = 'Sources/RNToolsTouchHandlerHelper.h'
|
|
26
26
|
# Swift/Objective-C compatibility
|
|
27
27
|
s.pod_target_xcconfig = {
|
|
28
28
|
'DEFINES_MODULE' => 'YES',
|
|
29
|
-
'OTHER_SWIFT_FLAGS' =>
|
|
29
|
+
'OTHER_SWIFT_FLAGS' => '$(inherited)'
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
|
|
@@ -1,28 +1,45 @@
|
|
|
1
1
|
import ExpoModulesCore
|
|
2
2
|
|
|
3
|
+
struct SheetAppearance: Record {
|
|
4
|
+
@Field
|
|
5
|
+
var grabberVisible: Bool?
|
|
6
|
+
|
|
7
|
+
@Field
|
|
8
|
+
var backgroundColor: String?
|
|
9
|
+
|
|
10
|
+
@Field
|
|
11
|
+
var cornerRadius: Float?
|
|
12
|
+
}
|
|
13
|
+
|
|
3
14
|
public class RNToolsSheetsModule: Module {
|
|
4
15
|
public func definition() -> ModuleDefinition {
|
|
5
16
|
Name("RNToolsSheets")
|
|
6
17
|
|
|
7
18
|
View(RNToolsSheetsView.self) {
|
|
8
|
-
Events("onDismiss", "onStateChange")
|
|
19
|
+
Events("onDismiss", "onStateChange", "onDismissPrevented")
|
|
9
20
|
|
|
10
|
-
Prop("snapPoints") { (view, snapPoints: [
|
|
11
|
-
view.
|
|
21
|
+
Prop("snapPoints") { (view, snapPoints: [CGFloat]) in
|
|
22
|
+
view.updateSnapPoints(snapPoints)
|
|
12
23
|
}
|
|
13
24
|
|
|
14
25
|
Prop("isOpen") { (view, isOpen: Bool) in
|
|
15
|
-
view.
|
|
26
|
+
view.updateIsOpen(isOpen)
|
|
16
27
|
}
|
|
17
28
|
|
|
18
|
-
Prop("
|
|
19
|
-
view.
|
|
29
|
+
Prop("initialIndex") { (view, initialIndex: Int) in
|
|
30
|
+
view.updateInitialIndex(initialIndex)
|
|
20
31
|
}
|
|
21
|
-
|
|
32
|
+
|
|
22
33
|
Prop("appearanceIOS") { (view, appearance: SheetAppearance) in
|
|
23
|
-
view.
|
|
24
|
-
|
|
25
|
-
|
|
34
|
+
view.updateAppearance(
|
|
35
|
+
grabberVisible: appearance.grabberVisible ?? true,
|
|
36
|
+
backgroundColor: appearance.backgroundColor,
|
|
37
|
+
cornerRadius: appearance.cornerRadius
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
Prop("canDismiss") { (view, canDismiss: Bool) in
|
|
42
|
+
view.updateCanDismiss(canDismiss)
|
|
26
43
|
}
|
|
27
44
|
}
|
|
28
45
|
}
|
|
@@ -1,83 +1,106 @@
|
|
|
1
1
|
import ExpoModulesCore
|
|
2
2
|
import React
|
|
3
|
-
import
|
|
3
|
+
import UIKit
|
|
4
4
|
|
|
5
|
-
public class SheetProps
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
public class SheetProps {
|
|
6
|
+
var isOpen: Bool = false
|
|
7
|
+
var initialIndex: Int = 0
|
|
8
|
+
var snapPoints: [CGFloat] = []
|
|
9
|
+
var canDismiss: Bool = true
|
|
10
10
|
|
|
11
11
|
// Appearance props
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
var grabberVisible: Bool = true
|
|
13
|
+
var backgroundColor: String? = nil
|
|
14
|
+
var cornerRadius: Float? = nil
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@Field
|
|
22
|
-
var backgroundColor: String?
|
|
23
|
-
|
|
24
|
-
@Field
|
|
25
|
-
var cornerRadius: Float?
|
|
17
|
+
protocol RNToolsSheetsViewDelegate: AnyObject {
|
|
18
|
+
func handleSheetStateChange(index: Int)
|
|
19
|
+
func handleSheetDismissed()
|
|
20
|
+
func handleSheetCanDismiss() -> Bool
|
|
26
21
|
}
|
|
27
22
|
|
|
28
|
-
public class RNToolsSheetsView: ExpoView {
|
|
23
|
+
public class RNToolsSheetsView: ExpoView, RNToolsSheetsViewDelegate {
|
|
29
24
|
public var props = SheetProps()
|
|
25
|
+
|
|
30
26
|
var onDismiss = EventDispatcher()
|
|
31
27
|
var onStateChange = EventDispatcher()
|
|
28
|
+
var onDismissPrevented = EventDispatcher()
|
|
32
29
|
|
|
33
|
-
var
|
|
34
|
-
|
|
35
|
-
private lazy var sheetVC = SheetInternalViewController()
|
|
36
|
-
|
|
37
|
-
lazy var hostingController = UIHostingController(
|
|
38
|
-
rootView: ContentView(
|
|
39
|
-
props: props, sheetVC: sheetVC, onDismiss: onDismiss,
|
|
40
|
-
onStateChange: onStateChange))
|
|
30
|
+
private lazy var sheetVC = SheetViewController()
|
|
41
31
|
|
|
42
32
|
required init(appContext: AppContext? = nil) {
|
|
43
33
|
super.init(appContext: appContext)
|
|
44
34
|
|
|
45
|
-
|
|
46
|
-
|
|
35
|
+
sheetVC.appContext = appContext
|
|
36
|
+
sheetVC.delegate = self
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
deinit {
|
|
40
|
+
sheetVC.cleanup()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func updateSnapPoints(_ snapPoints: [CGFloat]) {
|
|
44
|
+
props.snapPoints = snapPoints
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func updateIsOpen(_ isOpen: Bool) {
|
|
48
|
+
props.isOpen = isOpen
|
|
49
|
+
if isOpen {
|
|
50
|
+
sheetVC.presentSheet(
|
|
51
|
+
openTo: props.initialIndex,
|
|
52
|
+
snapPoints: props.snapPoints,
|
|
53
|
+
grabberVisible: props.grabberVisible,
|
|
54
|
+
backgroundColor: props.backgroundColor,
|
|
55
|
+
cornerRadius: props.cornerRadius
|
|
56
|
+
)
|
|
57
|
+
} else {
|
|
58
|
+
sheetVC.dismissSheet()
|
|
47
59
|
}
|
|
60
|
+
}
|
|
48
61
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
]
|
|
52
|
-
hostingController.view.backgroundColor = UIColor.clear
|
|
62
|
+
func updateInitialIndex(_ initialIndex: Int) {
|
|
63
|
+
props.initialIndex = initialIndex
|
|
53
64
|
}
|
|
54
65
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
hostingController.view.frame = bounds
|
|
66
|
+
func updateCanDismiss(_ canDismiss: Bool) {
|
|
67
|
+
props.canDismiss = canDismiss
|
|
58
68
|
}
|
|
59
69
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
70
|
+
func updateAppearance(
|
|
71
|
+
grabberVisible: Bool,
|
|
72
|
+
backgroundColor: String?,
|
|
73
|
+
cornerRadius: Float?
|
|
74
|
+
) {
|
|
75
|
+
props.grabberVisible = grabberVisible
|
|
76
|
+
props.backgroundColor = backgroundColor
|
|
77
|
+
props.cornerRadius = cornerRadius
|
|
78
|
+
}
|
|
63
79
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
80
|
+
func handleSheetDismissed() {
|
|
81
|
+
onDismiss([:])
|
|
82
|
+
onStateChange(["type": "HIDDEN"])
|
|
83
|
+
}
|
|
67
84
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
85
|
+
func handleSheetStateChange(index: Int) {
|
|
86
|
+
onStateChange([
|
|
87
|
+
"type": "OPEN",
|
|
88
|
+
"payload": ["index": index],
|
|
89
|
+
])
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func handleSheetCanDismiss() -> Bool {
|
|
93
|
+
if !props.canDismiss {
|
|
94
|
+
onDismissPrevented([:])
|
|
73
95
|
}
|
|
96
|
+
return props.canDismiss
|
|
74
97
|
}
|
|
75
98
|
|
|
76
99
|
public override func reactSubviews() -> [UIView]! {
|
|
77
100
|
return []
|
|
78
101
|
}
|
|
79
102
|
|
|
80
|
-
|
|
103
|
+
|
|
81
104
|
public override func mountChildComponentView(
|
|
82
105
|
_ childComponentView: UIView,
|
|
83
106
|
index: Int
|
|
@@ -91,164 +114,109 @@ public class RNToolsSheetsView: ExpoView {
|
|
|
91
114
|
) {
|
|
92
115
|
childComponentView.removeFromSuperview()
|
|
93
116
|
}
|
|
117
|
+
// public override func insertReactSubview(
|
|
118
|
+
// _ subview: UIView!, at atIndex: Int
|
|
119
|
+
// ) {
|
|
120
|
+
// sheetVC.insertChild(subview, at: atIndex)
|
|
121
|
+
// }
|
|
122
|
+
//
|
|
123
|
+
// public override func removeReactSubview(_ subview: UIView!) {
|
|
124
|
+
// sheetVC.removeChild(subview)
|
|
125
|
+
// }
|
|
94
126
|
|
|
95
|
-
|
|
96
|
-
public override func insertReactSubview(
|
|
97
|
-
_ subview: UIView!, at atIndex: Int
|
|
98
|
-
) {
|
|
99
|
-
sheetVC.insertChild(subview, at: atIndex)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
public override func removeReactSubview(_ subview: UIView!) {
|
|
103
|
-
sheetVC.removeChild(subview)
|
|
104
|
-
}
|
|
127
|
+
}
|
|
105
128
|
|
|
106
|
-
|
|
129
|
+
final class SheetViewController: UIViewController,
|
|
130
|
+
UISheetPresentationControllerDelegate
|
|
131
|
+
{
|
|
132
|
+
weak var delegate: RNToolsSheetsViewDelegate?
|
|
107
133
|
|
|
108
|
-
|
|
134
|
+
var appContext: AppContext? {
|
|
135
|
+
didSet {
|
|
136
|
+
_ = view
|
|
137
|
+
}
|
|
138
|
+
}
|
|
109
139
|
|
|
110
|
-
|
|
111
|
-
@ObservedObject var props: SheetProps
|
|
112
|
-
var sheetVC: SheetInternalViewController
|
|
113
|
-
var onDismiss: EventDispatcher
|
|
114
|
-
var onStateChange: EventDispatcher
|
|
140
|
+
@available(*, unavailable) required init?(coder: NSCoder) { fatalError() }
|
|
115
141
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
142
|
+
init() {
|
|
143
|
+
super.init(nibName: nil, bundle: nil)
|
|
144
|
+
view.backgroundColor = .white
|
|
145
|
+
}
|
|
120
146
|
|
|
121
|
-
|
|
122
|
-
|
|
147
|
+
override func loadView() {
|
|
148
|
+
self.view = UIView()
|
|
149
|
+
RNToolsTouchHandlerHelper.createAndAttachTouchHandler(for: self.view)
|
|
123
150
|
}
|
|
151
|
+
|
|
124
152
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
let i = index,
|
|
128
|
-
detents.indices.contains(i)
|
|
129
|
-
else { return detents.first! }
|
|
130
|
-
return detents[i]
|
|
153
|
+
deinit {
|
|
154
|
+
overlayWindow = nil
|
|
131
155
|
}
|
|
132
156
|
|
|
133
|
-
private
|
|
134
|
-
for height: CGFloat,
|
|
135
|
-
snapPoints: [Int]
|
|
136
|
-
) -> Int {
|
|
137
|
-
guard !snapPoints.isEmpty else { return 0 }
|
|
157
|
+
private let detentTag = UUID().uuidString
|
|
138
158
|
|
|
139
|
-
|
|
140
|
-
if let i = sorted.firstIndex(where: { CGFloat($0) >= height }) {
|
|
141
|
-
return i
|
|
142
|
-
}
|
|
143
|
-
return sorted.count - 1
|
|
144
|
-
}
|
|
159
|
+
private var overlayWindow: UIWindow?
|
|
145
160
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
isDragging = true
|
|
165
|
-
onStateChange(["type": "DRAGGING"])
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
settleTimer?.invalidate()
|
|
169
|
-
settleTimer = Timer.scheduledTimer(
|
|
170
|
-
withTimeInterval: 0.15,
|
|
171
|
-
repeats: false
|
|
172
|
-
) { _ in
|
|
173
|
-
isDragging = false
|
|
174
|
-
onStateChange(["type": "SETTLING"])
|
|
175
|
-
|
|
176
|
-
DispatchQueue.main.asyncAfter(
|
|
177
|
-
deadline: .now() + 0.15
|
|
178
|
-
) {
|
|
179
|
-
let idx = upperSnapIndex(
|
|
180
|
-
for: newHeight,
|
|
181
|
-
snapPoints: props.snapPoints
|
|
182
|
-
)
|
|
183
|
-
onStateChange([
|
|
184
|
-
"type": "OPEN",
|
|
185
|
-
"payload": ["index": idx],
|
|
186
|
-
])
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
lastHeight = newHeight
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
)
|
|
195
|
-
.presentationBackground16_4(
|
|
196
|
-
props.backgroundColor != nil
|
|
197
|
-
? Color(hex: props.backgroundColor!) : Color.white
|
|
198
|
-
)
|
|
199
|
-
.presentationCornerRadius16_4(
|
|
200
|
-
props.cornerRadius.map { CGFloat($0) }
|
|
201
|
-
)
|
|
202
|
-
.presentationDragIndicator(
|
|
203
|
-
props.grabberVisible ? .visible : .hidden
|
|
204
|
-
)
|
|
205
|
-
.presentationDetents(
|
|
206
|
-
Set(detents),
|
|
207
|
-
selection: $selectedDetent
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
.onAppear {
|
|
211
|
-
selectedDetent = detent(for: props.openToIndex)
|
|
212
|
-
}
|
|
161
|
+
func presentSheet(
|
|
162
|
+
openTo index: Int = 0,
|
|
163
|
+
snapPoints: [CGFloat],
|
|
164
|
+
grabberVisible: Bool,
|
|
165
|
+
backgroundColor: String?,
|
|
166
|
+
cornerRadius: Float?
|
|
167
|
+
) {
|
|
168
|
+
guard overlayWindow == nil else { return }
|
|
169
|
+
|
|
170
|
+
modalPresentationStyle = .pageSheet
|
|
171
|
+
|
|
172
|
+
if let sheet = sheetPresentationController {
|
|
173
|
+
sheet.delegate = self
|
|
174
|
+
sheet.prefersGrabberVisible = grabberVisible
|
|
175
|
+
sheet.detents = makeDetents(from: snapPoints)
|
|
176
|
+
|
|
177
|
+
if let radius = cornerRadius {
|
|
178
|
+
sheet.preferredCornerRadius = CGFloat(radius)
|
|
213
179
|
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
180
|
|
|
217
|
-
|
|
218
|
-
|
|
181
|
+
let detents = sheet.detents
|
|
182
|
+
if detents.indices.contains(index) {
|
|
183
|
+
sheet.selectedDetentIdentifier = detents[index].identifier
|
|
184
|
+
} else {
|
|
185
|
+
sheet.selectedDetentIdentifier = detents.first?.identifier
|
|
186
|
+
}
|
|
187
|
+
}
|
|
219
188
|
|
|
220
|
-
|
|
221
|
-
controller
|
|
222
|
-
}
|
|
223
|
-
func updateUIViewController(
|
|
224
|
-
_ uiViewController: SheetInternalViewController,
|
|
225
|
-
context: Context
|
|
226
|
-
) {}
|
|
227
|
-
}
|
|
189
|
+
view.backgroundColor = UIColor(hex: backgroundColor) ?? .white
|
|
228
190
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
191
|
+
let w = UIWindow(frame: UIScreen.main.bounds)
|
|
192
|
+
w.windowLevel = .statusBar + 2
|
|
193
|
+
w.rootViewController = UIViewController()
|
|
194
|
+
w.makeKeyAndVisible()
|
|
195
|
+
|
|
196
|
+
overlayWindow = w
|
|
197
|
+
|
|
198
|
+
let host = UIViewController()
|
|
199
|
+
host.modalPresentationStyle = .overFullScreen
|
|
200
|
+
host.view.backgroundColor = .clear
|
|
201
|
+
|
|
202
|
+
w.rootViewController?.present(host, animated: false) {
|
|
203
|
+
host.present(self, animated: true)
|
|
234
204
|
}
|
|
235
205
|
}
|
|
236
|
-
var surfaceTouchHandler = RNTSurfaceTouchHandlerWrapper()
|
|
237
|
-
var touchHandler: RCTTouchHandler?
|
|
238
206
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
207
|
+
func dismissSheet() {
|
|
208
|
+
dismiss(animated: true) { [weak self] in
|
|
209
|
+
self?.delegate?.handleSheetDismissed()
|
|
210
|
+
self?.overlayWindow?.isHidden = true
|
|
211
|
+
self?.overlayWindow = nil
|
|
212
|
+
}
|
|
242
213
|
}
|
|
243
214
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
self.surfaceTouchHandler.attach(to: self.view)
|
|
215
|
+
func cleanup() {
|
|
216
|
+
overlayWindow?.isHidden = true
|
|
217
|
+
overlayWindow = nil
|
|
248
218
|
}
|
|
249
219
|
|
|
250
|
-
@available(*, unavailable) required init?(coder: NSCoder) { fatalError() }
|
|
251
|
-
|
|
252
220
|
func insertChild(_ child: UIView, at index: Int) {
|
|
253
221
|
view.insertSubview(child, at: index)
|
|
254
222
|
|
|
@@ -257,64 +225,67 @@ final class SheetInternalViewController: UIViewController {
|
|
|
257
225
|
func removeChild(_ child: UIView) {
|
|
258
226
|
child.removeFromSuperview()
|
|
259
227
|
}
|
|
260
|
-
}
|
|
261
228
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
229
|
+
func sheetPresentationControllerDidChangeSelectedDetentIdentifier(
|
|
230
|
+
_ sheetPresentationController: UISheetPresentationController
|
|
231
|
+
) {
|
|
232
|
+
guard
|
|
233
|
+
let selectedID = sheetPresentationController
|
|
234
|
+
.selectedDetentIdentifier,
|
|
235
|
+
let index = sheetPresentationController.detents
|
|
236
|
+
.firstIndex(where: { $0.identifier == selectedID })
|
|
237
|
+
else { return }
|
|
238
|
+
|
|
239
|
+
delegate?.handleSheetStateChange(index: index)
|
|
267
240
|
}
|
|
268
|
-
}
|
|
269
241
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
while nextResponder != nil && nextResponder as? UIViewController == nil
|
|
276
|
-
{
|
|
277
|
-
nextResponder = nextResponder?.next
|
|
242
|
+
func presentationControllerShouldDismiss(
|
|
243
|
+
_ presentationController: UIPresentationController
|
|
244
|
+
) -> Bool {
|
|
245
|
+
if let d = delegate {
|
|
246
|
+
return d.handleSheetCanDismiss()
|
|
278
247
|
}
|
|
279
248
|
|
|
280
|
-
return
|
|
249
|
+
return true
|
|
281
250
|
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
extension Color {
|
|
285
|
-
init(hex: String) {
|
|
286
|
-
var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
287
|
-
hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "")
|
|
288
251
|
|
|
289
|
-
|
|
290
|
-
|
|
252
|
+
func presentationControllerDidDismiss(
|
|
253
|
+
_ presentationController: UIPresentationController
|
|
254
|
+
) {
|
|
255
|
+
delegate?.handleSheetDismissed()
|
|
256
|
+
cleanup()
|
|
257
|
+
}
|
|
291
258
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
259
|
+
private func makeDetents(from points: [CGFloat])
|
|
260
|
+
-> [UISheetPresentationController.Detent]
|
|
261
|
+
{
|
|
262
|
+
guard !points.isEmpty else { return [.large()] }
|
|
295
263
|
|
|
296
|
-
|
|
264
|
+
return points.enumerated().map { idx, raw in
|
|
265
|
+
.custom(identifier: .init("\(detentTag)_\(idx)")) { _ in
|
|
266
|
+
return raw
|
|
267
|
+
}
|
|
268
|
+
}
|
|
297
269
|
}
|
|
270
|
+
|
|
298
271
|
}
|
|
299
272
|
|
|
273
|
+
extension UIColor {
|
|
274
|
+
convenience init?(hex: String?) {
|
|
275
|
+
guard let hex, !hex.isEmpty else { return nil }
|
|
300
276
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
func presentationBackground16_4(_ color: Color?) -> some View {
|
|
304
|
-
if #available(iOS 16.4, *) {
|
|
305
|
-
self.presentationBackground(color ?? .white)
|
|
306
|
-
} else {
|
|
307
|
-
self
|
|
308
|
-
}
|
|
309
|
-
}
|
|
277
|
+
var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
278
|
+
hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "")
|
|
310
279
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
280
|
+
guard hexSanitized.count == 6 else { return nil }
|
|
281
|
+
|
|
282
|
+
var rgb: UInt64 = 0
|
|
283
|
+
guard Scanner(string: hexSanitized).scanHexInt64(&rgb) else { return nil }
|
|
284
|
+
|
|
285
|
+
let red = CGFloat((rgb & 0xFF0000) >> 16) / 255.0
|
|
286
|
+
let green = CGFloat((rgb & 0x00FF00) >> 8) / 255.0
|
|
287
|
+
let blue = CGFloat(rgb & 0x0000FF) / 255.0
|
|
288
|
+
|
|
289
|
+
self.init(red: red, green: green, blue: blue, alpha: 1.0)
|
|
318
290
|
}
|
|
319
291
|
}
|
|
320
|
-
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// RNToolsTouchHandlerHelper.h
|
|
2
|
+
|
|
3
|
+
#import <Foundation/Foundation.h>
|
|
4
|
+
#import <UIKit/UIKit.h>
|
|
5
|
+
|
|
6
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
7
|
+
|
|
8
|
+
@interface RNToolsTouchHandlerHelper : NSObject
|
|
9
|
+
|
|
10
|
+
+ (nullable UIGestureRecognizer *)createAndAttachTouchHandlerForView:(UIView *)view;
|
|
11
|
+
+ (void)detachTouchHandler:(nullable UIGestureRecognizer *)handler fromView:(UIView *)view;
|
|
12
|
+
|
|
13
|
+
@end
|
|
14
|
+
|
|
15
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// RNToolsTouchHandlerHelper.mm
|
|
2
|
+
|
|
3
|
+
#import "RNToolsTouchHandlerHelper.h"
|
|
4
|
+
#import <React/RCTSurfaceTouchHandler.h>
|
|
5
|
+
|
|
6
|
+
@implementation RNToolsTouchHandlerHelper
|
|
7
|
+
|
|
8
|
+
+ (nullable UIGestureRecognizer *)createAndAttachTouchHandlerForView:(UIView *)view {
|
|
9
|
+
for (UIGestureRecognizer *recognizer in [view.gestureRecognizers copy]) {
|
|
10
|
+
if ([recognizer isKindOfClass:[RCTSurfaceTouchHandler class]]) {
|
|
11
|
+
return nil;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
RCTSurfaceTouchHandler *touchHandler = [[RCTSurfaceTouchHandler alloc] init];
|
|
16
|
+
[touchHandler attachToView:view];
|
|
17
|
+
return touchHandler;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
+ (void)detachTouchHandler:(nullable UIGestureRecognizer *)handler fromView:(UIView *)view {
|
|
21
|
+
if (!handler) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if ([handler isKindOfClass:[RCTSurfaceTouchHandler class]]) {
|
|
26
|
+
RCTSurfaceTouchHandler *touchHandler = (RCTSurfaceTouchHandler *)handler;
|
|
27
|
+
[touchHandler detachFromView:view];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@end
|
package/package.json
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rn-tools/sheets",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
|
+
"description": "A React Native library for creating and managing native sheets in Expo applications.",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"build": "expo-module build",
|
|
8
|
-
"clean": "expo-module clean",
|
|
9
|
-
"lint": "expo-module lint",
|
|
10
|
-
"test": "expo-module test",
|
|
11
|
-
"prepare": "expo-module prepare",
|
|
12
|
-
"prepublishOnly": "expo-module prepublishOnly",
|
|
13
|
-
"expo-module": "expo-module",
|
|
14
7
|
"open:ios": "xed example/ios",
|
|
15
8
|
"open:android": "open -a \"Android Studio\" example/android"
|
|
16
9
|
},
|
|
@@ -28,10 +21,10 @@
|
|
|
28
21
|
"license": "MIT",
|
|
29
22
|
"homepage": "https://github.com/ajsmth/rn-tools#readme",
|
|
30
23
|
"devDependencies": {
|
|
31
|
-
"@types/react": "
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"
|
|
24
|
+
"@types/react": "18.3.12"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@rn-tools/core": "3.0.1"
|
|
35
28
|
},
|
|
36
29
|
"peerDependencies": {
|
|
37
30
|
"expo": "*",
|
|
@@ -6,8 +6,11 @@ import {
|
|
|
6
6
|
ViewStyle,
|
|
7
7
|
Platform,
|
|
8
8
|
LayoutChangeEvent,
|
|
9
|
+
StyleSheet,
|
|
10
|
+
useWindowDimensions,
|
|
9
11
|
} from "react-native";
|
|
10
12
|
import { requireNativeViewManager } from "expo-modules-core";
|
|
13
|
+
import { useSafeAreaInsets } from "@rn-tools/core";
|
|
11
14
|
|
|
12
15
|
type SheetState = "DRAGGING" | "OPEN" | "SETTLING" | "HIDDEN";
|
|
13
16
|
|
|
@@ -17,15 +20,9 @@ type ChangeEvent<T extends SheetState, P = unknown> = {
|
|
|
17
20
|
};
|
|
18
21
|
|
|
19
22
|
type OpenChangeEvent = ChangeEvent<"OPEN", { index: number }>;
|
|
20
|
-
type DraggingChangeEvent = ChangeEvent<"DRAGGING">;
|
|
21
|
-
type SettlingChangeEvent = ChangeEvent<"SETTLING">;
|
|
22
23
|
type HiddenChangeEvent = ChangeEvent<"HIDDEN">;
|
|
23
24
|
|
|
24
|
-
type SheetChangeEvent =
|
|
25
|
-
| OpenChangeEvent
|
|
26
|
-
| DraggingChangeEvent
|
|
27
|
-
| SettlingChangeEvent
|
|
28
|
-
| HiddenChangeEvent;
|
|
25
|
+
type SheetChangeEvent = OpenChangeEvent | HiddenChangeEvent;
|
|
29
26
|
|
|
30
27
|
type NativeOnChangeEvent = NativeSyntheticEvent<SheetChangeEvent>;
|
|
31
28
|
|
|
@@ -45,8 +42,10 @@ type NativeSheetViewProps = {
|
|
|
45
42
|
children: React.ReactNode;
|
|
46
43
|
snapPoints?: number[];
|
|
47
44
|
isOpen: boolean;
|
|
48
|
-
|
|
45
|
+
initialIndex: number;
|
|
49
46
|
onDismiss: () => void;
|
|
47
|
+
canDismiss?: boolean;
|
|
48
|
+
onDismissPrevented: () => void;
|
|
50
49
|
onStateChange: (event: NativeOnChangeEvent) => void;
|
|
51
50
|
appearanceAndroid?: AppearanceAndroid;
|
|
52
51
|
appearanceIOS?: AppearanceIOS;
|
|
@@ -60,17 +59,15 @@ export type BottomSheetProps = {
|
|
|
60
59
|
containerStyle?: ViewStyle;
|
|
61
60
|
snapPoints?: number[];
|
|
62
61
|
isOpen: boolean;
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
initialIndex?: number;
|
|
63
|
+
setIsOpen: (isOpen: boolean) => void;
|
|
64
|
+
canDismiss?: boolean;
|
|
65
|
+
onDismissPrevented?: () => void;
|
|
65
66
|
onStateChange?: (event: SheetChangeEvent) => void;
|
|
66
67
|
appearanceAndroid?: AppearanceAndroid;
|
|
67
68
|
appearanceIOS?: AppearanceIOS;
|
|
68
69
|
};
|
|
69
70
|
|
|
70
|
-
// TODO:
|
|
71
|
-
// - get sheet container height from native side and clamp maxHeight to that value
|
|
72
|
-
//
|
|
73
|
-
|
|
74
71
|
export function BottomSheet(props: BottomSheetProps) {
|
|
75
72
|
const {
|
|
76
73
|
onStateChange,
|
|
@@ -78,12 +75,21 @@ export function BottomSheet(props: BottomSheetProps) {
|
|
|
78
75
|
snapPoints = [],
|
|
79
76
|
containerStyle,
|
|
80
77
|
isOpen,
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
initialIndex = 0,
|
|
79
|
+
setIsOpen,
|
|
83
80
|
appearanceAndroid,
|
|
84
81
|
appearanceIOS,
|
|
82
|
+
canDismiss = true,
|
|
83
|
+
onDismissPrevented,
|
|
85
84
|
} = props;
|
|
86
85
|
|
|
86
|
+
const { height: windowHeight } = useWindowDimensions();
|
|
87
|
+
const insets = useSafeAreaInsets();
|
|
88
|
+
const maxSheetHeight = React.useMemo(
|
|
89
|
+
() => Math.max(0, windowHeight - insets.top - insets.bottom),
|
|
90
|
+
[windowHeight, insets.top, insets.bottom],
|
|
91
|
+
);
|
|
92
|
+
|
|
87
93
|
const [layout, setLayout] = React.useState<LayoutRectangle>({
|
|
88
94
|
height: 0,
|
|
89
95
|
width: 0,
|
|
@@ -92,12 +98,32 @@ export function BottomSheet(props: BottomSheetProps) {
|
|
|
92
98
|
});
|
|
93
99
|
|
|
94
100
|
const computedSnapPoints = React.useMemo(() => {
|
|
95
|
-
if (snapPoints.length === 0
|
|
96
|
-
|
|
101
|
+
if (snapPoints.length === 0) {
|
|
102
|
+
if (layout.height === 0) {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return [Math.min(layout.height, maxSheetHeight)];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let effectiveSnapPoints =
|
|
110
|
+
Platform.OS === "android" ? snapPoints.slice(0, 2) : [...snapPoints];
|
|
111
|
+
|
|
112
|
+
const snapPointsExceedingMaxHeight = snapPoints.filter(
|
|
113
|
+
(snapPoint) => snapPoint >= maxSheetHeight,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if (snapPointsExceedingMaxHeight.length > 0) {
|
|
117
|
+
effectiveSnapPoints = [
|
|
118
|
+
...effectiveSnapPoints.filter(
|
|
119
|
+
(snapPoint) => snapPoint < maxSheetHeight,
|
|
120
|
+
),
|
|
121
|
+
maxSheetHeight,
|
|
122
|
+
];
|
|
97
123
|
}
|
|
98
124
|
|
|
99
|
-
return
|
|
100
|
-
}, [
|
|
125
|
+
return effectiveSnapPoints;
|
|
126
|
+
}, [layout.height, maxSheetHeight, snapPoints]);
|
|
101
127
|
|
|
102
128
|
const maxHeight = React.useMemo(
|
|
103
129
|
() =>
|
|
@@ -110,9 +136,12 @@ export function BottomSheet(props: BottomSheetProps) {
|
|
|
110
136
|
const style = React.useMemo(() => {
|
|
111
137
|
return {
|
|
112
138
|
height: maxHeight,
|
|
139
|
+
borderTopLeftRadius: 16,
|
|
140
|
+
borderTopRightRadius: 16,
|
|
141
|
+
backgroundColor: "white",
|
|
113
142
|
...containerStyle,
|
|
114
143
|
};
|
|
115
|
-
}, [maxHeight]);
|
|
144
|
+
}, [maxHeight, containerStyle]);
|
|
116
145
|
|
|
117
146
|
const handleOnDismiss = React.useCallback(() => {
|
|
118
147
|
setIsOpen(false);
|
|
@@ -129,19 +158,48 @@ export function BottomSheet(props: BottomSheetProps) {
|
|
|
129
158
|
setLayout(event.nativeEvent.layout);
|
|
130
159
|
}, []);
|
|
131
160
|
|
|
161
|
+
const handleDismissWithChanges = React.useCallback(() => {
|
|
162
|
+
onDismissPrevented?.();
|
|
163
|
+
}, [onDismissPrevented]);
|
|
164
|
+
|
|
165
|
+
const isAutosized = React.useMemo(
|
|
166
|
+
() => snapPoints.length === 0,
|
|
167
|
+
[snapPoints],
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const pointerEvents = React.useMemo(() => {
|
|
171
|
+
return isOpen ? "box-none" : "none";
|
|
172
|
+
}, [isOpen]);
|
|
173
|
+
|
|
174
|
+
const computedIsOpen = React.useMemo(
|
|
175
|
+
() => isOpen && computedSnapPoints.length > 0,
|
|
176
|
+
[isOpen, computedSnapPoints],
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const innerStyle = React.useMemo(
|
|
180
|
+
() => (isAutosized ? undefined : StyleSheet.absoluteFill),
|
|
181
|
+
[isAutosized],
|
|
182
|
+
);
|
|
183
|
+
|
|
132
184
|
return (
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
{
|
|
144
|
-
|
|
145
|
-
|
|
185
|
+
<View style={StyleSheet.absoluteFill} pointerEvents={pointerEvents}>
|
|
186
|
+
<NativeSheetsView
|
|
187
|
+
isOpen={computedIsOpen}
|
|
188
|
+
canDismiss={canDismiss}
|
|
189
|
+
initialIndex={initialIndex}
|
|
190
|
+
onDismiss={handleOnDismiss}
|
|
191
|
+
onStateChange={handleStateChange}
|
|
192
|
+
onDismissPrevented={handleDismissWithChanges}
|
|
193
|
+
snapPoints={computedSnapPoints}
|
|
194
|
+
appearanceAndroid={appearanceAndroid}
|
|
195
|
+
appearanceIOS={appearanceIOS}
|
|
196
|
+
>
|
|
197
|
+
<View style={style} collapsable={false}>
|
|
198
|
+
<View onLayout={handleLayout} style={innerStyle} collapsable={false}>
|
|
199
|
+
{children}
|
|
200
|
+
</View>
|
|
201
|
+
</View>
|
|
202
|
+
</NativeSheetsView>
|
|
203
|
+
</View>
|
|
146
204
|
);
|
|
147
205
|
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
// RNTSurfaceTouchHandlerWrapper.mm (Objective‑C++, .mm extension!)
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
#import "RNTSurfaceTouchHandlerWrapper.h"
|
|
5
|
-
#if RCT_NEW_ARCH_ENABLED
|
|
6
|
-
#import <React/RCTSurfaceTouchHandler.h>
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@implementation RNTSurfaceTouchHandlerWrapper {
|
|
10
|
-
|
|
11
|
-
RCTSurfaceTouchHandler *_handler;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
- (instancetype)init {
|
|
15
|
-
if (self = [super init]) {
|
|
16
|
-
_handler = [[RCTSurfaceTouchHandler alloc] init];
|
|
17
|
-
}
|
|
18
|
-
return self;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
- (void)attachToView:(UIView *)view {
|
|
22
|
-
[_handler attachToView:view];
|
|
23
|
-
}
|
|
24
|
-
@end
|
|
25
|
-
#else
|
|
26
|
-
|
|
27
|
-
@implementation RNTSurfaceTouchHandlerWrapper {
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
- (instancetype)init {
|
|
31
|
-
if (self = [super init]) {
|
|
32
|
-
}
|
|
33
|
-
return self;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
- (void)attachToView:(UIView *)view {
|
|
37
|
-
}
|
|
38
|
-
@end
|
|
39
|
-
|
|
40
|
-
#endif
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|