@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
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const PACKAGE_NAME = '@shaquillehinds/react-native-bottom-sheet';
|
|
8
|
+
const SLUG = 'react-native-bottom-sheet';
|
|
9
|
+
const SOURCE = path.join(__dirname, '..', 'rules', 'AGENT_RULES.md');
|
|
10
|
+
|
|
11
|
+
const TARGETS = {
|
|
12
|
+
agents: 'AGENTS.md',
|
|
13
|
+
cursor: path.join('.cursor', 'rules', SLUG + '.mdc'),
|
|
14
|
+
claude: path.join('.claude', 'rules', SLUG + '.md'),
|
|
15
|
+
codex: path.join('.codex', 'rules', SLUG + '.md'),
|
|
16
|
+
copilot: path.join('.github', 'instructions', SLUG + '.instructions.md'),
|
|
17
|
+
windsurf: path.join('.windsurf', 'rules', SLUG + '.md'),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const HELP = `
|
|
21
|
+
rnbs-rules — install the ${PACKAGE_NAME} agent rules
|
|
22
|
+
|
|
23
|
+
Usage
|
|
24
|
+
npx rnbs-rules [target|path] [--force] [--print]
|
|
25
|
+
|
|
26
|
+
Targets
|
|
27
|
+
(none) ./AGENTS.md
|
|
28
|
+
agents ./AGENTS.md
|
|
29
|
+
cursor ./.cursor/rules/${SLUG}.mdc (alwaysApply)
|
|
30
|
+
claude ./.claude/rules/${SLUG}.md
|
|
31
|
+
codex ./.codex/rules/${SLUG}.md
|
|
32
|
+
copilot ./.github/instructions/${SLUG}.instructions.md
|
|
33
|
+
windsurf ./.windsurf/rules/${SLUG}.md
|
|
34
|
+
<path> any path ending in .md or .mdc
|
|
35
|
+
|
|
36
|
+
Options
|
|
37
|
+
--force overwrite an existing file
|
|
38
|
+
--print write nothing, print the rules to stdout
|
|
39
|
+
--help show this message
|
|
40
|
+
|
|
41
|
+
Examples
|
|
42
|
+
npx rnbs-rules
|
|
43
|
+
npx rnbs-rules cursor
|
|
44
|
+
npx rnbs-rules docs/ai/bottom-sheet.md --force
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
function frontmatterFor(target) {
|
|
48
|
+
if (target === 'cursor') {
|
|
49
|
+
return [
|
|
50
|
+
'---',
|
|
51
|
+
`description: How to use ${PACKAGE_NAME} correctly`,
|
|
52
|
+
'globs:',
|
|
53
|
+
'alwaysApply: true',
|
|
54
|
+
'---',
|
|
55
|
+
'',
|
|
56
|
+
'',
|
|
57
|
+
].join('\n');
|
|
58
|
+
}
|
|
59
|
+
if (target === 'copilot') {
|
|
60
|
+
return ['---', "applyTo: '**/*.tsx,**/*.ts'", '---', '', ''].join('\n');
|
|
61
|
+
}
|
|
62
|
+
if (target === 'windsurf') {
|
|
63
|
+
return ['---', 'trigger: always_on', '---', '', ''].join('\n');
|
|
64
|
+
}
|
|
65
|
+
return '';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resolveTarget(arg) {
|
|
69
|
+
if (!arg) return { target: 'agents', relPath: TARGETS.agents };
|
|
70
|
+
if (Object.prototype.hasOwnProperty.call(TARGETS, arg)) {
|
|
71
|
+
return { target: arg, relPath: TARGETS[arg] };
|
|
72
|
+
}
|
|
73
|
+
if (/\.mdx?$|\.mdc$/.test(arg)) {
|
|
74
|
+
return { target: 'custom', relPath: arg };
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function main() {
|
|
80
|
+
const argv = process.argv.slice(2);
|
|
81
|
+
const force = argv.includes('--force') || argv.includes('-f');
|
|
82
|
+
const print = argv.includes('--print');
|
|
83
|
+
const help = argv.includes('--help') || argv.includes('-h');
|
|
84
|
+
const positional = argv.filter((a) => !a.startsWith('-'))[0];
|
|
85
|
+
|
|
86
|
+
if (help) {
|
|
87
|
+
process.stdout.write(HELP);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let rules;
|
|
92
|
+
try {
|
|
93
|
+
rules = fs.readFileSync(SOURCE, 'utf8');
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.error(
|
|
96
|
+
`rnbs-rules: could not read the rules file at ${SOURCE}\n` +
|
|
97
|
+
`Is ${PACKAGE_NAME} installed?`
|
|
98
|
+
);
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (print) {
|
|
104
|
+
process.stdout.write(rules);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const resolved = resolveTarget(positional);
|
|
109
|
+
if (!resolved) {
|
|
110
|
+
console.error(
|
|
111
|
+
`rnbs-rules: unknown target "${positional}".\n` +
|
|
112
|
+
`Expected one of: ${Object.keys(TARGETS).join(', ')} — or a path ending in .md / .mdc.\n` +
|
|
113
|
+
`Run "npx rnbs-rules --help" for usage.`
|
|
114
|
+
);
|
|
115
|
+
process.exitCode = 1;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const { target, relPath } = resolved;
|
|
120
|
+
const outPath = path.resolve(process.cwd(), relPath);
|
|
121
|
+
|
|
122
|
+
if (fs.existsSync(outPath) && !force) {
|
|
123
|
+
console.error(
|
|
124
|
+
`rnbs-rules: ${relPath} already exists. Re-run with --force to overwrite.`
|
|
125
|
+
);
|
|
126
|
+
process.exitCode = 1;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
131
|
+
fs.writeFileSync(outPath, frontmatterFor(target) + rules, 'utf8');
|
|
132
|
+
|
|
133
|
+
console.log($lf(139), `rnbs-rules: wrote ${relPath}`);
|
|
134
|
+
if (target === 'agents') {
|
|
135
|
+
console.log(
|
|
136
|
+
'Tip: if you already have an AGENTS.md, use a custom path instead and ' +
|
|
137
|
+
'link to it, e.g. npx rnbs-rules docs/ai/bottom-sheet.md'
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
main();
|
|
143
|
+
function $lf(n) {
|
|
144
|
+
return '$lf|bin/install-rules.js:' + n + ' >';
|
|
145
|
+
// Automatically injected by Log Location Injector vscode extension
|
|
146
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shaquillehinds/react-native-bottom-sheet",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "A simple bottom sheet for react native that just works.",
|
|
5
5
|
"source": "./src/index.tsx",
|
|
6
6
|
"main": "./lib/commonjs/index.js",
|
|
@@ -20,12 +20,17 @@
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
},
|
|
23
|
+
"bin": {
|
|
24
|
+
"rnbs-rules": "./bin/install-rules.js"
|
|
25
|
+
},
|
|
23
26
|
"files": [
|
|
24
27
|
"lib",
|
|
25
28
|
"src",
|
|
26
29
|
"android",
|
|
27
30
|
"ios",
|
|
28
31
|
"cpp",
|
|
32
|
+
"rules",
|
|
33
|
+
"bin",
|
|
29
34
|
"*.podspec",
|
|
30
35
|
"react-native.config.js",
|
|
31
36
|
"!ios/build",
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
# Agent Rules — `@shaquillehinds/react-native-bottom-sheet`
|
|
2
|
+
|
|
3
|
+
Rules for AI coding agents writing or modifying code that uses this package.
|
|
4
|
+
Read this before writing any bottom sheet. Follow it over general knowledge of
|
|
5
|
+
other bottom sheet libraries — the API here is not `@gorhom/bottom-sheet` and
|
|
6
|
+
patterns from that library will not work.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 0. Non-negotiables
|
|
11
|
+
|
|
12
|
+
1. **Always split the sheet into a wrapper component and a `*Content` component.**
|
|
13
|
+
State, hooks, queries and derived data live in `*Content`, never in the wrapper.
|
|
14
|
+
See Rule 1 — this is the single most important rule in this file.
|
|
15
|
+
2. `BottomSheetPortalProvider` must wrap the app root, above the navigation container.
|
|
16
|
+
3. Scrollable content inside a sheet uses `BottomSheetFlatlist` or
|
|
17
|
+
`BottomSheetScrollView`. Never a bare `FlatList`, `ScrollView` or `FlashList`.
|
|
18
|
+
4. Do not add a `<Modal>`, `KeyboardAvoidingView`, `SafeAreaView` or
|
|
19
|
+
`TouchableWithoutFeedback` backdrop around the sheet. All of that is handled.
|
|
20
|
+
5. Never guess prop names. If a prop is not in this file, it does not exist.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 1. Render isolation: keep state in the content component
|
|
25
|
+
|
|
26
|
+
The sheet's children are only mounted while the sheet is open. Anything written
|
|
27
|
+
_inside the child component_ therefore costs nothing until the user opens it.
|
|
28
|
+
Anything written _in the wrapper's function body_ runs on every parent render,
|
|
29
|
+
open or closed.
|
|
30
|
+
|
|
31
|
+
So the wrapper is a thin shell that takes props and renders `<XContent />`.
|
|
32
|
+
All hooks — `useState`, `useEffect`, data fetching, store subscriptions,
|
|
33
|
+
context reads, expensive `useMemo`, list construction — go in `XContent`.
|
|
34
|
+
|
|
35
|
+
### Correct
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
export default function ChooseFlashcardsModal(props: ChooseFlashcardsProps) {
|
|
39
|
+
const { colors, mode } = useTheme();
|
|
40
|
+
return (
|
|
41
|
+
<SmoothBottomModal
|
|
42
|
+
showModal={props.toggled}
|
|
43
|
+
setShowModal={props.setToggled}
|
|
44
|
+
backgroundColor={colors.background[mode]}
|
|
45
|
+
showContentDelay={{ timeInMilliSecs: 250, type: 'mount' }}
|
|
46
|
+
snapPoints={[90]}
|
|
47
|
+
>
|
|
48
|
+
<ChooseFlashcardsContent {...props} />
|
|
49
|
+
</SmoothBottomModal>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Nothing in here runs until the sheet is open.
|
|
54
|
+
function ChooseFlashcardsContent(props: ChooseFlashcardsProps) {
|
|
55
|
+
const { relativeY, orientation } = useDeviceOrientation();
|
|
56
|
+
const t = useTranslation();
|
|
57
|
+
const { data } = useFlashcardSets(props.studySetId);
|
|
58
|
+
return <>{/* ... */}</>;
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Wrong
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
export default function ChooseFlashcardsModal(props: ChooseFlashcardsProps) {
|
|
66
|
+
// ❌ These run on every render of the parent screen, even with the sheet closed.
|
|
67
|
+
const { data } = useFlashcardSets(props.studySetId);
|
|
68
|
+
const [selected, setSelected] = useState<FlashcardSet[]>([]);
|
|
69
|
+
const rows = useMemo(() => buildRows(data), [data]);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<SmoothBottomModal
|
|
73
|
+
showModal={props.toggled}
|
|
74
|
+
setShowModal={props.setToggled}
|
|
75
|
+
>
|
|
76
|
+
{/* ❌ Inline JSX with logic — same problem, plus it re-renders with the screen. */}
|
|
77
|
+
{rows.map((r) => (
|
|
78
|
+
<Row key={r.id} {...r} />
|
|
79
|
+
))}
|
|
80
|
+
</SmoothBottomModal>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Why this works
|
|
86
|
+
|
|
87
|
+
`BottomSheetModal` passes its subtree through a `ComponentMounter` keyed on
|
|
88
|
+
`showModal`. While closed, the subtree is unmounted, so no hook inside a child
|
|
89
|
+
component has run yet. Creating the element `<XContent />` is nearly free; it is
|
|
90
|
+
_calling_ the component that is expensive, and that only happens on mount.
|
|
91
|
+
|
|
92
|
+
Hooks in the wrapper body are in the parent's render, outside the mounter, so
|
|
93
|
+
they always run. That is the whole distinction.
|
|
94
|
+
|
|
95
|
+
### Corollaries
|
|
96
|
+
|
|
97
|
+
- Do **not** hoist state out of `*Content` into the wrapper "so it survives close".
|
|
98
|
+
If state must survive, lift it to the screen that owns the sheet and pass it
|
|
99
|
+
down as props (as `ChooseFlashcards` does with `selectedFlashcards`).
|
|
100
|
+
- Do not read a store or context in the wrapper unless the _sheet's own props_
|
|
101
|
+
need it. Theme colours for `backgroundColor` are the normal exception, since
|
|
102
|
+
the sheet chrome needs them.
|
|
103
|
+
- The pattern applies to `BottomSheet` (inline) exactly as it does to `BottomSheetModal`.
|
|
104
|
+
|
|
105
|
+
### Pair it with `showContentDelay`
|
|
106
|
+
|
|
107
|
+
For anything non-trivial, add `showContentDelay={{ type: 'mount', timeInMilliSecs: 250 }}`.
|
|
108
|
+
Content then mounts _after_ the open animation, so a heavy first render never
|
|
109
|
+
competes with the animation.
|
|
110
|
+
|
|
111
|
+
`type: 'mount'` means the sheet has no content to measure when it opens, so it
|
|
112
|
+
needs to be told how tall to be. Either set `snapPoints` (preferred) or give
|
|
113
|
+
`contentContainerStyle` a `minHeight`. Without one of those, the sheet opens
|
|
114
|
+
collapsed.
|
|
115
|
+
|
|
116
|
+
Use `type: 'opacity'` (the default when `type` is omitted) only when content is
|
|
117
|
+
cheap and you just want a fade — it still renders immediately.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 2. Choosing a component
|
|
122
|
+
|
|
123
|
+
| Need | Use |
|
|
124
|
+
| -------------------------------------------------------------------- | ----------------------- |
|
|
125
|
+
| Standard sheet with backdrop | `BottomSheetModal` |
|
|
126
|
+
| Persistent / inline sheet, no backdrop, background stays interactive | `BottomSheet` |
|
|
127
|
+
| Scrollable list inside a sheet | `BottomSheetFlatlist` |
|
|
128
|
+
| Scrollable view inside a sheet | `BottomSheetScrollView` |
|
|
129
|
+
|
|
130
|
+
`BottomSheet` renders with `enableBackgroundContentPress`, so there is no backdrop
|
|
131
|
+
and taps pass through to whatever is behind it. Use it for filter panels, mini
|
|
132
|
+
players, persistent drawers. Use `BottomSheetModal` for everything else.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 3. Controlling the sheet
|
|
137
|
+
|
|
138
|
+
There are two mutually exclusive control modes. Pick one; do not mix them.
|
|
139
|
+
|
|
140
|
+
### 3a. State-controlled (default — use this)
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
const [showModal, setShowModal] = useState(false);
|
|
144
|
+
|
|
145
|
+
<BottomSheetModal
|
|
146
|
+
showModal={showModal}
|
|
147
|
+
setShowModal={setShowModal}
|
|
148
|
+
snapPoints={[85]}
|
|
149
|
+
>
|
|
150
|
+
<MyContent onDone={() => setShowModal(false)} />
|
|
151
|
+
</BottomSheetModal>;
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Close from inside the sheet with `setShowModal(false)` passed down, or with
|
|
155
|
+
`useBottomSheetRef()`.
|
|
156
|
+
|
|
157
|
+
### 3b. Ref-controlled (no `showModal` / `setShowModal` at all)
|
|
158
|
+
|
|
159
|
+
For a sheet mounted once, high in the tree, opened from anywhere:
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
export default function UploadMaterialModal() {
|
|
163
|
+
const ref = useRef<BottomModalRef>(null);
|
|
164
|
+
const setUploadModalRef = useApp((state) => state.setUploadModalRef);
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
setUploadModalRef(ref);
|
|
167
|
+
}, [ref.current]);
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<SmoothBottomModal
|
|
171
|
+
ref={ref}
|
|
172
|
+
disablePortal
|
|
173
|
+
dragArea="full"
|
|
174
|
+
snapPoints={[90]}
|
|
175
|
+
>
|
|
176
|
+
<UploadMaterialModalContent />
|
|
177
|
+
</SmoothBottomModal>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// elsewhere
|
|
182
|
+
uploadModalRef.current?.openModal();
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Omit `showModal` and `setShowModal` entirely. The ref drives the mounter directly.
|
|
186
|
+
Use `disablePortal` when the sheet is already mounted at root level — there is
|
|
187
|
+
nothing for the portal to lift it above.
|
|
188
|
+
|
|
189
|
+
### Ref typing
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
const ref = useRef<BottomModalRef>(null); // ✅
|
|
193
|
+
const ref = useRef<BottomModalRefObject>(null); // ❌ that alias is the ref *prop* type
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`BottomModalRefObject` is `React.Ref<BottomModalRef>` — it types the `ref` prop on
|
|
197
|
+
the component, not the object `useRef` returns.
|
|
198
|
+
|
|
199
|
+
### Ref API
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
202
|
+
ref.current?.openModal({ onOpen });
|
|
203
|
+
ref.current?.closeModal({
|
|
204
|
+
skipAnimation,
|
|
205
|
+
isNavigating,
|
|
206
|
+
duration,
|
|
207
|
+
easing,
|
|
208
|
+
onClose,
|
|
209
|
+
});
|
|
210
|
+
ref.current?.closeWithoutAnimation();
|
|
211
|
+
ref.current?.snapToIndex(1);
|
|
212
|
+
ref.current?.snapToPercentage(75); // or '75%'
|
|
213
|
+
ref.current?.getModalState(); // ModalState.CLOSED | OPENING | OPEN | CLOSING
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Behaviour worth knowing before you call these:
|
|
217
|
+
|
|
218
|
+
- On a state-controlled sheet, `openModal()` with no `onOpen` is just
|
|
219
|
+
`setShowModal(true)`. Passing `onOpen` routes through the mounter instead.
|
|
220
|
+
- On a state-controlled sheet, `closeModal()` with no `duration` and no `onClose`
|
|
221
|
+
is just `setShowModal(false)`.
|
|
222
|
+
- `closeModal({ isNavigating: true })` shortens the animation to 100ms. Use it
|
|
223
|
+
before `navigation.navigate` / `router.push` on Android.
|
|
224
|
+
- `closeModal({ skipAnimation: true })` unmounts immediately.
|
|
225
|
+
|
|
226
|
+
### Closing from inside the subtree
|
|
227
|
+
|
|
228
|
+
```tsx
|
|
229
|
+
import { useBottomSheetRef } from '@shaquillehinds/react-native-bottom-sheet';
|
|
230
|
+
|
|
231
|
+
function Footer() {
|
|
232
|
+
const { modalRef } = useBottomSheetRef();
|
|
233
|
+
return (
|
|
234
|
+
<Button title="Close" onPress={() => modalRef?.current?.closeModal()} />
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Only works inside the sheet's own subtree. `modalRef` is optional — always
|
|
240
|
+
optional-chain it.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## 4. Snap points
|
|
245
|
+
|
|
246
|
+
- `snapPoints` are percentages of screen height: `[25, 50, 75]`, `['50%']`, `[85]`.
|
|
247
|
+
- **Omitting `snapPoints` is a real mode, not an oversight.** The sheet measures
|
|
248
|
+
its content and opens to that height, capped at ~110% screen height. Use it for
|
|
249
|
+
short, content-sized sheets.
|
|
250
|
+
- With `snapPoints`, the sheet opens at index `0`, so order low → high the way you
|
|
251
|
+
want it to appear. `snapToIndex` is indexed into the array as written.
|
|
252
|
+
- `onSnapPointReach(index)` only fires when `snapPoints` is set.
|
|
253
|
+
- Keep the array referentially stable — module constant or `useMemo`.
|
|
254
|
+
- On orientation change the sheet recomputes snap points and re-snaps to index 0.
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## 5. `keepMounted` and `bottomOffset`
|
|
259
|
+
|
|
260
|
+
`keepMounted` changes what a downward drag does: instead of dismissing, the sheet
|
|
261
|
+
snaps to its lowest snap point.
|
|
262
|
+
|
|
263
|
+
**`keepMounted` requires `snapPoints`.** Without them it is ignored entirely.
|
|
264
|
+
|
|
265
|
+
Pair with `bottomOffset` to leave a peek visible:
|
|
266
|
+
|
|
267
|
+
```tsx
|
|
268
|
+
<BottomSheet keepMounted snapPoints={[10, 70]} bottomOffset={100} ... />
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
`keepMounted` does not stop `showModal={false}` from unmounting the sheet. It only
|
|
272
|
+
governs the drag gesture.
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## 6. Scrolling
|
|
277
|
+
|
|
278
|
+
```tsx
|
|
279
|
+
<BottomSheetModal snapPoints={[50, 90]} showModal={show} setShowModal={setShow}>
|
|
280
|
+
<BottomSheetFlatlist data={items} renderItem={renderItem} keyExtractor={k} />
|
|
281
|
+
</BottomSheetModal>
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
- These components hand off gesture state to the sheet so the sheet drags when
|
|
285
|
+
the list is at its scroll boundary and scrolls otherwise. A bare `FlatList`
|
|
286
|
+
breaks that and the sheet will fight the list.
|
|
287
|
+
- They accept the full underlying props (`FlatListPropsWithLayout` /
|
|
288
|
+
`AnimatedScrollViewProps`), minus a reworked `onScroll` that also accepts a
|
|
289
|
+
Reanimated scroll event.
|
|
290
|
+
- Custom ref: use `refFlatlist` / `refScrollView`, not `ref`.
|
|
291
|
+
- `inverted` lists are supported and reported to the sheet.
|
|
292
|
+
- Outside a sheet these degrade gracefully to plain animated list/scrollview.
|
|
293
|
+
- Do not set `bounces` — it is forced to `false` inside a sheet.
|
|
294
|
+
- Avoid `dragArea="full"` with a scrollable child; the two gestures compete.
|
|
295
|
+
Leave `dragArea` at its default `"bumper"`.
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## 7. Keyboard
|
|
300
|
+
|
|
301
|
+
- `avoidKeyboard` — pads content whenever the keyboard opens.
|
|
302
|
+
- `inputsForKeyboardToAvoid={[ref1, ref2]}` — only avoid for those specific inputs.
|
|
303
|
+
Prefer this when a sheet has inputs that should not shift the layout.
|
|
304
|
+
- Dragging is disabled while the keyboard is visible. Override with
|
|
305
|
+
`allowDragWhileKeyboardVisible` only if you have tested it.
|
|
306
|
+
- If opening the sheet from a focused input, dismiss the keyboard in the setter:
|
|
307
|
+
|
|
308
|
+
```tsx
|
|
309
|
+
setShowModal={(value: boolean) => {
|
|
310
|
+
Keyboard.dismiss();
|
|
311
|
+
props.setShowModal(value);
|
|
312
|
+
}}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Do not wrap sheet content in `KeyboardAvoidingView`.
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## 8. Portal
|
|
320
|
+
|
|
321
|
+
- `BottomSheetPortalProvider` goes above `NavigationContainer`, not inside it.
|
|
322
|
+
- Sheets use the portal automatically. Do not add `useBottomSheetPortalComponent`
|
|
323
|
+
around a sheet — it already does this internally.
|
|
324
|
+
- `disablePortal` — render in place. Correct when the sheet is already at root
|
|
325
|
+
level, or when you deliberately want it scoped to a subtree.
|
|
326
|
+
- `CustomPortalContext` — scoped portals. Both the provider and the sheet must be
|
|
327
|
+
given the same context object.
|
|
328
|
+
- `useBottomSheetPortal` / `useBottomSheetPortalComponent` are for non-sheet
|
|
329
|
+
overlays (toasts, loaders). Do not reach for them to solve z-index problems with
|
|
330
|
+
a sheet; fix the provider placement instead.
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## 9. Prop reference
|
|
335
|
+
|
|
336
|
+
Common to `BottomSheet` and `BottomSheetModal`:
|
|
337
|
+
|
|
338
|
+
`showModal` · `setShowModal` · `snapPoints` · `dragArea` (`'full' | 'bumper' | 'none'`,
|
|
339
|
+
default `'bumper'`) · `keepMounted` · `hideBumper` · `avoidKeyboard` ·
|
|
340
|
+
`allowDragWhileKeyboardVisible` · `inputsForKeyboardToAvoid` · `bottomOffset` ·
|
|
341
|
+
`showContentDelay` · `style` · `contentContainerStyle` · `bumperStyle` ·
|
|
342
|
+
`bumperContainerStyle` · `backgroundColor` · `BumperComponent` · `onModalShow` ·
|
|
343
|
+
`onModalClose` · `onSnapPointReach` · `disablePortal` · `CustomPortalContext`
|
|
344
|
+
|
|
345
|
+
`BottomSheetModal` adds:
|
|
346
|
+
|
|
347
|
+
`BackdropComponent` · `onBackDropPress` · `disableCloseOnBackdropPress` ·
|
|
348
|
+
`useNativeModal` · `enableBackgroundContentPress` · `disableAndroidBackButton`
|
|
349
|
+
|
|
350
|
+
Style targets, since they are easy to confuse:
|
|
351
|
+
|
|
352
|
+
- `style` → the sheet surface itself
|
|
353
|
+
- `contentContainerStyle` → the container around your children
|
|
354
|
+
- `backgroundColor` → sheet **and** bumper container; prefer this over setting
|
|
355
|
+
`backgroundColor` in `style`, which leaves the bumper mismatched
|
|
356
|
+
- `bumperStyle` → the grab handle; `bumperContainerStyle` → the area around it
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## 10. Review checklist
|
|
361
|
+
|
|
362
|
+
Before finishing any change involving this package:
|
|
363
|
+
|
|
364
|
+
- [ ] Wrapper holds no state; all hooks are in a `*Content` component
|
|
365
|
+
- [ ] `showContentDelay` set for non-trivial content, with `snapPoints` or `minHeight`
|
|
366
|
+
- [ ] `snapPoints` is stable (module constant or `useMemo`), ordered low → high
|
|
367
|
+
- [ ] Scrollables are `BottomSheetFlatlist` / `BottomSheetScrollView`
|
|
368
|
+
- [ ] `keepMounted` only used together with `snapPoints`
|
|
369
|
+
- [ ] Ref typed `useRef<BottomModalRef>(null)`
|
|
370
|
+
- [ ] Only one control mode — state or ref, not both
|
|
371
|
+
- [ ] No `Modal`, `KeyboardAvoidingView` or hand-rolled backdrop wrapping the sheet
|
|
372
|
+
- [ ] `closeModal({ isNavigating: true })` before navigating away
|
|
373
|
+
- [ ] Portal provider above the navigation container
|