@rn-tools/sheets 0.1.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/.eslintrc.js +1 -0
- package/CHANGELOG.md +11 -0
- package/README.md +82 -0
- package/android/build.gradle +75 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/expo/modules/sheets/RNToolsSheetsModule.kt +55 -0
- package/android/src/main/java/expo/modules/sheets/RNToolsSheetsView.kt +199 -0
- package/android/src/main/java/expo/modules/sheets/SheetProps.kt +31 -0
- package/android/src/main/java/expo/modules/sheets/SheetRootView.kt +87 -0
- package/expo-module.config.json +17 -0
- package/ios/RNToolsSheets.podspec +29 -0
- package/ios/RNToolsSheetsModule.swift +29 -0
- package/ios/RNToolsSheetsView.swift +219 -0
- package/package.json +41 -0
- package/src/index.ts +1 -0
- package/src/native-sheets-view.tsx +147 -0
- package/tsconfig.json +10 -0
package/.eslintrc.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.expores = require("@rn-tools/eslint-config")
|
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# @rn-tools/sheets
|
|
2
|
+
|
|
3
|
+
An expo module for rendering native bottom sheet components in iOS and Android.
|
|
4
|
+
|
|
5
|
+
Uses SwiftUI's sheet API and Android's BottomSheetDialog component to render React Native children in a modal bottom sheet
|
|
6
|
+
|
|
7
|
+
## Motivation
|
|
8
|
+
|
|
9
|
+
- Better performance and responsiveness than JS based solutions
|
|
10
|
+
|
|
11
|
+
- Native OS handling for gestures, keyboard, and navigation
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
`yarn add @rntools/sheets`
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
As with most non-core expo modules this requires a new native build
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { BottomSheet } from '@rn-tools/sheets'
|
|
25
|
+
|
|
26
|
+
export default function App() {
|
|
27
|
+
const [isOpen, setIsOpen] = React.useState(false);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<View className="flex-1">
|
|
31
|
+
<Button title="Show sheet" onPress={() => setIsOpen(!isOpen)} />
|
|
32
|
+
|
|
33
|
+
<BottomSheet
|
|
34
|
+
isOpen={isOpen}
|
|
35
|
+
onOpenChange={setIsOpen}
|
|
36
|
+
openToIndex={1}
|
|
37
|
+
onStateChange={(event) => console.log({ event })}
|
|
38
|
+
snapPoints={[400, 600, 750]}
|
|
39
|
+
appearanceAndroid={{
|
|
40
|
+
dimAmount: 0,
|
|
41
|
+
cornerRadius: 32.0,
|
|
42
|
+
backgroundColor: "#ffffff",
|
|
43
|
+
}}
|
|
44
|
+
appearanceIOS={{
|
|
45
|
+
cornerRadius: 16.0,
|
|
46
|
+
grabberVisible: true,
|
|
47
|
+
backgroundColor: "#ffffff",
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
{isOpen && <MyContent />}
|
|
51
|
+
</BottomSheet>
|
|
52
|
+
</View>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Props
|
|
58
|
+
|
|
59
|
+
- `isOpen / onOpenChange` - Controller props for toggling the sheet open and closed - this is required
|
|
60
|
+
|
|
61
|
+
- `openToIndex` - will open the bottom sheet to the defined snapPoint index
|
|
62
|
+
|
|
63
|
+
- `onStateChange` - callback to track the internal state of the sheet. The following events are emitted:
|
|
64
|
+
|
|
65
|
+
- { type: "HIDDEN" }
|
|
66
|
+
- { type: "OPEN", payload: { index: number }}
|
|
67
|
+
- { type: "SETTLING" }
|
|
68
|
+
- { type: "DRAGGING" }
|
|
69
|
+
|
|
70
|
+
- `snapPoints` - a list of sizes that the sheet will "snap" to
|
|
71
|
+
|
|
72
|
+
- if you do not specify snapPoints, the sheet will size to its content. This means any flex based layout needs to have an explicit container size
|
|
73
|
+
|
|
74
|
+
- **Android will only use the first two snapPoints!**
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
## Caveats
|
|
79
|
+
|
|
80
|
+
- (Android) can have a maximum of 2 snap points
|
|
81
|
+
|
|
82
|
+
- (Android) use the `nestedScrollEnabled` prop for nested scrollviews
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
apply plugin: 'com.android.library'
|
|
2
|
+
|
|
3
|
+
group = 'expo.modules.sheets'
|
|
4
|
+
version = '0.1.0'
|
|
5
|
+
|
|
6
|
+
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
7
|
+
apply from: expoModulesCorePlugin
|
|
8
|
+
applyKotlinExpoModulesCorePlugin()
|
|
9
|
+
useCoreDependencies()
|
|
10
|
+
useExpoPublishing()
|
|
11
|
+
|
|
12
|
+
// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
|
|
13
|
+
// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
|
|
14
|
+
// Most of the time, you may like to manage the Android SDK versions yourself.
|
|
15
|
+
def useManagedAndroidSdkVersions = false
|
|
16
|
+
if (useManagedAndroidSdkVersions) {
|
|
17
|
+
useDefaultAndroidSdkVersions()
|
|
18
|
+
} else {
|
|
19
|
+
buildscript {
|
|
20
|
+
// Simple helper that allows the root project to override versions declared by this library.
|
|
21
|
+
ext.safeExtGet = { prop, fallback ->
|
|
22
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
project.android {
|
|
26
|
+
compileSdkVersion safeExtGet("compileSdkVersion", 34)
|
|
27
|
+
defaultConfig {
|
|
28
|
+
minSdkVersion safeExtGet("minSdkVersion", 21)
|
|
29
|
+
targetSdkVersion safeExtGet("targetSdkVersion", 34)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
android {
|
|
35
|
+
namespace "expo.modules.sheets"
|
|
36
|
+
defaultConfig {
|
|
37
|
+
versionCode 1
|
|
38
|
+
versionName "0.1.0"
|
|
39
|
+
}
|
|
40
|
+
lintOptions {
|
|
41
|
+
abortOnError false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
// Enables Compose functionality
|
|
46
|
+
buildFeatures {
|
|
47
|
+
compose true
|
|
48
|
+
}
|
|
49
|
+
composeOptions {
|
|
50
|
+
kotlinCompilerExtensionVersion = "1.5.15"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
lintOptions {
|
|
54
|
+
abortOnError false
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
repositories {
|
|
58
|
+
mavenCentral()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
dependencies {
|
|
62
|
+
implementation(platform("androidx.compose:compose-bom:2024.10.01"))
|
|
63
|
+
implementation "androidx.compose.material:material:1.7.5"
|
|
64
|
+
implementation "com.google.android.material:material:1.12.0"
|
|
65
|
+
// Possible view model props passer not used yet
|
|
66
|
+
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
|
|
67
|
+
|
|
68
|
+
// OPTIONAL - Android Studio Preview support
|
|
69
|
+
implementation("androidx.compose.ui:ui-tooling-preview")
|
|
70
|
+
debugImplementation("androidx.compose.ui:ui-tooling")
|
|
71
|
+
|
|
72
|
+
implementation "androidx.navigation:navigation-compose:2.8.5"
|
|
73
|
+
implementation 'com.facebook.react:react-native:+'
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
package expo.modules.sheets
|
|
2
|
+
|
|
3
|
+
import android.view.View
|
|
4
|
+
import expo.modules.kotlin.modules.Module
|
|
5
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
6
|
+
|
|
7
|
+
class RNToolsSheetsModule : Module() {
|
|
8
|
+
override fun definition() = ModuleDefinition {
|
|
9
|
+
Name("RNToolsSheets")
|
|
10
|
+
|
|
11
|
+
View(RNToolsSheetsView::class) {
|
|
12
|
+
GroupView<RNToolsSheetsView> {
|
|
13
|
+
AddChildView { parent, child: View, index ->
|
|
14
|
+
parent.rootViewGroup.addView(child, index)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
GetChildCount { parent ->
|
|
18
|
+
return@GetChildCount parent.rootViewGroup.childCount
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
GetChildViewAt { parent, index ->
|
|
22
|
+
parent.rootViewGroup.getChildAt(index)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
RemoveChildView { parent, child: View ->
|
|
26
|
+
parent.rootViewGroup.removeView(child)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
RemoveChildViewAt { parent, index ->
|
|
30
|
+
parent.rootViewGroup.removeViewAt(index)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
Events("onDismiss", "onStateChange")
|
|
35
|
+
|
|
36
|
+
Prop("isOpen") { view: RNToolsSheetsView, isOpen: Boolean ->
|
|
37
|
+
view.props.isOpen = isOpen
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
Prop("openToIndex") { view: RNToolsSheetsView, openToIndex: Int ->
|
|
41
|
+
view.props.openToIndex = openToIndex
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
Prop("snapPoints") { view: RNToolsSheetsView, snapPoints: List<Int> ->
|
|
45
|
+
view.props.snapPoints = snapPoints.map { view.convertToPx(it) }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
Prop("appearanceAndroid") { view: RNToolsSheetsView, appearance: SheetAppearance ->
|
|
49
|
+
view.props.dimAmount = appearance.dimAmount ?: 0.56f
|
|
50
|
+
view.props.backgroundColor = appearance.backgroundColor
|
|
51
|
+
view.props.cornerRadius = appearance.cornerRadius?.toFloat()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
package expo.modules.sheets
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.drawable.GradientDrawable
|
|
5
|
+
import android.view.View
|
|
6
|
+
import android.view.ViewGroup
|
|
7
|
+
import android.widget.FrameLayout
|
|
8
|
+
import androidx.compose.runtime.LaunchedEffect
|
|
9
|
+
import androidx.compose.ui.platform.ComposeView
|
|
10
|
+
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
11
|
+
import com.google.android.material.bottomsheet.BottomSheetDialog
|
|
12
|
+
import expo.modules.kotlin.AppContext
|
|
13
|
+
import expo.modules.kotlin.viewevent.EventDispatcher
|
|
14
|
+
import expo.modules.kotlin.views.ExpoView
|
|
15
|
+
import android.graphics.Color
|
|
16
|
+
|
|
17
|
+
class RNToolsSheetsView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
|
|
18
|
+
val onDismiss by EventDispatcher()
|
|
19
|
+
val onStateChange by EventDispatcher()
|
|
20
|
+
|
|
21
|
+
var rootViewGroup = SheetRootView(context, appContext)
|
|
22
|
+
private var composeView: ComposeView
|
|
23
|
+
|
|
24
|
+
private var bottomSheetDialog: BottomSheetDialog? = null
|
|
25
|
+
|
|
26
|
+
val props = SheetProps()
|
|
27
|
+
|
|
28
|
+
init {
|
|
29
|
+
layoutParams = LayoutParams(
|
|
30
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
31
|
+
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
props.rootViewGroup = rootViewGroup
|
|
35
|
+
|
|
36
|
+
composeView = ComposeView(context).apply {
|
|
37
|
+
setContent {
|
|
38
|
+
LaunchedEffect(props.isOpen) {
|
|
39
|
+
if (props.isOpen) {
|
|
40
|
+
showSheet()
|
|
41
|
+
} else {
|
|
42
|
+
hideSheet()
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
addView(composeView)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
override fun setId(id: Int) {
|
|
52
|
+
super.setId(id)
|
|
53
|
+
rootViewGroup.id = id
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private fun hideSheet() {
|
|
57
|
+
bottomSheetDialog?.dismiss()
|
|
58
|
+
bottomSheetDialog = null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private fun showSheet() {
|
|
62
|
+
(rootViewGroup.parent as? ViewGroup)?.removeView(rootViewGroup)
|
|
63
|
+
|
|
64
|
+
val frameLayout = FrameLayout(context).apply {
|
|
65
|
+
layoutParams = LayoutParams(
|
|
66
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
67
|
+
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
addView(rootViewGroup)
|
|
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
|
|
80
|
+
|
|
81
|
+
bottomSheetDialog = BottomSheetDialog(context).apply {
|
|
82
|
+
setContentView(frameLayout)
|
|
83
|
+
|
|
84
|
+
window?.setDimAmount(props.dimAmount)
|
|
85
|
+
|
|
86
|
+
window?.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)?.let { bottomSheet ->
|
|
87
|
+
|
|
88
|
+
val backgroundColor = props.backgroundColor?.let {
|
|
89
|
+
try {
|
|
90
|
+
Color.parseColor(it) // Convert hex string to Color
|
|
91
|
+
} catch (e: IllegalArgumentException) {
|
|
92
|
+
Color.TRANSPARENT
|
|
93
|
+
}
|
|
94
|
+
} ?: Color.TRANSPARENT
|
|
95
|
+
|
|
96
|
+
val drawable = GradientDrawable().apply {
|
|
97
|
+
setColor(backgroundColor)
|
|
98
|
+
cornerRadius = props.cornerRadius ?: 0f
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
bottomSheet.background = drawable
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
val behavior = behavior
|
|
105
|
+
|
|
106
|
+
setOnDismissListener {
|
|
107
|
+
onDismiss(mapOf())
|
|
108
|
+
if (behavior.state != BottomSheetBehavior.STATE_HIDDEN) {
|
|
109
|
+
onStateChange(mapOf(
|
|
110
|
+
"type" to "HIDDEN",
|
|
111
|
+
))
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (peekHeight > 0) {
|
|
116
|
+
behavior.peekHeight = peekHeight
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (expandedHeight > 0) {
|
|
120
|
+
frameLayout.layoutParams.height = expandedHeight
|
|
121
|
+
frameLayout.requestLayout()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
|
|
125
|
+
override fun onStateChanged(bottomSheet: android.view.View, newState: Int) {
|
|
126
|
+
handleSheetStateChange(newState)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
override fun onSlide(bottomSheet: android.view.View, slideOffset: Float) {
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
show()
|
|
134
|
+
|
|
135
|
+
if (initialHeight == peekHeight) {
|
|
136
|
+
behavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
|
137
|
+
} else {
|
|
138
|
+
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
handleSheetStateChange(behavior.state)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
fun convertToPx(height: Int): Int {
|
|
146
|
+
val density = context.resources.displayMetrics.density
|
|
147
|
+
return (height * density).toInt()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
fun handleSheetStateChange(newState: Int) {
|
|
151
|
+
when (newState) {
|
|
152
|
+
BottomSheetBehavior.STATE_HIDDEN -> {
|
|
153
|
+
onStateChange(mapOf(
|
|
154
|
+
"type" to "HIDDEN",
|
|
155
|
+
))
|
|
156
|
+
|
|
157
|
+
}
|
|
158
|
+
BottomSheetBehavior.STATE_SETTLING -> {
|
|
159
|
+
onStateChange(mapOf(
|
|
160
|
+
"type" to "SETTLING",
|
|
161
|
+
))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
BottomSheetBehavior.STATE_COLLAPSED -> {
|
|
165
|
+
onStateChange(mapOf(
|
|
166
|
+
"type" to "OPEN",
|
|
167
|
+
"payload" to mapOf(
|
|
168
|
+
"index" to 0
|
|
169
|
+
)
|
|
170
|
+
))
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
BottomSheetBehavior.STATE_HALF_EXPANDED -> {
|
|
174
|
+
onStateChange(mapOf(
|
|
175
|
+
"type" to "OPEN",
|
|
176
|
+
"payload" to mapOf(
|
|
177
|
+
"index" to 0
|
|
178
|
+
)
|
|
179
|
+
))
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
BottomSheetBehavior.STATE_EXPANDED -> {
|
|
183
|
+
onStateChange(mapOf(
|
|
184
|
+
"type" to "OPEN",
|
|
185
|
+
"payload" to mapOf(
|
|
186
|
+
"index" to 1
|
|
187
|
+
)
|
|
188
|
+
))
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
BottomSheetBehavior.STATE_DRAGGING -> {
|
|
192
|
+
onStateChange(mapOf(
|
|
193
|
+
"type" to "DRAGGING",
|
|
194
|
+
))
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
package expo.modules.sheets
|
|
2
|
+
|
|
3
|
+
import androidx.compose.runtime.getValue
|
|
4
|
+
import androidx.compose.runtime.mutableIntStateOf
|
|
5
|
+
import androidx.compose.runtime.mutableStateOf
|
|
6
|
+
import androidx.compose.runtime.setValue
|
|
7
|
+
import expo.modules.kotlin.records.Field
|
|
8
|
+
import expo.modules.kotlin.records.Record
|
|
9
|
+
|
|
10
|
+
class SheetProps {
|
|
11
|
+
var isOpen by mutableStateOf(false)
|
|
12
|
+
var openToIndex by mutableIntStateOf(0)
|
|
13
|
+
var snapPoints by mutableStateOf<List<Int>>(emptyList())
|
|
14
|
+
lateinit var rootViewGroup: SheetRootView
|
|
15
|
+
|
|
16
|
+
// Appearance props
|
|
17
|
+
var dimAmount by mutableStateOf(0.56f)
|
|
18
|
+
var backgroundColor by mutableStateOf<String?>(null)
|
|
19
|
+
var cornerRadius by mutableStateOf<Float?>(null)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class SheetAppearance : Record {
|
|
23
|
+
@Field
|
|
24
|
+
var dimAmount: Float? = 0.56f
|
|
25
|
+
|
|
26
|
+
@Field
|
|
27
|
+
var backgroundColor: String? = null
|
|
28
|
+
|
|
29
|
+
@Field
|
|
30
|
+
var cornerRadius: Float? = null
|
|
31
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
package expo.modules.sheets
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.view.MotionEvent
|
|
5
|
+
import android.view.View
|
|
6
|
+
import com.facebook.infer.annotation.SuppressLint
|
|
7
|
+
import com.facebook.react.config.ReactFeatureFlags
|
|
8
|
+
import com.facebook.react.uimanager.JSPointerDispatcher
|
|
9
|
+
import com.facebook.react.uimanager.JSTouchDispatcher
|
|
10
|
+
import com.facebook.react.uimanager.RootView
|
|
11
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
12
|
+
import com.facebook.react.uimanager.UIManagerHelper
|
|
13
|
+
import com.facebook.react.uimanager.events.EventDispatcher
|
|
14
|
+
import com.facebook.react.views.view.ReactViewGroup
|
|
15
|
+
|
|
16
|
+
import expo.modules.kotlin.AppContext
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SheetRootView internal constructor(context: Context, appContext: AppContext) : ReactViewGroup(context),
|
|
20
|
+
RootView {
|
|
21
|
+
internal var eventDispatcher: EventDispatcher? = null
|
|
22
|
+
|
|
23
|
+
private val jSTouchDispatcher: JSTouchDispatcher = JSTouchDispatcher(this)
|
|
24
|
+
private var jSPointerDispatcher: JSPointerDispatcher? = null
|
|
25
|
+
|
|
26
|
+
private val reactContext: ThemedReactContext
|
|
27
|
+
get() = context as ThemedReactContext
|
|
28
|
+
|
|
29
|
+
init {
|
|
30
|
+
eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id)
|
|
31
|
+
|
|
32
|
+
if (ReactFeatureFlags.dispatchPointerEvents) {
|
|
33
|
+
jSPointerDispatcher = JSPointerDispatcher(this)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
override fun handleException(t: Throwable) {
|
|
38
|
+
reactContext.reactApplicationContext.handleException(RuntimeException(t))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
|
42
|
+
eventDispatcher?.let { eventDispatcher ->
|
|
43
|
+
jSTouchDispatcher.handleTouchEvent(event, eventDispatcher, reactContext)
|
|
44
|
+
jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, true)
|
|
45
|
+
}
|
|
46
|
+
return super.onInterceptTouchEvent(event)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@SuppressLint("ClickableViewAccessibility")
|
|
50
|
+
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
51
|
+
eventDispatcher?.let { eventDispatcher ->
|
|
52
|
+
jSTouchDispatcher.handleTouchEvent(event, eventDispatcher, reactContext)
|
|
53
|
+
jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, false)
|
|
54
|
+
}
|
|
55
|
+
super.onTouchEvent(event)
|
|
56
|
+
// In case when there is no children interested in handling touch event, we return true from
|
|
57
|
+
// the root view in order to receive subsequent events related to that gesture
|
|
58
|
+
return true
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
override fun onInterceptHoverEvent(event: MotionEvent): Boolean {
|
|
62
|
+
eventDispatcher?.let { jSPointerDispatcher?.handleMotionEvent(event, it, true) }
|
|
63
|
+
return super.onHoverEvent(event)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
override fun onHoverEvent(event: MotionEvent): Boolean {
|
|
67
|
+
eventDispatcher?.let { jSPointerDispatcher?.handleMotionEvent(event, it, false) }
|
|
68
|
+
return super.onHoverEvent(event)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
override fun onChildStartedNativeGesture(childView: View?, ev: MotionEvent) {
|
|
72
|
+
eventDispatcher?.let { eventDispatcher ->
|
|
73
|
+
jSTouchDispatcher.onChildStartedNativeGesture(ev, eventDispatcher)
|
|
74
|
+
jSPointerDispatcher?.onChildStartedNativeGesture(childView, ev, eventDispatcher)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
override fun onChildEndedNativeGesture(childView: View, ev: MotionEvent) {
|
|
79
|
+
eventDispatcher?.let { jSTouchDispatcher.onChildEndedNativeGesture(ev, it) }
|
|
80
|
+
jSPointerDispatcher?.onChildEndedNativeGesture()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
|
|
84
|
+
// No-op - override in order to still receive events to onInterceptTouchEvent
|
|
85
|
+
// even when some other view disallow that
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'RNToolsSheets'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.description = package['description']
|
|
10
|
+
s.license = package['license']
|
|
11
|
+
s.author = package['author']
|
|
12
|
+
s.homepage = package['homepage']
|
|
13
|
+
s.platforms = {
|
|
14
|
+
:ios => '16.4',
|
|
15
|
+
:tvos => '16.4'
|
|
16
|
+
}
|
|
17
|
+
s.swift_version = '5.4'
|
|
18
|
+
s.source = { git: 'https://github.com/ajsmth/rn-tools' }
|
|
19
|
+
s.static_framework = true
|
|
20
|
+
|
|
21
|
+
s.dependency 'ExpoModulesCore'
|
|
22
|
+
|
|
23
|
+
# Swift/Objective-C compatibility
|
|
24
|
+
s.pod_target_xcconfig = {
|
|
25
|
+
'DEFINES_MODULE' => 'YES',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
|
|
29
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
|
|
3
|
+
public class RNToolsSheetsModule: Module {
|
|
4
|
+
public func definition() -> ModuleDefinition {
|
|
5
|
+
Name("RNToolsSheets")
|
|
6
|
+
|
|
7
|
+
View(RNToolsSheetsView.self) {
|
|
8
|
+
Events("onDismiss", "onStateChange")
|
|
9
|
+
|
|
10
|
+
Prop("snapPoints") { (view, snapPoints: [Int]) in
|
|
11
|
+
view.props.snapPoints = snapPoints
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
Prop("isOpen") { (view, isOpen: Bool) in
|
|
15
|
+
view.props.isOpen = isOpen
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
Prop("openToIndex") { (view, openToIndex: Int) in
|
|
19
|
+
view.props.openToIndex = openToIndex
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
Prop("appearanceIOS") { (view, appearance: SheetAppearance) in
|
|
23
|
+
view.props.grabberVisible = appearance.grabberVisible ?? true
|
|
24
|
+
view.props.backgroundColor = appearance.backgroundColor
|
|
25
|
+
view.props.cornerRadius = appearance.cornerRadius
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import SwiftUI
|
|
3
|
+
|
|
4
|
+
public class SheetProps: ObservableObject {
|
|
5
|
+
@Published var children: [UIView] = []
|
|
6
|
+
@Published var isOpen: Bool = false
|
|
7
|
+
@Published var openToIndex: Int = 0
|
|
8
|
+
@Published var snapPoints: [Int] = []
|
|
9
|
+
|
|
10
|
+
// Appearance props
|
|
11
|
+
@Published var grabberVisible: Bool = true
|
|
12
|
+
@Published var backgroundColor: String? = nil
|
|
13
|
+
@Published var cornerRadius: Float? = nil
|
|
14
|
+
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
struct SheetAppearance: Record {
|
|
18
|
+
@Field
|
|
19
|
+
var grabberVisible: Bool?
|
|
20
|
+
|
|
21
|
+
@Field
|
|
22
|
+
var backgroundColor: String?
|
|
23
|
+
|
|
24
|
+
@Field
|
|
25
|
+
var cornerRadius: Float?
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public class RNToolsSheetsView: ExpoView {
|
|
29
|
+
public var props = SheetProps()
|
|
30
|
+
var onDismiss = EventDispatcher()
|
|
31
|
+
var onStateChange = EventDispatcher()
|
|
32
|
+
|
|
33
|
+
var touchHandler: RCTTouchHandler?
|
|
34
|
+
|
|
35
|
+
lazy var hostingController = UIHostingController(
|
|
36
|
+
rootView: ContentView(
|
|
37
|
+
props: props, onDismiss: onDismiss, onStateChange: onStateChange))
|
|
38
|
+
|
|
39
|
+
required init(appContext: AppContext? = nil) {
|
|
40
|
+
super.init(appContext: appContext)
|
|
41
|
+
|
|
42
|
+
if let bridge = appContext?.reactBridge {
|
|
43
|
+
touchHandler = RCTTouchHandler(bridge: bridge)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
hostingController.view.autoresizingMask = [
|
|
47
|
+
.flexibleWidth, .flexibleHeight,
|
|
48
|
+
]
|
|
49
|
+
hostingController.view.backgroundColor = UIColor.clear
|
|
50
|
+
hostingController.rootView = ContentView(
|
|
51
|
+
props: props, onDismiss: onDismiss, onStateChange: onStateChange)
|
|
52
|
+
|
|
53
|
+
addSubview(hostingController.view)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public override func reactSubviews() -> [UIView]! {
|
|
57
|
+
return []
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public override func insertReactSubview(_ subview: UIView!, at atIndex: Int)
|
|
61
|
+
{
|
|
62
|
+
super.insertReactSubview(subview, at: atIndex)
|
|
63
|
+
props.children.insert(subview, at: atIndex)
|
|
64
|
+
if atIndex == 0 {
|
|
65
|
+
touchHandler?.attach(to: subview)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public override func removeReactSubview(_ subview: UIView!) {
|
|
70
|
+
super.removeReactSubview(subview)
|
|
71
|
+
if let index = props.children.firstIndex(of: subview) {
|
|
72
|
+
props.children.remove(at: index)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
struct ContentView: View {
|
|
78
|
+
@ObservedObject var props: SheetProps
|
|
79
|
+
var onDismiss: EventDispatcher
|
|
80
|
+
var onStateChange: EventDispatcher
|
|
81
|
+
|
|
82
|
+
@State private var selectedDetent: PresentationDetent = .height(400.0)
|
|
83
|
+
@State private var lastHeight: CGFloat = 0
|
|
84
|
+
@State private var isDragging = false
|
|
85
|
+
@State private var settleTimer: Timer?
|
|
86
|
+
|
|
87
|
+
private var detents: [PresentationDetent] {
|
|
88
|
+
props.snapPoints.map { .height(CGFloat($0)) }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private func detent(for index: Int?) -> PresentationDetent {
|
|
92
|
+
guard
|
|
93
|
+
let i = index,
|
|
94
|
+
detents.indices.contains(i)
|
|
95
|
+
else { return detents.first! }
|
|
96
|
+
return detents[i]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private func upperSnapIndex(
|
|
100
|
+
for height: CGFloat,
|
|
101
|
+
snapPoints: [Int]
|
|
102
|
+
) -> Int {
|
|
103
|
+
guard !snapPoints.isEmpty else { return 0 }
|
|
104
|
+
|
|
105
|
+
let sorted = snapPoints.sorted()
|
|
106
|
+
if let i = sorted.firstIndex(where: { CGFloat($0) >= height }) {
|
|
107
|
+
return i
|
|
108
|
+
}
|
|
109
|
+
return sorted.count - 1
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
var body: some View {
|
|
113
|
+
|
|
114
|
+
Color.clear
|
|
115
|
+
.sheet(
|
|
116
|
+
isPresented: $props.isOpen,
|
|
117
|
+
onDismiss: {
|
|
118
|
+
onDismiss([:])
|
|
119
|
+
onStateChange(["type": "HIDDEN"])
|
|
120
|
+
}
|
|
121
|
+
) {
|
|
122
|
+
VStack {
|
|
123
|
+
ForEach(Array(props.children.enumerated()), id: \.offset) {
|
|
124
|
+
index, child in
|
|
125
|
+
RepresentableView(view: child)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
.background(
|
|
129
|
+
GeometryReader { geometry in
|
|
130
|
+
Color.clear
|
|
131
|
+
.onChange(of: geometry.size.height) { newHeight in
|
|
132
|
+
if abs(newHeight - lastHeight) > 2 {
|
|
133
|
+
if !isDragging {
|
|
134
|
+
isDragging = true
|
|
135
|
+
onStateChange(["type": "DRAGGING"])
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
settleTimer?.invalidate()
|
|
139
|
+
settleTimer = Timer.scheduledTimer(
|
|
140
|
+
withTimeInterval: 0.15, repeats: false
|
|
141
|
+
) { _ in
|
|
142
|
+
isDragging = false
|
|
143
|
+
onStateChange(["type": "SETTLING"])
|
|
144
|
+
|
|
145
|
+
DispatchQueue.main.asyncAfter(
|
|
146
|
+
deadline: .now() + 0.15
|
|
147
|
+
) {
|
|
148
|
+
let idx = upperSnapIndex(
|
|
149
|
+
for: newHeight,
|
|
150
|
+
snapPoints: props.snapPoints
|
|
151
|
+
)
|
|
152
|
+
onStateChange([
|
|
153
|
+
"type": "OPEN",
|
|
154
|
+
"payload": ["index": idx],
|
|
155
|
+
])
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
lastHeight = newHeight
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
.presentationBackground(props.backgroundColor != nil ? Color(hex: props.backgroundColor!) : Color.white)
|
|
165
|
+
.presentationDragIndicator(
|
|
166
|
+
props.grabberVisible ? .visible : .hidden
|
|
167
|
+
)
|
|
168
|
+
.presentationDetents(
|
|
169
|
+
Set(detents),
|
|
170
|
+
selection: $selectedDetent
|
|
171
|
+
)
|
|
172
|
+
.presentationCornerRadius(props.cornerRadius.map { CGFloat($0) })
|
|
173
|
+
.onAppear {
|
|
174
|
+
selectedDetent = detent(for: props.openToIndex)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
struct RepresentableView: UIViewRepresentable {
|
|
181
|
+
var view: UIView
|
|
182
|
+
|
|
183
|
+
func makeUIView(context: Context) -> UIView {
|
|
184
|
+
let containerView = UIView()
|
|
185
|
+
containerView.backgroundColor = .clear
|
|
186
|
+
|
|
187
|
+
view.translatesAutoresizingMaskIntoConstraints = false
|
|
188
|
+
containerView.addSubview(view)
|
|
189
|
+
|
|
190
|
+
NSLayoutConstraint.activate([
|
|
191
|
+
view.topAnchor.constraint(equalTo: containerView.topAnchor),
|
|
192
|
+
view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
|
|
193
|
+
view.trailingAnchor.constraint(
|
|
194
|
+
equalTo: containerView.trailingAnchor),
|
|
195
|
+
view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
|
196
|
+
])
|
|
197
|
+
|
|
198
|
+
return containerView
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
func updateUIView(_ uiView: UIView, context: Context) {}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
extension Color {
|
|
206
|
+
init(hex: String) {
|
|
207
|
+
var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
208
|
+
hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "")
|
|
209
|
+
|
|
210
|
+
var rgb: UInt64 = 0
|
|
211
|
+
Scanner(string: hexSanitized).scanHexInt64(&rgb)
|
|
212
|
+
|
|
213
|
+
let red = Double((rgb & 0xFF0000) >> 16) / 255.0
|
|
214
|
+
let green = Double((rgb & 0x00FF00) >> 8) / 255.0
|
|
215
|
+
let blue = Double(rgb & 0x0000FF) / 255.0
|
|
216
|
+
|
|
217
|
+
self.init(red: red, green: green, blue: blue)
|
|
218
|
+
}
|
|
219
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rn-tools/sheets",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "My new module",
|
|
5
|
+
"main": "src/index.ts",
|
|
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
|
+
"open:ios": "xed example/ios",
|
|
15
|
+
"open:android": "open -a \"Android Studio\" example/android"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"react-native",
|
|
19
|
+
"expo",
|
|
20
|
+
"sheets",
|
|
21
|
+
"RNToolsSheets"
|
|
22
|
+
],
|
|
23
|
+
"repository": "https://github.com/ajsmth/rn-tools",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/ajsmth/rn-tools/issues"
|
|
26
|
+
},
|
|
27
|
+
"author": "andy <andydevs123@gmail.com> (ajsmth)",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"homepage": "https://github.com/ajsmth/rn-tools#readme",
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/react": "~18.3.12",
|
|
32
|
+
"expo": "~52.0.0",
|
|
33
|
+
"expo-module-scripts": "^4.0.3",
|
|
34
|
+
"react-native": "0.76.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"expo": "*",
|
|
38
|
+
"react": "*",
|
|
39
|
+
"react-native": "*"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './native-sheets-view'
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
LayoutRectangle,
|
|
4
|
+
NativeSyntheticEvent,
|
|
5
|
+
View,
|
|
6
|
+
ViewStyle,
|
|
7
|
+
Platform,
|
|
8
|
+
LayoutChangeEvent,
|
|
9
|
+
} from "react-native";
|
|
10
|
+
import { requireNativeViewManager } from "expo-modules-core";
|
|
11
|
+
|
|
12
|
+
type SheetState = "DRAGGING" | "OPEN" | "SETTLING" | "HIDDEN";
|
|
13
|
+
|
|
14
|
+
type ChangeEvent<T extends SheetState, P = unknown> = {
|
|
15
|
+
type: T;
|
|
16
|
+
payload?: P;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type OpenChangeEvent = ChangeEvent<"OPEN", { index: number }>;
|
|
20
|
+
type DraggingChangeEvent = ChangeEvent<"DRAGGING">;
|
|
21
|
+
type SettlingChangeEvent = ChangeEvent<"SETTLING">;
|
|
22
|
+
type HiddenChangeEvent = ChangeEvent<"HIDDEN">;
|
|
23
|
+
|
|
24
|
+
type SheetChangeEvent =
|
|
25
|
+
| OpenChangeEvent
|
|
26
|
+
| DraggingChangeEvent
|
|
27
|
+
| SettlingChangeEvent
|
|
28
|
+
| HiddenChangeEvent;
|
|
29
|
+
|
|
30
|
+
type NativeOnChangeEvent = NativeSyntheticEvent<SheetChangeEvent>;
|
|
31
|
+
|
|
32
|
+
type AppearanceIOS = {
|
|
33
|
+
grabberVisible?: boolean;
|
|
34
|
+
backgroundColor?: string;
|
|
35
|
+
cornerRadius?: number;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type AppearanceAndroid = {
|
|
39
|
+
dimAmount?: number;
|
|
40
|
+
cornerRadius?: number;
|
|
41
|
+
backgroundColor?: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type NativeSheetViewProps = {
|
|
45
|
+
children: React.ReactNode;
|
|
46
|
+
snapPoints?: number[];
|
|
47
|
+
isOpen: boolean;
|
|
48
|
+
openToIndex: number;
|
|
49
|
+
onDismiss: () => void;
|
|
50
|
+
onStateChange: (event: NativeOnChangeEvent) => void;
|
|
51
|
+
appearanceAndroid?: AppearanceAndroid;
|
|
52
|
+
appearanceIOS?: AppearanceIOS;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const NativeSheetsView =
|
|
56
|
+
requireNativeViewManager<NativeSheetViewProps>("RNToolsSheets");
|
|
57
|
+
|
|
58
|
+
export type BottomSheetProps = {
|
|
59
|
+
children: React.ReactNode;
|
|
60
|
+
containerStyle?: ViewStyle;
|
|
61
|
+
snapPoints?: number[];
|
|
62
|
+
isOpen: boolean;
|
|
63
|
+
openToIndex?: number;
|
|
64
|
+
onOpenChange: (isOpen: boolean) => void;
|
|
65
|
+
onStateChange?: (event: SheetChangeEvent) => void;
|
|
66
|
+
appearanceAndroid?: AppearanceAndroid;
|
|
67
|
+
appearanceIOS?: AppearanceIOS;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// TODO:
|
|
71
|
+
// - get sheet container height from native side and clamp maxHeight to that value
|
|
72
|
+
//
|
|
73
|
+
|
|
74
|
+
export function BottomSheet(props: BottomSheetProps) {
|
|
75
|
+
const {
|
|
76
|
+
onStateChange,
|
|
77
|
+
children,
|
|
78
|
+
snapPoints = [],
|
|
79
|
+
containerStyle,
|
|
80
|
+
isOpen,
|
|
81
|
+
openToIndex = 0,
|
|
82
|
+
onOpenChange: setIsOpen,
|
|
83
|
+
appearanceAndroid,
|
|
84
|
+
appearanceIOS,
|
|
85
|
+
} = props;
|
|
86
|
+
|
|
87
|
+
const [layout, setLayout] = React.useState<LayoutRectangle>({
|
|
88
|
+
height: 0,
|
|
89
|
+
width: 0,
|
|
90
|
+
x: 0,
|
|
91
|
+
y: 0,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const computedSnapPoints = React.useMemo(() => {
|
|
95
|
+
if (snapPoints.length === 0 && layout.height > 0) {
|
|
96
|
+
return [layout.height];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return Platform.OS === "android" ? snapPoints.slice(0, 2) : [...snapPoints];
|
|
100
|
+
}, [snapPoints, layout]);
|
|
101
|
+
|
|
102
|
+
const maxHeight = React.useMemo(
|
|
103
|
+
() =>
|
|
104
|
+
computedSnapPoints.length === 0
|
|
105
|
+
? undefined
|
|
106
|
+
: Math.max(...computedSnapPoints),
|
|
107
|
+
[computedSnapPoints],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const style = React.useMemo(() => {
|
|
111
|
+
return {
|
|
112
|
+
height: maxHeight,
|
|
113
|
+
...containerStyle,
|
|
114
|
+
};
|
|
115
|
+
}, [maxHeight]);
|
|
116
|
+
|
|
117
|
+
const handleOnDismiss = React.useCallback(() => {
|
|
118
|
+
setIsOpen(false);
|
|
119
|
+
}, []);
|
|
120
|
+
|
|
121
|
+
const handleStateChange = React.useCallback(
|
|
122
|
+
(event: NativeOnChangeEvent) => {
|
|
123
|
+
onStateChange?.(event.nativeEvent);
|
|
124
|
+
},
|
|
125
|
+
[onStateChange],
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const handleLayout = React.useCallback((event: LayoutChangeEvent) => {
|
|
129
|
+
setLayout(event.nativeEvent.layout);
|
|
130
|
+
}, []);
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<NativeSheetsView
|
|
134
|
+
isOpen={isOpen}
|
|
135
|
+
openToIndex={openToIndex}
|
|
136
|
+
onDismiss={handleOnDismiss}
|
|
137
|
+
onStateChange={handleStateChange}
|
|
138
|
+
snapPoints={computedSnapPoints}
|
|
139
|
+
appearanceAndroid={appearanceAndroid}
|
|
140
|
+
appearanceIOS={appearanceIOS}
|
|
141
|
+
>
|
|
142
|
+
<View style={style} onLayout={handleLayout}>
|
|
143
|
+
{children}
|
|
144
|
+
</View>
|
|
145
|
+
</NativeSheetsView>
|
|
146
|
+
);
|
|
147
|
+
}
|