@ceed/cds 1.22.3 → 1.22.4
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/dist/components/data-display/InfoSign.md +74 -91
- package/dist/components/data-display/Typography.md +363 -63
- package/dist/components/feedback/CircularProgress.md +257 -0
- package/dist/components/feedback/Skeleton.md +280 -0
- package/dist/components/feedback/llms.txt +2 -0
- package/dist/components/inputs/ButtonGroup.md +115 -104
- package/dist/components/inputs/CurrencyInput.md +181 -8
- package/dist/components/inputs/DatePicker.md +108 -436
- package/dist/components/inputs/DateRangePicker.md +130 -496
- package/dist/components/inputs/FilterableCheckboxGroup.md +141 -20
- package/dist/components/inputs/FormControl.md +368 -0
- package/dist/components/inputs/IconButton.md +137 -88
- package/dist/components/inputs/Input.md +203 -77
- package/dist/components/inputs/MonthPicker.md +95 -427
- package/dist/components/inputs/MonthRangePicker.md +89 -471
- package/dist/components/inputs/PercentageInput.md +183 -19
- package/dist/components/inputs/RadioButton.md +163 -35
- package/dist/components/inputs/RadioList.md +241 -0
- package/dist/components/inputs/RadioTileGroup.md +146 -62
- package/dist/components/inputs/Select.md +219 -328
- package/dist/components/inputs/Slider.md +334 -0
- package/dist/components/inputs/Switch.md +143 -376
- package/dist/components/inputs/Textarea.md +209 -11
- package/dist/components/inputs/Uploader/Uploader.md +145 -66
- package/dist/components/inputs/llms.txt +3 -0
- package/dist/components/navigation/Breadcrumbs.md +57 -308
- package/dist/components/navigation/Drawer.md +180 -0
- package/dist/components/navigation/Dropdown.md +98 -215
- package/dist/components/navigation/IconMenuButton.md +40 -502
- package/dist/components/navigation/InsetDrawer.md +281 -650
- package/dist/components/navigation/Link.md +31 -348
- package/dist/components/navigation/Menu.md +92 -285
- package/dist/components/navigation/MenuButton.md +55 -448
- package/dist/components/navigation/Pagination.md +47 -338
- package/dist/components/navigation/Stepper.md +160 -28
- package/dist/components/navigation/Tabs.md +57 -316
- package/dist/components/surfaces/Accordions.md +49 -804
- package/dist/components/surfaces/Card.md +97 -157
- package/dist/components/surfaces/Divider.md +83 -234
- package/dist/components/surfaces/Sheet.md +152 -327
- package/dist/guides/ThemeProvider.md +89 -0
- package/dist/guides/llms.txt +9 -0
- package/dist/llms.txt +8 -0
- package/package.json +1 -1
|
@@ -2,25 +2,134 @@
|
|
|
2
2
|
|
|
3
3
|
## Introduction
|
|
4
4
|
|
|
5
|
-
InsetDrawer is a slide-out panel that appears from any edge of the screen, providing a secondary space for navigation, filters, settings, or detailed content without leaving the current page. Unlike Modal which overlays the entire
|
|
5
|
+
InsetDrawer is a slide-out panel that appears from any edge of the screen, providing a secondary space for navigation, filters, settings, or detailed content without leaving the current page. Unlike a Modal, which overlays the entire viewport, InsetDrawer slides in from a specific edge and pushes or covers only part of the interface.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
It is commonly used for mobile navigation menus, filter panels, configuration sidebars, and detail views. The component supports four anchor positions (left, right, top, bottom) and three size presets, making it adaptable to a wide range of layout requirements.
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
<InsetDrawer {...args}>
|
|
11
|
+
<Sheet sx={{
|
|
12
|
+
borderRadius: 'md',
|
|
13
|
+
p: 2,
|
|
14
|
+
display: 'flex',
|
|
15
|
+
flexDirection: 'column',
|
|
16
|
+
gap: 2,
|
|
17
|
+
height: '100%',
|
|
18
|
+
overflow: 'auto'
|
|
19
|
+
}}>
|
|
20
|
+
<DialogTitle>Filters</DialogTitle>
|
|
21
|
+
<ModalClose />
|
|
22
|
+
<Divider sx={{
|
|
23
|
+
mt: 'auto'
|
|
24
|
+
}} />
|
|
25
|
+
<DialogContent>
|
|
26
|
+
<FormControl>
|
|
27
|
+
<FormLabel sx={{
|
|
28
|
+
typography: 'title-md',
|
|
29
|
+
fontWeight: 'bold'
|
|
30
|
+
}}>Property type</FormLabel>
|
|
31
|
+
<RadioGroup>
|
|
32
|
+
<Box sx={{
|
|
33
|
+
display: 'grid',
|
|
34
|
+
gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
|
|
35
|
+
gap: 1.5
|
|
36
|
+
}}>
|
|
37
|
+
{[{
|
|
38
|
+
name: 'House',
|
|
39
|
+
icon: <HomeRoundedIcon />
|
|
40
|
+
}, {
|
|
41
|
+
name: 'Apartment',
|
|
42
|
+
icon: <ApartmentRoundedIcon />
|
|
43
|
+
}, {
|
|
44
|
+
name: 'Guesthouse',
|
|
45
|
+
icon: <MeetingRoomRoundedIcon />
|
|
46
|
+
}, {
|
|
47
|
+
name: 'Hotel',
|
|
48
|
+
icon: <HotelRoundedIcon />
|
|
49
|
+
}].map(item => <Card key={item.name} sx={{
|
|
50
|
+
boxShadow: 'none',
|
|
51
|
+
'&:hover': {
|
|
52
|
+
bgcolor: 'background.level1'
|
|
53
|
+
}
|
|
54
|
+
}}>
|
|
55
|
+
<CardContent>
|
|
56
|
+
{item.icon}
|
|
57
|
+
<Typography level="title-md">{item.name}</Typography>
|
|
58
|
+
</CardContent>
|
|
59
|
+
<Radio disableIcon overlay variant="outlined" color="neutral" value={item.name} sx={{
|
|
60
|
+
mt: -2
|
|
61
|
+
}} slotProps={{
|
|
62
|
+
action: {
|
|
63
|
+
sx: {
|
|
64
|
+
'&:hover': {
|
|
65
|
+
bgcolor: 'transparent'
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}} />
|
|
70
|
+
</Card>)}
|
|
71
|
+
</Box>
|
|
72
|
+
</RadioGroup>
|
|
73
|
+
</FormControl>
|
|
74
|
+
|
|
75
|
+
<Typography level="title-md" fontWeight="bold" sx={{
|
|
76
|
+
mt: 2
|
|
77
|
+
}}>
|
|
78
|
+
Booking options
|
|
79
|
+
</Typography>
|
|
80
|
+
<FormControl orientation="horizontal">
|
|
81
|
+
<Box sx={{
|
|
82
|
+
flex: 1,
|
|
83
|
+
pr: 1
|
|
84
|
+
}}>
|
|
85
|
+
<FormLabel sx={{
|
|
86
|
+
typography: 'title-sm'
|
|
87
|
+
}}>Instant booking</FormLabel>
|
|
88
|
+
<FormHelperText sx={{
|
|
89
|
+
typography: 'body-sm'
|
|
90
|
+
}}>
|
|
91
|
+
Listings that you can book without waiting for host approval.
|
|
92
|
+
</FormHelperText>
|
|
93
|
+
</Box>
|
|
94
|
+
<Switch />
|
|
95
|
+
</FormControl>
|
|
96
|
+
|
|
97
|
+
<FormControl orientation="horizontal">
|
|
98
|
+
<Box sx={{
|
|
99
|
+
flex: 1,
|
|
100
|
+
mt: 1,
|
|
101
|
+
mr: 1
|
|
102
|
+
}}>
|
|
103
|
+
<FormLabel sx={{
|
|
104
|
+
typography: 'title-sm'
|
|
105
|
+
}}>Self check-in</FormLabel>
|
|
106
|
+
<FormHelperText sx={{
|
|
107
|
+
typography: 'body-sm'
|
|
108
|
+
}}>
|
|
109
|
+
Easy access to the property when you arrive.
|
|
110
|
+
</FormHelperText>
|
|
111
|
+
</Box>
|
|
112
|
+
<Switch />
|
|
113
|
+
</FormControl>
|
|
114
|
+
</DialogContent>
|
|
115
|
+
|
|
116
|
+
<Divider sx={{
|
|
117
|
+
mt: 'auto'
|
|
118
|
+
}} />
|
|
119
|
+
<Stack direction="row" justifyContent="space-between" useFlexGap spacing={1}>
|
|
120
|
+
<Button variant="outlined" color="neutral">
|
|
121
|
+
Clear
|
|
122
|
+
</Button>
|
|
123
|
+
<Button>Show 165 properties</Button>
|
|
124
|
+
</Stack>
|
|
125
|
+
</Sheet>
|
|
126
|
+
</InsetDrawer>
|
|
9
127
|
```
|
|
10
128
|
|
|
11
129
|
| Field | Description | Default |
|
|
12
130
|
| ---------------------------- | ----------- | ------- |
|
|
13
131
|
| Controls resolved at runtime | — | — |
|
|
14
132
|
|
|
15
|
-
> ⚠️ **Usage Warning** ⚠️
|
|
16
|
-
>
|
|
17
|
-
> Consider these factors before using InsetDrawer:
|
|
18
|
-
>
|
|
19
|
-
> - **Mobile-first design**: InsetDrawer works best on mobile; consider persistent sidebars on desktop
|
|
20
|
-
> - **Content hierarchy**: Don't hide critical content in drawers that users need frequently
|
|
21
|
-
> - **Anchor position**: Use `left` for navigation, `right` for details/filters, `bottom` for actions
|
|
22
|
-
> - **Accessibility**: Ensure proper focus management and keyboard navigation
|
|
23
|
-
|
|
24
133
|
## Usage
|
|
25
134
|
|
|
26
135
|
```tsx
|
|
@@ -46,71 +155,133 @@ function FilterDrawer() {
|
|
|
46
155
|
}
|
|
47
156
|
```
|
|
48
157
|
|
|
49
|
-
##
|
|
50
|
-
|
|
51
|
-
### Playground
|
|
52
|
-
|
|
53
|
-
Interactive example with controls for anchor position and size.
|
|
54
|
-
|
|
55
|
-
```
|
|
56
|
-
<Canvas of={InsetDrawer.Playground} />
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
### Anchors
|
|
60
|
-
|
|
61
|
-
Different anchor positions: left, right, top, bottom.
|
|
62
|
-
|
|
63
|
-
```
|
|
64
|
-
<Canvas of={InsetDrawer.Anchors} />
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### Sizes
|
|
68
|
-
|
|
69
|
-
Available size options: small, medium, large.
|
|
70
|
-
|
|
71
|
-
```
|
|
72
|
-
<Canvas of={InsetDrawer.Sizes} />
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
### Navigation Menu
|
|
76
|
-
|
|
77
|
-
A mobile navigation menu using InsetDrawer.
|
|
78
|
-
|
|
79
|
-
```
|
|
80
|
-
<Canvas of={InsetDrawer.NavigationMenu} />
|
|
81
|
-
```
|
|
158
|
+
## Default
|
|
82
159
|
|
|
83
|
-
|
|
160
|
+
The default InsetDrawer with a filter panel layout, demonstrating the component's structure with `DialogTitle`, `ModalClose`, `DialogContent`, and action buttons.
|
|
84
161
|
|
|
85
|
-
|
|
162
|
+
```tsx
|
|
163
|
+
<InsetDrawer {...args}>
|
|
164
|
+
<Sheet sx={{
|
|
165
|
+
borderRadius: 'md',
|
|
166
|
+
p: 2,
|
|
167
|
+
display: 'flex',
|
|
168
|
+
flexDirection: 'column',
|
|
169
|
+
gap: 2,
|
|
170
|
+
height: '100%',
|
|
171
|
+
overflow: 'auto'
|
|
172
|
+
}}>
|
|
173
|
+
<DialogTitle>Filters</DialogTitle>
|
|
174
|
+
<ModalClose />
|
|
175
|
+
<Divider sx={{
|
|
176
|
+
mt: 'auto'
|
|
177
|
+
}} />
|
|
178
|
+
<DialogContent>
|
|
179
|
+
<FormControl>
|
|
180
|
+
<FormLabel sx={{
|
|
181
|
+
typography: 'title-md',
|
|
182
|
+
fontWeight: 'bold'
|
|
183
|
+
}}>Property type</FormLabel>
|
|
184
|
+
<RadioGroup>
|
|
185
|
+
<Box sx={{
|
|
186
|
+
display: 'grid',
|
|
187
|
+
gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
|
|
188
|
+
gap: 1.5
|
|
189
|
+
}}>
|
|
190
|
+
{[{
|
|
191
|
+
name: 'House',
|
|
192
|
+
icon: <HomeRoundedIcon />
|
|
193
|
+
}, {
|
|
194
|
+
name: 'Apartment',
|
|
195
|
+
icon: <ApartmentRoundedIcon />
|
|
196
|
+
}, {
|
|
197
|
+
name: 'Guesthouse',
|
|
198
|
+
icon: <MeetingRoomRoundedIcon />
|
|
199
|
+
}, {
|
|
200
|
+
name: 'Hotel',
|
|
201
|
+
icon: <HotelRoundedIcon />
|
|
202
|
+
}].map(item => <Card key={item.name} sx={{
|
|
203
|
+
boxShadow: 'none',
|
|
204
|
+
'&:hover': {
|
|
205
|
+
bgcolor: 'background.level1'
|
|
206
|
+
}
|
|
207
|
+
}}>
|
|
208
|
+
<CardContent>
|
|
209
|
+
{item.icon}
|
|
210
|
+
<Typography level="title-md">{item.name}</Typography>
|
|
211
|
+
</CardContent>
|
|
212
|
+
<Radio disableIcon overlay variant="outlined" color="neutral" value={item.name} sx={{
|
|
213
|
+
mt: -2
|
|
214
|
+
}} slotProps={{
|
|
215
|
+
action: {
|
|
216
|
+
sx: {
|
|
217
|
+
'&:hover': {
|
|
218
|
+
bgcolor: 'transparent'
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}} />
|
|
223
|
+
</Card>)}
|
|
224
|
+
</Box>
|
|
225
|
+
</RadioGroup>
|
|
226
|
+
</FormControl>
|
|
86
227
|
|
|
228
|
+
<Typography level="title-md" fontWeight="bold" sx={{
|
|
229
|
+
mt: 2
|
|
230
|
+
}}>
|
|
231
|
+
Booking options
|
|
232
|
+
</Typography>
|
|
233
|
+
<FormControl orientation="horizontal">
|
|
234
|
+
<Box sx={{
|
|
235
|
+
flex: 1,
|
|
236
|
+
pr: 1
|
|
237
|
+
}}>
|
|
238
|
+
<FormLabel sx={{
|
|
239
|
+
typography: 'title-sm'
|
|
240
|
+
}}>Instant booking</FormLabel>
|
|
241
|
+
<FormHelperText sx={{
|
|
242
|
+
typography: 'body-sm'
|
|
243
|
+
}}>
|
|
244
|
+
Listings that you can book without waiting for host approval.
|
|
245
|
+
</FormHelperText>
|
|
246
|
+
</Box>
|
|
247
|
+
<Switch />
|
|
248
|
+
</FormControl>
|
|
249
|
+
|
|
250
|
+
<FormControl orientation="horizontal">
|
|
251
|
+
<Box sx={{
|
|
252
|
+
flex: 1,
|
|
253
|
+
mt: 1,
|
|
254
|
+
mr: 1
|
|
255
|
+
}}>
|
|
256
|
+
<FormLabel sx={{
|
|
257
|
+
typography: 'title-sm'
|
|
258
|
+
}}>Self check-in</FormLabel>
|
|
259
|
+
<FormHelperText sx={{
|
|
260
|
+
typography: 'body-sm'
|
|
261
|
+
}}>
|
|
262
|
+
Easy access to the property when you arrive.
|
|
263
|
+
</FormHelperText>
|
|
264
|
+
</Box>
|
|
265
|
+
<Switch />
|
|
266
|
+
</FormControl>
|
|
267
|
+
</DialogContent>
|
|
268
|
+
|
|
269
|
+
<Divider sx={{
|
|
270
|
+
mt: 'auto'
|
|
271
|
+
}} />
|
|
272
|
+
<Stack direction="row" justifyContent="space-between" useFlexGap spacing={1}>
|
|
273
|
+
<Button variant="outlined" color="neutral">
|
|
274
|
+
Clear
|
|
275
|
+
</Button>
|
|
276
|
+
<Button>Show 165 properties</Button>
|
|
277
|
+
</Stack>
|
|
278
|
+
</Sheet>
|
|
279
|
+
</InsetDrawer>
|
|
87
280
|
```
|
|
88
|
-
<Canvas of={InsetDrawer.FilterPanel} />
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
## When to Use
|
|
92
|
-
|
|
93
|
-
### ✅ Good Use Cases
|
|
94
|
-
|
|
95
|
-
- **Mobile navigation**: Hamburger menu that slides in from the left
|
|
96
|
-
- **Filter panels**: Complex filters that would clutter the main UI
|
|
97
|
-
- **Detail views**: Additional information without navigating away
|
|
98
|
-
- **Settings panels**: Quick access to preferences or configurations
|
|
99
|
-
- **Shopping cart**: E-commerce cart preview on the side
|
|
100
|
-
- **Help/Support**: Context-sensitive help or chat interfaces
|
|
101
|
-
|
|
102
|
-
### ❌ When Not to Use
|
|
103
|
-
|
|
104
|
-
- **Critical actions**: Don't hide essential features in drawers
|
|
105
|
-
- **Primary navigation on desktop**: Consider a persistent sidebar instead
|
|
106
|
-
- **Simple choices**: Use Select or Dropdown for simple selections
|
|
107
|
-
- **Confirmations**: Use Dialog for confirmation messages
|
|
108
|
-
- **Notifications**: Use Toast or Alert for feedback messages
|
|
109
|
-
- **Large forms**: Complex forms should have their own page
|
|
110
281
|
|
|
111
282
|
## Common Use Cases
|
|
112
283
|
|
|
113
|
-
###
|
|
284
|
+
### Navigation Drawer
|
|
114
285
|
|
|
115
286
|
```tsx
|
|
116
287
|
function NavigationDrawer({ open, onClose, currentPath }) {
|
|
@@ -124,20 +295,14 @@ function NavigationDrawer({ open, onClose, currentPath }) {
|
|
|
124
295
|
<InsetDrawer open={open} onClose={onClose} anchor="left">
|
|
125
296
|
<Sheet sx={{ width: 280, height: '100%', p: 2 }}>
|
|
126
297
|
<Stack gap={1}>
|
|
127
|
-
<
|
|
128
|
-
<Avatar src="/logo.png" />
|
|
129
|
-
<Typography level="title-lg" sx={{ ml: 1 }}>My App</Typography>
|
|
130
|
-
</Box>
|
|
298
|
+
<Typography level="title-lg">My App</Typography>
|
|
131
299
|
<Divider />
|
|
132
300
|
<List>
|
|
133
301
|
{navItems.map((item) => (
|
|
134
302
|
<ListItem key={item.path}>
|
|
135
303
|
<ListItemButton
|
|
136
304
|
selected={currentPath === item.path}
|
|
137
|
-
onClick={() => {
|
|
138
|
-
navigate(item.path);
|
|
139
|
-
onClose();
|
|
140
|
-
}}
|
|
305
|
+
onClick={() => { navigate(item.path); onClose(); }}
|
|
141
306
|
>
|
|
142
307
|
<ListItemDecorator>{item.icon}</ListItemDecorator>
|
|
143
308
|
{item.label}
|
|
@@ -155,17 +320,8 @@ function NavigationDrawer({ open, onClose, currentPath }) {
|
|
|
155
320
|
### Filter Panel
|
|
156
321
|
|
|
157
322
|
```tsx
|
|
158
|
-
function ProductFilters({ open, onClose,
|
|
159
|
-
const [localFilters, setLocalFilters] = useState(
|
|
160
|
-
|
|
161
|
-
const handleApply = () => {
|
|
162
|
-
onApply(localFilters);
|
|
163
|
-
onClose();
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
const handleClear = () => {
|
|
167
|
-
setLocalFilters({});
|
|
168
|
-
};
|
|
323
|
+
function ProductFilters({ open, onClose, onApply }) {
|
|
324
|
+
const [localFilters, setLocalFilters] = useState({});
|
|
169
325
|
|
|
170
326
|
return (
|
|
171
327
|
<InsetDrawer open={open} onClose={onClose} anchor="right" size="md">
|
|
@@ -174,71 +330,17 @@ function ProductFilters({ open, onClose, filters, onApply }) {
|
|
|
174
330
|
<DialogTitle>Filters</DialogTitle>
|
|
175
331
|
<ModalClose />
|
|
176
332
|
</Box>
|
|
177
|
-
|
|
178
333
|
<DialogContent sx={{ flex: 1, overflow: 'auto', p: 2 }}>
|
|
179
|
-
|
|
180
|
-
<FormControl>
|
|
181
|
-
<FormLabel>Category</FormLabel>
|
|
182
|
-
<Select
|
|
183
|
-
value={localFilters.category || ''}
|
|
184
|
-
onChange={(_, value) =>
|
|
185
|
-
setLocalFilters({ ...localFilters, category: value })
|
|
186
|
-
}
|
|
187
|
-
>
|
|
188
|
-
<Option value="electronics">Electronics</Option>
|
|
189
|
-
<Option value="clothing">Clothing</Option>
|
|
190
|
-
<Option value="books">Books</Option>
|
|
191
|
-
</Select>
|
|
192
|
-
</FormControl>
|
|
193
|
-
|
|
194
|
-
<FormControl>
|
|
195
|
-
<FormLabel>Price Range</FormLabel>
|
|
196
|
-
<Stack direction="row" gap={1}>
|
|
197
|
-
<Input
|
|
198
|
-
type="number"
|
|
199
|
-
placeholder="Min"
|
|
200
|
-
value={localFilters.minPrice || ''}
|
|
201
|
-
onChange={(e) =>
|
|
202
|
-
setLocalFilters({ ...localFilters, minPrice: e.target.value })
|
|
203
|
-
}
|
|
204
|
-
/>
|
|
205
|
-
<Input
|
|
206
|
-
type="number"
|
|
207
|
-
placeholder="Max"
|
|
208
|
-
value={localFilters.maxPrice || ''}
|
|
209
|
-
onChange={(e) =>
|
|
210
|
-
setLocalFilters({ ...localFilters, maxPrice: e.target.value })
|
|
211
|
-
}
|
|
212
|
-
/>
|
|
213
|
-
</Stack>
|
|
214
|
-
</FormControl>
|
|
215
|
-
|
|
216
|
-
<FormControl>
|
|
217
|
-
<FormLabel>Rating</FormLabel>
|
|
218
|
-
<RadioGroup
|
|
219
|
-
value={localFilters.rating || ''}
|
|
220
|
-
onChange={(e) =>
|
|
221
|
-
setLocalFilters({ ...localFilters, rating: e.target.value })
|
|
222
|
-
}
|
|
223
|
-
>
|
|
224
|
-
{[4, 3, 2, 1].map((rating) => (
|
|
225
|
-
<Radio
|
|
226
|
-
key={rating}
|
|
227
|
-
value={String(rating)}
|
|
228
|
-
label={`${rating}+ stars`}
|
|
229
|
-
/>
|
|
230
|
-
))}
|
|
231
|
-
</RadioGroup>
|
|
232
|
-
</FormControl>
|
|
233
|
-
</Stack>
|
|
334
|
+
{/* Filter form controls */}
|
|
234
335
|
</DialogContent>
|
|
235
|
-
|
|
236
336
|
<Box sx={{ p: 2, borderTop: '1px solid', borderColor: 'divider' }}>
|
|
237
337
|
<Stack direction="row" gap={1} justifyContent="space-between">
|
|
238
|
-
<Button variant="outlined" color="neutral" onClick={
|
|
338
|
+
<Button variant="outlined" color="neutral" onClick={() => setLocalFilters({})}>
|
|
239
339
|
Clear All
|
|
240
340
|
</Button>
|
|
241
|
-
<Button onClick={
|
|
341
|
+
<Button onClick={() => { onApply(localFilters); onClose(); }}>
|
|
342
|
+
Apply Filters
|
|
343
|
+
</Button>
|
|
242
344
|
</Stack>
|
|
243
345
|
</Box>
|
|
244
346
|
</Sheet>
|
|
@@ -247,189 +349,26 @@ function ProductFilters({ open, onClose, filters, onApply }) {
|
|
|
247
349
|
}
|
|
248
350
|
```
|
|
249
351
|
|
|
250
|
-
###
|
|
251
|
-
|
|
252
|
-
```tsx
|
|
253
|
-
function CartDrawer({ open, onClose, items, onCheckout }) {
|
|
254
|
-
const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
|
|
255
|
-
|
|
256
|
-
return (
|
|
257
|
-
<InsetDrawer open={open} onClose={onClose} anchor="right" size="lg">
|
|
258
|
-
<Sheet sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
|
259
|
-
<Box sx={{ p: 2, borderBottom: '1px solid', borderColor: 'divider' }}>
|
|
260
|
-
<DialogTitle>Shopping Cart ({items.length})</DialogTitle>
|
|
261
|
-
<ModalClose />
|
|
262
|
-
</Box>
|
|
263
|
-
|
|
264
|
-
<DialogContent sx={{ flex: 1, overflow: 'auto', p: 0 }}>
|
|
265
|
-
{items.length === 0 ? (
|
|
266
|
-
<Box sx={{ p: 4, textAlign: 'center' }}>
|
|
267
|
-
<ShoppingCartIcon sx={{ fontSize: 48, color: 'neutral.400' }} />
|
|
268
|
-
<Typography level="body-lg" sx={{ mt: 2 }}>
|
|
269
|
-
Your cart is empty
|
|
270
|
-
</Typography>
|
|
271
|
-
</Box>
|
|
272
|
-
) : (
|
|
273
|
-
<List>
|
|
274
|
-
{items.map((item) => (
|
|
275
|
-
<ListItem key={item.id}>
|
|
276
|
-
<ListItemDecorator>
|
|
277
|
-
<img
|
|
278
|
-
src={item.image}
|
|
279
|
-
alt={item.name}
|
|
280
|
-
style={{ width: 60, height: 60, objectFit: 'cover' }}
|
|
281
|
-
/>
|
|
282
|
-
</ListItemDecorator>
|
|
283
|
-
<ListItemContent>
|
|
284
|
-
<Typography level="title-sm">{item.name}</Typography>
|
|
285
|
-
<Typography level="body-sm">
|
|
286
|
-
${item.price} x {item.quantity}
|
|
287
|
-
</Typography>
|
|
288
|
-
</ListItemContent>
|
|
289
|
-
<IconButton size="sm" color="danger">
|
|
290
|
-
<DeleteIcon />
|
|
291
|
-
</IconButton>
|
|
292
|
-
</ListItem>
|
|
293
|
-
))}
|
|
294
|
-
</List>
|
|
295
|
-
)}
|
|
296
|
-
</DialogContent>
|
|
297
|
-
|
|
298
|
-
{items.length > 0 && (
|
|
299
|
-
<Box sx={{ p: 2, borderTop: '1px solid', borderColor: 'divider' }}>
|
|
300
|
-
<Stack gap={2}>
|
|
301
|
-
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
302
|
-
<Typography level="title-md">Total</Typography>
|
|
303
|
-
<Typography level="title-md">${total.toFixed(2)}</Typography>
|
|
304
|
-
</Box>
|
|
305
|
-
<Button fullWidth onClick={onCheckout}>
|
|
306
|
-
Proceed to Checkout
|
|
307
|
-
</Button>
|
|
308
|
-
<Button variant="outlined" fullWidth onClick={onClose}>
|
|
309
|
-
Continue Shopping
|
|
310
|
-
</Button>
|
|
311
|
-
</Stack>
|
|
312
|
-
</Box>
|
|
313
|
-
)}
|
|
314
|
-
</Sheet>
|
|
315
|
-
</InsetDrawer>
|
|
316
|
-
);
|
|
317
|
-
}
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
### Settings Drawer
|
|
321
|
-
|
|
322
|
-
```tsx
|
|
323
|
-
function SettingsDrawer({ open, onClose }) {
|
|
324
|
-
const [settings, setSettings] = useState({
|
|
325
|
-
notifications: true,
|
|
326
|
-
darkMode: false,
|
|
327
|
-
language: 'en',
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
return (
|
|
331
|
-
<InsetDrawer open={open} onClose={onClose} anchor="right" size="sm">
|
|
332
|
-
<Sheet sx={{ height: '100%', p: 2 }}>
|
|
333
|
-
<DialogTitle>Settings</DialogTitle>
|
|
334
|
-
<ModalClose />
|
|
335
|
-
<Divider sx={{ my: 2 }} />
|
|
336
|
-
|
|
337
|
-
<DialogContent>
|
|
338
|
-
<Stack gap={3}>
|
|
339
|
-
<FormControl orientation="horizontal">
|
|
340
|
-
<Box sx={{ flex: 1 }}>
|
|
341
|
-
<FormLabel>Notifications</FormLabel>
|
|
342
|
-
<FormHelperText>Receive push notifications</FormHelperText>
|
|
343
|
-
</Box>
|
|
344
|
-
<Switch
|
|
345
|
-
checked={settings.notifications}
|
|
346
|
-
onChange={(e) =>
|
|
347
|
-
setSettings({ ...settings, notifications: e.target.checked })
|
|
348
|
-
}
|
|
349
|
-
/>
|
|
350
|
-
</FormControl>
|
|
351
|
-
|
|
352
|
-
<FormControl orientation="horizontal">
|
|
353
|
-
<Box sx={{ flex: 1 }}>
|
|
354
|
-
<FormLabel>Dark Mode</FormLabel>
|
|
355
|
-
<FormHelperText>Use dark color theme</FormHelperText>
|
|
356
|
-
</Box>
|
|
357
|
-
<Switch
|
|
358
|
-
checked={settings.darkMode}
|
|
359
|
-
onChange={(e) =>
|
|
360
|
-
setSettings({ ...settings, darkMode: e.target.checked })
|
|
361
|
-
}
|
|
362
|
-
/>
|
|
363
|
-
</FormControl>
|
|
364
|
-
|
|
365
|
-
<FormControl>
|
|
366
|
-
<FormLabel>Language</FormLabel>
|
|
367
|
-
<Select
|
|
368
|
-
value={settings.language}
|
|
369
|
-
onChange={(_, value) =>
|
|
370
|
-
setSettings({ ...settings, language: value || 'en' })
|
|
371
|
-
}
|
|
372
|
-
>
|
|
373
|
-
<Option value="en">English</Option>
|
|
374
|
-
<Option value="ko">한국어</Option>
|
|
375
|
-
<Option value="ja">日本語</Option>
|
|
376
|
-
</Select>
|
|
377
|
-
</FormControl>
|
|
378
|
-
</Stack>
|
|
379
|
-
</DialogContent>
|
|
380
|
-
</Sheet>
|
|
381
|
-
</InsetDrawer>
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
### Bottom Sheet for Mobile Actions
|
|
352
|
+
### Bottom Sheet for Mobile
|
|
387
353
|
|
|
388
354
|
```tsx
|
|
389
355
|
function ActionSheet({ open, onClose, onAction }) {
|
|
390
|
-
const actions = [
|
|
391
|
-
{ id: 'edit', label: 'Edit', icon: <EditIcon /> },
|
|
392
|
-
{ id: 'share', label: 'Share', icon: <ShareIcon /> },
|
|
393
|
-
{ id: 'duplicate', label: 'Duplicate', icon: <ContentCopyIcon /> },
|
|
394
|
-
{ id: 'delete', label: 'Delete', icon: <DeleteIcon />, color: 'danger' },
|
|
395
|
-
];
|
|
396
|
-
|
|
397
356
|
return (
|
|
398
357
|
<InsetDrawer open={open} onClose={onClose} anchor="bottom">
|
|
399
358
|
<Sheet sx={{ borderRadius: 'lg lg 0 0', p: 2 }}>
|
|
400
|
-
<Box
|
|
401
|
-
sx={{
|
|
402
|
-
width: 40,
|
|
403
|
-
height: 4,
|
|
404
|
-
bgcolor: 'neutral.300',
|
|
405
|
-
borderRadius: 'xl',
|
|
406
|
-
mx: 'auto',
|
|
407
|
-
mb: 2,
|
|
408
|
-
}}
|
|
409
|
-
/>
|
|
410
359
|
<List>
|
|
411
|
-
|
|
412
|
-
<
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
{action.label}
|
|
422
|
-
</ListItemButton>
|
|
423
|
-
</ListItem>
|
|
424
|
-
))}
|
|
360
|
+
<ListItem>
|
|
361
|
+
<ListItemButton onClick={() => { onAction('edit'); onClose(); }}>
|
|
362
|
+
Edit
|
|
363
|
+
</ListItemButton>
|
|
364
|
+
</ListItem>
|
|
365
|
+
<ListItem>
|
|
366
|
+
<ListItemButton onClick={() => { onAction('delete'); onClose(); }}>
|
|
367
|
+
Delete
|
|
368
|
+
</ListItemButton>
|
|
369
|
+
</ListItem>
|
|
425
370
|
</List>
|
|
426
|
-
<Button
|
|
427
|
-
variant="soft"
|
|
428
|
-
color="neutral"
|
|
429
|
-
fullWidth
|
|
430
|
-
sx={{ mt: 1 }}
|
|
431
|
-
onClick={onClose}
|
|
432
|
-
>
|
|
371
|
+
<Button variant="soft" color="neutral" fullWidth onClick={onClose}>
|
|
433
372
|
Cancel
|
|
434
373
|
</Button>
|
|
435
374
|
</Sheet>
|
|
@@ -438,353 +377,45 @@ function ActionSheet({ open, onClose, onAction }) {
|
|
|
438
377
|
}
|
|
439
378
|
```
|
|
440
379
|
|
|
441
|
-
### Help/Support Panel
|
|
442
|
-
|
|
443
|
-
```tsx
|
|
444
|
-
function HelpDrawer({ open, onClose }) {
|
|
445
|
-
const [searchQuery, setSearchQuery] = useState('');
|
|
446
|
-
|
|
447
|
-
const faqItems = [
|
|
448
|
-
{ question: 'How do I reset my password?', answer: '...' },
|
|
449
|
-
{ question: 'How do I change my email?', answer: '...' },
|
|
450
|
-
{ question: 'How do I delete my account?', answer: '...' },
|
|
451
|
-
];
|
|
452
|
-
|
|
453
|
-
return (
|
|
454
|
-
<InsetDrawer open={open} onClose={onClose} anchor="right" size="lg">
|
|
455
|
-
<Sheet sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
|
456
|
-
<Box sx={{ p: 2, borderBottom: '1px solid', borderColor: 'divider' }}>
|
|
457
|
-
<DialogTitle>Help Center</DialogTitle>
|
|
458
|
-
<ModalClose />
|
|
459
|
-
<Input
|
|
460
|
-
placeholder="Search for help..."
|
|
461
|
-
startDecorator={<SearchIcon />}
|
|
462
|
-
value={searchQuery}
|
|
463
|
-
onChange={(e) => setSearchQuery(e.target.value)}
|
|
464
|
-
sx={{ mt: 2 }}
|
|
465
|
-
/>
|
|
466
|
-
</Box>
|
|
467
|
-
|
|
468
|
-
<DialogContent sx={{ flex: 1, overflow: 'auto', p: 2 }}>
|
|
469
|
-
<Typography level="title-md" sx={{ mb: 2 }}>
|
|
470
|
-
Frequently Asked Questions
|
|
471
|
-
</Typography>
|
|
472
|
-
<Accordion>
|
|
473
|
-
{faqItems
|
|
474
|
-
.filter((item) =>
|
|
475
|
-
item.question.toLowerCase().includes(searchQuery.toLowerCase())
|
|
476
|
-
)
|
|
477
|
-
.map((item, index) => (
|
|
478
|
-
<AccordionItem key={index}>
|
|
479
|
-
<AccordionSummary>{item.question}</AccordionSummary>
|
|
480
|
-
<AccordionDetails>{item.answer}</AccordionDetails>
|
|
481
|
-
</AccordionItem>
|
|
482
|
-
))}
|
|
483
|
-
</Accordion>
|
|
484
|
-
</DialogContent>
|
|
485
|
-
|
|
486
|
-
<Box sx={{ p: 2, borderTop: '1px solid', borderColor: 'divider' }}>
|
|
487
|
-
<Typography level="body-sm" sx={{ mb: 1 }}>
|
|
488
|
-
Can't find what you're looking for?
|
|
489
|
-
</Typography>
|
|
490
|
-
<Button fullWidth startDecorator={<ChatIcon />}>
|
|
491
|
-
Contact Support
|
|
492
|
-
</Button>
|
|
493
|
-
</Box>
|
|
494
|
-
</Sheet>
|
|
495
|
-
</InsetDrawer>
|
|
496
|
-
);
|
|
497
|
-
}
|
|
498
|
-
```
|
|
499
|
-
|
|
500
|
-
## Props and Customization
|
|
501
|
-
|
|
502
|
-
### Key Props
|
|
503
|
-
|
|
504
|
-
| Prop | Type | Default | Description |
|
|
505
|
-
| ---------- | ---------------------------------------- | -------- | ------------------------------------------------ |
|
|
506
|
-
| `open` | `boolean` | `false` | Controls whether the drawer is visible |
|
|
507
|
-
| `onClose` | `() => void` | - | Callback fired when the drawer requests to close |
|
|
508
|
-
| `anchor` | `'left' \| 'right' \| 'top' \| 'bottom'` | `'left'` | Edge from which the drawer slides in |
|
|
509
|
-
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Size of the drawer |
|
|
510
|
-
| `children` | `ReactNode` | - | Content to render inside the drawer |
|
|
511
|
-
|
|
512
|
-
### Anchor Positions
|
|
513
|
-
|
|
514
|
-
```tsx
|
|
515
|
-
// Left drawer (navigation menus)
|
|
516
|
-
<InsetDrawer anchor="left" open={open}>
|
|
517
|
-
<Sheet sx={{ width: 280 }}>Navigation</Sheet>
|
|
518
|
-
</InsetDrawer>
|
|
519
|
-
|
|
520
|
-
// Right drawer (details, filters)
|
|
521
|
-
<InsetDrawer anchor="right" open={open}>
|
|
522
|
-
<Sheet sx={{ width: 400 }}>Details</Sheet>
|
|
523
|
-
</InsetDrawer>
|
|
524
|
-
|
|
525
|
-
// Top drawer (search, notifications)
|
|
526
|
-
<InsetDrawer anchor="top" open={open}>
|
|
527
|
-
<Sheet sx={{ height: 200 }}>Search</Sheet>
|
|
528
|
-
</InsetDrawer>
|
|
529
|
-
|
|
530
|
-
// Bottom drawer (mobile actions)
|
|
531
|
-
<InsetDrawer anchor="bottom" open={open}>
|
|
532
|
-
<Sheet sx={{ maxHeight: '80vh' }}>Actions</Sheet>
|
|
533
|
-
</InsetDrawer>
|
|
534
|
-
```
|
|
535
|
-
|
|
536
|
-
### Size Options
|
|
537
|
-
|
|
538
|
-
The `size` prop controls the width (for left/right anchors) or height (for top/bottom anchors):
|
|
539
|
-
|
|
540
|
-
```tsx
|
|
541
|
-
// Small drawer (~280px)
|
|
542
|
-
<InsetDrawer size="sm" anchor="right" />
|
|
543
|
-
|
|
544
|
-
// Medium drawer (~400px) - default
|
|
545
|
-
<InsetDrawer size="md" anchor="right" />
|
|
546
|
-
|
|
547
|
-
// Large drawer (~600px)
|
|
548
|
-
<InsetDrawer size="lg" anchor="right" />
|
|
549
|
-
```
|
|
550
|
-
|
|
551
|
-
### Custom Sizing
|
|
552
|
-
|
|
553
|
-
For more control, use `Sheet` with custom styles:
|
|
554
|
-
|
|
555
|
-
```tsx
|
|
556
|
-
<InsetDrawer open={open} anchor="right">
|
|
557
|
-
<Sheet sx={{ width: { xs: '100vw', sm: 450 }, height: '100%' }}>
|
|
558
|
-
Content
|
|
559
|
-
</Sheet>
|
|
560
|
-
</InsetDrawer>
|
|
561
|
-
```
|
|
562
|
-
|
|
563
|
-
### Drawer Content Structure
|
|
564
|
-
|
|
565
|
-
Use these components for consistent drawer layout:
|
|
566
|
-
|
|
567
|
-
```tsx
|
|
568
|
-
<InsetDrawer open={open}>
|
|
569
|
-
<Sheet sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
|
570
|
-
{/* Header */}
|
|
571
|
-
<Box sx={{ p: 2, borderBottom: '1px solid', borderColor: 'divider' }}>
|
|
572
|
-
<DialogTitle>Drawer Title</DialogTitle>
|
|
573
|
-
<ModalClose />
|
|
574
|
-
</Box>
|
|
575
|
-
|
|
576
|
-
{/* Scrollable Content */}
|
|
577
|
-
<DialogContent sx={{ flex: 1, overflow: 'auto', p: 2 }}>
|
|
578
|
-
{/* Main content */}
|
|
579
|
-
</DialogContent>
|
|
580
|
-
|
|
581
|
-
{/* Footer */}
|
|
582
|
-
<Box sx={{ p: 2, borderTop: '1px solid', borderColor: 'divider' }}>
|
|
583
|
-
<Stack direction="row" gap={1}>
|
|
584
|
-
<Button variant="outlined">Cancel</Button>
|
|
585
|
-
<Button>Save</Button>
|
|
586
|
-
</Stack>
|
|
587
|
-
</Box>
|
|
588
|
-
</Sheet>
|
|
589
|
-
</InsetDrawer>
|
|
590
|
-
```
|
|
591
|
-
|
|
592
|
-
## Accessibility
|
|
593
|
-
|
|
594
|
-
InsetDrawer includes built-in accessibility features:
|
|
595
|
-
|
|
596
|
-
### ARIA Attributes
|
|
597
|
-
|
|
598
|
-
- Drawer container has appropriate `role="dialog"` or `role="presentation"`
|
|
599
|
-
- Focus is trapped within the drawer when open
|
|
600
|
-
- Proper `aria-modal` attribute when modal
|
|
601
|
-
|
|
602
|
-
### Keyboard Navigation
|
|
603
|
-
|
|
604
|
-
- **Escape**: Close the drawer
|
|
605
|
-
- **Tab**: Navigate between focusable elements within drawer
|
|
606
|
-
- **Shift + Tab**: Navigate backwards
|
|
607
|
-
|
|
608
|
-
### Focus Management
|
|
609
|
-
|
|
610
|
-
```tsx
|
|
611
|
-
// Focus automatically moves to drawer content when opened
|
|
612
|
-
<InsetDrawer
|
|
613
|
-
open={open}
|
|
614
|
-
onClose={onClose}
|
|
615
|
-
// Focus is trapped within drawer
|
|
616
|
-
>
|
|
617
|
-
<Sheet>
|
|
618
|
-
{/* First focusable element receives focus */}
|
|
619
|
-
<Input autoFocus placeholder="Search..." />
|
|
620
|
-
</Sheet>
|
|
621
|
-
</InsetDrawer>
|
|
622
|
-
```
|
|
623
|
-
|
|
624
|
-
### Screen Reader Support
|
|
625
|
-
|
|
626
|
-
```tsx
|
|
627
|
-
// Provide descriptive title for screen readers
|
|
628
|
-
<InsetDrawer open={open}>
|
|
629
|
-
<Sheet role="dialog" aria-labelledby="drawer-title">
|
|
630
|
-
<DialogTitle id="drawer-title">Filters</DialogTitle>
|
|
631
|
-
{/* Content */}
|
|
632
|
-
</Sheet>
|
|
633
|
-
</InsetDrawer>
|
|
634
|
-
```
|
|
635
|
-
|
|
636
380
|
## Best Practices
|
|
637
381
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
1. **Use appropriate anchor positions**: Match drawer position to content type
|
|
382
|
+
1. **Use appropriate anchor positions**: Match the drawer position to the content type for intuitive UX.
|
|
641
383
|
|
|
642
384
|
```tsx
|
|
643
|
-
// ✅ Good: Navigation on left, details on right
|
|
644
|
-
<InsetDrawer anchor="left"> {/* Navigation */}
|
|
645
|
-
<InsetDrawer anchor="right"> {/*
|
|
646
|
-
<InsetDrawer anchor="bottom">{/* Mobile
|
|
385
|
+
// ✅ Good: Navigation on the left, details on the right
|
|
386
|
+
<InsetDrawer anchor="left"> {/* Navigation menu */}
|
|
387
|
+
<InsetDrawer anchor="right"> {/* Detail panel or filters */}
|
|
388
|
+
<InsetDrawer anchor="bottom">{/* Mobile action sheet */}
|
|
647
389
|
```
|
|
648
390
|
|
|
649
|
-
2. **Provide clear close affordances**:
|
|
391
|
+
2. **Provide clear close affordances**: Always give users multiple ways to close the drawer -- a close button, a cancel button, and backdrop click.
|
|
650
392
|
|
|
651
393
|
```tsx
|
|
652
|
-
// ✅ Good: Multiple
|
|
394
|
+
// ✅ Good: Multiple close methods
|
|
653
395
|
<InsetDrawer open={open} onClose={handleClose}>
|
|
654
396
|
<Sheet>
|
|
655
|
-
<ModalClose /> {/* X button */}
|
|
656
|
-
<Button onClick={handleClose}>Cancel</Button> {/* Cancel button */}
|
|
657
|
-
</Sheet>
|
|
658
|
-
</InsetDrawer>
|
|
659
|
-
```
|
|
660
|
-
|
|
661
|
-
3. **Use consistent header/footer patterns**: Structure content predictably
|
|
662
|
-
|
|
663
|
-
```tsx
|
|
664
|
-
// ✅ Good: Clear structure
|
|
665
|
-
<Sheet>
|
|
666
|
-
<Box sx={{ borderBottom: '1px solid', borderColor: 'divider' }}>
|
|
667
|
-
<DialogTitle>Title</DialogTitle>
|
|
668
397
|
<ModalClose />
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
<Box sx={{ borderTop: '1px solid', borderColor: 'divider' }}>
|
|
672
|
-
{/* Action buttons */}
|
|
673
|
-
</Box>
|
|
674
|
-
</Sheet>
|
|
675
|
-
```
|
|
676
|
-
|
|
677
|
-
4. **Handle mobile responsiveness**: Full width on small screens
|
|
678
|
-
|
|
679
|
-
```tsx
|
|
680
|
-
// ✅ Good: Responsive width
|
|
681
|
-
<Sheet sx={{ width: { xs: '100vw', sm: 400 } }}>
|
|
682
|
-
```
|
|
683
|
-
|
|
684
|
-
### ❌ Don't
|
|
685
|
-
|
|
686
|
-
1. **Don't hide critical content**: Keep essential features accessible
|
|
687
|
-
|
|
688
|
-
```tsx
|
|
689
|
-
// ❌ Bad: Primary action hidden in drawer
|
|
690
|
-
<InsetDrawer>
|
|
691
|
-
<Button>Complete Purchase</Button> {/* Should be on main page */}
|
|
398
|
+
<Button onClick={handleClose}>Cancel</Button>
|
|
399
|
+
</Sheet>
|
|
692
400
|
</InsetDrawer>
|
|
693
401
|
```
|
|
694
402
|
|
|
695
|
-
|
|
403
|
+
3. **Structure content consistently**: Use a header/body/footer layout with clear dividers so users can quickly scan the drawer's content.
|
|
696
404
|
|
|
697
|
-
|
|
698
|
-
// ❌ Bad: Drawer within drawer
|
|
699
|
-
<InsetDrawer>
|
|
700
|
-
<Button onClick={() => setNestedOpen(true)}>Open Another</Button>
|
|
701
|
-
<InsetDrawer open={nestedOpen}>{/* Nested drawer */}</InsetDrawer>
|
|
702
|
-
</InsetDrawer>
|
|
703
|
-
```
|
|
704
|
-
|
|
705
|
-
3. **Don't use for confirmations**: Use Dialog instead
|
|
405
|
+
4. **Do not nest drawers**: Opening a drawer inside another drawer creates a confusing experience.
|
|
706
406
|
|
|
707
407
|
```tsx
|
|
708
|
-
// ❌ Bad:
|
|
408
|
+
// ❌ Bad: Nested drawers
|
|
709
409
|
<InsetDrawer>
|
|
710
|
-
<
|
|
711
|
-
<Button>Confirm</Button>
|
|
410
|
+
<InsetDrawer>{/* Avoid this pattern */}</InsetDrawer>
|
|
712
411
|
</InsetDrawer>
|
|
713
|
-
|
|
714
|
-
// ✅ Good: Use Dialog
|
|
715
|
-
<Dialog>
|
|
716
|
-
<Typography>Are you sure?</Typography>
|
|
717
|
-
<Button>Confirm</Button>
|
|
718
|
-
</Dialog>
|
|
719
412
|
```
|
|
720
413
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
## Performance Considerations
|
|
724
|
-
|
|
725
|
-
### Lazy Loading Content
|
|
726
|
-
|
|
727
|
-
For drawers with heavy content, load data when drawer opens:
|
|
728
|
-
|
|
729
|
-
```tsx
|
|
730
|
-
function DetailDrawer({ open, itemId }) {
|
|
731
|
-
const [data, setData] = useState(null);
|
|
732
|
-
|
|
733
|
-
useEffect(() => {
|
|
734
|
-
if (open && itemId) {
|
|
735
|
-
fetchItemDetails(itemId).then(setData);
|
|
736
|
-
}
|
|
737
|
-
}, [open, itemId]);
|
|
414
|
+
5. **Do not use for confirmations**: Use Dialog or Modal for simple confirmation prompts. InsetDrawer is designed for rich, scrollable content.
|
|
738
415
|
|
|
739
|
-
|
|
740
|
-
<InsetDrawer open={open}>
|
|
741
|
-
<Sheet>
|
|
742
|
-
{data ? <ItemDetails data={data} /> : <Skeleton />}
|
|
743
|
-
</Sheet>
|
|
744
|
-
</InsetDrawer>
|
|
745
|
-
);
|
|
746
|
-
}
|
|
747
|
-
```
|
|
748
|
-
|
|
749
|
-
### Memoize Handlers
|
|
750
|
-
|
|
751
|
-
```tsx
|
|
752
|
-
const handleClose = useCallback(() => {
|
|
753
|
-
setOpen(false);
|
|
754
|
-
}, []);
|
|
755
|
-
|
|
756
|
-
const handleApply = useCallback((filters) => {
|
|
757
|
-
applyFilters(filters);
|
|
758
|
-
setOpen(false);
|
|
759
|
-
}, [applyFilters]);
|
|
760
|
-
|
|
761
|
-
<InsetDrawer open={open} onClose={handleClose}>
|
|
762
|
-
<FilterPanel onApply={handleApply} />
|
|
763
|
-
</InsetDrawer>
|
|
764
|
-
```
|
|
765
|
-
|
|
766
|
-
### Unmount When Closed
|
|
767
|
-
|
|
768
|
-
For memory-intensive content, conditionally render:
|
|
769
|
-
|
|
770
|
-
```tsx
|
|
771
|
-
{open && (
|
|
772
|
-
<InsetDrawer open={open} onClose={handleClose}>
|
|
773
|
-
<HeavyContentComponent />
|
|
774
|
-
</InsetDrawer>
|
|
775
|
-
)}
|
|
776
|
-
```
|
|
777
|
-
|
|
778
|
-
### Avoid Layout Shifts
|
|
779
|
-
|
|
780
|
-
Use fixed dimensions to prevent content shifts:
|
|
781
|
-
|
|
782
|
-
```tsx
|
|
783
|
-
<Sheet sx={{
|
|
784
|
-
width: 400,
|
|
785
|
-
height: '100%',
|
|
786
|
-
// Content won't cause width changes
|
|
787
|
-
}}>
|
|
788
|
-
```
|
|
416
|
+
## Accessibility
|
|
789
417
|
|
|
790
|
-
InsetDrawer
|
|
418
|
+
- InsetDrawer traps focus within the drawer when open, ensuring keyboard users cannot accidentally interact with content behind it.
|
|
419
|
+
- Pressing **Escape** closes the drawer, and **Tab** / **Shift+Tab** cycles through focusable elements inside.
|
|
420
|
+
- Use `DialogTitle` with a matching `id` and `aria-labelledby` on the Sheet to provide a descriptive label for screen readers.
|
|
421
|
+
- When the drawer closes, focus returns to the element that triggered it, maintaining a predictable navigation flow.
|