Skip to content

Commit 6de3007

Browse files
committed
feat(rnative): add BottomSheet and sub-components
Signed-off-by: Simon Bruneaud <[email protected]>
1 parent 900f624 commit 6de3007

File tree

19 files changed

+1736
-22
lines changed

19 files changed

+1736
-22
lines changed

apps/app-sandbox-rnative/src/app/blocks/BottomSheets.tsx

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
import { BottomSheetView } from '@gorhom/bottom-sheet';
21
import {
32
BottomSheet,
43
BottomSheetHeader,
54
Button,
6-
BottomSheetScrollView,
5+
BottomSheetFlatList,
76
} from '@ledgerhq/ldls-ui-rnative';
87
import { forwardRef } from 'react';
9-
import { Text, TextInput } from 'react-native';
8+
import { Text, View } from 'react-native';
109

1110
export const BottomSheetsButton = ({ onPress }: any) => {
1211
return (
@@ -18,23 +17,37 @@ export const BottomSheetsButton = ({ onPress }: any) => {
1817

1918
export const BottomSheets = forwardRef<React.ElementRef<typeof BottomSheet>>(
2019
(props, ref) => {
20+
const data = Array.from({ length: 100 }, (_, i) => ({
21+
id: i.toString(),
22+
title: `Item ${i + 1}`,
23+
description: 'Lorem ipsum dolor sit amet consectetur adipisicing elit.',
24+
}));
25+
2126
return (
22-
<BottomSheet closeable snapPoints='half' ref={ref}>
23-
<BottomSheetView>
24-
<BottomSheetHeader
25-
title='Title'
26-
appearance='compact'
27-
description='Description'
28-
/>
29-
{Array.from({ length: 2 }).map((_, index) => (
30-
<Text className='mb-24 text-base' key={index}>
31-
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Vitae
32-
excepturi odit, quis tenetur iste perspiciatis mollitia porro
33-
velit laborum quasi numquam reiciendis dolor! Et quia voluptates
34-
eum, sunt asperiores quod.
35-
</Text>
36-
))}
37-
</BottomSheetView>
27+
<BottomSheet {...props} ref={ref} closeable>
28+
<BottomSheetHeader
29+
spacing
30+
title='Virtual List'
31+
appearance='compact'
32+
description='This bottom sheet contains a virtualized list'
33+
/>
34+
<BottomSheetFlatList
35+
data={data}
36+
keyExtractor={(item) => (item as any).id}
37+
renderItem={({ item }) => {
38+
const typedItem = item as any;
39+
return (
40+
<View className='flex flex-col gap-4 border-b border-base py-12'>
41+
<Text className='text-base body-2-semi-bold'>
42+
{typedItem.title}
43+
</Text>
44+
<Text className='text-muted body-3'>
45+
{typedItem.description}
46+
</Text>
47+
</View>
48+
);
49+
}}
50+
/>
3851
</BottomSheet>
3952
);
4053
},

libs/ui-rnative/.storybook/Decorator.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { ThemeProvider } from '@ledgerhq/ldls-ui-rnative';
21
import type { Decorator } from '@storybook/react-native-web-vite';
2+
33
import { GestureHandlerRootView } from 'react-native-gesture-handler';
4+
import { ThemeProvider } from '../src/lib/Components';
45

56
const createThemeDecorator = (
67
globalName: string,
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
import { Meta, Story, Canvas, Controls } from '@storybook/addon-docs/blocks';
2+
import * as BottomSheetStories from './BottomSheet.stories';
3+
import { BottomSheet } from './BottomSheet';
4+
import { CustomTabs, Tab } from '../../../../.storybook/CustomTabs';
5+
6+
<Meta title="Components/BottomSheet" of={BottomSheetStories} />
7+
8+
# 📋 BottomSheet
9+
10+
<CustomTabs>
11+
<Tab label="Overview">
12+
13+
## Introduction
14+
15+
BottomSheet is a modal component that slides up from the bottom of the screen, built on top of [@gorhom/bottom-sheet v5](https://gorhom.dev/react-native-bottom-sheet/). It provides a flexible and performant way to present content, forms, or actions in a mobile-friendly interface with support for scrollable content, dynamic sizing, and gesture controls.
16+
17+
> View in [Figma](https://www.figma.com/design/JxaLVMTWirCpU0rsbZ30k7/2.-Components-Library?node-id=XXXXX&p=f&m=dev).
18+
19+
## Anatomy
20+
21+
<Canvas of={BottomSheetStories.Base} />
22+
23+
- **Handle**: Visual indicator for draggable area
24+
- **Backdrop**: Semi-transparent overlay behind the sheet
25+
- **Header**: Optional header with title, description, back button, and close button
26+
- **Content**: Scrollable or static content using specialized scrollable components
27+
28+
## Properties
29+
30+
### Overview
31+
32+
The base bottom sheet with standard configuration. This example uses `BottomSheetView` for static content, shows a compact header, and snaps to full height. Use this as your starting point for most implementations.
33+
34+
<Canvas of={BottomSheetStories.Base} />
35+
<Controls of={BottomSheetStories.Base} />
36+
37+
### Header Appearances
38+
39+
Bottom sheets support two header appearances:
40+
41+
- **compact**: Centered title with optional description (ideal for shorter titles)
42+
- **expanded**: Left-aligned title with larger typography (ideal for longer titles and detailed descriptions)
43+
44+
The expanded appearance provides more breathing room for content-heavy headers and is recommended when your title exceeds 2-3 words or when you need to display substantial description text.
45+
46+
<Canvas of={BottomSheetStories.TitleExpanded} />
47+
48+
### Scrollable Content
49+
50+
#### ScrollView
51+
52+
Use `BottomSheetScrollView` when you have a moderate amount of scrollable content (typically under 20-30 items) that can safely fit in memory. Perfect for forms, article content, or settings menus. Note: Always use `BottomSheetScrollView`, not React Native's regular `ScrollView`.
53+
54+
<Canvas of={BottomSheetStories.ScrollView} />
55+
56+
#### FlatList (Virtual List)
57+
58+
Use `BottomSheetFlatList` for efficiently rendering large lists (100+ items) with simple data structures. It virtualizes items to improve performance by only rendering what's visible on screen. Ideal for contact lists, search results, or product catalogs. This example demonstrates a list with 100 items.
59+
60+
<Canvas of={BottomSheetStories.VirtualList} />
61+
62+
#### VirtualizedList
63+
64+
Use `BottomSheetVirtualizedList` when you need more control over item access patterns or work with complex data structures. Requires explicit `getItem` and `getItemCount` implementations. This is the lower-level API that `FlatList` is built upon—use it for custom optimizations or non-array data sources.
65+
66+
<Canvas of={BottomSheetStories.VirtualizedList} />
67+
68+
> **Additional List Types:**
69+
>
70+
> - **SectionList**: Available via `BottomSheetSectionList` but **not compatible with react-native-web**. See [Gorhom documentation](https://gorhom.dev/react-native-bottom-sheet/components/bottomsheetsectionlist) for usage details.
71+
> - **FlashList**: High-performance list from Shopify. Requires `@shopify/flash-list` package. See [Gorhom documentation](https://gorhom.dev/react-native-bottom-sheet/components/bottomsheetflashlist) for usage details.
72+
73+
### Dynamic Sizing
74+
75+
Bottom sheets can automatically adapt to content height instead of using fixed snap points. Enable this with `enableDynamicSizing` prop. The sheet will automatically resize as content changes—perfect for dynamic forms, expandable sections, or content that varies in length. This example shows a sheet that sizes itself to fit 5 items.
76+
77+
<Canvas of={BottomSheetStories.DynamicSizing} />
78+
79+
When using dynamic sizing with potentially long content, set `maxDynamicContentSize` to prevent the sheet from taking over the entire screen. The content becomes scrollable once it exceeds this limit. This example constrains the height to 400px and displays 15 items, making the content scrollable.
80+
81+
<Canvas of={BottomSheetStories.DynamicSizingAndMaxSize} />
82+
83+
### Non-Closeable
84+
85+
Prevent user dismissal for critical flows where users must complete an action or make a decision. Set both `closeable={false}` and `enablePanDownToClose={false}` to disable all dismiss mechanisms. Use this pattern for required confirmations, important notices, or multi-step processes. The sheet can only be closed programmatically via ref.
86+
87+
<Canvas of={BottomSheetStories.NonCloseable} />
88+
89+
## Accessibility
90+
91+
To be implemented:
92+
93+
- **Keyboard navigation**: Focus management and tab order
94+
- **Screen readers**: Proper ARIA labels and announcements
95+
- **Gesture alternatives**: Keyboard shortcuts for dismiss actions
96+
- **Focus trapping**: Keep focus within modal when open
97+
- **Reduced motion**: Respect user motion preferences
98+
99+
</Tab>
100+
<Tab label="Implementation">
101+
102+
## Setup
103+
104+
Install and set up the library with our [Setup Guide →](?path=/docs/getting-started-setup--docs).
105+
106+
### Basic Usage
107+
108+
```tsx
109+
import { BottomSheet, BottomSheetView, BottomSheetHeader, useBottomSheetRef } from '@ledgerhq/ldls-ui-rnative';
110+
import { Button, View, Text } from 'react-native';
111+
112+
function MyComponent() {
113+
const bottomSheetRef = useBottomSheetRef();
114+
115+
return (
116+
<>
117+
<Button onPress={() => bottomSheetRef.current?.expand()}>
118+
Open Bottom Sheet
119+
</Button>
120+
121+
<BottomSheet ref={bottomSheetRef} snapPoints="full" closeable>
122+
<BottomSheetView>
123+
<BottomSheetHeader
124+
title="Welcome"
125+
appearance="compact"
126+
description="Get started with our platform"
127+
/>
128+
<Text>Your content here</Text>
129+
</BottomSheetView>
130+
</BottomSheet>
131+
</>
132+
);
133+
}
134+
```
135+
136+
### useBottomSheetRef Hook
137+
138+
The `useBottomSheetRef` hook provides a typed ref for programmatic control:
139+
140+
```tsx
141+
import { useBottomSheetRef } from '@ledgerhq/ldls-ui-rnative';
142+
143+
function MyComponent() {
144+
const bottomSheetRef = useBottomSheetRef();
145+
146+
return (
147+
<BottomSheet ref={bottomSheetRef} snapPoints={['25%', '50%', '90%']}>
148+
{/* Content */}
149+
</BottomSheet>
150+
);
151+
}
152+
```
153+
154+
<table style={{width: "100%"}}>
155+
<thead>
156+
<tr>
157+
<th style={{textAlign: "left"}}>Method</th>
158+
<th style={{textAlign: "left"}}>Description</th>
159+
</tr>
160+
</thead>
161+
<tbody>
162+
<tr>
163+
<td><b>expand()</b></td>
164+
<td>Expand to the highest snap point</td>
165+
</tr>
166+
<tr>
167+
<td><b>collapse()</b></td>
168+
<td>Collapse to the lowest snap point</td>
169+
</tr>
170+
<tr>
171+
<td><b>close()</b></td>
172+
<td>Close the bottom sheet completely</td>
173+
</tr>
174+
<tr>
175+
<td><b>snapToIndex(index: number)</b></td>
176+
<td>Snap to a specific snap point by index</td>
177+
</tr>
178+
<tr>
179+
<td><b>snapToPosition(position: string | number)</b></td>
180+
<td>Snap to a specific position</td>
181+
</tr>
182+
<tr>
183+
<td><b>forceClose()</b></td>
184+
<td>Force close without animation</td>
185+
</tr>
186+
</tbody>
187+
</table>
188+
189+
### Snap Points
190+
191+
```tsx
192+
// Preset snap points
193+
<BottomSheet snapPoints="full"> {/* 95% of screen */}
194+
<BottomSheet snapPoints="half"> {/* 50% of screen */}
195+
<BottomSheet snapPoints="quarter"> {/* 25% of screen */}
196+
197+
// Custom snap points (pixels, percentages, or mixed)
198+
<BottomSheet snapPoints={[200, 400, 600]}>
199+
<BottomSheet snapPoints={['30%', '60%', '90%']}>
200+
<BottomSheet snapPoints={[150, '50%', '90%']}>
201+
```
202+
203+
### Scrollable Components
204+
205+
Always use specialized scrollable components, never regular React Native components:
206+
207+
```tsx
208+
import {
209+
BottomSheetView, // Static content
210+
BottomSheetScrollView, // Scrollable content
211+
BottomSheetFlatList, // Large lists (100+ items)
212+
BottomSheetVirtualizedList, // Custom data access patterns
213+
} from '@ledgerhq/ldls-ui-rnative';
214+
215+
// Use spacing prop on header when using list components
216+
<BottomSheet ref={ref} snapPoints="full">
217+
<BottomSheetHeader spacing title="List" appearance="compact" />
218+
<BottomSheetFlatList data={data} renderItem={renderItem} />
219+
</BottomSheet>
220+
```
221+
222+
### Important Notes
223+
224+
- **Dynamic Sizing**: When using `enableDynamicSizing`, do not define `snapPoints`
225+
- **Header Spacing**: Use `spacing` prop on `BottomSheetHeader` when using FlatList/VirtualizedList
226+
- **SectionList**: Not compatible with react-native-web. See [Gorhom documentation](https://gorhom.dev/react-native-bottom-sheet/components/bottomsheetsectionlist).
227+
- **FlashList**: Requires `@shopify/flash-list` package. See [Gorhom docs](https://gorhom.dev/react-native-bottom-sheet/components/bottomsheetflashlist)
228+
229+
## Do's and Don'ts
230+
231+
✅ **Do**
232+
233+
```tsx
234+
// Use specialized scrollable components
235+
<BottomSheet ref={bottomSheetRef} snapPoints="full">
236+
<BottomSheetScrollView>
237+
<BottomSheetHeader title="Title" appearance="compact" />
238+
{/* Content */}
239+
</BottomSheetScrollView>
240+
</BottomSheet>
241+
242+
// Use the provided hook
243+
const bottomSheetRef = useBottomSheetRef();
244+
bottomSheetRef.current?.expand();
245+
246+
// Disable both for non-dismissible sheets
247+
<BottomSheet closeable={false} enablePanDownToClose={false}>
248+
```
249+
250+
❌ **Don't**
251+
252+
```tsx
253+
// Don't use regular React Native components
254+
<BottomSheet ref={ref}>
255+
<ScrollView> {/* Wrong! Use BottomSheetScrollView */}
256+
<Text>Content</Text>
257+
</ScrollView>
258+
</BottomSheet>
259+
260+
// Don't mix snapPoints with enableDynamicSizing
261+
<BottomSheet snapPoints="half" enableDynamicSizing> {/* Conflicting! */}
262+
263+
// Don't call methods without optional chaining
264+
bottomSheetRef.current.expand(); // May crash! Use current?.expand()
265+
```
266+
267+
## Learn More
268+
269+
For advanced features and customization options, refer to the [@gorhom/bottom-sheet documentation](https://gorhom.dev/react-native-bottom-sheet/).
270+
271+
</Tab>
272+
</CustomTabs>

0 commit comments

Comments
 (0)