@lodev09/react-native-true-sheet 0.9.7 → 0.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,8 +16,7 @@ The true native bottom sheet 💩
16
16
  * ✅ Handles your _Footer_ needs, natively.
17
17
  * ✅ Handles your _Keyboard_ needs, natively.
18
18
  * ✅ Asynchronus `ref` [methods](#methods).
19
- * ✅ Works with Expo by default.
20
- * ✅ Bonus! [Blur](#blurtint) support on iOS 😎
19
+ * ✅ Bonus! [Blur](#blurtint) support on IOS 😎
21
20
 
22
21
  ## Installation
23
22
 
@@ -29,9 +28,11 @@ yarn add @lodev09/react-native-true-sheet
29
28
  npm i @lodev09/react-native-true-sheet
30
29
  ```
31
30
 
31
+ > This package is not compatible with [Expo Go](https://docs.expo.dev/get-started/expo-go/). Use this with [Expo CNG](https://docs.expo.dev/workflow/continuous-native-generation/) instead.
32
+
32
33
  ## Usage
33
34
 
34
- ```ts
35
+ ```tsx
35
36
  import { TrueSheet } from "@lodev09/react-native-true-sheet"
36
37
 
37
38
  // ...
@@ -65,6 +66,7 @@ Extends `ViewProps`
65
66
  | Prop | Type | Default | Description | 🍎 | 🤖 |
66
67
  | - | - | - | - | - | - |
67
68
  | sizes | [`SheetSize[]`](#sheetsize) | `["medium", "large"]` | Array of sizes you want the sheet to support. Maximum of _**3 sizes**_ only! **_collapsed_**, **_half-expanded_**, and **_expanded_**. Example: `size={["auto", "60%", "large"]}`| ✅ | ✅ |
69
+ | name | `string` | - | The name to reference this sheet. It has to be **_unique_**. You can then present this sheet globally using its name. | ✅ | ✅ |
68
70
  | backgroundColor | `ColorValue` | `"white"` | The sheet's background color. | ✅ | ✅ |
69
71
  | cornerRadius | `number` | - | the sheet corner radius. | ✅ | ✅ |
70
72
  | maxHeight | `number` | - | Overrides `"large"` or `"100%"` height. | ✅ | ✅ |
@@ -73,12 +75,12 @@ Extends `ViewProps`
73
75
  | dismissible | `boolean` | `true` | If set to `false`, the sheet will prevent interactive dismissal via dragging or clicking outside of it. | ✅ | ✅ |
74
76
  | grabber | `boolean` | `true` | Shows a grabber (or handle). Native on IOS and styled `View` on Android. | ✅ | ✅ |
75
77
  | grabberProps | [`TrueSheetGrabberProps`](#truesheetgrabberprops) | - | Overrides the grabber props for android. | | ✅ |
76
- | blurTint | [`BlurTint`](#blurtint) | - | The blur effect style on iOS. Overrides `backgroundColor` if set. Example: `"light"`, `"dark"`, etc. | ✅ | |
77
- | scrollRef | `RefObject<...>` | - | The main scrollable ref that the sheet should handle on iOS. | ✅ | |
78
+ | blurTint | [`BlurTint`](#blurtint) | - | The blur effect style on IOS. Overrides `backgroundColor` if set. Example: `"light"`, `"dark"`, etc. | ✅ | |
79
+ | scrollRef | `RefObject<...>` | - | The main scrollable ref that the sheet should handle on IOS. | ✅ | |
78
80
 
79
81
  ## Methods
80
82
 
81
- ```ts
83
+ ```tsx
82
84
  const sheet = useRef<TrueSheet>(null)
83
85
 
84
86
  const resize = () => {
@@ -106,9 +108,40 @@ return (
106
108
  | resize | `index: number` | Resizes the sheet programmatically by `index`. This is an alias of the `present(index)` method. |
107
109
  | dismiss | - | Dismisses the sheet. |
108
110
 
109
- ## Events
111
+ ### Static Methods
112
+
113
+ You can also call the above methods statically without having access to a sheet's `ref`. This is particularly useful when you want to present a sheet from anywhere.
114
+
115
+ The API is similar to the ref methods except for the required `name` prop.
110
116
 
111
117
  ```ts
118
+ TrueSheet.present('SHEET-NAME')
119
+ TrueSheet.dismiss('SHEET-NAME')
120
+ TrueSheet.resize('SHEET-NAME', 1)
121
+ ```
122
+
123
+ Example:
124
+ ```tsx
125
+ // Define the sheet as usual
126
+ <TrueSheet name="my-sheet">
127
+ // ...
128
+ </TrueSheet>
129
+ ```
130
+ ```tsx
131
+ // Somewhere in your screen
132
+ const presentMySheet = async () => {
133
+ await TrueSheet.present('my-sheet')
134
+ console.log('🎉')
135
+ }
136
+
137
+ return (
138
+ <Button onPress={presentMySheet} />
139
+ )
140
+ ```
141
+
142
+ ## Events
143
+
144
+ ```tsx
112
145
  const handleSizeChange = (info: SizeInfo) => {
113
146
  console.log(info)
114
147
  }
@@ -130,7 +163,7 @@ return (
130
163
 
131
164
  ### `SheetSize`
132
165
 
133
- ```ts
166
+ ```tsx
134
167
  <TrueSheet sizes={['auto', '80%', 'large']}>
135
168
  // ...
136
169
  </TrueSheet>
@@ -142,8 +175,8 @@ return (
142
175
  | `"small"` | Translates to 25% | **_16+_** | ✅ |
143
176
  | `"medium"` | Translates to 50% | **_15+_** | ✅ |
144
177
  | `"large"` | Translates to 100% | ✅ | ✅ |
178
+ | `"${number}%"` | Fixed height in % | **_16+_** | ✅ |
145
179
  | `number` | Fixed height | **_16+_** | ✅ |
146
- | `${number}%` | Fixed height in % | **_16+_** | ✅ |
147
180
 
148
181
  > [!NOTE]
149
182
  > `auto` is not guaranteed to be accurate if your content depends on various rendering logic. Experiment with it and try to keep your content size as fixed as possible.
@@ -164,9 +197,9 @@ Grabber props to be used for android grabber or handle.
164
197
 
165
198
  ### `BlurTint`
166
199
 
167
- Blur tint that is mapped into native values in iOS.
200
+ Blur tint that is mapped into native values in IOS.
168
201
 
169
- ```ts
202
+ ```tsx
170
203
  <TrueSheet blurTint="dark">
171
204
  // ...
172
205
  </TrueSheet>
@@ -200,7 +233,7 @@ Blur tint that is mapped into native values in iOS.
200
233
 
201
234
  `Object` that comes with some events.
202
235
 
203
- ```ts
236
+ ```tsx
204
237
  {
205
238
  index: 1,
206
239
  value: 69
@@ -212,32 +245,101 @@ Blur tint that is mapped into native values in iOS.
212
245
  | index | `number` | The size index from the provided sizes. See `sizes` prop. |
213
246
  | value | `number` | The actual height value of the size. |
214
247
 
248
+ ## Testing
249
+
250
+ When using `jest`, simply mock the entire package.
251
+
252
+ ```tsx
253
+ jest.mock('@lodev09/react-native-true-sheet')
254
+ ```
255
+
215
256
  ## Troubleshooting
216
257
 
258
+ ### Presenting a sheet on top of an existing sheet on **IOS**
259
+
260
+ On IOS, presenting a sheet on top of an existing sheet will cause error.
261
+
262
+ ```console
263
+ Attempt to present <TrueSheet.TrueSheetViewController: 0x11829f800> on <UIViewController: 0x10900eb10> (from <RNSScreen: 0x117dbf400>) which is already presenting <TrueSheet.TrueSheetViewController: 0x11a0b9200>
264
+ ```
265
+
266
+ There are _two_ ways to resolve this.
267
+
268
+ 1. Dismiss the "parent" sheet first
269
+ ```tsx
270
+ const presentSheet2 = async () => {
271
+ await sheet1.current?.dismiss() // Wait for sheet 1 to dismiss ✅
272
+ await sheet2.current?.present() // Sheet 2 will now present 🎉
273
+ }
274
+
275
+ return (
276
+ <>
277
+ <TrueSheet ref={sheet1}>
278
+ <Button onPress={presentSheet2} title="Present Sheet 2" />
279
+ // ...
280
+ </TrueSheet>
281
+
282
+ <TrueSheet ref={sheet2}>
283
+ // ...
284
+ </TrueSheet>
285
+ </>
286
+ )
287
+ ```
288
+ 2. Define the 2nd sheet within the "parent" sheet. IOS handles this automatically by default 😎.
289
+ ```tsx
290
+ const presentSheet2 = async () => {
291
+ await sheet2.current?.present() // Sheet 2 will now present 🎉
292
+ }
293
+
294
+ return (
295
+ <TrueSheet ref={sheet1}>
296
+ <Button onPress={presentSheet2} title="Present Sheet 2" />
297
+
298
+ // ...
299
+
300
+ <TrueSheet ref={sheet2}>
301
+ // ...
302
+ </TrueSheet>
303
+ </TrueSheet>
304
+ )
305
+ ```
306
+
307
+ ### Handling `ScrollView` on **Android**
308
+
309
+ On Android, `nestedScrollEnabled` needs to be enabled so that scrolling works when the sheet is expanded to its `maxHeight`.
310
+
311
+ ```tsx
312
+ <TrueSheet ref={sheet}>
313
+ <ScrollView nestedScrollEnabled>
314
+ // ...
315
+ </ScrollView>
316
+ </TrueSheet>
317
+ ```
318
+
217
319
  ### Using `react-native-gesture-handler` on **Android**
218
320
 
219
321
  On Android, RNGH does not work by default because modals are not located under React Native Root view in native hierarchy. To fix that, components need to be wrapped with `GestureHandlerRootView`.
220
322
 
221
323
  Example:
222
- ```ts
324
+ ```tsx
223
325
  import { GestureHandlerRootView } from 'react-native-gesture-handler'
224
326
  ```
225
- ```ts
327
+ ```tsx
226
328
  return (
227
329
  <TrueSheet ref={sheet}>
228
330
  <GestureHandlerRootView>
229
- <MyComponent />
331
+ // ...
230
332
  </GestureHandlerRootView>
231
333
  </TrueSheet>
232
334
  )
233
335
  ```
234
336
 
235
- ### Integrating `@react-navigation/native` on iOS
337
+ ### Integrating `@react-navigation/native` on **IOS**
236
338
 
237
- On iOS, navigating to a [React Navigation](https://reactnavigation.org) screen from within the Sheet can cause issues. To resolve this, dismiss the sheet before navigating!
339
+ On IOS, navigating to a [React Navigation](https://reactnavigation.org) screen from within the Sheet can cause issues. To resolve this, dismiss the sheet before navigating!
238
340
 
239
341
  Example:
240
- ```ts
342
+ ```tsx
241
343
  const sheet = useRef<TrueSheet>(null)
242
344
 
243
345
  const navigate = async () => {
@@ -3,38 +3,34 @@ package com.lodev09.truesheet
3
3
  import android.graphics.Color
4
4
  import android.view.ViewGroup
5
5
  import android.view.WindowManager
6
- import android.widget.LinearLayout
7
- import androidx.coordinatorlayout.widget.CoordinatorLayout
8
6
  import com.facebook.react.uimanager.ThemedReactContext
7
+ import com.google.android.material.bottomsheet.BottomSheetBehavior
9
8
  import com.google.android.material.bottomsheet.BottomSheetDialog
10
9
  import com.lodev09.truesheet.core.KeyboardManager
11
- import com.lodev09.truesheet.core.RootViewGroup
10
+ import com.lodev09.truesheet.core.RootSheetView
12
11
  import com.lodev09.truesheet.core.Utils
13
12
 
14
- class TrueSheetDialog(
15
- private val reactContext: ThemedReactContext,
16
- private val behavior: TrueSheetBehavior,
17
- private val rootViewGroup: RootViewGroup
18
- ) : BottomSheetDialog(reactContext) {
13
+ data class SizeInfo(val index: Int, val value: Float)
14
+
15
+ class TrueSheetDialog(private val reactContext: ThemedReactContext, private val rootSheetView: RootSheetView) :
16
+ BottomSheetDialog(reactContext) {
19
17
 
20
18
  private var keyboardManager = KeyboardManager(reactContext)
21
19
 
22
- var sheetView: ViewGroup
20
+ var maxScreenHeight: Int = 0
21
+ var maxSheetHeight: Int? = null
23
22
 
24
- init {
25
- LinearLayout(reactContext).apply {
26
- addView(rootViewGroup)
27
- setContentView(this)
23
+ var contentView: ViewGroup? = null
24
+ var footerView: ViewGroup? = null
28
25
 
29
- sheetView = parent as ViewGroup
26
+ var sizes: Array<Any> = arrayOf("medium", "large")
30
27
 
31
- // Set to transparent background to support corner radius
32
- sheetView.setBackgroundColor(Color.TRANSPARENT)
28
+ private var sheetView: ViewGroup
33
29
 
34
- // Assign our main BottomSheetBehavior
35
- val sheetViewParams = sheetView.layoutParams as CoordinatorLayout.LayoutParams
36
- sheetViewParams.behavior = behavior
37
- }
30
+ init {
31
+ setContentView(rootSheetView)
32
+ sheetView = rootSheetView.parent as ViewGroup
33
+ sheetView.setBackgroundColor(Color.TRANSPARENT)
38
34
 
39
35
  // Setup window params to adjust layout based on Keyboard state.
40
36
  window?.apply {
@@ -43,19 +39,111 @@ class TrueSheetDialog(
43
39
  WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
44
40
  )
45
41
  }
42
+
43
+ // Update the usable sheet height
44
+ maxScreenHeight = Utils.screenHeight(reactContext)
46
45
  }
47
46
 
48
47
  fun show(sizeIndex: Int) {
49
48
  if (isShowing) {
50
- behavior.setStateForSizeIndex(sizeIndex)
49
+ setStateForSizeIndex(sizeIndex)
51
50
  } else {
52
- behavior.configure()
53
- behavior.setStateForSizeIndex(sizeIndex)
51
+ configure()
52
+ setStateForSizeIndex(sizeIndex)
54
53
 
55
54
  this.show()
56
55
  }
57
56
  }
58
57
 
58
+ fun positionFooter() {
59
+ footerView?.apply {
60
+ y = (maxScreenHeight - sheetView.top - height).toFloat()
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Set the state based on the given size index.
66
+ */
67
+ private fun setStateForSizeIndex(index: Int) {
68
+ behavior.state = getStateForSizeIndex(index)
69
+ }
70
+
71
+ /**
72
+ * Get the height value based on the size config value.
73
+ */
74
+ private fun getSizeHeight(size: Any, contentHeight: Int): Int {
75
+ val height =
76
+ when (size) {
77
+ is Double -> Utils.toPixel(size)
78
+
79
+ is Int -> Utils.toPixel(size.toDouble())
80
+
81
+ is String -> {
82
+ when (size) {
83
+ "auto" -> contentHeight
84
+
85
+ "large" -> maxScreenHeight
86
+
87
+ "medium" -> (maxScreenHeight * 0.50).toInt()
88
+
89
+ "small" -> (maxScreenHeight * 0.25).toInt()
90
+
91
+ else -> {
92
+ if (size.endsWith('%')) {
93
+ val percent = size.trim('%').toDoubleOrNull()
94
+ if (percent == null) {
95
+ 0
96
+ } else {
97
+ ((percent / 100) * maxScreenHeight).toInt()
98
+ }
99
+ } else {
100
+ val fixedHeight = size.toDoubleOrNull()
101
+ if (fixedHeight == null) {
102
+ 0
103
+ } else {
104
+ Utils.toPixel(fixedHeight)
105
+ }
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ else -> (maxScreenHeight * 0.5).toInt()
112
+ }
113
+
114
+ return when (maxSheetHeight) {
115
+ null -> height
116
+ else -> minOf(height, maxSheetHeight ?: maxScreenHeight)
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Determines the state based on the given size index.
122
+ */
123
+ private fun getStateForSizeIndex(index: Int) =
124
+ when (sizes.size) {
125
+ 1 -> BottomSheetBehavior.STATE_EXPANDED
126
+
127
+ 2 -> {
128
+ when (index) {
129
+ 0 -> BottomSheetBehavior.STATE_COLLAPSED
130
+ 1 -> BottomSheetBehavior.STATE_EXPANDED
131
+ else -> BottomSheetBehavior.STATE_HIDDEN
132
+ }
133
+ }
134
+
135
+ 3 -> {
136
+ when (index) {
137
+ 0 -> BottomSheetBehavior.STATE_COLLAPSED
138
+ 1 -> BottomSheetBehavior.STATE_HALF_EXPANDED
139
+ 2 -> BottomSheetBehavior.STATE_EXPANDED
140
+ else -> BottomSheetBehavior.STATE_HIDDEN
141
+ }
142
+ }
143
+
144
+ else -> BottomSheetBehavior.STATE_HIDDEN
145
+ }
146
+
59
147
  /**
60
148
  * Handle keyboard state changes and adjust maxScreenHeight (sheet max height) accordingly.
61
149
  * Also update footer's Y position.
@@ -63,14 +151,12 @@ class TrueSheetDialog(
63
151
  fun registerKeyboardManager() {
64
152
  keyboardManager.registerKeyboardListener(object : KeyboardManager.OnKeyboardListener {
65
153
  override fun onKeyboardStateChange(isVisible: Boolean, visibleHeight: Int?) {
66
- when (isVisible) {
67
- true -> behavior.maxScreenHeight = visibleHeight ?: 0
68
- else -> behavior.maxScreenHeight = Utils.screenHeight(reactContext)
154
+ maxScreenHeight = when (isVisible) {
155
+ true -> visibleHeight ?: 0
156
+ else -> Utils.screenHeight(reactContext)
69
157
  }
70
158
 
71
- behavior.footerView?.apply {
72
- y = (behavior.maxScreenHeight - (sheetView.top ?: 0) - height).toFloat()
73
- }
159
+ positionFooter()
74
160
  }
75
161
  })
76
162
  }
@@ -82,6 +168,90 @@ class TrueSheetDialog(
82
168
  keyboardManager.unregisterKeyboardListener()
83
169
  }
84
170
 
171
+ /**
172
+ * Configure the sheet based on size preferences.
173
+ */
174
+ fun configure() {
175
+ var contentHeight = 0
176
+
177
+ contentView?.let { contentHeight = it.height }
178
+ footerView?.let { contentHeight += it.height }
179
+
180
+ // Configure sheet sizes
181
+ behavior.apply {
182
+ skipCollapsed = false
183
+ isFitToContents = true
184
+
185
+ // m3 max width 640dp
186
+ maxWidth = Utils.toPixel(640.0)
187
+
188
+ when (sizes.size) {
189
+ 1 -> {
190
+ maxHeight = getSizeHeight(sizes[0], contentHeight)
191
+ setPeekHeight(maxHeight, isShowing)
192
+ skipCollapsed = true
193
+ }
194
+
195
+ 2 -> {
196
+ setPeekHeight(getSizeHeight(sizes[0], contentHeight), isShowing)
197
+ maxHeight = getSizeHeight(sizes[1], contentHeight)
198
+ }
199
+
200
+ 3 -> {
201
+ // Enables half expanded
202
+ isFitToContents = false
203
+
204
+ setPeekHeight(getSizeHeight(sizes[0], contentHeight), isShowing)
205
+ halfExpandedRatio = getSizeHeight(sizes[1], contentHeight).toFloat() / maxScreenHeight.toFloat()
206
+ maxHeight = getSizeHeight(sizes[2], contentHeight)
207
+ }
208
+ }
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Get the SizeInfo data by state.
214
+ */
215
+ fun getSizeInfoForState(state: Int): SizeInfo? =
216
+ when (sizes.size) {
217
+ 1 -> {
218
+ when (state) {
219
+ BottomSheetBehavior.STATE_EXPANDED -> SizeInfo(0, Utils.toDIP(behavior.maxHeight))
220
+ else -> null
221
+ }
222
+ }
223
+
224
+ 2 -> {
225
+ when (state) {
226
+ BottomSheetBehavior.STATE_COLLAPSED -> SizeInfo(0, Utils.toDIP(behavior.peekHeight))
227
+ BottomSheetBehavior.STATE_EXPANDED -> SizeInfo(1, Utils.toDIP(behavior.maxHeight))
228
+ else -> null
229
+ }
230
+ }
231
+
232
+ 3 -> {
233
+ when (state) {
234
+ BottomSheetBehavior.STATE_COLLAPSED -> SizeInfo(0, Utils.toDIP(behavior.peekHeight))
235
+
236
+ BottomSheetBehavior.STATE_HALF_EXPANDED -> {
237
+ val height = behavior.halfExpandedRatio * maxScreenHeight
238
+ SizeInfo(1, Utils.toDIP(height.toInt()))
239
+ }
240
+
241
+ BottomSheetBehavior.STATE_EXPANDED -> SizeInfo(2, Utils.toDIP(behavior.maxHeight))
242
+
243
+ else -> null
244
+ }
245
+ }
246
+
247
+ else -> null
248
+ }
249
+
250
+ /**
251
+ * Get SizeInfo data for given size index.
252
+ */
253
+ fun getSizeInfoForIndex(index: Int) = getSizeInfoForState(getStateForSizeIndex(index)) ?: SizeInfo(0, 0f)
254
+
85
255
  companion object {
86
256
  const val TAG = "TrueSheetView"
87
257
  }