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