@shaquillehinds/react-native-bottom-sheet 0.0.6 → 0.0.7
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 +630 -1173
- package/bin/install-rules.js +146 -0
- package/package.json +6 -1
- package/rules/AGENT_RULES.md +373 -0
package/README.md
CHANGED
|
@@ -1,30 +1,31 @@
|
|
|
1
1
|
# @shaquillehinds/react-native-bottom-sheet
|
|
2
2
|
|
|
3
|
-
A performant, highly customizable bottom sheet
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
|
|
3
|
+
A performant, highly customizable bottom sheet for React Native. Built on
|
|
4
|
+
Reanimated and Gesture Handler, with render isolation baked into the mounting
|
|
5
|
+
model so closed sheets cost nothing.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Contents
|
|
10
|
+
|
|
11
|
+
- [Installation](#installation)
|
|
12
|
+
- [Setup](#setup)
|
|
13
|
+
- [Quick start](#quick-start)
|
|
14
|
+
- [The content component pattern](#the-content-component-pattern)
|
|
15
|
+
- [Components](#components)
|
|
16
|
+
- [Controlling the sheet](#controlling-the-sheet)
|
|
17
|
+
- [Snap points](#snap-points)
|
|
18
|
+
- [Scrollable content](#scrollable-content)
|
|
19
|
+
- [Keyboard handling](#keyboard-handling)
|
|
20
|
+
- [Persistent sheets](#persistent-sheets)
|
|
21
|
+
- [Appearance](#appearance)
|
|
22
|
+
- [Portal system](#portal-system)
|
|
23
|
+
- [API reference](#api-reference)
|
|
24
|
+
- [Recipes](#recipes)
|
|
25
|
+
- [Troubleshooting](#troubleshooting)
|
|
26
|
+
- [AI agent rules](#ai-agent-rules)
|
|
27
|
+
|
|
28
|
+
---
|
|
28
29
|
|
|
29
30
|
## Installation
|
|
30
31
|
|
|
@@ -32,23 +33,31 @@ A performant, highly customizable bottom sheet component for React Native that j
|
|
|
32
33
|
npm install @shaquillehinds/react-native-bottom-sheet
|
|
33
34
|
```
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
Peer dependencies:
|
|
36
37
|
|
|
37
38
|
```bash
|
|
38
|
-
|
|
39
|
+
npm install react-native-reanimated react-native-gesture-handler @shaquillehinds/react-native-essentials
|
|
39
40
|
```
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
| Package | Version |
|
|
43
|
+
| ----------------------------------------- | ------- |
|
|
44
|
+
| `react-native-reanimated` | ^3.0.0 |
|
|
45
|
+
| `react-native-gesture-handler` | ^2.0.0 |
|
|
46
|
+
| `@shaquillehinds/react-native-essentials` | ^1.8.0 |
|
|
42
47
|
|
|
43
|
-
|
|
48
|
+
Reanimated's Babel plugin must be last in your plugin list:
|
|
44
49
|
|
|
45
|
-
```
|
|
46
|
-
|
|
50
|
+
```js
|
|
51
|
+
// babel.config.js
|
|
52
|
+
module.exports = {
|
|
53
|
+
presets: ['module:metro-react-native-babel-preset'],
|
|
54
|
+
plugins: ['react-native-reanimated/plugin'],
|
|
55
|
+
};
|
|
47
56
|
```
|
|
48
57
|
|
|
49
|
-
|
|
58
|
+
## Setup
|
|
50
59
|
|
|
51
|
-
Wrap your app
|
|
60
|
+
Wrap your app root — **above** the navigation container:
|
|
52
61
|
|
|
53
62
|
```tsx
|
|
54
63
|
import { BottomSheetPortalProvider } from '@shaquillehinds/react-native-bottom-sheet';
|
|
@@ -56,333 +65,335 @@ import { BottomSheetPortalProvider } from '@shaquillehinds/react-native-bottom-s
|
|
|
56
65
|
export default function App() {
|
|
57
66
|
return (
|
|
58
67
|
<BottomSheetPortalProvider>
|
|
59
|
-
|
|
68
|
+
<NavigationContainer>
|
|
69
|
+
<RootNavigator />
|
|
70
|
+
</NavigationContainer>
|
|
60
71
|
</BottomSheetPortalProvider>
|
|
61
72
|
);
|
|
62
73
|
}
|
|
63
74
|
```
|
|
64
75
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
## Quick Start
|
|
68
|
-
|
|
69
|
-
First, wrap your app with the portal provider:
|
|
70
|
-
|
|
71
|
-
```tsx
|
|
72
|
-
// App.tsx
|
|
73
|
-
import { BottomSheetPortalProvider } from '@shaquillehinds/react-native-bottom-sheet';
|
|
76
|
+
Sheets render into this portal by default, which is what puts them above
|
|
77
|
+
navigation, tab bars and headers.
|
|
74
78
|
|
|
75
|
-
|
|
76
|
-
return (
|
|
77
|
-
<BottomSheetPortalProvider>
|
|
78
|
-
<YourApp />
|
|
79
|
-
</BottomSheetPortalProvider>
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
```
|
|
79
|
+
---
|
|
83
80
|
|
|
84
|
-
|
|
81
|
+
## Quick start
|
|
85
82
|
|
|
86
83
|
```tsx
|
|
87
|
-
import
|
|
84
|
+
import { useState } from 'react';
|
|
88
85
|
import { View, Text, Button } from 'react-native';
|
|
89
|
-
import {
|
|
90
|
-
BottomSheetModal,
|
|
91
|
-
useBottomSheetRef,
|
|
92
|
-
} from '@shaquillehinds/react-native-bottom-sheet';
|
|
93
|
-
import type { BottomModalRefObject } from '@shaquillehinds/react-native-bottom-sheet';
|
|
86
|
+
import { BottomSheetModal } from '@shaquillehinds/react-native-bottom-sheet';
|
|
94
87
|
|
|
95
|
-
export default function
|
|
88
|
+
export default function Screen() {
|
|
96
89
|
const [showModal, setShowModal] = useState(false);
|
|
97
|
-
const bottomSheetRef = useRef<BottomModalRefObject>(null);
|
|
98
90
|
|
|
99
91
|
return (
|
|
100
|
-
<View style={{ flex: 1
|
|
101
|
-
<Button title="Open
|
|
92
|
+
<View style={{ flex: 1 }}>
|
|
93
|
+
<Button title="Open" onPress={() => setShowModal(true)} />
|
|
102
94
|
|
|
103
95
|
<BottomSheetModal
|
|
104
|
-
ref={bottomSheetRef}
|
|
105
96
|
showModal={showModal}
|
|
106
97
|
setShowModal={setShowModal}
|
|
107
|
-
snapPoints={[50,
|
|
98
|
+
snapPoints={[50, 90]}
|
|
108
99
|
>
|
|
109
|
-
<
|
|
110
|
-
<Text>Bottom Sheet Content</Text>
|
|
111
|
-
</View>
|
|
100
|
+
<SheetContent onDone={() => setShowModal(false)} />
|
|
112
101
|
</BottomSheetModal>
|
|
113
102
|
</View>
|
|
114
103
|
);
|
|
115
104
|
}
|
|
116
105
|
```
|
|
117
106
|
|
|
118
|
-
|
|
107
|
+
Note the children are a component, not inline JSX. That is the pattern the whole
|
|
108
|
+
library is built around — the next section explains why.
|
|
119
109
|
|
|
120
|
-
|
|
121
|
-
import { BottomSheet } from '@shaquillehinds/react-native-bottom-sheet';
|
|
110
|
+
---
|
|
122
111
|
|
|
123
|
-
|
|
124
|
-
showModal={showModal}
|
|
125
|
-
setShowModal={setShowModal}
|
|
126
|
-
snapPoints={[30, 60, 90]}
|
|
127
|
-
keepMounted={true}
|
|
128
|
-
bottomOffset={50}
|
|
129
|
-
>
|
|
130
|
-
<YourContent />
|
|
131
|
-
</BottomSheet>;
|
|
132
|
-
```
|
|
112
|
+
## The content component pattern
|
|
133
113
|
|
|
134
|
-
|
|
114
|
+
### The idea
|
|
135
115
|
|
|
136
|
-
|
|
116
|
+
A closed sheet has its entire subtree unmounted. Anything you put in a **child
|
|
117
|
+
component** therefore does no work until the sheet opens: no state initialises, no
|
|
118
|
+
effects run, no queries fire, no lists build. Anything you put in the **wrapper's
|
|
119
|
+
function body** runs on every render of the screen that owns it, open or closed.
|
|
137
120
|
|
|
138
|
-
|
|
121
|
+
So: the wrapper is a shell. All state lives one level down.
|
|
139
122
|
|
|
140
|
-
|
|
123
|
+
```tsx
|
|
124
|
+
export default function ChooseFlashcards(props: ChooseFlashcardsProps) {
|
|
125
|
+
const { colors, mode } = useTheme();
|
|
141
126
|
|
|
142
|
-
|
|
127
|
+
return (
|
|
128
|
+
<SmoothBottomModal
|
|
129
|
+
showModal={props.toggled}
|
|
130
|
+
setShowModal={props.setToggled}
|
|
131
|
+
backgroundColor={colors.background[mode]}
|
|
132
|
+
showContentDelay={{ timeInMilliSecs: 250, type: 'mount' }}
|
|
133
|
+
snapPoints={[90]}
|
|
134
|
+
>
|
|
135
|
+
<ChooseFlashcardsContent {...props} />
|
|
136
|
+
</SmoothBottomModal>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
143
139
|
|
|
144
|
-
|
|
140
|
+
function ChooseFlashcardsContent(props: ChooseFlashcardsProps) {
|
|
141
|
+
const { relativeY, orientation } = useDeviceOrientation();
|
|
142
|
+
const t = useTranslation();
|
|
143
|
+
// ...everything else
|
|
144
|
+
}
|
|
145
|
+
```
|
|
145
146
|
|
|
146
|
-
|
|
147
|
+
Compare with the version that looks equivalent and is not:
|
|
147
148
|
|
|
148
|
-
|
|
149
|
+
```tsx
|
|
150
|
+
export default function ChooseFlashcards(props: ChooseFlashcardsProps) {
|
|
151
|
+
// These run whenever the parent screen renders, with the sheet closed.
|
|
152
|
+
const { data, isLoading } = useFlashcardSets(props.studySetId);
|
|
153
|
+
const [selected, setSelected] = useState<FlashcardSet[]>([]);
|
|
154
|
+
const rows = useMemo(() => buildRows(data), [data]);
|
|
149
155
|
|
|
150
|
-
|
|
156
|
+
return (
|
|
157
|
+
<SmoothBottomModal
|
|
158
|
+
showModal={props.toggled}
|
|
159
|
+
setShowModal={props.setToggled}
|
|
160
|
+
>
|
|
161
|
+
{rows.map((r) => (
|
|
162
|
+
<Row key={r.id} {...r} />
|
|
163
|
+
))}
|
|
164
|
+
</SmoothBottomModal>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
```
|
|
151
168
|
|
|
152
|
-
|
|
169
|
+
Every screen that renders a closed sheet pays for that hook stack. Across a dozen
|
|
170
|
+
sheets on a busy screen it is the difference between an instant navigation and a
|
|
171
|
+
visible stall.
|
|
153
172
|
|
|
154
|
-
|
|
173
|
+
### Why it works
|
|
155
174
|
|
|
156
|
-
|
|
175
|
+
`BottomSheetModal` passes its subtree through a `ComponentMounter` keyed on
|
|
176
|
+
`showModal`. While `showModal` is false, that subtree is not mounted. Constructing
|
|
177
|
+
the element `<ChooseFlashcardsContent {...props} />` is cheap — it is a plain
|
|
178
|
+
object. _Calling_ the component is the expensive part, and React only does that on
|
|
179
|
+
mount.
|
|
157
180
|
|
|
158
|
-
|
|
181
|
+
Hooks written directly in the wrapper are part of the parent's render, outside
|
|
182
|
+
the mounter, so they are unconditional.
|
|
159
183
|
|
|
160
|
-
|
|
161
|
-
import { BottomSheetPortalProvider } from '@shaquillehinds/react-native-bottom-sheet';
|
|
184
|
+
### Pairing with `showContentDelay`
|
|
162
185
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
);
|
|
169
|
-
}
|
|
186
|
+
The pattern removes work while closed. `showContentDelay` moves the remaining work
|
|
187
|
+
out of the opening animation:
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
showContentDelay={{ type: 'mount', timeInMilliSecs: 250 }}
|
|
170
191
|
```
|
|
171
192
|
|
|
172
|
-
|
|
193
|
+
The sheet animates open first, then mounts content behind a short fade. The user
|
|
194
|
+
sees a 60fps open instead of a stutter.
|
|
195
|
+
|
|
196
|
+
`type: 'mount'` means there is no content to measure when the sheet opens, so it
|
|
197
|
+
must be told its height. Either set `snapPoints` (preferred) or give
|
|
198
|
+
`contentContainerStyle` a `minHeight`. Without one, the sheet opens collapsed.
|
|
173
199
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
| `unMountBufferTimeMS` | `number` | `100` | Delay before removing portal items (prevents premature unmounting) |
|
|
177
|
-
| `updateBufferTimeMS` | `number` | - | Throttle time for portal updates (prevents infinite update loops) |
|
|
200
|
+
`type: 'opacity'` (the default when `type` is omitted) renders content immediately
|
|
201
|
+
and fades it in. Use it for light content where you only want the visual polish.
|
|
178
202
|
|
|
179
|
-
###
|
|
203
|
+
### State that must survive closing
|
|
180
204
|
|
|
181
|
-
|
|
205
|
+
State inside `*Content` is destroyed when the sheet closes — usually what you want.
|
|
206
|
+
When it needs to persist, own it in the screen and pass it down:
|
|
182
207
|
|
|
183
208
|
```tsx
|
|
184
|
-
|
|
209
|
+
// screen
|
|
210
|
+
const [selectedFlashcards, setSelectedFlashcards] = useState<FlashcardSet[]>(
|
|
211
|
+
[]
|
|
212
|
+
);
|
|
185
213
|
|
|
186
|
-
|
|
187
|
-
|
|
214
|
+
<ChooseFlashcards
|
|
215
|
+
toggled={toggled}
|
|
216
|
+
setToggled={setToggled}
|
|
217
|
+
selectedFlashcards={selectedFlashcards}
|
|
218
|
+
setSelectedFlashcards={setSelectedFlashcards}
|
|
219
|
+
onFlashcardSelect={handleSelect}
|
|
220
|
+
studySetId={studySetId}
|
|
221
|
+
/>;
|
|
222
|
+
```
|
|
188
223
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const key = portal.mount('my-portal-key', <MyPortalContent />);
|
|
192
|
-
return () => portal.unmount(key);
|
|
193
|
-
}
|
|
194
|
-
}, []);
|
|
224
|
+
Do not solve this by hoisting the state into the sheet wrapper. That reintroduces
|
|
225
|
+
exactly the cost the pattern removes.
|
|
195
226
|
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
```
|
|
227
|
+
---
|
|
199
228
|
|
|
200
|
-
|
|
229
|
+
## Components
|
|
201
230
|
|
|
202
|
-
|
|
203
|
-
{
|
|
204
|
-
mount: (key: string | number, element: ReactNode, onMount?: (key) => void) => PortalKey;
|
|
205
|
-
update: (key: string | number, element: ReactNode) => void;
|
|
206
|
-
unmount: (key: string | number, onUnMount?: (key) => void) => void;
|
|
207
|
-
}
|
|
208
|
-
```
|
|
231
|
+
### `BottomSheetModal`
|
|
209
232
|
|
|
210
|
-
|
|
233
|
+
The default. Renders with an animated backdrop, closes on backdrop press, handles
|
|
234
|
+
the Android back button.
|
|
211
235
|
|
|
212
|
-
|
|
236
|
+
### `BottomSheet`
|
|
213
237
|
|
|
214
|
-
|
|
215
|
-
|
|
238
|
+
Inline sheet with no backdrop. Renders with background content press enabled, so
|
|
239
|
+
taps pass through to what is behind it. Use for filter panels, mini players,
|
|
240
|
+
persistent drawers.
|
|
216
241
|
|
|
217
|
-
|
|
218
|
-
const [showOverlay, setShowOverlay] = useState(true);
|
|
242
|
+
### `BottomSheetFlatlist` / `BottomSheetScrollView`
|
|
219
243
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
Component: showOverlay ? <OverlayContent /> : null,
|
|
223
|
-
disable: !showOverlay,
|
|
224
|
-
});
|
|
244
|
+
Scroll containers that coordinate with the sheet's drag gesture. See
|
|
245
|
+
[Scrollable content](#scrollable-content).
|
|
225
246
|
|
|
226
|
-
|
|
227
|
-
}
|
|
228
|
-
```
|
|
247
|
+
---
|
|
229
248
|
|
|
230
|
-
|
|
249
|
+
## Controlling the sheet
|
|
231
250
|
|
|
232
|
-
|
|
233
|
-
| --------------------- | --------------- | ------------------------------------------------ |
|
|
234
|
-
| `name` | `string` | Unique identifier for the portal component |
|
|
235
|
-
| `Component` | `ReactNode` | The component to render in the portal |
|
|
236
|
-
| `disable` | `boolean` | Disables portal rendering when true |
|
|
237
|
-
| `CustomPortalContext` | `React.Context` | Use a custom portal context (for scoped portals) |
|
|
251
|
+
Two modes. Pick one.
|
|
238
252
|
|
|
239
|
-
###
|
|
253
|
+
### State-controlled
|
|
240
254
|
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
PortalItem,
|
|
244
|
-
PortalKey,
|
|
245
|
-
PortalContextValue,
|
|
246
|
-
} from '@shaquillehinds/react-native-bottom-sheet';
|
|
255
|
+
```tsx
|
|
256
|
+
const [showModal, setShowModal] = useState(false);
|
|
247
257
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
258
|
+
<BottomSheetModal showModal={showModal} setShowModal={setShowModal}>
|
|
259
|
+
<Content />
|
|
260
|
+
</BottomSheetModal>;
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
You can wrap the setter to run side effects on open and close:
|
|
252
264
|
|
|
253
|
-
|
|
265
|
+
```tsx
|
|
266
|
+
setShowModal={(value: boolean) => {
|
|
267
|
+
Keyboard.dismiss();
|
|
268
|
+
props.setShowModal(value);
|
|
269
|
+
}}
|
|
254
270
|
```
|
|
255
271
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
### Props
|
|
259
|
-
|
|
260
|
-
#### `BottomSheetProps` (Common to both components)
|
|
261
|
-
|
|
262
|
-
| Prop | Type | Default | Description |
|
|
263
|
-
| ------------------------------- | ----------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
264
|
-
| `showModal` | `boolean` | - | Controls visibility of the bottom sheet |
|
|
265
|
-
| `setShowModal` | `(bool: boolean) => void` | - | Callback to update visibility state |
|
|
266
|
-
| `snapPoints` | `(number \| string)[]` | `[25, 50, 75]` | Array of snap positions as percentages of screen height. Accepts numbers or strings (e.g., `[25, "50", "75%"]`) |
|
|
267
|
-
| `dragArea` | `'full' \| 'bumper' \| 'none'` | `'bumper'` | Defines draggable area of the modal |
|
|
268
|
-
| `keepMounted` | `boolean` | `false` | Prevents full unmounting when closed. Use with `bottomOffset` to keep visible |
|
|
269
|
-
| `hideBumper` | `boolean` | `false` | Hides the draggable bumper at top |
|
|
270
|
-
| `avoidKeyboard` | `boolean` | `false` | Adds padding when keyboard is visible |
|
|
271
|
-
| `allowDragWhileKeyboardVisible` | `boolean` | `false` | Allows dragging when keyboard is open (disabled by default to prevent conflicts) |
|
|
272
|
-
| `inputsForKeyboardToAvoid` | `React.RefObject<TextInput>[]` | - | Specific inputs that trigger keyboard avoidance |
|
|
273
|
-
| `bottomOffset` | `number` | `0` | Pushes modal up from bottom. Useful with `keepMounted` |
|
|
274
|
-
| `style` | `StyleProp<ViewStyle>` | - | Style for the modal sheet container |
|
|
275
|
-
| `backgroundColor` | `string` | - | Background color for modal and bumper |
|
|
276
|
-
| `contentContainerStyle` | `StyleProp<ViewStyle>` | - | Style for the content container |
|
|
277
|
-
| `bumperStyle` | `StyleProp<ViewStyle>` | - | Style for the bumper element |
|
|
278
|
-
| `bumperContainerStyle` | `StyleProp<ViewStyle>` | - | Style for the bumper container |
|
|
279
|
-
| `BumperComponent` | `() => React.ReactNode` | - | Custom bumper component |
|
|
280
|
-
| `disablePortal` | `boolean` | `false` | Disables portal rendering |
|
|
281
|
-
| `CustomPortalContext` | `React.Context<PortalContextValue>` | - | Custom portal context for scoped portals |
|
|
282
|
-
|
|
283
|
-
#### `BottomSheetModalProps` (Extends BottomSheetProps)
|
|
284
|
-
|
|
285
|
-
| Prop | Type | Default | Description |
|
|
286
|
-
| ----------------------------- | ----------------- | ------- | ----------------------------------------- |
|
|
287
|
-
| `BackdropComponent` | `React.ReactNode` | - | Custom backdrop component |
|
|
288
|
-
| `onBackDropPress` | `() => void` | - | Callback when backdrop is pressed |
|
|
289
|
-
| `disableCloseOnBackdropPress` | `boolean` | `false` | Prevents closing on backdrop press |
|
|
290
|
-
| `useNativeModal` | `boolean` | `false` | Use React Native's native Modal component |
|
|
291
|
-
|
|
292
|
-
#### Callbacks
|
|
293
|
-
|
|
294
|
-
| Prop | Type | Description |
|
|
295
|
-
| ------------------ | --------------------------------------------------- | -------------------------------------- |
|
|
296
|
-
| `onModalShow` | `() => void \| Promise<void>` | Called when modal finishes mounting |
|
|
297
|
-
| `onModalClose` | `() => void \| Promise<void>` | Called when modal finishes unmounting |
|
|
298
|
-
| `onSnapPointReach` | `(snapPointIndex: number) => void \| Promise<void>` | Called when modal reaches a snap point |
|
|
299
|
-
|
|
300
|
-
#### Performance Optimization
|
|
301
|
-
|
|
302
|
-
| Prop | Type | Description |
|
|
303
|
-
| ------------------ | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
304
|
-
| `showContentDelay` | `{ type?: 'mount' \| 'opacity', timeInMilliSecs: number }` | Delays content rendering for heavy components. Use `'mount'` for better performance with sluggish animations. Provide appropriate `minHeight` when using `'mount'` |
|
|
305
|
-
|
|
306
|
-
### Ref Methods
|
|
307
|
-
|
|
308
|
-
#### `BottomModalRef` (for BottomSheetModal and BottomSheet)
|
|
309
|
-
|
|
310
|
-
```typescript
|
|
311
|
-
const bottomSheetRef = useRef<BottomModalRefObject>(null);
|
|
312
|
-
|
|
313
|
-
// Open modal programmatically
|
|
314
|
-
bottomSheetRef.current?.openModal({
|
|
315
|
-
onOpen: () => console.log('Modal opened'),
|
|
316
|
-
});
|
|
272
|
+
### Ref-controlled
|
|
317
273
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
skipAnimation: false, // Skip close animation
|
|
321
|
-
isNavigating: false, // Fast close for navigation
|
|
322
|
-
duration: 300, // Custom duration
|
|
323
|
-
easing: Easing.linear, // Custom easing
|
|
324
|
-
onClose: () => console.log('Modal closed'),
|
|
325
|
-
});
|
|
274
|
+
Omit `showModal` and `setShowModal` entirely. Useful for a sheet mounted once,
|
|
275
|
+
high in the tree, opened from anywhere in the app:
|
|
326
276
|
|
|
327
|
-
|
|
328
|
-
|
|
277
|
+
```tsx
|
|
278
|
+
export default function UploadMaterialModal() {
|
|
279
|
+
const ref = useRef<BottomModalRef>(null);
|
|
280
|
+
const setUploadModalRef = useApp((state) => state.setUploadModalRef);
|
|
329
281
|
|
|
330
|
-
|
|
331
|
-
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
setUploadModalRef(ref);
|
|
284
|
+
}, [ref.current]);
|
|
332
285
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
286
|
+
return (
|
|
287
|
+
<SmoothBottomModal
|
|
288
|
+
ref={ref}
|
|
289
|
+
disablePortal
|
|
290
|
+
dragArea="full"
|
|
291
|
+
snapPoints={[90]}
|
|
292
|
+
>
|
|
293
|
+
<UploadMaterialModalContent />
|
|
294
|
+
</SmoothBottomModal>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
336
297
|
|
|
337
|
-
//
|
|
338
|
-
|
|
339
|
-
// Returns: ModalState.CLOSED | OPENING | OPEN | CLOSING
|
|
298
|
+
// anywhere
|
|
299
|
+
useApp.getState().uploadModalRef?.current?.openModal();
|
|
340
300
|
```
|
|
341
301
|
|
|
342
|
-
|
|
302
|
+
`disablePortal` is right here because the sheet is already mounted at root level —
|
|
303
|
+
there is nothing above it for the portal to lift it past.
|
|
304
|
+
|
|
305
|
+
### Ref methods
|
|
343
306
|
|
|
344
|
-
```
|
|
345
|
-
const
|
|
307
|
+
```tsx
|
|
308
|
+
const ref = useRef<BottomModalRef>(null);
|
|
346
309
|
|
|
347
|
-
|
|
348
|
-
|
|
310
|
+
ref.current?.openModal({ onOpen: () => {} });
|
|
311
|
+
ref.current?.closeModal({
|
|
312
|
+
skipAnimation: false,
|
|
313
|
+
isNavigating: false,
|
|
349
314
|
duration: 300,
|
|
350
|
-
easing: Easing.
|
|
315
|
+
easing: Easing.linear,
|
|
316
|
+
onClose: () => {},
|
|
351
317
|
});
|
|
318
|
+
ref.current?.closeWithoutAnimation();
|
|
319
|
+
ref.current?.snapToIndex(1);
|
|
320
|
+
ref.current?.snapToPercentage(75); // or '75%'
|
|
321
|
+
ref.current?.getModalState(); // ModalState.CLOSED | OPENING | OPEN | CLOSING
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Behaviour to be aware of on a **state-controlled** sheet:
|
|
352
325
|
|
|
353
|
-
|
|
354
|
-
|
|
326
|
+
| Call | What actually happens |
|
|
327
|
+
| ------------------------------------- | ----------------------------------------------- |
|
|
328
|
+
| `openModal()` | `setShowModal(true)` |
|
|
329
|
+
| `openModal({ onOpen })` | routes through the mounter so `onOpen` can fire |
|
|
330
|
+
| `closeModal()` | `setShowModal(false)` |
|
|
331
|
+
| `closeModal({ duration \| onClose })` | animates, then clears state after the animation |
|
|
332
|
+
| `closeModal({ isNavigating: true })` | 100ms close — use before navigating |
|
|
333
|
+
| `closeModal({ skipAnimation: true })` | immediate unmount |
|
|
355
334
|
|
|
356
|
-
|
|
357
|
-
sheetRef.current?.snapToPercentage(50);
|
|
335
|
+
### Ref typing
|
|
358
336
|
|
|
359
|
-
|
|
360
|
-
const
|
|
337
|
+
```tsx
|
|
338
|
+
const ref = useRef<BottomModalRef>(null); // ✅
|
|
361
339
|
```
|
|
362
340
|
|
|
363
|
-
|
|
341
|
+
`BottomModalRefObject` is `React.Ref<BottomModalRef>` — it types the `ref` _prop_
|
|
342
|
+
on the component, not the object `useRef` gives you. Using it in `useRef` produces
|
|
343
|
+
a doubly-wrapped type.
|
|
364
344
|
|
|
365
|
-
|
|
345
|
+
### Closing from inside the sheet
|
|
366
346
|
|
|
367
347
|
```tsx
|
|
368
348
|
import { useBottomSheetRef } from '@shaquillehinds/react-native-bottom-sheet';
|
|
369
349
|
|
|
370
|
-
function
|
|
350
|
+
function Footer() {
|
|
371
351
|
const { modalRef } = useBottomSheetRef();
|
|
352
|
+
return (
|
|
353
|
+
<Button title="Close" onPress={() => modalRef?.current?.closeModal()} />
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
```
|
|
372
357
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
};
|
|
358
|
+
Available anywhere inside the sheet's subtree. Returns `{ modalRef }` where
|
|
359
|
+
`modalRef` may be undefined outside a sheet, so optional-chain it.
|
|
376
360
|
|
|
377
|
-
|
|
378
|
-
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## Snap points
|
|
364
|
+
|
|
365
|
+
Percentages of screen height. Numbers or strings both work:
|
|
366
|
+
|
|
367
|
+
```tsx
|
|
368
|
+
snapPoints={[25, 50, 75]}
|
|
369
|
+
snapPoints={['25', '50%', 90]}
|
|
379
370
|
```
|
|
380
371
|
|
|
381
|
-
|
|
372
|
+
- The sheet opens at index `0`. Order the array in the direction you want it read;
|
|
373
|
+
`snapToIndex` uses the array's own indices.
|
|
374
|
+
- `onSnapPointReach(index)` fires when the sheet settles on one. Only active when
|
|
375
|
+
`snapPoints` is set.
|
|
376
|
+
- Keep the array referentially stable — a module constant or `useMemo`.
|
|
377
|
+
- On orientation change, snap points are recomputed and the sheet re-snaps to
|
|
378
|
+
index 0.
|
|
382
379
|
|
|
383
|
-
###
|
|
380
|
+
### Content-height mode
|
|
384
381
|
|
|
385
|
-
|
|
382
|
+
Omitting `snapPoints` is a supported mode, not an oversight. The sheet measures its
|
|
383
|
+
content on layout and opens to exactly that height, capped at roughly 110% of
|
|
384
|
+
screen height. Good for short, content-sized sheets — confirmation dialogs, small
|
|
385
|
+
action lists.
|
|
386
|
+
|
|
387
|
+
Dragging behaviour differs: with no snap points, a drag past half the content
|
|
388
|
+
height or a fast flick dismisses the sheet.
|
|
389
|
+
|
|
390
|
+
Content-height mode is incompatible with `showContentDelay: { type: 'mount' }`,
|
|
391
|
+
since there is nothing to measure at open time. Use `snapPoints`, or set a
|
|
392
|
+
`minHeight` on `contentContainerStyle`.
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## Scrollable content
|
|
386
397
|
|
|
387
398
|
```tsx
|
|
388
399
|
import {
|
|
@@ -390,1076 +401,522 @@ import {
|
|
|
390
401
|
BottomSheetFlatlist,
|
|
391
402
|
} from '@shaquillehinds/react-native-bottom-sheet';
|
|
392
403
|
|
|
393
|
-
<BottomSheetModal
|
|
394
|
-
showModal={showModal}
|
|
395
|
-
setShowModal={setShowModal}
|
|
396
|
-
snapPoints={[50, 90]}
|
|
397
|
-
>
|
|
404
|
+
<BottomSheetModal snapPoints={[50, 90]} showModal={show} setShowModal={setShow}>
|
|
398
405
|
<BottomSheetFlatlist
|
|
399
406
|
data={items}
|
|
400
|
-
renderItem={({ item }) => <
|
|
407
|
+
renderItem={({ item }) => <Row item={item} />}
|
|
401
408
|
keyExtractor={(item) => item.id}
|
|
402
409
|
/>
|
|
403
410
|
</BottomSheetModal>;
|
|
404
411
|
```
|
|
405
412
|
|
|
406
|
-
|
|
413
|
+
These wrap the underlying Reanimated list in a drag gesture that coordinates with
|
|
414
|
+
the sheet: the sheet drags when the list sits at a scroll boundary, the list
|
|
415
|
+
scrolls otherwise. A bare `FlatList` or `ScrollView` has no such coordination and
|
|
416
|
+
the two gestures will fight.
|
|
407
417
|
|
|
408
|
-
|
|
409
|
-
import {
|
|
410
|
-
BottomSheetModal,
|
|
411
|
-
BottomSheetScrollView,
|
|
412
|
-
} from '@shaquillehinds/react-native-bottom-sheet';
|
|
418
|
+
Notes:
|
|
413
419
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
420
|
+
- Props are the full underlying set — `FlatListPropsWithLayout` for the list,
|
|
421
|
+
`AnimatedScrollViewProps` for the scroll view — with `onScroll` widened to also
|
|
422
|
+
accept a Reanimated scroll event.
|
|
423
|
+
- Pass a custom ref via `refFlatlist` / `refScrollView`, not `ref`.
|
|
424
|
+
- `inverted` lists are supported; the sheet is told about the inversion.
|
|
425
|
+
- `bounces` is forced to `false` inside a sheet.
|
|
426
|
+
- Used outside a sheet, both fall back to a plain animated list/scroll view.
|
|
427
|
+
- Avoid `dragArea="full"` alongside a scrollable child. Keep the default `"bumper"`.
|
|
418
428
|
|
|
419
|
-
|
|
429
|
+
---
|
|
420
430
|
|
|
421
|
-
|
|
431
|
+
## Keyboard handling
|
|
432
|
+
|
|
433
|
+
### Whole-sheet avoidance
|
|
422
434
|
|
|
423
435
|
```tsx
|
|
424
|
-
<BottomSheetModal
|
|
425
|
-
avoidKeyboard={true}
|
|
426
|
-
showModal={showModal}
|
|
427
|
-
setShowModal={setShowModal}
|
|
428
|
-
>
|
|
436
|
+
<BottomSheetModal avoidKeyboard showModal={show} setShowModal={setShow}>
|
|
429
437
|
<TextInput placeholder="Email" />
|
|
430
|
-
<TextInput placeholder="Password" />
|
|
431
438
|
</BottomSheetModal>
|
|
432
439
|
```
|
|
433
440
|
|
|
434
|
-
|
|
441
|
+
### Per-input avoidance
|
|
435
442
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
const emailRef = useRef<TextInput>(null);
|
|
439
|
-
const passwordRef = useRef<TextInput>(null);
|
|
440
|
-
|
|
441
|
-
return (
|
|
442
|
-
<BottomSheetModal
|
|
443
|
-
inputsForKeyboardToAvoid={[emailRef, passwordRef]}
|
|
444
|
-
showModal={showModal}
|
|
445
|
-
setShowModal={setShowModal}
|
|
446
|
-
>
|
|
447
|
-
<TextInput ref={emailRef} placeholder="Email" />
|
|
448
|
-
<TextInput ref={passwordRef} placeholder="Password" />
|
|
449
|
-
<TextInput placeholder="Not tracked" />
|
|
450
|
-
</BottomSheetModal>
|
|
451
|
-
);
|
|
452
|
-
}
|
|
453
|
-
```
|
|
454
|
-
|
|
455
|
-
### Custom Bumper
|
|
443
|
+
Only shift for specific inputs — useful when a search field should move but a
|
|
444
|
+
comment box further down should not:
|
|
456
445
|
|
|
457
446
|
```tsx
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
<View style={{ padding: 20, alignItems: 'center' }}>
|
|
461
|
-
<View
|
|
462
|
-
style={{
|
|
463
|
-
width: 100,
|
|
464
|
-
height: 5,
|
|
465
|
-
backgroundColor: 'blue',
|
|
466
|
-
borderRadius: 3,
|
|
467
|
-
}}
|
|
468
|
-
/>
|
|
469
|
-
<Text style={{ marginTop: 10 }}>Drag me</Text>
|
|
470
|
-
</View>
|
|
471
|
-
);
|
|
472
|
-
}
|
|
447
|
+
const emailRef = useRef<TextInput>(null);
|
|
448
|
+
const passwordRef = useRef<TextInput>(null);
|
|
473
449
|
|
|
474
450
|
<BottomSheetModal
|
|
475
|
-
|
|
476
|
-
showModal={
|
|
477
|
-
setShowModal={
|
|
451
|
+
inputsForKeyboardToAvoid={[emailRef, passwordRef]}
|
|
452
|
+
showModal={show}
|
|
453
|
+
setShowModal={setShow}
|
|
478
454
|
>
|
|
479
|
-
<
|
|
455
|
+
<TextInput ref={emailRef} />
|
|
456
|
+
<TextInput ref={passwordRef} />
|
|
457
|
+
<TextInput placeholder="Not tracked" />
|
|
480
458
|
</BottomSheetModal>;
|
|
481
459
|
```
|
|
482
460
|
|
|
483
|
-
|
|
461
|
+
Dragging is disabled while the keyboard is open, since the two interactions
|
|
462
|
+
conflict. Override with `allowDragWhileKeyboardVisible` if you have a reason.
|
|
484
463
|
|
|
485
|
-
|
|
486
|
-
function CustomBackdrop() {
|
|
487
|
-
return (
|
|
488
|
-
<View
|
|
489
|
-
style={{
|
|
490
|
-
flex: 1,
|
|
491
|
-
backgroundColor: 'rgba(255, 0, 0, 0.3)',
|
|
492
|
-
}}
|
|
493
|
-
/>
|
|
494
|
-
);
|
|
495
|
-
}
|
|
464
|
+
Do not add `KeyboardAvoidingView` — it will fight the built-in handling.
|
|
496
465
|
|
|
497
|
-
|
|
498
|
-
BackdropComponent={<CustomBackdrop />}
|
|
499
|
-
showModal={showModal}
|
|
500
|
-
setShowModal={setShowModal}
|
|
501
|
-
>
|
|
502
|
-
<YourContent />
|
|
503
|
-
</BottomSheetModal>;
|
|
504
|
-
```
|
|
466
|
+
---
|
|
505
467
|
|
|
506
|
-
|
|
468
|
+
## Persistent sheets
|
|
507
469
|
|
|
508
|
-
|
|
470
|
+
`keepMounted` changes what a downward drag does: instead of dismissing, the sheet
|
|
471
|
+
snaps to its lowest snap point.
|
|
509
472
|
|
|
510
473
|
```tsx
|
|
511
474
|
<BottomSheet
|
|
512
|
-
keepMounted
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
showModal={
|
|
516
|
-
setShowModal={
|
|
475
|
+
keepMounted
|
|
476
|
+
snapPoints={[10, 70]}
|
|
477
|
+
bottomOffset={100}
|
|
478
|
+
showModal={show}
|
|
479
|
+
setShowModal={setShow}
|
|
517
480
|
>
|
|
518
|
-
<
|
|
481
|
+
<FilterPanel />
|
|
519
482
|
</BottomSheet>
|
|
520
483
|
```
|
|
521
484
|
|
|
522
|
-
|
|
485
|
+
Two things to know:
|
|
523
486
|
|
|
524
|
-
|
|
487
|
+
- **`keepMounted` requires `snapPoints`.** With no snap points it is ignored and
|
|
488
|
+
a drag down will dismiss as usual.
|
|
489
|
+
- It governs the drag gesture only. `showModal={false}` still unmounts the sheet.
|
|
525
490
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
showModal={showModal}
|
|
529
|
-
setShowModal={setShowModal}
|
|
530
|
-
contentContainerStyle={{ minHeight: 400 }}
|
|
531
|
-
showContentDelay={{
|
|
532
|
-
type: 'mount', // Delays mounting entirely
|
|
533
|
-
timeInMilliSecs: 100,
|
|
534
|
-
}}
|
|
535
|
-
>
|
|
536
|
-
<HeavyComponent />
|
|
537
|
-
</BottomSheetModal>
|
|
538
|
-
```
|
|
491
|
+
`bottomOffset` pushes the sheet up from the bottom of the screen, so its "closed"
|
|
492
|
+
position stays visible as a peek.
|
|
539
493
|
|
|
540
|
-
|
|
494
|
+
---
|
|
541
495
|
|
|
542
|
-
|
|
543
|
-
<BottomSheetModal
|
|
544
|
-
showContentDelay={{
|
|
545
|
-
type: 'opacity', // Fades in content
|
|
546
|
-
timeInMilliSecs: 200,
|
|
547
|
-
}}
|
|
548
|
-
showModal={showModal}
|
|
549
|
-
setShowModal={setShowModal}
|
|
550
|
-
>
|
|
551
|
-
<YourContent />
|
|
552
|
-
</BottomSheetModal>
|
|
553
|
-
```
|
|
496
|
+
## Appearance
|
|
554
497
|
|
|
555
|
-
|
|
498
|
+
| Prop | Targets |
|
|
499
|
+
| ----------------------- | -------------------------------------- |
|
|
500
|
+
| `backgroundColor` | Sheet surface **and** bumper container |
|
|
501
|
+
| `style` | The sheet surface |
|
|
502
|
+
| `contentContainerStyle` | The container wrapping your children |
|
|
503
|
+
| `bumperStyle` | The grab handle itself |
|
|
504
|
+
| `bumperContainerStyle` | The area around the handle |
|
|
556
505
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
snapPoints={[25, 50, 75]}
|
|
560
|
-
onSnapPointReach={(index) => {
|
|
561
|
-
console.log(`Reached snap point: ${index}`);
|
|
562
|
-
if (index === 2) {
|
|
563
|
-
// Reached highest snap point
|
|
564
|
-
}
|
|
565
|
-
}}
|
|
566
|
-
showModal={showModal}
|
|
567
|
-
setShowModal={setShowModal}
|
|
568
|
-
>
|
|
569
|
-
<YourContent />
|
|
570
|
-
</BottomSheetModal>
|
|
571
|
-
```
|
|
506
|
+
Use `backgroundColor` rather than setting a background in `style` — the former
|
|
507
|
+
also colours the bumper container, so the handle area matches.
|
|
572
508
|
|
|
573
|
-
###
|
|
509
|
+
### Custom bumper
|
|
574
510
|
|
|
575
|
-
|
|
511
|
+
The custom component becomes the drag area when `dragArea` is `"bumper"`.
|
|
576
512
|
|
|
577
513
|
```tsx
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
>
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
```
|
|
586
|
-
|
|
587
|
-
#### Disable Dragging
|
|
514
|
+
function Bumper() {
|
|
515
|
+
return (
|
|
516
|
+
<View style={{ padding: 20, alignItems: 'center' }}>
|
|
517
|
+
<View style={{ width: 100, height: 5, backgroundColor: '#888', borderRadius: 3 }} />
|
|
518
|
+
</View>
|
|
519
|
+
);
|
|
520
|
+
}
|
|
588
521
|
|
|
589
|
-
|
|
590
|
-
<BottomSheetModal
|
|
591
|
-
dragArea="none"
|
|
592
|
-
showModal={showModal}
|
|
593
|
-
setShowModal={setShowModal}
|
|
594
|
-
>
|
|
595
|
-
<YourContent />
|
|
596
|
-
</BottomSheetModal>
|
|
522
|
+
<BottomSheetModal BumperComponent={Bumper} ... />
|
|
597
523
|
```
|
|
598
524
|
|
|
599
|
-
|
|
525
|
+
### Custom backdrop
|
|
600
526
|
|
|
601
527
|
```tsx
|
|
602
528
|
<BottomSheetModal
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
>
|
|
608
|
-
<TextInput />
|
|
609
|
-
</BottomSheetModal>
|
|
529
|
+
BackdropComponent={<BlurView intensity={40} style={StyleSheet.absoluteFill} />}
|
|
530
|
+
onBackDropPress={() => track('dismiss')}
|
|
531
|
+
...
|
|
532
|
+
/>
|
|
610
533
|
```
|
|
611
534
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
By default, bottom sheets use the portal system to render at the root level, ensuring they appear above all content.
|
|
615
|
-
|
|
616
|
-
#### Using Default Portal (Recommended)
|
|
535
|
+
`disableCloseOnBackdropPress` keeps `onBackDropPress` firing without closing.
|
|
617
536
|
|
|
618
|
-
|
|
537
|
+
### Drag area
|
|
619
538
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
539
|
+
| Value | Behaviour |
|
|
540
|
+
| -------------------- | ----------------------------------------------------- |
|
|
541
|
+
| `"bumper"` (default) | Only the handle drags |
|
|
542
|
+
| `"full"` | The whole sheet drags — avoid with scrollable content |
|
|
543
|
+
| `"none"` | No dragging; close programmatically or via backdrop |
|
|
623
544
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
<BottomSheetPortalProvider>
|
|
627
|
-
<NavigationContainer>
|
|
628
|
-
<Stack.Navigator>
|
|
629
|
-
<Stack.Screen name="Home" component={HomeScreen} />
|
|
630
|
-
</Stack.Navigator>
|
|
631
|
-
</NavigationContainer>
|
|
632
|
-
</BottomSheetPortalProvider>
|
|
633
|
-
);
|
|
634
|
-
}
|
|
545
|
+
`hideBumper` removes the handle. With `dragArea="bumper"` and `hideBumper`, there
|
|
546
|
+
is nothing left to drag — pair `hideBumper` with `"full"` or `"none"`.
|
|
635
547
|
|
|
636
|
-
|
|
637
|
-
<BottomSheetModal showModal={showModal} setShowModal={setShowModal}>
|
|
638
|
-
<YourContent />
|
|
639
|
-
</BottomSheetModal>;
|
|
640
|
-
```
|
|
548
|
+
---
|
|
641
549
|
|
|
642
|
-
|
|
550
|
+
## Portal system
|
|
643
551
|
|
|
644
|
-
|
|
552
|
+
Sheets render into a portal at app root so they sit above navigation. This is
|
|
553
|
+
automatic once `BottomSheetPortalProvider` is in place.
|
|
645
554
|
|
|
646
|
-
|
|
647
|
-
<BottomSheetModal
|
|
648
|
-
disablePortal={true}
|
|
649
|
-
showModal={showModal}
|
|
650
|
-
setShowModal={setShowModal}
|
|
651
|
-
>
|
|
652
|
-
<YourContent />
|
|
653
|
-
</BottomSheetModal>
|
|
654
|
-
```
|
|
555
|
+
### `BottomSheetPortalProvider`
|
|
655
556
|
|
|
656
|
-
|
|
557
|
+
| Prop | Type | Default | Description |
|
|
558
|
+
| --------------------- | --------------- | ------- | --------------------------------------- |
|
|
559
|
+
| `unMountBufferTimeMS` | `number` | `100` | Delay before removing portal items |
|
|
560
|
+
| `updateBufferTimeMS` | `number` | – | Throttle for portal updates |
|
|
561
|
+
| `CustomPortalContext` | `React.Context` | – | Scope this provider to a custom context |
|
|
657
562
|
|
|
658
|
-
|
|
563
|
+
### `disablePortal`
|
|
659
564
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
BottomSheetPortalProvider,
|
|
663
|
-
useBottomSheetPortal,
|
|
664
|
-
} from '@shaquillehinds/react-native-bottom-sheet';
|
|
665
|
-
import { createContext } from 'react';
|
|
666
|
-
import type { PortalContextValue } from '@shaquillehinds/react-native-bottom-sheet';
|
|
565
|
+
Renders the sheet where it sits in the tree. Correct when the sheet is already
|
|
566
|
+
mounted at root level, or when you want it scoped to a subtree.
|
|
667
567
|
|
|
668
|
-
|
|
669
|
-
const MyCustomPortalContext = createContext<PortalContextValue | undefined>(
|
|
670
|
-
undefined
|
|
671
|
-
);
|
|
568
|
+
### Scoped portals
|
|
672
569
|
|
|
673
|
-
|
|
674
|
-
function MySection() {
|
|
675
|
-
return (
|
|
676
|
-
<BottomSheetPortalProvider CustomPortalContext={MyCustomPortalContext}>
|
|
677
|
-
<MySectionContent />
|
|
678
|
-
</BottomSheetPortalProvider>
|
|
679
|
-
);
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
// Use the custom context in your bottom sheet
|
|
683
|
-
<BottomSheetModal
|
|
684
|
-
CustomPortalContext={MyCustomPortalContext}
|
|
685
|
-
showModal={showModal}
|
|
686
|
-
setShowModal={setShowModal}
|
|
687
|
-
>
|
|
688
|
-
<YourContent />
|
|
689
|
-
</BottomSheetModal>;
|
|
690
|
-
```
|
|
691
|
-
|
|
692
|
-
#### Manual Portal Control
|
|
693
|
-
|
|
694
|
-
For advanced use cases where you need direct portal control:
|
|
570
|
+
Both provider and sheet must be handed the same context object:
|
|
695
571
|
|
|
696
572
|
```tsx
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
function CustomPortalComponent() {
|
|
700
|
-
const portal = useBottomSheetPortal();
|
|
701
|
-
const [portalKey, setPortalKey] = useState<string | number | null>(null);
|
|
702
|
-
|
|
703
|
-
const mountContent = () => {
|
|
704
|
-
if (portal) {
|
|
705
|
-
const key = portal.mount(
|
|
706
|
-
'custom-content',
|
|
707
|
-
<View style={{ padding: 20, backgroundColor: 'white' }}>
|
|
708
|
-
<Text>Portal Content</Text>
|
|
709
|
-
</View>,
|
|
710
|
-
(key) => console.log('Mounted:', key)
|
|
711
|
-
);
|
|
712
|
-
setPortalKey(key);
|
|
713
|
-
}
|
|
714
|
-
};
|
|
715
|
-
|
|
716
|
-
const updateContent = () => {
|
|
717
|
-
if (portal && portalKey) {
|
|
718
|
-
portal.update(
|
|
719
|
-
portalKey,
|
|
720
|
-
<View style={{ padding: 20, backgroundColor: 'blue' }}>
|
|
721
|
-
<Text>Updated Content</Text>
|
|
722
|
-
</View>
|
|
723
|
-
);
|
|
724
|
-
}
|
|
725
|
-
};
|
|
726
|
-
|
|
727
|
-
const unmountContent = () => {
|
|
728
|
-
if (portal && portalKey) {
|
|
729
|
-
portal.unmount(portalKey, (key) => console.log('Unmounted:', key));
|
|
730
|
-
setPortalKey(null);
|
|
731
|
-
}
|
|
732
|
-
};
|
|
573
|
+
const ScreenPortalContext = createContext<PortalContextValue | undefined>(undefined);
|
|
733
574
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
</View>
|
|
740
|
-
);
|
|
741
|
-
}
|
|
575
|
+
<BottomSheetPortalProvider CustomPortalContext={ScreenPortalContext}>
|
|
576
|
+
<BottomSheetModal CustomPortalContext={ScreenPortalContext} ...>
|
|
577
|
+
<Content />
|
|
578
|
+
</BottomSheetModal>
|
|
579
|
+
</BottomSheetPortalProvider>
|
|
742
580
|
```
|
|
743
581
|
|
|
744
|
-
|
|
582
|
+
### Non-sheet overlays
|
|
745
583
|
|
|
746
|
-
|
|
584
|
+
`useBottomSheetPortalComponent` mounts arbitrary content into the same portal with
|
|
585
|
+
automatic cleanup — toasts, loading overlays, anything that needs to sit above
|
|
586
|
+
everything:
|
|
747
587
|
|
|
748
588
|
```tsx
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
// Automatically mounts/unmounts based on show state
|
|
756
|
-
useBottomSheetPortalComponent({
|
|
757
|
-
name: 'toast-notification',
|
|
758
|
-
Component: show ? (
|
|
759
|
-
<View style={{ position: 'absolute', top: 50, alignSelf: 'center' }}>
|
|
760
|
-
<Text>{message}</Text>
|
|
761
|
-
</View>
|
|
762
|
-
) : null,
|
|
763
|
-
disable: !show,
|
|
764
|
-
});
|
|
765
|
-
|
|
766
|
-
const showToast = (msg: string) => {
|
|
767
|
-
setMessage(msg);
|
|
768
|
-
setShow(true);
|
|
769
|
-
setTimeout(() => setShow(false), 3000);
|
|
770
|
-
};
|
|
771
|
-
|
|
772
|
-
return <Button title="Show Toast" onPress={() => showToast('Hello!')} />;
|
|
773
|
-
}
|
|
589
|
+
useBottomSheetPortalComponent({
|
|
590
|
+
name: 'toast',
|
|
591
|
+
Component: visible ? <Toast message={message} /> : null,
|
|
592
|
+
disable: !visible,
|
|
593
|
+
});
|
|
774
594
|
```
|
|
775
595
|
|
|
776
|
-
|
|
596
|
+
`useBottomSheetPortal` gives the raw `{ mount, update, unmount }` API for manual
|
|
597
|
+
control.
|
|
598
|
+
|
|
599
|
+
You do not need either of these for a bottom sheet — the sheet portals itself.
|
|
600
|
+
|
|
601
|
+
---
|
|
602
|
+
|
|
603
|
+
## API reference
|
|
604
|
+
|
|
605
|
+
### `BottomSheetProps`
|
|
606
|
+
|
|
607
|
+
| Prop | Type | Default | Description |
|
|
608
|
+
| ------------------------------- | ---------------------------------------------------------- | ---------- | --------------------------------------------------------------------------------- |
|
|
609
|
+
| `showModal` | `boolean` | – | Visibility. Omit for a ref-controlled sheet |
|
|
610
|
+
| `setShowModal` | `(bool: boolean) => void` | – | Visibility setter |
|
|
611
|
+
| `snapPoints` | `(number \| string)[]` | – | Percentages of screen height. Omit for content-height mode |
|
|
612
|
+
| `dragArea` | `'full' \| 'bumper' \| 'none'` | `'bumper'` | Draggable region |
|
|
613
|
+
| `keepMounted` | `boolean` | `false` | Drag down snaps to lowest snap point instead of dismissing. Requires `snapPoints` |
|
|
614
|
+
| `hideBumper` | `boolean` | `false` | Remove the grab handle |
|
|
615
|
+
| `avoidKeyboard` | `boolean` | `false` | Pad content when the keyboard is open |
|
|
616
|
+
| `allowDragWhileKeyboardVisible` | `boolean` | `false` | Permit dragging with the keyboard open |
|
|
617
|
+
| `inputsForKeyboardToAvoid` | `React.RefObject<TextInput>[]` | – | Only avoid for these inputs |
|
|
618
|
+
| `bottomOffset` | `number` | `0` | Push the sheet up from the bottom |
|
|
619
|
+
| `showContentDelay` | `{ type?: 'mount' \| 'opacity'; timeInMilliSecs: number }` | – | Defer content render past the open animation |
|
|
620
|
+
| `style` | `StyleProp<ViewStyle>` | – | Sheet surface |
|
|
621
|
+
| `contentContainerStyle` | `StyleProp<ViewStyle>` | – | Content container |
|
|
622
|
+
| `bumperStyle` | `StyleProp<ViewStyle>` | – | Grab handle |
|
|
623
|
+
| `bumperContainerStyle` | `StyleProp<ViewStyle>` | – | Handle container |
|
|
624
|
+
| `backgroundColor` | `string` | – | Sheet and bumper container background |
|
|
625
|
+
| `BumperComponent` | `() => React.ReactNode` | – | Replace the default handle |
|
|
626
|
+
| `onModalShow` | `() => void \| Promise<void>` | – | Fires when the sheet finishes mounting |
|
|
627
|
+
| `onModalClose` | `() => void \| Promise<void>` | – | Fires when the sheet finishes unmounting |
|
|
628
|
+
| `onSnapPointReach` | `(index: number) => void \| Promise<void>` | – | Fires on settling at a snap point |
|
|
629
|
+
| `disablePortal` | `boolean` | `false` | Render in place |
|
|
630
|
+
| `CustomPortalContext` | `React.Context<PortalContextValue>` | – | Scoped portal |
|
|
631
|
+
|
|
632
|
+
### `BottomSheetModalProps`
|
|
633
|
+
|
|
634
|
+
Extends `BottomSheetProps`.
|
|
635
|
+
|
|
636
|
+
| Prop | Type | Default | Description |
|
|
637
|
+
| ------------------------------ | ----------------- | ------- | -------------------------------------------- |
|
|
638
|
+
| `BackdropComponent` | `React.ReactNode` | – | Custom backdrop |
|
|
639
|
+
| `onBackDropPress` | `() => void` | – | Backdrop press callback |
|
|
640
|
+
| `disableCloseOnBackdropPress` | `boolean` | `false` | Keep open on backdrop press |
|
|
641
|
+
| `useNativeModal` | `boolean` | `false` | Render inside RN's native `Modal` |
|
|
642
|
+
| `enableBackgroundContentPress` | `boolean` | `false` | Drop the backdrop; taps reach content behind |
|
|
643
|
+
| `disableAndroidBackButton` | `boolean` | `false` | Ignore the hardware back button |
|
|
644
|
+
|
|
645
|
+
### `BottomSheetFlatlistProps<T>`
|
|
646
|
+
|
|
647
|
+
`Omit<FlatListPropsWithLayout<T>, 'onScroll'>` plus:
|
|
648
|
+
|
|
649
|
+
| Prop | Type |
|
|
650
|
+
| ------------- | --------------------------------------- |
|
|
651
|
+
| `onScroll` | `ReanimatedOnScroll \| DefaultOnScroll` |
|
|
652
|
+
| `refFlatlist` | `React.RefObject<FlatList>` |
|
|
653
|
+
|
|
654
|
+
### `BottomSheetScrollViewProps`
|
|
655
|
+
|
|
656
|
+
`Omit<AnimatedScrollViewProps, 'onScroll'>` plus:
|
|
657
|
+
|
|
658
|
+
| Prop | Type |
|
|
659
|
+
| --------------- | --------------------------------------- |
|
|
660
|
+
| `onScroll` | `ReanimatedOnScroll \| DefaultOnScroll` |
|
|
661
|
+
| `refScrollView` | `React.RefObject<AnimatedScrollView>` |
|
|
662
|
+
|
|
663
|
+
### Ref types
|
|
664
|
+
|
|
665
|
+
```ts
|
|
666
|
+
type BottomModalRef = {
|
|
667
|
+
getModalState: () => ModalState | undefined;
|
|
668
|
+
openModal: (props?: OpenModalProps) => void;
|
|
669
|
+
closeModal: (props?: CloseModalProps) => void;
|
|
670
|
+
closeWithoutAnimation: () => void;
|
|
671
|
+
snapToIndex: (index: number) => void;
|
|
672
|
+
snapToPercentage: (percentage: number | string) => void;
|
|
673
|
+
};
|
|
777
674
|
|
|
778
|
-
|
|
675
|
+
type OpenModalProps = { onOpen?: () => void };
|
|
779
676
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
},
|
|
787
|
-
});
|
|
677
|
+
type CloseModalProps = {
|
|
678
|
+
skipAnimation?: boolean;
|
|
679
|
+
isNavigating?: boolean;
|
|
680
|
+
onClose?: () => void;
|
|
681
|
+
duration?: number;
|
|
682
|
+
easing?: EasingFunction | EasingFunctionFactory;
|
|
788
683
|
};
|
|
684
|
+
|
|
685
|
+
enum ModalState {
|
|
686
|
+
CLOSED = 0,
|
|
687
|
+
OPENING = 1,
|
|
688
|
+
OPEN = 2,
|
|
689
|
+
CLOSING = 3,
|
|
690
|
+
}
|
|
789
691
|
```
|
|
790
692
|
|
|
791
|
-
|
|
693
|
+
### Exports
|
|
792
694
|
|
|
793
|
-
```
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
onClose: () => navigation.navigate('NextScreen'),
|
|
797
|
-
});
|
|
798
|
-
```
|
|
695
|
+
```ts
|
|
696
|
+
// Components
|
|
697
|
+
(BottomSheet, BottomSheetModal, BottomSheetFlatlist, BottomSheetScrollView);
|
|
799
698
|
|
|
800
|
-
|
|
699
|
+
// Hooks
|
|
700
|
+
(useBottomSheetRef, useBottomSheetPortal, useBottomSheetPortalComponent);
|
|
801
701
|
|
|
802
|
-
|
|
702
|
+
// Provider
|
|
703
|
+
BottomSheetPortalProvider;
|
|
803
704
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
import type {
|
|
807
|
-
BottomSheetProps,
|
|
705
|
+
// Types
|
|
706
|
+
(BottomSheetProps,
|
|
808
707
|
BottomSheetModalProps,
|
|
809
708
|
BottomSheetFlatlistProps,
|
|
810
709
|
BottomSheetScrollViewProps,
|
|
811
|
-
} from '@shaquillehinds/react-native-bottom-sheet';
|
|
812
|
-
|
|
813
|
-
// Ref Types
|
|
814
|
-
import type {
|
|
815
|
-
BottomModalRefObject,
|
|
816
|
-
BottomSheetRefObject,
|
|
817
710
|
BottomModalRef,
|
|
818
711
|
BottomSheetRef,
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
// State and Config Types
|
|
822
|
-
import type {
|
|
712
|
+
BottomModalRefObject,
|
|
713
|
+
BottomSheetRefObject,
|
|
823
714
|
ModalState,
|
|
824
|
-
AnimateCloseModalProps,
|
|
825
|
-
CloseModalProps,
|
|
826
715
|
OpenModalProps,
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
// Portal Types
|
|
830
|
-
import type {
|
|
716
|
+
CloseModalProps,
|
|
717
|
+
AnimateCloseModalProps,
|
|
831
718
|
PortalItem,
|
|
832
719
|
PortalKey,
|
|
833
|
-
PortalContextValue
|
|
834
|
-
} from '@shaquillehinds/react-native-bottom-sheet';
|
|
720
|
+
PortalContextValue);
|
|
835
721
|
```
|
|
836
722
|
|
|
837
|
-
|
|
723
|
+
---
|
|
838
724
|
|
|
839
|
-
|
|
725
|
+
## Recipes
|
|
840
726
|
|
|
841
|
-
|
|
842
|
-
import React, { useRef, useState } from 'react';
|
|
843
|
-
import { View, Text, Button, TextInput } from 'react-native';
|
|
844
|
-
import {
|
|
845
|
-
BottomSheetModal,
|
|
846
|
-
BottomSheetScrollView,
|
|
847
|
-
useBottomSheetRef,
|
|
848
|
-
} from '@shaquillehinds/react-native-bottom-sheet';
|
|
849
|
-
import type { BottomModalRefObject } from '@shaquillehinds/react-native-bottom-sheet';
|
|
850
|
-
|
|
851
|
-
export default function CompleteExample() {
|
|
852
|
-
const [showModal, setShowModal] = useState(false);
|
|
853
|
-
const bottomSheetRef = useRef<BottomModalRefObject>(null);
|
|
854
|
-
|
|
855
|
-
const handleOpen = () => {
|
|
856
|
-
bottomSheetRef.current?.openModal({
|
|
857
|
-
onOpen: () => console.log('Modal opened!'),
|
|
858
|
-
});
|
|
859
|
-
};
|
|
860
|
-
|
|
861
|
-
const handleSnapToTop = () => {
|
|
862
|
-
bottomSheetRef.current?.snapToIndex(2);
|
|
863
|
-
};
|
|
727
|
+
### Action list
|
|
864
728
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
<BottomSheetModal
|
|
870
|
-
ref={bottomSheetRef}
|
|
871
|
-
showModal={showModal}
|
|
872
|
-
setShowModal={setShowModal}
|
|
873
|
-
snapPoints={[30, 60, 90]}
|
|
874
|
-
backgroundColor="#ffffff"
|
|
875
|
-
avoidKeyboard={true}
|
|
876
|
-
onModalShow={() => console.log('Modal shown')}
|
|
877
|
-
onModalClose={() => console.log('Modal closed')}
|
|
878
|
-
onSnapPointReach={(index) => console.log('Snap point:', index)}
|
|
879
|
-
>
|
|
880
|
-
<BottomSheetScrollView>
|
|
881
|
-
<View style={{ padding: 20 }}>
|
|
882
|
-
<Text style={{ fontSize: 24, marginBottom: 20 }}>My Modal</Text>
|
|
883
|
-
|
|
884
|
-
<TextInput
|
|
885
|
-
placeholder="Enter text..."
|
|
886
|
-
style={{
|
|
887
|
-
borderWidth: 1,
|
|
888
|
-
borderColor: '#ccc',
|
|
889
|
-
padding: 10,
|
|
890
|
-
marginBottom: 20,
|
|
891
|
-
}}
|
|
892
|
-
/>
|
|
893
|
-
|
|
894
|
-
<Button title="Snap to Top" onPress={handleSnapToTop} />
|
|
895
|
-
|
|
896
|
-
<InnerComponent />
|
|
897
|
-
</View>
|
|
898
|
-
</BottomSheetScrollView>
|
|
899
|
-
</BottomSheetModal>
|
|
900
|
-
</View>
|
|
901
|
-
);
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
function InnerComponent() {
|
|
905
|
-
const { modalRef } = useBottomSheetRef();
|
|
906
|
-
|
|
907
|
-
return (
|
|
908
|
-
<Button
|
|
909
|
-
title="Close from Inside"
|
|
910
|
-
onPress={() => modalRef?.current?.closeModal()}
|
|
911
|
-
/>
|
|
912
|
-
);
|
|
913
|
-
}
|
|
914
|
-
```
|
|
915
|
-
|
|
916
|
-
### Form with Validation Example
|
|
917
|
-
|
|
918
|
-
```tsx
|
|
919
|
-
import React, { useRef, useState } from 'react';
|
|
920
|
-
import { View, Text, TextInput, Button } from 'react-native';
|
|
921
|
-
import { BottomSheetModal } from '@shaquillehinds/react-native-bottom-sheet';
|
|
922
|
-
import type { BottomModalRefObject } from '@shaquillehinds/react-native-bottom-sheet';
|
|
923
|
-
|
|
924
|
-
export default function FormExample() {
|
|
925
|
-
const [showModal, setShowModal] = useState(false);
|
|
926
|
-
const [email, setEmail] = useState('');
|
|
927
|
-
const [password, setPassword] = useState('');
|
|
928
|
-
const emailRef = useRef<TextInput>(null);
|
|
929
|
-
const passwordRef = useRef<TextInput>(null);
|
|
930
|
-
const bottomSheetRef = useRef<BottomModalRefObject>(null);
|
|
931
|
-
|
|
932
|
-
const handleSubmit = () => {
|
|
933
|
-
if (email && password) {
|
|
934
|
-
console.log('Submitting:', { email, password });
|
|
935
|
-
bottomSheetRef.current?.closeModal({
|
|
936
|
-
onClose: () => {
|
|
937
|
-
// Clear form after animation
|
|
938
|
-
setEmail('');
|
|
939
|
-
setPassword('');
|
|
940
|
-
},
|
|
941
|
-
});
|
|
942
|
-
}
|
|
943
|
-
};
|
|
944
|
-
|
|
945
|
-
return (
|
|
946
|
-
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
|
947
|
-
<Button title="Login" onPress={() => setShowModal(true)} />
|
|
948
|
-
|
|
949
|
-
<BottomSheetModal
|
|
950
|
-
ref={bottomSheetRef}
|
|
951
|
-
showModal={showModal}
|
|
952
|
-
setShowModal={setShowModal}
|
|
953
|
-
snapPoints={[60]}
|
|
954
|
-
inputsForKeyboardToAvoid={[emailRef, passwordRef]}
|
|
955
|
-
contentContainerStyle={{ padding: 20 }}
|
|
956
|
-
>
|
|
957
|
-
<Text style={{ fontSize: 24, marginBottom: 20 }}>Login</Text>
|
|
958
|
-
|
|
959
|
-
<TextInput
|
|
960
|
-
ref={emailRef}
|
|
961
|
-
value={email}
|
|
962
|
-
onChangeText={setEmail}
|
|
963
|
-
placeholder="Email"
|
|
964
|
-
keyboardType="email-address"
|
|
965
|
-
autoCapitalize="none"
|
|
966
|
-
style={{
|
|
967
|
-
borderWidth: 1,
|
|
968
|
-
borderColor: '#ccc',
|
|
969
|
-
padding: 10,
|
|
970
|
-
marginBottom: 15,
|
|
971
|
-
}}
|
|
972
|
-
/>
|
|
973
|
-
|
|
974
|
-
<TextInput
|
|
975
|
-
ref={passwordRef}
|
|
976
|
-
value={password}
|
|
977
|
-
onChangeText={setPassword}
|
|
978
|
-
placeholder="Password"
|
|
979
|
-
secureTextEntry
|
|
980
|
-
style={{
|
|
981
|
-
borderWidth: 1,
|
|
982
|
-
borderColor: '#ccc',
|
|
983
|
-
padding: 10,
|
|
984
|
-
marginBottom: 20,
|
|
985
|
-
}}
|
|
986
|
-
/>
|
|
987
|
-
|
|
988
|
-
<Button title="Submit" onPress={handleSubmit} />
|
|
989
|
-
</BottomSheetModal>
|
|
990
|
-
</View>
|
|
991
|
-
);
|
|
992
|
-
}
|
|
993
|
-
```
|
|
994
|
-
|
|
995
|
-
### Custom Portal Overlay Example
|
|
729
|
+
The wrapper stays thin; the option array — which closes over `router` and the
|
|
730
|
+
setter — is built inside the content component, so it is never constructed while
|
|
731
|
+
the sheet is closed.
|
|
996
732
|
|
|
997
733
|
```tsx
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
} from '@shaquillehinds/react-native-bottom-sheet';
|
|
734
|
+
export default function ChatAppsModal(props: {
|
|
735
|
+
showModal: boolean;
|
|
736
|
+
setShowModal: React.Dispatch<React.SetStateAction<boolean>>;
|
|
737
|
+
}) {
|
|
738
|
+
const { colors, mode } = useTheme();
|
|
1004
739
|
|
|
1005
|
-
// App root with provider
|
|
1006
|
-
export default function App() {
|
|
1007
740
|
return (
|
|
1008
|
-
<
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
useBottomSheetPortalComponent({
|
|
1021
|
-
name: 'custom-overlay',
|
|
1022
|
-
Component: showOverlay ? (
|
|
1023
|
-
<TouchableOpacity
|
|
1024
|
-
style={styles.overlay}
|
|
1025
|
-
activeOpacity={1}
|
|
1026
|
-
onPress={() => setShowOverlay(false)}
|
|
1027
|
-
>
|
|
1028
|
-
<View style={styles.overlayContent}>
|
|
1029
|
-
<Text style={styles.overlayText}>{message}</Text>
|
|
1030
|
-
<Button title="Close" onPress={() => setShowOverlay(false)} />
|
|
1031
|
-
</View>
|
|
1032
|
-
</TouchableOpacity>
|
|
1033
|
-
) : null,
|
|
1034
|
-
disable: !showOverlay,
|
|
1035
|
-
});
|
|
1036
|
-
|
|
1037
|
-
const showCustomOverlay = (msg: string) => {
|
|
1038
|
-
setMessage(msg);
|
|
1039
|
-
setShowOverlay(true);
|
|
1040
|
-
};
|
|
1041
|
-
|
|
1042
|
-
return (
|
|
1043
|
-
<View style={styles.container}>
|
|
1044
|
-
<Button
|
|
1045
|
-
title="Show Portal Overlay"
|
|
1046
|
-
onPress={() => showCustomOverlay('This is rendered in the portal!')}
|
|
1047
|
-
/>
|
|
1048
|
-
</View>
|
|
1049
|
-
);
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
const styles = StyleSheet.create({
|
|
1053
|
-
container: {
|
|
1054
|
-
flex: 1,
|
|
1055
|
-
justifyContent: 'center',
|
|
1056
|
-
alignItems: 'center',
|
|
1057
|
-
},
|
|
1058
|
-
overlay: {
|
|
1059
|
-
...StyleSheet.absoluteFillObject,
|
|
1060
|
-
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
1061
|
-
justifyContent: 'center',
|
|
1062
|
-
alignItems: 'center',
|
|
1063
|
-
},
|
|
1064
|
-
overlayContent: {
|
|
1065
|
-
backgroundColor: 'white',
|
|
1066
|
-
padding: 30,
|
|
1067
|
-
borderRadius: 10,
|
|
1068
|
-
alignItems: 'center',
|
|
1069
|
-
},
|
|
1070
|
-
overlayText: {
|
|
1071
|
-
fontSize: 18,
|
|
1072
|
-
marginBottom: 20,
|
|
1073
|
-
},
|
|
1074
|
-
});
|
|
1075
|
-
```
|
|
1076
|
-
|
|
1077
|
-
### Multi-Level Portal Example
|
|
1078
|
-
|
|
1079
|
-
```tsx
|
|
1080
|
-
import React, { createContext, useState } from 'react';
|
|
1081
|
-
import { View, Button } from 'react-native';
|
|
1082
|
-
import {
|
|
1083
|
-
BottomSheetPortalProvider,
|
|
1084
|
-
BottomSheetModal,
|
|
1085
|
-
} from '@shaquillehinds/react-native-bottom-sheet';
|
|
1086
|
-
import type { PortalContextValue } from '@shaquillehinds/react-native-bottom-sheet';
|
|
1087
|
-
|
|
1088
|
-
// Create custom portal contexts for different levels
|
|
1089
|
-
const ScreenPortalContext = createContext<PortalContextValue | undefined>(
|
|
1090
|
-
undefined
|
|
1091
|
-
);
|
|
1092
|
-
const DialogPortalContext = createContext<PortalContextValue | undefined>(
|
|
1093
|
-
undefined
|
|
1094
|
-
);
|
|
1095
|
-
|
|
1096
|
-
export default function App() {
|
|
1097
|
-
return (
|
|
1098
|
-
// Global portal for app-wide modals
|
|
1099
|
-
<BottomSheetPortalProvider>
|
|
1100
|
-
<Navigation />
|
|
1101
|
-
</BottomSheetPortalProvider>
|
|
741
|
+
<SmoothBottomModal
|
|
742
|
+
showModal={props.showModal}
|
|
743
|
+
setShowModal={(value: boolean) => {
|
|
744
|
+
Keyboard.dismiss();
|
|
745
|
+
props.setShowModal(value);
|
|
746
|
+
}}
|
|
747
|
+
backgroundColor={colors.background[mode]}
|
|
748
|
+
showContentDelay={{ timeInMilliSecs: 250, type: 'mount' }}
|
|
749
|
+
snapPoints={[85]}
|
|
750
|
+
>
|
|
751
|
+
<ChatAppsModalContent {...props} mode={mode} colors={colors} />
|
|
752
|
+
</SmoothBottomModal>
|
|
1102
753
|
);
|
|
1103
754
|
}
|
|
1104
755
|
|
|
1105
|
-
function
|
|
1106
|
-
const
|
|
1107
|
-
|
|
756
|
+
function ChatAppsModalContent({ setShowModal, colors, mode }: ContentProps) {
|
|
757
|
+
const apps = [
|
|
758
|
+
{
|
|
759
|
+
id: 'flashcards',
|
|
760
|
+
title: 'Flashcards',
|
|
761
|
+
onPress: () => {
|
|
762
|
+
setShowModal(false);
|
|
763
|
+
router.push('/platform/practice/flashcards');
|
|
764
|
+
},
|
|
765
|
+
},
|
|
766
|
+
// ...
|
|
767
|
+
];
|
|
1108
768
|
|
|
1109
|
-
return
|
|
1110
|
-
// Screen-specific portal
|
|
1111
|
-
<BottomSheetPortalProvider CustomPortalContext={ScreenPortalContext}>
|
|
1112
|
-
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
|
1113
|
-
<Button
|
|
1114
|
-
title="Open Screen Modal"
|
|
1115
|
-
onPress={() => setShowScreenModal(true)}
|
|
1116
|
-
/>
|
|
1117
|
-
|
|
1118
|
-
{/* Modal using screen portal */}
|
|
1119
|
-
<BottomSheetModal
|
|
1120
|
-
CustomPortalContext={ScreenPortalContext}
|
|
1121
|
-
showModal={showScreenModal}
|
|
1122
|
-
setShowModal={setShowScreenModal}
|
|
1123
|
-
snapPoints={[50]}
|
|
1124
|
-
>
|
|
1125
|
-
<View style={{ padding: 20 }}>
|
|
1126
|
-
<Text>Screen-level modal</Text>
|
|
1127
|
-
<Button
|
|
1128
|
-
title="Open Dialog"
|
|
1129
|
-
onPress={() => setShowDialogModal(true)}
|
|
1130
|
-
/>
|
|
1131
|
-
|
|
1132
|
-
{/* Nested modal using dialog portal */}
|
|
1133
|
-
<BottomSheetPortalProvider
|
|
1134
|
-
CustomPortalContext={DialogPortalContext}
|
|
1135
|
-
>
|
|
1136
|
-
<BottomSheetModal
|
|
1137
|
-
CustomPortalContext={DialogPortalContext}
|
|
1138
|
-
showModal={showDialogModal}
|
|
1139
|
-
setShowModal={setShowDialogModal}
|
|
1140
|
-
snapPoints={[30]}
|
|
1141
|
-
>
|
|
1142
|
-
<View style={{ padding: 20 }}>
|
|
1143
|
-
<Text>Dialog-level modal</Text>
|
|
1144
|
-
</View>
|
|
1145
|
-
</BottomSheetModal>
|
|
1146
|
-
</BottomSheetPortalProvider>
|
|
1147
|
-
</View>
|
|
1148
|
-
</BottomSheetModal>
|
|
1149
|
-
</View>
|
|
1150
|
-
</BottomSheetPortalProvider>
|
|
1151
|
-
);
|
|
769
|
+
return <Layout padding={[2, 4]}>{/* render apps */}</Layout>;
|
|
1152
770
|
}
|
|
1153
771
|
```
|
|
1154
772
|
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
1. **Use `useImperativeHandle` pattern**: This package follows the render isolation pattern to prevent parent re-renders when animating
|
|
1158
|
-
|
|
1159
|
-
2. **Optimize heavy content**: Use `showContentDelay` with `type: 'mount'` for complex UIs:
|
|
1160
|
-
|
|
1161
|
-
```tsx
|
|
1162
|
-
<BottomSheetModal
|
|
1163
|
-
showContentDelay={{ type: 'mount', timeInMilliSecs: 100 }}
|
|
1164
|
-
contentContainerStyle={{ minHeight: 400 }}
|
|
1165
|
-
>
|
|
1166
|
-
<ComplexComponent />
|
|
1167
|
-
</BottomSheetModal>
|
|
1168
|
-
```
|
|
1169
|
-
|
|
1170
|
-
3. **Memoize callbacks**: Use `useCallback` for callbacks to prevent unnecessary re-renders:
|
|
1171
|
-
|
|
1172
|
-
```tsx
|
|
1173
|
-
const handleSnapPoint = useCallback((index: number) => {
|
|
1174
|
-
console.log('Snap point:', index);
|
|
1175
|
-
}, []);
|
|
1176
|
-
```
|
|
1177
|
-
|
|
1178
|
-
4. **Keep snap points stable**: Define snap points outside the component or use `useMemo`:
|
|
1179
|
-
```tsx
|
|
1180
|
-
const snapPoints = useMemo(() => [30, 60, 90], []);
|
|
1181
|
-
```
|
|
1182
|
-
|
|
1183
|
-
## Common Patterns
|
|
1184
|
-
|
|
1185
|
-
### Confirmation Dialog
|
|
773
|
+
### Global sheet driven from a store
|
|
1186
774
|
|
|
1187
775
|
```tsx
|
|
1188
|
-
function
|
|
1189
|
-
const
|
|
776
|
+
export default function UploadMaterialModal() {
|
|
777
|
+
const ref = useRef<BottomModalRef>(null);
|
|
778
|
+
const setUploadModalRef = useApp((state) => state.setUploadModalRef);
|
|
779
|
+
useEffect(() => {
|
|
780
|
+
setUploadModalRef(ref);
|
|
781
|
+
}, [ref.current]);
|
|
782
|
+
const { colors, mode } = useTheme();
|
|
1190
783
|
|
|
1191
784
|
return (
|
|
1192
|
-
<
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
785
|
+
<SmoothBottomModal
|
|
786
|
+
ref={ref}
|
|
787
|
+
disablePortal
|
|
788
|
+
dragArea="full"
|
|
789
|
+
snapPoints={[90]}
|
|
790
|
+
backgroundColor={colors.background[mode]}
|
|
1197
791
|
>
|
|
1198
|
-
<
|
|
1199
|
-
|
|
1200
|
-
<Button title="Cancel" onPress={() => setShow(false)} />
|
|
1201
|
-
<Button title="Confirm" onPress={handleConfirm} />
|
|
1202
|
-
</View>
|
|
1203
|
-
</BottomSheetModal>
|
|
792
|
+
<UploadMaterialModalContent />
|
|
793
|
+
</SmoothBottomModal>
|
|
1204
794
|
);
|
|
1205
795
|
}
|
|
1206
796
|
```
|
|
1207
797
|
|
|
1208
|
-
|
|
798
|
+
Mount it once near the app root. Anything can open it via the stored ref.
|
|
799
|
+
|
|
800
|
+
### Selection sheet with state owned by the screen
|
|
1209
801
|
|
|
1210
802
|
```tsx
|
|
1211
|
-
function
|
|
1212
|
-
const
|
|
803
|
+
function ChooseFlashcardsContent(props: ChooseFlashcardsProps) {
|
|
804
|
+
const t = useTranslation();
|
|
1213
805
|
|
|
1214
806
|
return (
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
<BottomSheetFlatlist
|
|
1221
|
-
data={options}
|
|
1222
|
-
renderItem={({ item }) => (
|
|
1223
|
-
<TouchableOpacity onPress={() => handleSelect(item)}>
|
|
1224
|
-
<Text style={{ padding: 15 }}>{item.label}</Text>
|
|
1225
|
-
</TouchableOpacity>
|
|
1226
|
-
)}
|
|
807
|
+
<>
|
|
808
|
+
<SelectFlashcardSetsList
|
|
809
|
+
selectedFlashcardSets={props.selectedFlashcards}
|
|
810
|
+
setSelectedFlashcardSets={props.setSelectedFlashcards}
|
|
811
|
+
searchBar
|
|
1227
812
|
/>
|
|
1228
|
-
|
|
813
|
+
<PrimaryButton
|
|
814
|
+
disabled={props.selectedFlashcards.length === 0}
|
|
815
|
+
onPress={() => {
|
|
816
|
+
props.onFlashcardSelect(props.selectedFlashcards);
|
|
817
|
+
props.setToggled(false);
|
|
818
|
+
}}
|
|
819
|
+
>
|
|
820
|
+
{t('confirm')}
|
|
821
|
+
</PrimaryButton>
|
|
822
|
+
</>
|
|
1229
823
|
);
|
|
1230
824
|
}
|
|
1231
825
|
```
|
|
1232
826
|
|
|
1233
|
-
|
|
827
|
+
Selection survives close and reopen because the screen holds it.
|
|
1234
828
|
|
|
1235
|
-
|
|
1236
|
-
function FilterPanel() {
|
|
1237
|
-
const [show, setShow] = useState(false);
|
|
1238
|
-
|
|
1239
|
-
return (
|
|
1240
|
-
<BottomSheet
|
|
1241
|
-
keepMounted={true}
|
|
1242
|
-
bottomOffset={100}
|
|
1243
|
-
snapPoints={[10, 70]}
|
|
1244
|
-
showModal={show}
|
|
1245
|
-
setShowModal={setShow}
|
|
1246
|
-
>
|
|
1247
|
-
<BottomSheetScrollView>
|
|
1248
|
-
<FilterOptions />
|
|
1249
|
-
</BottomSheetScrollView>
|
|
1250
|
-
</BottomSheet>
|
|
1251
|
-
);
|
|
1252
|
-
}
|
|
1253
|
-
```
|
|
1254
|
-
|
|
1255
|
-
### Toast Notification (Using Portal)
|
|
829
|
+
### Closing before navigating
|
|
1256
830
|
|
|
1257
831
|
```tsx
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
const [visible, setVisible] = useState(false);
|
|
1263
|
-
|
|
1264
|
-
useBottomSheetPortalComponent({
|
|
1265
|
-
name: 'toast',
|
|
1266
|
-
Component: visible ? (
|
|
1267
|
-
<Animated.View
|
|
1268
|
-
entering={SlideInDown}
|
|
1269
|
-
exiting={SlideOutUp}
|
|
1270
|
-
style={{
|
|
1271
|
-
position: 'absolute',
|
|
1272
|
-
top: 50,
|
|
1273
|
-
alignSelf: 'center',
|
|
1274
|
-
backgroundColor: '#333',
|
|
1275
|
-
padding: 15,
|
|
1276
|
-
borderRadius: 8,
|
|
1277
|
-
}}
|
|
1278
|
-
>
|
|
1279
|
-
<Text style={{ color: 'white' }}>{message}</Text>
|
|
1280
|
-
</Animated.View>
|
|
1281
|
-
) : null,
|
|
1282
|
-
disable: !visible,
|
|
1283
|
-
});
|
|
1284
|
-
|
|
1285
|
-
const show = (msg: string) => {
|
|
1286
|
-
setMessage(msg);
|
|
1287
|
-
setVisible(true);
|
|
1288
|
-
setTimeout(() => setVisible(false), 3000);
|
|
1289
|
-
};
|
|
1290
|
-
|
|
1291
|
-
return { show };
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
// Usage
|
|
1295
|
-
function MyComponent() {
|
|
1296
|
-
const toast = useToast();
|
|
1297
|
-
|
|
1298
|
-
return <Button title="Show Toast" onPress={() => toast.show('Hello!')} />;
|
|
1299
|
-
}
|
|
832
|
+
ref.current?.closeModal({
|
|
833
|
+
isNavigating: true,
|
|
834
|
+
onClose: () => navigation.navigate('NextScreen'),
|
|
835
|
+
});
|
|
1300
836
|
```
|
|
1301
837
|
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
```tsx
|
|
1305
|
-
import { useBottomSheetPortalComponent } from '@shaquillehinds/react-native-bottom-sheet';
|
|
1306
|
-
|
|
1307
|
-
function useLoadingOverlay() {
|
|
1308
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
1309
|
-
|
|
1310
|
-
useBottomSheetPortalComponent({
|
|
1311
|
-
name: 'loading-overlay',
|
|
1312
|
-
Component: isLoading ? (
|
|
1313
|
-
<View
|
|
1314
|
-
style={{
|
|
1315
|
-
...StyleSheet.absoluteFillObject,
|
|
1316
|
-
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
1317
|
-
justifyContent: 'center',
|
|
1318
|
-
alignItems: 'center',
|
|
1319
|
-
}}
|
|
1320
|
-
>
|
|
1321
|
-
<ActivityIndicator size="large" color="#fff" />
|
|
1322
|
-
</View>
|
|
1323
|
-
) : null,
|
|
1324
|
-
disable: !isLoading,
|
|
1325
|
-
});
|
|
1326
|
-
|
|
1327
|
-
return { setIsLoading };
|
|
1328
|
-
}
|
|
838
|
+
Or skip the animation entirely with `skipAnimation: true`.
|
|
1329
839
|
|
|
1330
|
-
|
|
1331
|
-
function MyComponent() {
|
|
1332
|
-
const { setIsLoading } = useLoadingOverlay();
|
|
1333
|
-
|
|
1334
|
-
const handleSubmit = async () => {
|
|
1335
|
-
setIsLoading(true);
|
|
1336
|
-
await api.submit();
|
|
1337
|
-
setIsLoading(false);
|
|
1338
|
-
};
|
|
1339
|
-
|
|
1340
|
-
return <Button title="Submit" onPress={handleSubmit} />;
|
|
1341
|
-
}
|
|
1342
|
-
```
|
|
840
|
+
---
|
|
1343
841
|
|
|
1344
842
|
## Troubleshooting
|
|
1345
843
|
|
|
1346
|
-
|
|
844
|
+
**Sheet does not appear.** Check the Reanimated Babel plugin is installed and last
|
|
845
|
+
in the list, and that `BottomSheetPortalProvider` is mounted at the app root.
|
|
1347
846
|
|
|
1348
|
-
|
|
847
|
+
**Sheet renders behind navigation.** The provider is too low in the tree. It must
|
|
848
|
+
wrap `NavigationContainer`, not sit inside it.
|
|
1349
849
|
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
presets: ['module:metro-react-native-babel-preset'],
|
|
1354
|
-
plugins: ['react-native-reanimated/plugin'],
|
|
1355
|
-
};
|
|
1356
|
-
```
|
|
1357
|
-
|
|
1358
|
-
**Also verify `BottomSheetPortalProvider` is set up at your app root:**
|
|
850
|
+
**Sheet opens collapsed or very short.** Using `showContentDelay: { type: 'mount' }`
|
|
851
|
+
without `snapPoints` or a `minHeight` on `contentContainerStyle`. There is nothing
|
|
852
|
+
to measure at open time.
|
|
1359
853
|
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
import { BottomSheetPortalProvider } from '@shaquillehinds/react-native-bottom-sheet';
|
|
854
|
+
**Scrolling and dragging fight each other.** Swap the bare `FlatList` / `ScrollView`
|
|
855
|
+
for `BottomSheetFlatlist` / `BottomSheetScrollView`, and drop `dragArea="full"`.
|
|
1363
856
|
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
<BottomSheetPortalProvider>
|
|
1367
|
-
<YourNavigator />
|
|
1368
|
-
</BottomSheetPortalProvider>
|
|
1369
|
-
);
|
|
1370
|
-
}
|
|
1371
|
-
```
|
|
857
|
+
**Drag down still dismisses despite `keepMounted`.** `keepMounted` has no effect
|
|
858
|
+
without `snapPoints`.
|
|
1372
859
|
|
|
1373
|
-
|
|
860
|
+
**Sheet cannot be dragged.** Either `dragArea="none"`, or `hideBumper` combined
|
|
861
|
+
with the default `dragArea="bumper"`, or the keyboard is open — dragging is
|
|
862
|
+
disabled while it is, unless `allowDragWhileKeyboardVisible` is set.
|
|
1374
863
|
|
|
1375
|
-
|
|
864
|
+
**Content flickers or unmounts early.** Increase `unMountBufferTimeMS` on the
|
|
865
|
+
provider.
|
|
1376
866
|
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
<BottomSheetPortalProvider>
|
|
1380
|
-
<NavigationContainer>
|
|
1381
|
-
<Stack.Navigator>
|
|
1382
|
-
{/* screens */}
|
|
1383
|
-
</Stack.Navigator>
|
|
1384
|
-
</NavigationContainer>
|
|
1385
|
-
</BottomSheetPortalProvider>
|
|
867
|
+
**Sheet is slow to open.** Move state into a `*Content` component and add
|
|
868
|
+
`showContentDelay={{ type: 'mount', timeInMilliSecs: 250 }}`.
|
|
1386
869
|
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
<BottomSheetPortalProvider>
|
|
1390
|
-
<Stack.Navigator>
|
|
1391
|
-
{/* screens */}
|
|
1392
|
-
</Stack.Navigator>
|
|
1393
|
-
</BottomSheetPortalProvider>
|
|
1394
|
-
</NavigationContainer>
|
|
1395
|
-
```
|
|
870
|
+
**Screen feels sluggish even with sheets closed.** Hooks are sitting in a sheet
|
|
871
|
+
wrapper's function body. Move them into the content component.
|
|
1396
872
|
|
|
1397
|
-
|
|
873
|
+
---
|
|
1398
874
|
|
|
1399
|
-
|
|
875
|
+
## AI agent rules
|
|
1400
876
|
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
877
|
+
The package ships a rules file written for AI coding agents (Claude Code, Cursor,
|
|
878
|
+
Codex, Copilot, etc.) at `rules/AGENT_RULES.md`. It tells an agent to keep sheet
|
|
879
|
+
state in a separate content component so nothing loads until the sheet is opened,
|
|
880
|
+
to reach for `BottomSheetFlatlist` / `BottomSheetScrollView` instead of raw
|
|
881
|
+
scrollables, and it lists every prop and ref method so it cannot invent APIs or
|
|
882
|
+
fall back to patterns from a different bottom sheet library. Point your agent at
|
|
883
|
+
it with any of the following.
|
|
1406
884
|
|
|
1407
|
-
|
|
885
|
+
**Copy it into your project (recommended)**
|
|
1408
886
|
|
|
1409
|
-
|
|
887
|
+
```sh
|
|
888
|
+
npx rnbs-rules # writes ./AGENTS.md
|
|
889
|
+
npx rnbs-rules cursor # writes ./.cursor/rules/react-native-bottom-sheet.mdc (alwaysApply)
|
|
890
|
+
npx rnbs-rules claude # writes ./.claude/rules/react-native-bottom-sheet.md
|
|
891
|
+
npx rnbs-rules codex # writes ./.codex/rules/react-native-bottom-sheet.md
|
|
892
|
+
npx rnbs-rules copilot # writes ./.github/instructions/react-native-bottom-sheet.instructions.md
|
|
893
|
+
npx rnbs-rules windsurf # writes ./.windsurf/rules/react-native-bottom-sheet.md
|
|
894
|
+
npx rnbs-rules docs/ai/bottom-sheet.md # custom path
|
|
895
|
+
```
|
|
1410
896
|
|
|
1411
|
-
|
|
897
|
+
Add `--force` to overwrite an existing file. `--print` writes the rules to stdout
|
|
898
|
+
instead of to disk. Re-run after upgrading the package to pick up rule changes.
|
|
1412
899
|
|
|
1413
|
-
|
|
900
|
+
**Reference it without copying (Claude Code)**
|
|
1414
901
|
|
|
1415
|
-
|
|
902
|
+
`CLAUDE.md` supports `@path` imports, so a single line keeps the rules in sync with
|
|
903
|
+
the installed version:
|
|
1416
904
|
|
|
1417
|
-
|
|
905
|
+
```md
|
|
906
|
+
# CLAUDE.md
|
|
1418
907
|
|
|
1419
|
-
|
|
1420
|
-
bottomSheetRef.current?.closeModal({
|
|
1421
|
-
isNavigating: true,
|
|
1422
|
-
onClose: () => navigation.navigate('NextScreen'),
|
|
1423
|
-
});
|
|
908
|
+
@node_modules/@shaquillehinds/react-native-bottom-sheet/rules/AGENT_RULES.md
|
|
1424
909
|
```
|
|
1425
910
|
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
- `react-native-reanimated` ^3.0.0
|
|
1429
|
-
- `react-native-gesture-handler` ^2.0.0
|
|
1430
|
-
- `@shaquillehinds/react-native-essentials` ^1.8.0
|
|
911
|
+
**Reference it from a generic `AGENTS.md`**
|
|
1431
912
|
|
|
1432
|
-
|
|
913
|
+
```md
|
|
914
|
+
Before writing any bottom sheet, read and follow
|
|
915
|
+
node_modules/@shaquillehinds/react-native-bottom-sheet/rules/AGENT_RULES.md.
|
|
916
|
+
```
|
|
1433
917
|
|
|
1434
|
-
|
|
918
|
+
---
|
|
1435
919
|
|
|
1436
920
|
## License
|
|
1437
921
|
|
|
1438
922
|
MIT © [Shaquille Hinds](https://github.com/shaquillehinds)
|
|
1439
|
-
|
|
1440
|
-
## Author
|
|
1441
|
-
|
|
1442
|
-
**Shaquille Hinds**
|
|
1443
|
-
|
|
1444
|
-
- GitHub: [@shaquillehinds](https://github.com/shaquillehinds)
|
|
1445
|
-
- Email: shaqdulove@gmail.com
|
|
1446
|
-
|
|
1447
|
-
## Related Packages
|
|
1448
|
-
|
|
1449
|
-
- [@shaquillehinds/react-native-essentials](https://www.npmjs.com/package/@shaquillehinds/react-native-essentials) - Essential utilities and components for React Native
|
|
1450
|
-
|
|
1451
|
-
## Changelog
|
|
1452
|
-
|
|
1453
|
-
See [Releases](https://github.com/shaquillehinds/react-native-bottom-sheet/releases) for version history.
|
|
1454
|
-
|
|
1455
|
-
## Support
|
|
1456
|
-
|
|
1457
|
-
If you encounter any issues or have questions:
|
|
1458
|
-
|
|
1459
|
-
- Open an issue on [GitHub](https://github.com/shaquillehinds/react-native-bottom-sheet/issues)
|
|
1460
|
-
- Check existing issues for solutions
|
|
1461
|
-
- Review the examples in this README
|
|
1462
|
-
|
|
1463
|
-
## Acknowledgments
|
|
1464
|
-
|
|
1465
|
-
Built with React Native Reanimated and Gesture Handler for optimal performance and smooth interactions.
|