@rn-tools/sheets 0.1.3 → 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 +23 -8
- 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 +5 -5
- package/ios/RNToolsSheetsModule.swift +27 -10
- package/ios/RNToolsSheetsView.swift +199 -206
- 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 -21
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -2,7 +2,12 @@
|
|
|
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
|
+
|
|
7
|
+
Supports stacking multiple sheets on top of each other
|
|
8
|
+
|
|
9
|
+
https://github.com/user-attachments/assets/426c77e6-74c6-4748-8010-477267fa9433
|
|
10
|
+
|
|
6
11
|
|
|
7
12
|
## Motivation
|
|
8
13
|
|
|
@@ -23,7 +28,7 @@ Update your minimum iOS deployment target to 16 in `app.json`:
|
|
|
23
28
|
"expo-build-properties",
|
|
24
29
|
{
|
|
25
30
|
"ios": {
|
|
26
|
-
"deploymentTarget": "16.
|
|
31
|
+
"deploymentTarget": "16.0"
|
|
27
32
|
}
|
|
28
33
|
}
|
|
29
34
|
]
|
|
@@ -48,9 +53,11 @@ export default function App() {
|
|
|
48
53
|
|
|
49
54
|
<BottomSheet
|
|
50
55
|
isOpen={isOpen}
|
|
51
|
-
|
|
52
|
-
|
|
56
|
+
setIsOpen={setIsOpen}
|
|
57
|
+
initialIndex={1}
|
|
53
58
|
onStateChange={(event) => console.log({ event })}
|
|
59
|
+
canDismiss={true}
|
|
60
|
+
onDismissPrevented={() => console.log("dismiss prevented")}
|
|
54
61
|
snapPoints={[400, 600, 750]}
|
|
55
62
|
appearanceAndroid={{
|
|
56
63
|
dimAmount: 0,
|
|
@@ -72,16 +79,18 @@ export default function App() {
|
|
|
72
79
|
|
|
73
80
|
## Props
|
|
74
81
|
|
|
75
|
-
- `isOpen /
|
|
82
|
+
- `isOpen / setIsOpen` - Controller props for toggling the sheet open and closed - this is required
|
|
76
83
|
|
|
77
|
-
- `
|
|
84
|
+
- `initialIndex` - will open the bottom sheet to the defined snapPoint index
|
|
78
85
|
|
|
79
86
|
- `onStateChange` - callback to track the internal state of the sheet. The following events are emitted:
|
|
80
87
|
|
|
81
88
|
- { type: "HIDDEN" }
|
|
82
89
|
- { type: "OPEN", payload: { index: number }}
|
|
83
|
-
|
|
84
|
-
|
|
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}`
|
|
85
94
|
|
|
86
95
|
- `snapPoints` - a list of sizes that the sheet will "snap" to
|
|
87
96
|
|
|
@@ -93,6 +102,12 @@ export default function App() {
|
|
|
93
102
|
|
|
94
103
|
## Caveats
|
|
95
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
|
+
|
|
96
111
|
- (Android) can have a maximum of 2 snap points
|
|
97
112
|
|
|
98
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
|
|
|
@@ -13,20 +14,19 @@ Pod::Spec.new do |s|
|
|
|
13
14
|
s.author = package['author']
|
|
14
15
|
s.homepage = package['homepage']
|
|
15
16
|
s.platforms = {
|
|
16
|
-
:ios => '16.
|
|
17
|
-
:tvos => '16.
|
|
17
|
+
:ios => '16.0',
|
|
18
|
+
:tvos => '16.0'
|
|
18
19
|
}
|
|
19
20
|
s.swift_version = '5.4'
|
|
20
21
|
s.source = { git: 'https://github.com/ajsmth/rn-tools' }
|
|
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,163 +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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
159
|
+
private var overlayWindow: UIWindow?
|
|
160
|
+
|
|
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 }
|
|
145
169
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
) {
|
|
156
|
-
SheetInternalVCRepresentable(controller: sheetVC)
|
|
157
|
-
.background(
|
|
158
|
-
GeometryReader { geometry in
|
|
159
|
-
Color.clear
|
|
160
|
-
.onChange(of: geometry.size.height) {
|
|
161
|
-
newHeight in
|
|
162
|
-
if abs(newHeight - lastHeight) > 2 {
|
|
163
|
-
if !isDragging {
|
|
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
|
-
.presentationBackground(
|
|
196
|
-
props.backgroundColor != nil
|
|
197
|
-
? Color(hex: props.backgroundColor!) : Color.white
|
|
198
|
-
)
|
|
199
|
-
.presentationDragIndicator(
|
|
200
|
-
props.grabberVisible ? .visible : .hidden
|
|
201
|
-
)
|
|
202
|
-
.presentationDetents(
|
|
203
|
-
Set(detents),
|
|
204
|
-
selection: $selectedDetent
|
|
205
|
-
)
|
|
206
|
-
.presentationCornerRadius(
|
|
207
|
-
props.cornerRadius.map { CGFloat($0) }
|
|
208
|
-
)
|
|
209
|
-
.onAppear {
|
|
210
|
-
selectedDetent = detent(for: props.openToIndex)
|
|
211
|
-
}
|
|
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)
|
|
212
179
|
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
180
|
|
|
216
|
-
|
|
217
|
-
|
|
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
|
+
}
|
|
218
188
|
|
|
219
|
-
|
|
220
|
-
controller
|
|
221
|
-
}
|
|
222
|
-
func updateUIViewController(
|
|
223
|
-
_ uiViewController: SheetInternalViewController,
|
|
224
|
-
context: Context
|
|
225
|
-
) {}
|
|
226
|
-
}
|
|
189
|
+
view.backgroundColor = UIColor(hex: backgroundColor) ?? .white
|
|
227
190
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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)
|
|
233
204
|
}
|
|
234
205
|
}
|
|
235
|
-
var surfaceTouchHandler = RNTSurfaceTouchHandlerWrapper()
|
|
236
|
-
var touchHandler: RCTTouchHandler?
|
|
237
206
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
207
|
+
func dismissSheet() {
|
|
208
|
+
dismiss(animated: true) { [weak self] in
|
|
209
|
+
self?.delegate?.handleSheetDismissed()
|
|
210
|
+
self?.overlayWindow?.isHidden = true
|
|
211
|
+
self?.overlayWindow = nil
|
|
212
|
+
}
|
|
241
213
|
}
|
|
242
214
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
self.surfaceTouchHandler.attach(to: self.view)
|
|
215
|
+
func cleanup() {
|
|
216
|
+
overlayWindow?.isHidden = true
|
|
217
|
+
overlayWindow = nil
|
|
247
218
|
}
|
|
248
219
|
|
|
249
|
-
@available(*, unavailable) required init?(coder: NSCoder) { fatalError() }
|
|
250
|
-
|
|
251
220
|
func insertChild(_ child: UIView, at index: Int) {
|
|
252
221
|
view.insertSubview(child, at: index)
|
|
253
222
|
|
|
@@ -256,43 +225,67 @@ final class SheetInternalViewController: UIViewController {
|
|
|
256
225
|
func removeChild(_ child: UIView) {
|
|
257
226
|
child.removeFromSuperview()
|
|
258
227
|
}
|
|
259
|
-
}
|
|
260
228
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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)
|
|
266
240
|
}
|
|
267
|
-
}
|
|
268
241
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
while nextResponder != nil && nextResponder as? UIViewController == nil
|
|
275
|
-
{
|
|
276
|
-
nextResponder = nextResponder?.next
|
|
242
|
+
func presentationControllerShouldDismiss(
|
|
243
|
+
_ presentationController: UIPresentationController
|
|
244
|
+
) -> Bool {
|
|
245
|
+
if let d = delegate {
|
|
246
|
+
return d.handleSheetCanDismiss()
|
|
277
247
|
}
|
|
278
248
|
|
|
279
|
-
return
|
|
249
|
+
return true
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
func presentationControllerDidDismiss(
|
|
253
|
+
_ presentationController: UIPresentationController
|
|
254
|
+
) {
|
|
255
|
+
delegate?.handleSheetDismissed()
|
|
256
|
+
cleanup()
|
|
280
257
|
}
|
|
258
|
+
|
|
259
|
+
private func makeDetents(from points: [CGFloat])
|
|
260
|
+
-> [UISheetPresentationController.Detent]
|
|
261
|
+
{
|
|
262
|
+
guard !points.isEmpty else { return [.large()] }
|
|
263
|
+
|
|
264
|
+
return points.enumerated().map { idx, raw in
|
|
265
|
+
.custom(identifier: .init("\(detentTag)_\(idx)")) { _ in
|
|
266
|
+
return raw
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
281
271
|
}
|
|
282
272
|
|
|
283
|
-
extension
|
|
284
|
-
init(hex: String) {
|
|
273
|
+
extension UIColor {
|
|
274
|
+
convenience init?(hex: String?) {
|
|
275
|
+
guard let hex, !hex.isEmpty else { return nil }
|
|
276
|
+
|
|
285
277
|
var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
286
278
|
hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "")
|
|
287
279
|
|
|
280
|
+
guard hexSanitized.count == 6 else { return nil }
|
|
281
|
+
|
|
288
282
|
var rgb: UInt64 = 0
|
|
289
|
-
Scanner(string: hexSanitized).scanHexInt64(&rgb)
|
|
283
|
+
guard Scanner(string: hexSanitized).scanHexInt64(&rgb) else { return nil }
|
|
290
284
|
|
|
291
|
-
let red =
|
|
292
|
-
let green =
|
|
293
|
-
let blue =
|
|
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
|
|
294
288
|
|
|
295
|
-
self.init(red: red, green: green, blue: blue)
|
|
289
|
+
self.init(red: red, green: green, blue: blue, alpha: 1.0)
|
|
296
290
|
}
|
|
297
291
|
}
|
|
298
|
-
|
|
@@ -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,21 +0,0 @@
|
|
|
1
|
-
// RNTSurfaceTouchHandlerWrapper.mm (Objective‑C++, .mm extension!)
|
|
2
|
-
#import "RNTSurfaceTouchHandlerWrapper.h"
|
|
3
|
-
#import <React/RCTSurfaceTouchHandler.h>
|
|
4
|
-
|
|
5
|
-
@implementation RNTSurfaceTouchHandlerWrapper {
|
|
6
|
-
RCTSurfaceTouchHandler *_handler;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
- (instancetype)init {
|
|
10
|
-
if (self = [super init]) {
|
|
11
|
-
_handler = [[RCTSurfaceTouchHandler alloc] init];
|
|
12
|
-
}
|
|
13
|
-
return self;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
- (void)attachToView:(UIView *)view {
|
|
17
|
-
[_handler attachToView:view];
|
|
18
|
-
}
|
|
19
|
-
@end
|
|
20
|
-
|
|
21
|
-
|