@ceed/cds 1.28.0 → 1.29.0-next.1
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/CurrencyInput/CurrencyInput.d.ts +1 -1
- package/dist/components/CurrencyInput/hooks/use-currency-setting.d.ts +2 -2
- package/dist/components/DataTable/hooks.d.ts +2 -1
- package/dist/components/DataTable/utils.d.ts +1 -0
- package/dist/components/SearchBar/SearchBar.d.ts +21 -0
- package/dist/components/SearchBar/index.d.ts +3 -0
- package/dist/components/data-display/DataTable.md +1 -1
- package/dist/components/data-display/InfoSign.md +91 -74
- package/dist/components/data-display/Typography.md +94 -411
- package/dist/components/feedback/Dialog.md +62 -76
- package/dist/components/feedback/Modal.md +138 -430
- package/dist/components/feedback/llms.txt +0 -2
- package/dist/components/index.d.ts +2 -1
- package/dist/components/inputs/Autocomplete.md +107 -356
- package/dist/components/inputs/ButtonGroup.md +104 -115
- package/dist/components/inputs/CurrencyInput.md +5 -183
- package/dist/components/inputs/DatePicker.md +431 -108
- package/dist/components/inputs/DateRangePicker.md +492 -131
- package/dist/components/inputs/FilterableCheckboxGroup.md +19 -145
- package/dist/components/inputs/IconButton.md +88 -137
- package/dist/components/inputs/Input.md +73 -204
- package/dist/components/inputs/MonthPicker.md +422 -95
- package/dist/components/inputs/MonthRangePicker.md +466 -89
- package/dist/components/inputs/PercentageInput.md +16 -185
- package/dist/components/inputs/RadioButton.md +35 -163
- package/dist/components/inputs/SearchBar.md +44 -0
- package/dist/components/inputs/Select.md +326 -222
- package/dist/components/inputs/Switch.md +376 -143
- package/dist/components/inputs/Textarea.md +10 -213
- package/dist/components/inputs/Uploader/Uploader.md +66 -145
- package/dist/components/inputs/llms.txt +1 -4
- package/dist/components/navigation/Breadcrumbs.md +308 -57
- package/dist/components/navigation/Drawer.md +0 -180
- package/dist/components/navigation/Dropdown.md +215 -98
- package/dist/components/navigation/IconMenuButton.md +502 -40
- package/dist/components/navigation/InsetDrawer.md +650 -281
- package/dist/components/navigation/Link.md +348 -31
- package/dist/components/navigation/Menu.md +285 -92
- package/dist/components/navigation/MenuButton.md +448 -55
- package/dist/components/navigation/Pagination.md +338 -47
- package/dist/components/navigation/Stepper.md +28 -160
- package/dist/components/navigation/Tabs.md +316 -57
- package/dist/components/surfaces/Accordions.md +804 -49
- package/dist/components/surfaces/Card.md +157 -97
- package/dist/components/surfaces/Divider.md +234 -83
- package/dist/components/surfaces/Sheet.md +328 -153
- package/dist/index.cjs +435 -577
- package/dist/index.d.ts +1 -1
- package/dist/index.js +424 -510
- package/dist/llms.txt +1 -9
- package/framer/index.js +1 -1
- package/package.json +17 -22
- package/dist/chunks/rehype-accent-FZRUD7VI.js +0 -39
- package/dist/components/RadioTileGroup/RadioTileGroup.d.ts +0 -56
- package/dist/components/RadioTileGroup/index.d.ts +0 -3
- package/dist/components/feedback/CircularProgress.md +0 -257
- package/dist/components/feedback/Skeleton.md +0 -280
- package/dist/components/inputs/FormControl.md +0 -361
- package/dist/components/inputs/RadioList.md +0 -241
- package/dist/components/inputs/RadioTileGroup.md +0 -507
- package/dist/components/inputs/Slider.md +0 -334
- package/dist/guides/ThemeProvider.md +0 -89
- package/dist/guides/llms.txt +0 -9
- package/dist/index.browser.js +0 -224
- package/dist/index.browser.js.map +0 -7
|
@@ -2,134 +2,25 @@
|
|
|
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
|
|
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 screen, InsetDrawer slides in from an edge and can optionally allow interaction with the main content behind it. It's commonly used for mobile navigation menus, filter panels, and configuration sidebars.
|
|
6
6
|
|
|
7
|
-
|
|
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>
|
|
7
|
+
```
|
|
8
|
+
<Canvas of={InsetDrawer.Playground} />
|
|
127
9
|
```
|
|
128
10
|
|
|
129
11
|
| Field | Description | Default |
|
|
130
12
|
| ---------------------------- | ----------- | ------- |
|
|
131
13
|
| Controls resolved at runtime | — | — |
|
|
132
14
|
|
|
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
|
+
|
|
133
24
|
## Usage
|
|
134
25
|
|
|
135
26
|
```tsx
|
|
@@ -155,133 +46,71 @@ function FilterDrawer() {
|
|
|
155
46
|
}
|
|
156
47
|
```
|
|
157
48
|
|
|
158
|
-
##
|
|
49
|
+
## Examples
|
|
159
50
|
|
|
160
|
-
|
|
51
|
+
### Playground
|
|
161
52
|
|
|
162
|
-
|
|
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>
|
|
53
|
+
Interactive example with controls for anchor position and size.
|
|
227
54
|
|
|
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>
|
|
280
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
|
+
```
|
|
82
|
+
|
|
83
|
+
### Filter Panel
|
|
84
|
+
|
|
85
|
+
A filter panel with form controls sliding from the right.
|
|
86
|
+
|
|
87
|
+
```
|
|
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
|
|
281
110
|
|
|
282
111
|
## Common Use Cases
|
|
283
112
|
|
|
284
|
-
### Navigation Drawer
|
|
113
|
+
### Mobile Navigation Drawer
|
|
285
114
|
|
|
286
115
|
```tsx
|
|
287
116
|
function NavigationDrawer({ open, onClose, currentPath }) {
|
|
@@ -295,14 +124,20 @@ function NavigationDrawer({ open, onClose, currentPath }) {
|
|
|
295
124
|
<InsetDrawer open={open} onClose={onClose} anchor="left">
|
|
296
125
|
<Sheet sx={{ width: 280, height: '100%', p: 2 }}>
|
|
297
126
|
<Stack gap={1}>
|
|
298
|
-
<
|
|
127
|
+
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
|
128
|
+
<Avatar src="/logo.png" />
|
|
129
|
+
<Typography level="title-lg" sx={{ ml: 1 }}>My App</Typography>
|
|
130
|
+
</Box>
|
|
299
131
|
<Divider />
|
|
300
132
|
<List>
|
|
301
133
|
{navItems.map((item) => (
|
|
302
134
|
<ListItem key={item.path}>
|
|
303
135
|
<ListItemButton
|
|
304
136
|
selected={currentPath === item.path}
|
|
305
|
-
onClick={() => {
|
|
137
|
+
onClick={() => {
|
|
138
|
+
navigate(item.path);
|
|
139
|
+
onClose();
|
|
140
|
+
}}
|
|
306
141
|
>
|
|
307
142
|
<ListItemDecorator>{item.icon}</ListItemDecorator>
|
|
308
143
|
{item.label}
|
|
@@ -320,8 +155,17 @@ function NavigationDrawer({ open, onClose, currentPath }) {
|
|
|
320
155
|
### Filter Panel
|
|
321
156
|
|
|
322
157
|
```tsx
|
|
323
|
-
function ProductFilters({ open, onClose, onApply }) {
|
|
324
|
-
const [localFilters, setLocalFilters] = useState(
|
|
158
|
+
function ProductFilters({ open, onClose, filters, onApply }) {
|
|
159
|
+
const [localFilters, setLocalFilters] = useState(filters);
|
|
160
|
+
|
|
161
|
+
const handleApply = () => {
|
|
162
|
+
onApply(localFilters);
|
|
163
|
+
onClose();
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const handleClear = () => {
|
|
167
|
+
setLocalFilters({});
|
|
168
|
+
};
|
|
325
169
|
|
|
326
170
|
return (
|
|
327
171
|
<InsetDrawer open={open} onClose={onClose} anchor="right" size="md">
|
|
@@ -330,17 +174,71 @@ function ProductFilters({ open, onClose, onApply }) {
|
|
|
330
174
|
<DialogTitle>Filters</DialogTitle>
|
|
331
175
|
<ModalClose />
|
|
332
176
|
</Box>
|
|
177
|
+
|
|
333
178
|
<DialogContent sx={{ flex: 1, overflow: 'auto', p: 2 }}>
|
|
334
|
-
{
|
|
179
|
+
<Stack gap={3}>
|
|
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>
|
|
335
234
|
</DialogContent>
|
|
235
|
+
|
|
336
236
|
<Box sx={{ p: 2, borderTop: '1px solid', borderColor: 'divider' }}>
|
|
337
237
|
<Stack direction="row" gap={1} justifyContent="space-between">
|
|
338
|
-
<Button variant="outlined" color="neutral" onClick={
|
|
238
|
+
<Button variant="outlined" color="neutral" onClick={handleClear}>
|
|
339
239
|
Clear All
|
|
340
240
|
</Button>
|
|
341
|
-
<Button onClick={
|
|
342
|
-
Apply Filters
|
|
343
|
-
</Button>
|
|
241
|
+
<Button onClick={handleApply}>Apply Filters</Button>
|
|
344
242
|
</Stack>
|
|
345
243
|
</Box>
|
|
346
244
|
</Sheet>
|
|
@@ -349,26 +247,189 @@ function ProductFilters({ open, onClose, onApply }) {
|
|
|
349
247
|
}
|
|
350
248
|
```
|
|
351
249
|
|
|
352
|
-
###
|
|
250
|
+
### Shopping Cart Drawer
|
|
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
|
|
353
387
|
|
|
354
388
|
```tsx
|
|
355
389
|
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
|
+
|
|
356
397
|
return (
|
|
357
398
|
<InsetDrawer open={open} onClose={onClose} anchor="bottom">
|
|
358
399
|
<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
|
+
/>
|
|
359
410
|
<List>
|
|
360
|
-
|
|
361
|
-
<
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
411
|
+
{actions.map((action) => (
|
|
412
|
+
<ListItem key={action.id}>
|
|
413
|
+
<ListItemButton
|
|
414
|
+
color={action.color}
|
|
415
|
+
onClick={() => {
|
|
416
|
+
onAction(action.id);
|
|
417
|
+
onClose();
|
|
418
|
+
}}
|
|
419
|
+
>
|
|
420
|
+
<ListItemDecorator>{action.icon}</ListItemDecorator>
|
|
421
|
+
{action.label}
|
|
422
|
+
</ListItemButton>
|
|
423
|
+
</ListItem>
|
|
424
|
+
))}
|
|
370
425
|
</List>
|
|
371
|
-
<Button
|
|
426
|
+
<Button
|
|
427
|
+
variant="soft"
|
|
428
|
+
color="neutral"
|
|
429
|
+
fullWidth
|
|
430
|
+
sx={{ mt: 1 }}
|
|
431
|
+
onClick={onClose}
|
|
432
|
+
>
|
|
372
433
|
Cancel
|
|
373
434
|
</Button>
|
|
374
435
|
</Sheet>
|
|
@@ -377,45 +438,353 @@ function ActionSheet({ open, onClose, onAction }) {
|
|
|
377
438
|
}
|
|
378
439
|
```
|
|
379
440
|
|
|
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
|
+
|
|
380
636
|
## Best Practices
|
|
381
637
|
|
|
382
|
-
|
|
638
|
+
### ✅ Do
|
|
639
|
+
|
|
640
|
+
1. **Use appropriate anchor positions**: Match drawer position to content type
|
|
383
641
|
|
|
384
642
|
```tsx
|
|
385
|
-
// ✅ Good: Navigation on
|
|
386
|
-
<InsetDrawer anchor="left"> {/* Navigation
|
|
387
|
-
<InsetDrawer anchor="right"> {/*
|
|
388
|
-
<InsetDrawer anchor="bottom">{/* Mobile
|
|
643
|
+
// ✅ Good: Navigation on left, details on right
|
|
644
|
+
<InsetDrawer anchor="left"> {/* Navigation */}
|
|
645
|
+
<InsetDrawer anchor="right"> {/* Details/filters */}
|
|
646
|
+
<InsetDrawer anchor="bottom">{/* Mobile actions */}
|
|
389
647
|
```
|
|
390
648
|
|
|
391
|
-
2. **Provide clear close affordances**:
|
|
649
|
+
2. **Provide clear close affordances**: Include close button and backdrop click
|
|
392
650
|
|
|
393
651
|
```tsx
|
|
394
|
-
// ✅ Good: Multiple close
|
|
652
|
+
// ✅ Good: Multiple ways to close
|
|
395
653
|
<InsetDrawer open={open} onClose={handleClose}>
|
|
396
654
|
<Sheet>
|
|
397
|
-
<ModalClose />
|
|
398
|
-
<Button onClick={handleClose}>Cancel</Button>
|
|
655
|
+
<ModalClose /> {/* X button */}
|
|
656
|
+
<Button onClick={handleClose}>Cancel</Button> {/* Cancel button */}
|
|
399
657
|
</Sheet>
|
|
400
658
|
</InsetDrawer>
|
|
401
659
|
```
|
|
402
660
|
|
|
403
|
-
3. **
|
|
661
|
+
3. **Use consistent header/footer patterns**: Structure content predictably
|
|
404
662
|
|
|
405
|
-
|
|
663
|
+
```tsx
|
|
664
|
+
// ✅ Good: Clear structure
|
|
665
|
+
<Sheet>
|
|
666
|
+
<Box sx={{ borderBottom: '1px solid', borderColor: 'divider' }}>
|
|
667
|
+
<DialogTitle>Title</DialogTitle>
|
|
668
|
+
<ModalClose />
|
|
669
|
+
</Box>
|
|
670
|
+
<DialogContent>{/* Scrollable content */}</DialogContent>
|
|
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
|
|
406
687
|
|
|
407
688
|
```tsx
|
|
408
|
-
// ❌ Bad:
|
|
689
|
+
// ❌ Bad: Primary action hidden in drawer
|
|
409
690
|
<InsetDrawer>
|
|
410
|
-
<
|
|
691
|
+
<Button>Complete Purchase</Button> {/* Should be on main page */}
|
|
411
692
|
</InsetDrawer>
|
|
412
693
|
```
|
|
413
694
|
|
|
414
|
-
|
|
695
|
+
2. **Don't nest drawers**: Leads to confusing UX
|
|
415
696
|
|
|
416
|
-
|
|
697
|
+
```tsx
|
|
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
|
|
706
|
+
|
|
707
|
+
```tsx
|
|
708
|
+
// ❌ Bad: Drawer for confirmation
|
|
709
|
+
<InsetDrawer>
|
|
710
|
+
<Typography>Are you sure?</Typography>
|
|
711
|
+
<Button>Confirm</Button>
|
|
712
|
+
</InsetDrawer>
|
|
713
|
+
|
|
714
|
+
// ✅ Good: Use Dialog
|
|
715
|
+
<Dialog>
|
|
716
|
+
<Typography>Are you sure?</Typography>
|
|
717
|
+
<Button>Confirm</Button>
|
|
718
|
+
</Dialog>
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
4. **Don't forget about keyboard users**: Ensure proper focus management
|
|
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]);
|
|
738
|
+
|
|
739
|
+
return (
|
|
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
|
+
```
|
|
417
789
|
|
|
418
|
-
|
|
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.
|
|
790
|
+
InsetDrawer provides flexible secondary content areas that slide in from screen edges. Use it for navigation menus, filter panels, and detail views while keeping primary actions visible on the main screen. Always consider the user's workflow and ensure drawers enhance rather than hinder the experience.
|