@varialkit/drawer 0.1.0
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/docs.md +78 -0
- package/examples/index.tsx +1 -0
- package/examples.tsx +407 -0
- package/package.json +29 -0
- package/src/Drawer.scss +334 -0
- package/src/Drawer.tsx +450 -0
- package/src/Drawer.types.ts +69 -0
- package/src/index.ts +11 -0
package/docs.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Drawer
|
|
2
|
+
|
|
3
|
+
Drawer slides in from the side to show supporting tasks or context without leaving the page.
|
|
4
|
+
Use it for secondary flows, filters, and detailed panels.
|
|
5
|
+
|
|
6
|
+
## How to Use
|
|
7
|
+
|
|
8
|
+
```tsx
|
|
9
|
+
import { Drawer } from "@solara/drawer";
|
|
10
|
+
|
|
11
|
+
export function Example() {
|
|
12
|
+
return (
|
|
13
|
+
<Drawer isOpen onClose={() => {}}>
|
|
14
|
+
<Drawer.Header onClose={() => {}}>
|
|
15
|
+
<Drawer.Title>Filters</Drawer.Title>
|
|
16
|
+
</Drawer.Header>
|
|
17
|
+
<Drawer.Content>Drawer content goes here.</Drawer.Content>
|
|
18
|
+
<Drawer.Footer
|
|
19
|
+
primaryButton={{ label: "Apply", onClick: () => {} }}
|
|
20
|
+
secondaryButton={{ label: "Reset", onClick: () => {} }}
|
|
21
|
+
/>
|
|
22
|
+
</Drawer>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Best Practices
|
|
28
|
+
|
|
29
|
+
- Keep drawers task-focused and easy to dismiss.
|
|
30
|
+
- Use drawers for secondary workflows instead of primary actions.
|
|
31
|
+
- Avoid stacking multiple drawers.
|
|
32
|
+
- Use `sliding` when people need to resize the drawer from the exposed edge without shifting the main drawer surface.
|
|
33
|
+
|
|
34
|
+
## Props
|
|
35
|
+
|
|
36
|
+
### Drawer
|
|
37
|
+
|
|
38
|
+
| Prop | Type | Default | Description |
|
|
39
|
+
| --- | --- | --- | --- |
|
|
40
|
+
| `isOpen` | `boolean` | _Required_ | Controls visibility. |
|
|
41
|
+
| `onClose` | `() => void` | _Required_ | Close handler. |
|
|
42
|
+
| `position` | `"left" \| "right"` | `"right"` | Drawer side. |
|
|
43
|
+
| `overlay` | `boolean` | `true` | Show an overlay behind the drawer. |
|
|
44
|
+
| `closeOnOverlayClick` | `boolean` | `true` | Close on overlay click. |
|
|
45
|
+
| `closeOnEscape` | `boolean` | `true` | Close on Escape. |
|
|
46
|
+
| `size` | `"sm" \| "md" \| "lg" \| "xl" \| string \| number` | `"md"` | Drawer width. Can be a preset size or a custom CSS value (e.g., "500px"). |
|
|
47
|
+
| `sliding` | `boolean` | `false` | Enables a resize handle that sits just outside the drawer edge. Right drawers expose the handle on the left; left drawers expose it on the right. |
|
|
48
|
+
| `minWidth` | `number \| \`${number}px\`` | `280` | Minimum width when `sliding` is enabled. |
|
|
49
|
+
| `maxWidth` | `number \| \`${number}px\`` | `viewport width - 48px` | Maximum width when `sliding` is enabled. |
|
|
50
|
+
| `overlayClassName` | `string` | | Extra overlay class. |
|
|
51
|
+
| `allowContentOverflow` | `boolean` | `false` | Allow content to overflow. |
|
|
52
|
+
| `floating` | `boolean` | `false` | Renders the drawer with a margin around it. |
|
|
53
|
+
| `className` | `string` | | Custom class name. |
|
|
54
|
+
|
|
55
|
+
### DrawerHeader
|
|
56
|
+
|
|
57
|
+
| Prop | Type | Default | Description |
|
|
58
|
+
| --- | --- | --- | --- |
|
|
59
|
+
| `showCloseButton` | `boolean` | `true` | Show close icon. |
|
|
60
|
+
| `closeButtonAriaLabel` | `string` | `"Close drawer"` | Close button aria label. |
|
|
61
|
+
| `rightContent` | `ReactNode` | | Right-side header content. |
|
|
62
|
+
| `subheader` | `ReactNode` | | Secondary header text. |
|
|
63
|
+
| `bordered` | `boolean` | `true` | Show header divider. |
|
|
64
|
+
| `onClose` | `() => void` | _Required_ | Close handler. |
|
|
65
|
+
|
|
66
|
+
### DrawerContent
|
|
67
|
+
|
|
68
|
+
| Prop | Type | Default | Description |
|
|
69
|
+
| --- | --- | --- | --- |
|
|
70
|
+
| `noPadding` | `boolean` | `false` | Remove default padding. |
|
|
71
|
+
|
|
72
|
+
### DrawerFooter
|
|
73
|
+
|
|
74
|
+
| Prop | Type | Default | Description |
|
|
75
|
+
| --- | --- | --- | --- |
|
|
76
|
+
| `primaryButton` | `FooterButtonProps` | | Primary action. |
|
|
77
|
+
| `secondaryButton` | `FooterButtonProps` | | Secondary action. |
|
|
78
|
+
| `dangerButton` | `FooterButtonProps` | | Destructive action. |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { stories } from "../examples";
|
package/examples.tsx
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Drawer } from "./src/Drawer";
|
|
3
|
+
import type { DrawerProps } from "./src/Drawer.types";
|
|
4
|
+
import { Button } from "@solara/button";
|
|
5
|
+
|
|
6
|
+
type DrawerStoryProps = DrawerProps & {
|
|
7
|
+
noPadding?: boolean;
|
|
8
|
+
showSubheader?: boolean;
|
|
9
|
+
showRightContent?: boolean;
|
|
10
|
+
floating?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const DrawerPlayground = (props: DrawerStoryProps) => {
|
|
14
|
+
const isControlled = typeof props.isOpen === "boolean";
|
|
15
|
+
const [open, setOpen] = React.useState(props.isOpen ?? false);
|
|
16
|
+
|
|
17
|
+
React.useEffect(() => {
|
|
18
|
+
if (isControlled) {
|
|
19
|
+
setOpen(props.isOpen as boolean);
|
|
20
|
+
}
|
|
21
|
+
}, [isControlled, props.isOpen]);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
|
25
|
+
<Button label="Open drawer" onClick={() => setOpen(true)} />
|
|
26
|
+
<Drawer
|
|
27
|
+
isOpen={open}
|
|
28
|
+
onClose={() => setOpen(false)}
|
|
29
|
+
position={props.position}
|
|
30
|
+
overlay={props.overlay}
|
|
31
|
+
closeOnOverlayClick={props.closeOnOverlayClick}
|
|
32
|
+
closeOnEscape={props.closeOnEscape}
|
|
33
|
+
size={props.size}
|
|
34
|
+
sliding={props.sliding}
|
|
35
|
+
minWidth={props.minWidth}
|
|
36
|
+
maxWidth={props.maxWidth}
|
|
37
|
+
allowContentOverflow={props.allowContentOverflow}
|
|
38
|
+
floating={props.floating}
|
|
39
|
+
>
|
|
40
|
+
<Drawer.Header
|
|
41
|
+
onClose={() => setOpen(false)}
|
|
42
|
+
subheader={
|
|
43
|
+
props.showSubheader ? (
|
|
44
|
+
<span style={{ fontSize: "0.75rem", color: "var(--color-text-secondary)" }}>
|
|
45
|
+
Updated just now
|
|
46
|
+
</span>
|
|
47
|
+
) : undefined
|
|
48
|
+
}
|
|
49
|
+
rightContent={
|
|
50
|
+
props.showRightContent ? (
|
|
51
|
+
<span style={{ fontSize: "0.75rem", color: "var(--color-text-secondary)" }}>
|
|
52
|
+
3 filters
|
|
53
|
+
</span>
|
|
54
|
+
) : null
|
|
55
|
+
}
|
|
56
|
+
>
|
|
57
|
+
<Drawer.Title>Filters</Drawer.Title>
|
|
58
|
+
</Drawer.Header>
|
|
59
|
+
<Drawer.Content noPadding={props.noPadding}>
|
|
60
|
+
<div style={{ display: "grid", gap: "0.75rem" }}>
|
|
61
|
+
<p style={{ margin: 0 }}>
|
|
62
|
+
Add filters, secondary tasks, or contextual notes without leaving the current page.
|
|
63
|
+
</p>
|
|
64
|
+
<ul style={{ margin: 0, paddingLeft: "1.25rem" }}>
|
|
65
|
+
<li>Status: Active</li>
|
|
66
|
+
<li>Owner: Design</li>
|
|
67
|
+
<li>Last updated: 2 days ago</li>
|
|
68
|
+
</ul>
|
|
69
|
+
</div>
|
|
70
|
+
</Drawer.Content>
|
|
71
|
+
<Drawer.Footer
|
|
72
|
+
primaryButton={{ label: "Apply", onClick: () => setOpen(false) }}
|
|
73
|
+
secondaryButton={{ label: "Reset", onClick: () => setOpen(false) }}
|
|
74
|
+
/>
|
|
75
|
+
</Drawer>
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const DrawerSlidingStory = () => {
|
|
81
|
+
const [open, setOpen] = React.useState(false);
|
|
82
|
+
return (
|
|
83
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
|
84
|
+
<Button label="Open sliding drawer" onClick={() => setOpen(true)} />
|
|
85
|
+
<Drawer
|
|
86
|
+
isOpen={open}
|
|
87
|
+
onClose={() => setOpen(false)}
|
|
88
|
+
position="right"
|
|
89
|
+
size="lg"
|
|
90
|
+
sliding
|
|
91
|
+
minWidth={320}
|
|
92
|
+
maxWidth={720}
|
|
93
|
+
>
|
|
94
|
+
<Drawer.Header onClose={() => setOpen(false)}>
|
|
95
|
+
<Drawer.Title>Resizable drawer</Drawer.Title>
|
|
96
|
+
</Drawer.Header>
|
|
97
|
+
<Drawer.Content>
|
|
98
|
+
<p style={{ marginTop: 0 }}>
|
|
99
|
+
Drag the handle that sits outside the drawer edge to resize it without moving the main content area.
|
|
100
|
+
</p>
|
|
101
|
+
<p style={{ marginBottom: 0 }}>
|
|
102
|
+
On right drawers the handle appears on the left edge. On left drawers it appears on the right edge.
|
|
103
|
+
</p>
|
|
104
|
+
</Drawer.Content>
|
|
105
|
+
</Drawer>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const DrawerHeaderStory = () => {
|
|
111
|
+
const [open, setOpen] = React.useState(false);
|
|
112
|
+
return (
|
|
113
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
|
114
|
+
<Button label="Open drawer" onClick={() => setOpen(true)} />
|
|
115
|
+
<Drawer isOpen={open} onClose={() => setOpen(false)} overlay closeOnOverlayClick closeOnEscape>
|
|
116
|
+
<Drawer.Header
|
|
117
|
+
onClose={() => setOpen(false)}
|
|
118
|
+
subheader={<span style={{ fontSize: "0.75rem" }}>Header-only</span>}
|
|
119
|
+
rightContent={<span style={{ fontSize: "0.75rem" }}>3 filters</span>}
|
|
120
|
+
>
|
|
121
|
+
<Drawer.Title>Header example</Drawer.Title>
|
|
122
|
+
</Drawer.Header>
|
|
123
|
+
</Drawer>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const DrawerFooterStory = () => {
|
|
129
|
+
const [open, setOpen] = React.useState(false);
|
|
130
|
+
return (
|
|
131
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
|
132
|
+
<Button label="Open drawer" onClick={() => setOpen(true)} />
|
|
133
|
+
<Drawer isOpen={open} onClose={() => setOpen(false)} overlay closeOnOverlayClick closeOnEscape>
|
|
134
|
+
<Drawer.Footer
|
|
135
|
+
primaryButton={{ label: "Apply", onClick: () => setOpen(false) }}
|
|
136
|
+
secondaryButton={{ label: "Cancel", onClick: () => setOpen(false) }}
|
|
137
|
+
/>
|
|
138
|
+
</Drawer>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const DrawerNoOverlayStory = () => {
|
|
144
|
+
const [open, setOpen] = React.useState(false);
|
|
145
|
+
return (
|
|
146
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
|
147
|
+
<Button label="Open drawer (no overlay)" onClick={() => setOpen(true)} />
|
|
148
|
+
<Drawer isOpen={open} onClose={() => setOpen(false)} overlay={false}>
|
|
149
|
+
<Drawer.Header onClose={() => setOpen(false)}>
|
|
150
|
+
<Drawer.Title>Drawer without overlay</Drawer.Title>
|
|
151
|
+
</Drawer.Header>
|
|
152
|
+
<Drawer.Content>
|
|
153
|
+
<p>This drawer does not have an overlay.</p>
|
|
154
|
+
</Drawer.Content>
|
|
155
|
+
</Drawer>
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const DrawerFloatingStory = () => {
|
|
161
|
+
const [open, setOpen] = React.useState(false);
|
|
162
|
+
return (
|
|
163
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
|
164
|
+
<Button label="Open floating drawer" onClick={() => setOpen(true)} />
|
|
165
|
+
<Drawer isOpen={open} onClose={() => setOpen(false)} floating>
|
|
166
|
+
<Drawer.Header onClose={() => setOpen(false)}>
|
|
167
|
+
<Drawer.Title>Floating Drawer</Drawer.Title>
|
|
168
|
+
</Drawer.Header>
|
|
169
|
+
<Drawer.Content>
|
|
170
|
+
<p>This drawer has a margin around it.</p>
|
|
171
|
+
</Drawer.Content>
|
|
172
|
+
</Drawer>
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const DrawerCustomWidthStory = () => {
|
|
178
|
+
const [open, setOpen] = React.useState(false);
|
|
179
|
+
return (
|
|
180
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
|
181
|
+
<Button label="Open drawer (custom width)" onClick={() => setOpen(true)} />
|
|
182
|
+
<Drawer isOpen={open} onClose={() => setOpen(false)} size="500px">
|
|
183
|
+
<Drawer.Header onClose={() => setOpen(false)}>
|
|
184
|
+
<Drawer.Title>Drawer with custom width</Drawer.Title>
|
|
185
|
+
</Drawer.Header>
|
|
186
|
+
<Drawer.Content>
|
|
187
|
+
<p>This drawer has a custom width of 500px.</p>
|
|
188
|
+
</Drawer.Content>
|
|
189
|
+
</Drawer>
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export const stories = {
|
|
195
|
+
playground: {
|
|
196
|
+
title: "Playground",
|
|
197
|
+
description: "Adjust the Drawer props to explore behavior.",
|
|
198
|
+
render: (props: DrawerStoryProps) => <DrawerPlayground {...props} />,
|
|
199
|
+
controls: [
|
|
200
|
+
{ name: "size", type: "select", options: ["sm", "md", "lg", "xl"] },
|
|
201
|
+
{ name: "position", type: "select", options: ["left", "right"] },
|
|
202
|
+
{ name: "floating", type: "boolean" },
|
|
203
|
+
{ name: "sliding", type: "boolean" },
|
|
204
|
+
{ name: "minWidth", type: "text" },
|
|
205
|
+
{ name: "maxWidth", type: "text" },
|
|
206
|
+
{ name: "overlay", type: "boolean" },
|
|
207
|
+
{ name: "closeOnOverlayClick", type: "boolean" },
|
|
208
|
+
{ name: "closeOnEscape", type: "boolean" },
|
|
209
|
+
{ name: "allowContentOverflow", type: "boolean" },
|
|
210
|
+
{ name: "noPadding", type: "boolean" },
|
|
211
|
+
{ name: "showSubheader", type: "boolean" },
|
|
212
|
+
{ name: "showRightContent", type: "boolean" },
|
|
213
|
+
{ name: "isOpen", type: "boolean" },
|
|
214
|
+
],
|
|
215
|
+
initialProps: {
|
|
216
|
+
size: "md",
|
|
217
|
+
position: "right",
|
|
218
|
+
floating: false,
|
|
219
|
+
sliding: false,
|
|
220
|
+
minWidth: 280,
|
|
221
|
+
maxWidth: 960,
|
|
222
|
+
overlay: true,
|
|
223
|
+
closeOnOverlayClick: true,
|
|
224
|
+
closeOnEscape: true,
|
|
225
|
+
allowContentOverflow: false,
|
|
226
|
+
noPadding: false,
|
|
227
|
+
showSubheader: true,
|
|
228
|
+
showRightContent: true,
|
|
229
|
+
isOpen: false,
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
sliding: {
|
|
233
|
+
title: "Sliding",
|
|
234
|
+
description: "Resizable drawer with an external drag handle anchored to the exposed edge.",
|
|
235
|
+
showProps: false,
|
|
236
|
+
render: () => <DrawerSlidingStory />,
|
|
237
|
+
code: `import React from "react";
|
|
238
|
+
import { Button } from "@solara/button";
|
|
239
|
+
import { Drawer } from "@solara/drawer";
|
|
240
|
+
|
|
241
|
+
export function Example() {
|
|
242
|
+
const [open, setOpen] = React.useState(false);
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
|
246
|
+
<Button label="Open sliding drawer" onClick={() => setOpen(true)} />
|
|
247
|
+
<Drawer
|
|
248
|
+
isOpen={open}
|
|
249
|
+
onClose={() => setOpen(false)}
|
|
250
|
+
position="right"
|
|
251
|
+
size="lg"
|
|
252
|
+
sliding
|
|
253
|
+
minWidth={320}
|
|
254
|
+
maxWidth={720}
|
|
255
|
+
>
|
|
256
|
+
<Drawer.Header onClose={() => setOpen(false)}>
|
|
257
|
+
<Drawer.Title>Resizable drawer</Drawer.Title>
|
|
258
|
+
</Drawer.Header>
|
|
259
|
+
<Drawer.Content>
|
|
260
|
+
<p>Drag the outside handle to resize the drawer.</p>
|
|
261
|
+
</Drawer.Content>
|
|
262
|
+
</Drawer>
|
|
263
|
+
</div>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
`,
|
|
267
|
+
},
|
|
268
|
+
header: {
|
|
269
|
+
title: "Header",
|
|
270
|
+
description: "Header-only configuration with subheader and right content.",
|
|
271
|
+
showProps: false,
|
|
272
|
+
render: () => <DrawerHeaderStory />,
|
|
273
|
+
code: `import React from "react";
|
|
274
|
+
import { Button } from "@solara/button";
|
|
275
|
+
import { Drawer } from "@solara/drawer";
|
|
276
|
+
|
|
277
|
+
export function Example() {
|
|
278
|
+
const [open, setOpen] = React.useState(false);
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
|
282
|
+
<Button label="Open drawer" onClick={() => setOpen(true)} />
|
|
283
|
+
<Drawer isOpen={open} onClose={() => setOpen(false)} overlay closeOnOverlayClick closeOnEscape>
|
|
284
|
+
<Drawer.Header
|
|
285
|
+
onClose={() => setOpen(false)}
|
|
286
|
+
subheader={<span style={{ fontSize: "0.75rem" }}>Header-only</span>}
|
|
287
|
+
rightContent={<span style={{ fontSize: "0.75rem" }}>3 filters</span>}
|
|
288
|
+
>
|
|
289
|
+
<Drawer.Title>Header example</Drawer.Title>
|
|
290
|
+
</Drawer.Header>
|
|
291
|
+
</Drawer>
|
|
292
|
+
</div>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
`,
|
|
296
|
+
},
|
|
297
|
+
footer: {
|
|
298
|
+
title: "Footer",
|
|
299
|
+
description: "Footer-only configuration with primary/secondary actions.",
|
|
300
|
+
showProps: false,
|
|
301
|
+
render: () => <DrawerFooterStory />,
|
|
302
|
+
code: `import React from "react";
|
|
303
|
+
import { Button } from "@solara/button";
|
|
304
|
+
import { Drawer } from "@solara/drawer";
|
|
305
|
+
|
|
306
|
+
export function Example() {
|
|
307
|
+
const [open, setOpen] = React.useState(false);
|
|
308
|
+
|
|
309
|
+
return (
|
|
310
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
|
311
|
+
<Button label="Open drawer" onClick={() => setOpen(true)} />
|
|
312
|
+
<Drawer isOpen={open} onClose={() => setOpen(false)} overlay closeOnOverlayClick closeOnEscape>
|
|
313
|
+
<Drawer.Footer
|
|
314
|
+
primaryButton={{ label: "Apply", onClick: () => setOpen(false) }}
|
|
315
|
+
secondaryButton={{ label: "Cancel", onClick: () => setOpen(false) }}
|
|
316
|
+
/>
|
|
317
|
+
</Drawer>
|
|
318
|
+
</div>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
`,
|
|
322
|
+
},
|
|
323
|
+
noOverlay: {
|
|
324
|
+
title: "No Overlay",
|
|
325
|
+
description: "Drawer without a background overlay.",
|
|
326
|
+
showProps: false,
|
|
327
|
+
render: () => <DrawerNoOverlayStory />,
|
|
328
|
+
code: `import React from "react";
|
|
329
|
+
import { Button } from "@solara/button";
|
|
330
|
+
import { Drawer } from "@solara/drawer";
|
|
331
|
+
|
|
332
|
+
export function Example() {
|
|
333
|
+
const [open, setOpen] = React.useState(false);
|
|
334
|
+
|
|
335
|
+
return (
|
|
336
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
|
337
|
+
<Button label="Open drawer (no overlay)" onClick={() => setOpen(true)} />
|
|
338
|
+
<Drawer isOpen={open} onClose={() => setOpen(false)} overlay={false}>
|
|
339
|
+
<Drawer.Header onClose={() => setOpen(false)}>
|
|
340
|
+
<Drawer.Title>Drawer without overlay</Drawer.Title>
|
|
341
|
+
</Drawer.Header>
|
|
342
|
+
<Drawer.Content>
|
|
343
|
+
<p>This drawer does not have an overlay.</p>
|
|
344
|
+
</Drawer.Content>
|
|
345
|
+
</Drawer>
|
|
346
|
+
</div>
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
`,
|
|
350
|
+
},
|
|
351
|
+
floating: {
|
|
352
|
+
title: "Floating",
|
|
353
|
+
description: "Drawer with a margin around it.",
|
|
354
|
+
showProps: false,
|
|
355
|
+
render: () => <DrawerFloatingStory />,
|
|
356
|
+
code: `import React from "react";
|
|
357
|
+
import { Button } from "@solara/button";
|
|
358
|
+
import { Drawer } from "@solara/drawer";
|
|
359
|
+
|
|
360
|
+
export function Example() {
|
|
361
|
+
const [open, setOpen] = React.useState(false);
|
|
362
|
+
|
|
363
|
+
return (
|
|
364
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
|
365
|
+
<Button label="Open floating drawer" onClick={() => setOpen(true)} />
|
|
366
|
+
<Drawer isOpen={open} onClose={() => setOpen(false)} floating>
|
|
367
|
+
<Drawer.Header onClose={() => setOpen(false)}>
|
|
368
|
+
<Drawer.Title>Floating Drawer</Drawer.Title>
|
|
369
|
+
</Drawer.Header>
|
|
370
|
+
<Drawer.Content>
|
|
371
|
+
<p>This drawer has a margin around it.</p>
|
|
372
|
+
</Drawer.Content>
|
|
373
|
+
</Drawer>
|
|
374
|
+
</div>
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
`,
|
|
378
|
+
},
|
|
379
|
+
customWidth: {
|
|
380
|
+
title: "Custom Width",
|
|
381
|
+
description: "Drawer with a custom width.",
|
|
382
|
+
showProps: false,
|
|
383
|
+
render: () => <DrawerCustomWidthStory />,
|
|
384
|
+
code: `import React from "react";
|
|
385
|
+
import { Button } from "@solara/button";
|
|
386
|
+
import { Drawer } from "@solara/drawer";
|
|
387
|
+
|
|
388
|
+
export function Example() {
|
|
389
|
+
const [open, setOpen] = React.useState(false);
|
|
390
|
+
|
|
391
|
+
return (
|
|
392
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
|
393
|
+
<Button label="Open drawer (custom width)" onClick={() => setOpen(true)} />
|
|
394
|
+
<Drawer isOpen={open} onClose={() => setOpen(false)} size="500px">
|
|
395
|
+
<Drawer.Header onClose={() => setOpen(false)}>
|
|
396
|
+
<Drawer.Title>Drawer with custom width</Drawer.Title>
|
|
397
|
+
</Drawer.Header>
|
|
398
|
+
<Drawer.Content>
|
|
399
|
+
<p>This drawer has a custom width of 500px.</p>
|
|
400
|
+
</Drawer.Content>
|
|
401
|
+
</Drawer>
|
|
402
|
+
</div>
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
`,
|
|
406
|
+
},
|
|
407
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@varialkit/drawer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts",
|
|
9
|
+
"./examples": "./examples/index.tsx"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@varialkit/button": "0.1.0"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"docs.md",
|
|
17
|
+
"examples",
|
|
18
|
+
"examples.tsx"
|
|
19
|
+
],
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"react": "^19.0.0",
|
|
22
|
+
"react-dom": "^19.0.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/react": "19.0.10",
|
|
26
|
+
"react": "19.0.0",
|
|
27
|
+
"react-dom": "19.0.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/Drawer.scss
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
@keyframes solara-drawer-fade-in {
|
|
2
|
+
from {
|
|
3
|
+
opacity: 0;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
to {
|
|
7
|
+
opacity: 1;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@keyframes solara-drawer-slide-in-right {
|
|
12
|
+
from {
|
|
13
|
+
transform: translateX(32px);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
to {
|
|
17
|
+
transform: translateX(0);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@keyframes solara-drawer-slide-in-left {
|
|
22
|
+
from {
|
|
23
|
+
transform: translateX(-32px);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
to {
|
|
27
|
+
transform: translateX(0);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.solara-drawer__overlay {
|
|
32
|
+
position: fixed;
|
|
33
|
+
inset: 0;
|
|
34
|
+
background-color: rgba(var(--overlay-backdrop-rgb), var(--overlay-backdrop-opacity));
|
|
35
|
+
z-index: 3000;
|
|
36
|
+
backdrop-filter: blur(var(--overlay-backdrop-blur));
|
|
37
|
+
animation: solara-drawer-fade-in 0.2s ease-out;
|
|
38
|
+
|
|
39
|
+
@media (prefers-reduced-motion: reduce) {
|
|
40
|
+
animation: none;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.solara-drawer {
|
|
45
|
+
position: fixed;
|
|
46
|
+
top: 0;
|
|
47
|
+
bottom: 0;
|
|
48
|
+
width: 100%;
|
|
49
|
+
max-width: 28rem;
|
|
50
|
+
--drawer-surface-color: var(--color-surface-100);
|
|
51
|
+
--drawer-surface-color-rgb: var(--color-surface-100-rgb);
|
|
52
|
+
--surface-border-color: var(--color-divider-secondary);
|
|
53
|
+
--surface-border-color-rgb: var(--color-divider-secondary-rgb);
|
|
54
|
+
--surface-opacity: 1;
|
|
55
|
+
--surface-blur: 0px;
|
|
56
|
+
--surface-shadow: var(--shadow-md);
|
|
57
|
+
--drawer-resize-grip-color: color-mix(in srgb, var(--color-text-secondary) 78%, var(--color-divider-primary));
|
|
58
|
+
--drawer-resize-grip-hover-color: color-mix(in srgb, var(--color-text-primary) 72%, var(--color-divider-primary));
|
|
59
|
+
--drawer-resize-grip-ring: color-mix(in srgb, var(--color-surface-0) 92%, var(--color-divider-primary));
|
|
60
|
+
background-color: rgba(var(--drawer-surface-color-rgb), var(--surface-opacity));
|
|
61
|
+
border-left: 1px solid var(--surface-border-color);
|
|
62
|
+
border-right: 1px solid var(--surface-border-color);
|
|
63
|
+
box-shadow: var(--surface-shadow);
|
|
64
|
+
backdrop-filter: blur(var(--surface-blur));
|
|
65
|
+
display: flex;
|
|
66
|
+
flex-direction: column;
|
|
67
|
+
overflow: hidden;
|
|
68
|
+
animation: solara-drawer-slide-in-right 0.24s ease-out;
|
|
69
|
+
|
|
70
|
+
@media (prefers-reduced-motion: reduce) {
|
|
71
|
+
animation: none;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.solara-drawer--left {
|
|
76
|
+
left: 0;
|
|
77
|
+
border-left: none;
|
|
78
|
+
animation-name: solara-drawer-slide-in-left;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.solara-drawer--right {
|
|
82
|
+
right: 0;
|
|
83
|
+
border-right: none;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.solara-drawer--overflow {
|
|
87
|
+
overflow: visible;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.solara-drawer--sliding {
|
|
91
|
+
overflow: visible;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.solara-drawer--size-sm {
|
|
95
|
+
max-width: 20rem;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.solara-drawer--size-md {
|
|
99
|
+
max-width: 28rem;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.solara-drawer--size-lg {
|
|
103
|
+
max-width: 36rem;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.solara-drawer--size-xl {
|
|
107
|
+
max-width: 48rem;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@media (max-width: 640px) {
|
|
111
|
+
.solara-drawer {
|
|
112
|
+
max-width: calc(100vw - 1rem);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.solara-drawer--floating {
|
|
117
|
+
top: 1rem;
|
|
118
|
+
bottom: 1rem;
|
|
119
|
+
border-radius: var(--radius-lg);
|
|
120
|
+
|
|
121
|
+
&.solara-drawer--left {
|
|
122
|
+
left: 1rem;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
&.solara-drawer--right {
|
|
126
|
+
right: 1rem;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.solara-drawer__resize-handle {
|
|
131
|
+
position: absolute;
|
|
132
|
+
top: 0;
|
|
133
|
+
bottom: 0;
|
|
134
|
+
width: 20px;
|
|
135
|
+
padding: 0;
|
|
136
|
+
border: none;
|
|
137
|
+
background: transparent;
|
|
138
|
+
cursor: ew-resize;
|
|
139
|
+
touch-action: none;
|
|
140
|
+
z-index: 2;
|
|
141
|
+
display: flex;
|
|
142
|
+
align-items: center;
|
|
143
|
+
justify-content: center;
|
|
144
|
+
|
|
145
|
+
&:focus-visible {
|
|
146
|
+
outline: none;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.solara-drawer__resize-grip {
|
|
151
|
+
display: block;
|
|
152
|
+
width: 6px;
|
|
153
|
+
height: 60px;
|
|
154
|
+
border-radius: 9999px;
|
|
155
|
+
background-color: var(--drawer-resize-grip-color);
|
|
156
|
+
box-shadow: 0 0 0 1px var(--drawer-resize-grip-ring);
|
|
157
|
+
transition: background-color 0.2s ease, transform 0.2s ease;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.solara-drawer__resize-handle:hover .solara-drawer__resize-grip,
|
|
161
|
+
.solara-drawer__resize-handle:focus-visible .solara-drawer__resize-grip,
|
|
162
|
+
.solara-drawer__resize-handle:active .solara-drawer__resize-grip {
|
|
163
|
+
background-color: var(--drawer-resize-grip-hover-color);
|
|
164
|
+
transform: scaleX(1.08);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.solara-drawer__resize-handle--left {
|
|
168
|
+
left: -18px;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.solara-drawer__resize-handle--right {
|
|
172
|
+
right: -18px;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.solara-drawer--floating .solara-drawer__resize-handle {
|
|
176
|
+
top: calc(var(--space-4) * -1);
|
|
177
|
+
bottom: calc(var(--space-4) * -1);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.solara-drawer__header {
|
|
181
|
+
padding: calc(var(--space-4) * var(--spacing-multiplier)) calc(var(--space-5) * var(--spacing-multiplier));
|
|
182
|
+
display: flex;
|
|
183
|
+
align-items: flex-start;
|
|
184
|
+
justify-content: space-between;
|
|
185
|
+
gap: calc(var(--space-3) * var(--spacing-multiplier));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.solara-drawer__header--bordered {
|
|
189
|
+
border-bottom: 1px solid var(--surface-border-color);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.solara-drawer__header-content {
|
|
193
|
+
flex: 1;
|
|
194
|
+
min-width: 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.solara-drawer__header-actions {
|
|
198
|
+
display: flex;
|
|
199
|
+
align-items: center;
|
|
200
|
+
gap: calc(var(--space-2) * var(--spacing-multiplier));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.solara-drawer__title {
|
|
204
|
+
margin: 0;
|
|
205
|
+
font-size: var(--font-size-h5-scaled);
|
|
206
|
+
line-height: var(--line-height-body-scaled);
|
|
207
|
+
font-weight: 600;
|
|
208
|
+
color: var(--color-text-primary);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.solara-drawer__subheader {
|
|
212
|
+
margin-top: calc(var(--space-1) * var(--spacing-multiplier));
|
|
213
|
+
color: var(--color-text-secondary);
|
|
214
|
+
font-size: var(--font-size-caption-scaled);
|
|
215
|
+
line-height: var(--line-height-caption-scaled);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.solara-drawer__close {
|
|
219
|
+
color: var(--color-text-secondary);
|
|
220
|
+
transition: color 0.2s ease, background-color 0.2s ease;
|
|
221
|
+
border-radius: 9999px;
|
|
222
|
+
width: 32px;
|
|
223
|
+
height: 32px;
|
|
224
|
+
display: flex;
|
|
225
|
+
align-items: center;
|
|
226
|
+
justify-content: center;
|
|
227
|
+
background: none;
|
|
228
|
+
border: none;
|
|
229
|
+
cursor: pointer;
|
|
230
|
+
padding: 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.solara-drawer__close:hover,
|
|
234
|
+
.solara-drawer__close:focus-visible {
|
|
235
|
+
color: var(--color-text-primary);
|
|
236
|
+
background-color: var(--color-surface-200);
|
|
237
|
+
outline: none;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.solara-drawer__content {
|
|
241
|
+
padding: calc(var(--space-4) * var(--spacing-multiplier)) calc(var(--space-5) * var(--spacing-multiplier));
|
|
242
|
+
overflow-y: auto;
|
|
243
|
+
overflow-x: visible;
|
|
244
|
+
flex: 1;
|
|
245
|
+
color: var(--color-text-secondary);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.solara-drawer__content--overflow {
|
|
249
|
+
overflow: visible;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.solara-drawer__content--no-padding {
|
|
253
|
+
padding: 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.solara-drawer__footer {
|
|
257
|
+
padding: calc(var(--space-4) * var(--spacing-multiplier)) calc(var(--space-5) * var(--spacing-multiplier));
|
|
258
|
+
border-top: 1px solid var(--surface-border-color);
|
|
259
|
+
background-color: rgba(var(--drawer-surface-color-rgb), var(--surface-opacity));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
:root[data-surface-default="translucent"] .solara-drawer,
|
|
263
|
+
:root[data-surface-drawer="translucent"] .solara-drawer,
|
|
264
|
+
.solara-drawer[data-surface-style="translucent"] {
|
|
265
|
+
--surface-opacity: var(--opacity-translucent-medium);
|
|
266
|
+
--surface-blur: 0px;
|
|
267
|
+
--drawer-surface-color-rgb: var(--surface-translucent-tint-rgb);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
:root[data-surface-default="glass"] .solara-drawer,
|
|
271
|
+
:root[data-surface-drawer="glass"] .solara-drawer,
|
|
272
|
+
.solara-drawer[data-surface-style="glass"] {
|
|
273
|
+
--surface-opacity: var(--opacity-translucent-heavy);
|
|
274
|
+
--surface-blur: var(--blur-medium);
|
|
275
|
+
--surface-border-color: rgba(var(--surface-border-color-rgb), var(--surface-border-alpha-glass));
|
|
276
|
+
--drawer-surface-color-rgb: var(--surface-translucent-tint-rgb);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.solara-drawer[data-surface-style="solid"] {
|
|
280
|
+
--surface-opacity: 1;
|
|
281
|
+
--surface-blur: 0px;
|
|
282
|
+
--surface-border-color: var(--color-divider-secondary);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
:root[data-surface-drawer="solid"] .solara-drawer {
|
|
286
|
+
--surface-opacity: 1;
|
|
287
|
+
--surface-blur: 0px;
|
|
288
|
+
--surface-border-color: var(--color-divider-secondary);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
:root[data-surface-default="translucent"] .solara-drawer__footer,
|
|
292
|
+
:root[data-surface-drawer="translucent"] .solara-drawer__footer,
|
|
293
|
+
.solara-drawer[data-surface-style="translucent"] .solara-drawer__footer,
|
|
294
|
+
:root[data-surface-default="glass"] .solara-drawer__footer,
|
|
295
|
+
:root[data-surface-drawer="glass"] .solara-drawer__footer,
|
|
296
|
+
.solara-drawer[data-surface-style="glass"] .solara-drawer__footer {
|
|
297
|
+
background-color: transparent;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
:root[data-surface-default="translucent"] .solara-drawer,
|
|
301
|
+
:root[data-surface-drawer="translucent"] .solara-drawer,
|
|
302
|
+
.solara-drawer[data-surface-style="translucent"],
|
|
303
|
+
:root[data-surface-default="glass"] .solara-drawer,
|
|
304
|
+
:root[data-surface-drawer="glass"] .solara-drawer,
|
|
305
|
+
.solara-drawer[data-surface-style="glass"] {
|
|
306
|
+
overflow: hidden;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
:root[data-surface-default="translucent"] .solara-drawer--sliding,
|
|
310
|
+
:root[data-surface-drawer="translucent"] .solara-drawer--sliding,
|
|
311
|
+
.solara-drawer--sliding[data-surface-style="translucent"],
|
|
312
|
+
:root[data-surface-default="glass"] .solara-drawer--sliding,
|
|
313
|
+
:root[data-surface-drawer="glass"] .solara-drawer--sliding,
|
|
314
|
+
.solara-drawer--sliding[data-surface-style="glass"] {
|
|
315
|
+
overflow: visible;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.solara-drawer__footer-actions {
|
|
319
|
+
display: flex;
|
|
320
|
+
justify-content: flex-end;
|
|
321
|
+
gap: calc(var(--space-3) * var(--spacing-multiplier));
|
|
322
|
+
flex-wrap: wrap;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
@media (max-width: 640px) {
|
|
326
|
+
.solara-drawer__footer-actions {
|
|
327
|
+
flex-direction: column;
|
|
328
|
+
width: 100%;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.solara-drawer__footer-actions>* {
|
|
332
|
+
width: 100%;
|
|
333
|
+
}
|
|
334
|
+
}
|
package/src/Drawer.tsx
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { createContext, forwardRef, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
import { Button } from "@solara/button";
|
|
6
|
+
import type {
|
|
7
|
+
DrawerContentProps,
|
|
8
|
+
DrawerFooterProps,
|
|
9
|
+
DrawerHeaderProps,
|
|
10
|
+
DrawerProps,
|
|
11
|
+
DrawerTitleProps,
|
|
12
|
+
FooterButtonProps,
|
|
13
|
+
DrawerSize,
|
|
14
|
+
DrawerSide,
|
|
15
|
+
DrawerResizeConstraint,
|
|
16
|
+
} from "./Drawer.types";
|
|
17
|
+
import "./Drawer.scss";
|
|
18
|
+
|
|
19
|
+
const classNames = (...classes: Array<string | undefined | false | null>) =>
|
|
20
|
+
classes.filter(Boolean).join(" ");
|
|
21
|
+
|
|
22
|
+
const PRESET_DRAWER_WIDTHS: Record<DrawerSize, number> = {
|
|
23
|
+
sm: 320,
|
|
24
|
+
md: 448,
|
|
25
|
+
lg: 576,
|
|
26
|
+
xl: 768,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const DEFAULT_MIN_WIDTH = 280;
|
|
30
|
+
const DEFAULT_MAX_WIDTH_OFFSET = 48;
|
|
31
|
+
|
|
32
|
+
const parseResizeConstraint = (value: DrawerResizeConstraint | undefined) => {
|
|
33
|
+
if (typeof value === "number") return value;
|
|
34
|
+
if (typeof value === "string") {
|
|
35
|
+
const parsed = Number.parseFloat(value);
|
|
36
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const getInitialDrawerWidth = (size: DrawerProps["size"]) => {
|
|
42
|
+
if (typeof size === "number") return size;
|
|
43
|
+
if (typeof size === "string") {
|
|
44
|
+
if (size in PRESET_DRAWER_WIDTHS) {
|
|
45
|
+
return PRESET_DRAWER_WIDTHS[size as DrawerSize];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const parsed = Number.parseFloat(size);
|
|
49
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return PRESET_DRAWER_WIDTHS.md;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const clampDrawerWidth = (width: number, minWidth: number, maxWidth: number) =>
|
|
56
|
+
Math.min(Math.max(width, minWidth), maxWidth);
|
|
57
|
+
|
|
58
|
+
const DrawerContext = createContext<{ allowContentOverflow?: boolean }>({});
|
|
59
|
+
|
|
60
|
+
const CloseIcon = () => (
|
|
61
|
+
<svg viewBox="0 0 20 20" width="16" height="16" aria-hidden="true">
|
|
62
|
+
<path
|
|
63
|
+
d="M5 5l10 10M15 5L5 15"
|
|
64
|
+
stroke="currentColor"
|
|
65
|
+
strokeWidth="1.6"
|
|
66
|
+
strokeLinecap="round"
|
|
67
|
+
/>
|
|
68
|
+
</svg>
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const DrawerHeader = forwardRef<HTMLDivElement, DrawerHeaderProps>(
|
|
72
|
+
(
|
|
73
|
+
{
|
|
74
|
+
className,
|
|
75
|
+
children,
|
|
76
|
+
showCloseButton = true,
|
|
77
|
+
closeButtonAriaLabel = "Close drawer",
|
|
78
|
+
rightContent,
|
|
79
|
+
subheader,
|
|
80
|
+
bordered = true,
|
|
81
|
+
onClose,
|
|
82
|
+
...props
|
|
83
|
+
},
|
|
84
|
+
ref
|
|
85
|
+
) => (
|
|
86
|
+
<div
|
|
87
|
+
className={classNames(
|
|
88
|
+
"solara-drawer__header",
|
|
89
|
+
bordered ? "solara-drawer__header--bordered" : undefined,
|
|
90
|
+
className
|
|
91
|
+
)}
|
|
92
|
+
ref={ref}
|
|
93
|
+
{...props}
|
|
94
|
+
>
|
|
95
|
+
<div className="solara-drawer__header-content">
|
|
96
|
+
{children}
|
|
97
|
+
{subheader ? <div className="solara-drawer__subheader">{subheader}</div> : null}
|
|
98
|
+
</div>
|
|
99
|
+
<div className="solara-drawer__header-actions">
|
|
100
|
+
{rightContent}
|
|
101
|
+
{showCloseButton ? (
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
onClick={onClose}
|
|
105
|
+
aria-label={closeButtonAriaLabel}
|
|
106
|
+
className="solara-drawer__close"
|
|
107
|
+
>
|
|
108
|
+
<CloseIcon />
|
|
109
|
+
</button>
|
|
110
|
+
) : null}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
)
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const DrawerTitle = forwardRef<HTMLHeadingElement, DrawerTitleProps>(
|
|
117
|
+
({ className, ...props }, ref) => (
|
|
118
|
+
<h2 className={classNames("solara-drawer__title", className)} ref={ref} {...props} />
|
|
119
|
+
)
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>(
|
|
123
|
+
({ className, noPadding, ...props }, ref) => {
|
|
124
|
+
const { allowContentOverflow } = React.useContext(DrawerContext);
|
|
125
|
+
return (
|
|
126
|
+
<div
|
|
127
|
+
className={classNames(
|
|
128
|
+
"solara-drawer__content",
|
|
129
|
+
allowContentOverflow ? "solara-drawer__content--overflow" : undefined,
|
|
130
|
+
noPadding ? "solara-drawer__content--no-padding" : undefined,
|
|
131
|
+
className
|
|
132
|
+
)}
|
|
133
|
+
ref={ref}
|
|
134
|
+
{...props}
|
|
135
|
+
/>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const DrawerFooter = forwardRef<HTMLDivElement, DrawerFooterProps>(
|
|
141
|
+
({ className, primaryButton, secondaryButton, dangerButton, ...props }, ref) => {
|
|
142
|
+
const renderButton = (button: FooterButtonProps, kind: "primary" | "secondary" | "danger") => {
|
|
143
|
+
const variant = kind === "primary" ? "primary" : "default";
|
|
144
|
+
const destructive = kind === "danger";
|
|
145
|
+
const isLoading = Boolean(button.loading);
|
|
146
|
+
const label = isLoading && button.loadingText ? button.loadingText : button.label;
|
|
147
|
+
const style = button.fullWidth ? { width: "100%" } : undefined;
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<Button
|
|
151
|
+
key={button.label}
|
|
152
|
+
type="button"
|
|
153
|
+
variant={variant}
|
|
154
|
+
destructive={destructive}
|
|
155
|
+
radius={button.radius}
|
|
156
|
+
onClick={button.onClick}
|
|
157
|
+
disabled={button.disabled || isLoading}
|
|
158
|
+
style={style}
|
|
159
|
+
label={label}
|
|
160
|
+
/>
|
|
161
|
+
);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<div className={classNames("solara-drawer__footer", className)} ref={ref} {...props}>
|
|
166
|
+
<div className="solara-drawer__footer-actions">
|
|
167
|
+
{secondaryButton ? renderButton(secondaryButton, "secondary") : null}
|
|
168
|
+
{dangerButton ? renderButton(dangerButton, "danger") : null}
|
|
169
|
+
{primaryButton ? renderButton(primaryButton, "primary") : null}
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const DrawerComponent = forwardRef<HTMLDivElement, DrawerProps>(
|
|
177
|
+
(
|
|
178
|
+
{
|
|
179
|
+
isOpen,
|
|
180
|
+
onClose,
|
|
181
|
+
children,
|
|
182
|
+
position = "right",
|
|
183
|
+
overlay = true,
|
|
184
|
+
closeOnOverlayClick = true,
|
|
185
|
+
closeOnOutsideClick = true,
|
|
186
|
+
closeOnEscape = true,
|
|
187
|
+
size = "md",
|
|
188
|
+
sliding = false,
|
|
189
|
+
minWidth,
|
|
190
|
+
maxWidth,
|
|
191
|
+
className,
|
|
192
|
+
overlayClassName,
|
|
193
|
+
allowContentOverflow = false,
|
|
194
|
+
floating = false,
|
|
195
|
+
surfaceStyle,
|
|
196
|
+
style,
|
|
197
|
+
...props
|
|
198
|
+
},
|
|
199
|
+
ref
|
|
200
|
+
) => {
|
|
201
|
+
const drawerRef = useRef<HTMLDivElement>(null);
|
|
202
|
+
const dragStateRef = useRef<{ pointerId: number; rafId: number | null } | null>(null);
|
|
203
|
+
const [drawerWidth, setDrawerWidth] = useState<number | undefined>(() => getInitialDrawerWidth(size));
|
|
204
|
+
React.useImperativeHandle(ref, () => drawerRef.current as HTMLDivElement);
|
|
205
|
+
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
if (!sliding) return;
|
|
208
|
+
setDrawerWidth(getInitialDrawerWidth(size));
|
|
209
|
+
}, [size, sliding]);
|
|
210
|
+
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (!isOpen) return;
|
|
213
|
+
const originalOverflow = document.body.style.overflow;
|
|
214
|
+
document.body.style.overflow = "hidden";
|
|
215
|
+
return () => {
|
|
216
|
+
document.body.style.overflow = originalOverflow;
|
|
217
|
+
};
|
|
218
|
+
}, [isOpen]);
|
|
219
|
+
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
if (!isOpen || !closeOnEscape) return;
|
|
222
|
+
const handleEscape = (event: KeyboardEvent) => {
|
|
223
|
+
if (event.key === "Escape") onClose();
|
|
224
|
+
};
|
|
225
|
+
document.addEventListener("keydown", handleEscape);
|
|
226
|
+
return () => document.removeEventListener("keydown", handleEscape);
|
|
227
|
+
}, [isOpen, closeOnEscape, onClose]);
|
|
228
|
+
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
if (!isOpen || !closeOnOutsideClick) return;
|
|
231
|
+
|
|
232
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
233
|
+
if (drawerRef.current && !drawerRef.current.contains(event.target as Node)) {
|
|
234
|
+
onClose();
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
239
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
240
|
+
}, [isOpen, closeOnOutsideClick, onClose]);
|
|
241
|
+
|
|
242
|
+
const contextValue = useMemo(
|
|
243
|
+
() => ({
|
|
244
|
+
allowContentOverflow,
|
|
245
|
+
}),
|
|
246
|
+
[allowContentOverflow]
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
return () => {
|
|
251
|
+
const dragState = dragStateRef.current;
|
|
252
|
+
if (dragState?.rafId !== null && dragState?.rafId !== undefined) {
|
|
253
|
+
window.cancelAnimationFrame(dragState.rafId);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
}, []);
|
|
257
|
+
|
|
258
|
+
if (!isOpen) return null;
|
|
259
|
+
|
|
260
|
+
const isPresetSize = ["sm", "md", "lg", "xl"].includes(size as string);
|
|
261
|
+
const resolvedMinWidth = parseResizeConstraint(minWidth) ?? DEFAULT_MIN_WIDTH;
|
|
262
|
+
const viewportMaxWidth = typeof window === "undefined" ? Number.POSITIVE_INFINITY : window.innerWidth - DEFAULT_MAX_WIDTH_OFFSET;
|
|
263
|
+
const resolvedMaxWidth = parseResizeConstraint(maxWidth) ?? viewportMaxWidth;
|
|
264
|
+
const normalizedMaxWidth = Math.max(resolvedMinWidth, resolvedMaxWidth);
|
|
265
|
+
|
|
266
|
+
const drawerClasses = classNames(
|
|
267
|
+
"solara-drawer",
|
|
268
|
+
`solara-drawer--${position}`,
|
|
269
|
+
isPresetSize ? `solara-drawer--size-${size}` : undefined,
|
|
270
|
+
allowContentOverflow ? "solara-drawer--overflow" : undefined,
|
|
271
|
+
floating ? "solara-drawer--floating" : undefined,
|
|
272
|
+
sliding ? "solara-drawer--sliding" : undefined,
|
|
273
|
+
className
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
// Surface treatment stays theme-driven via data attributes and CSS variables.
|
|
277
|
+
const surfaceAttributes: Record<string, string | undefined> = {};
|
|
278
|
+
const surfaceStyleVars: React.CSSProperties & Record<string, string | undefined> = {};
|
|
279
|
+
|
|
280
|
+
if (surfaceStyle) {
|
|
281
|
+
if (typeof surfaceStyle === "string") {
|
|
282
|
+
surfaceAttributes["data-surface-style"] = surfaceStyle;
|
|
283
|
+
} else {
|
|
284
|
+
surfaceAttributes["data-surface-style"] = "custom";
|
|
285
|
+
if (surfaceStyle.opacity !== undefined) {
|
|
286
|
+
surfaceStyleVars["--surface-opacity"] = surfaceStyle.opacity.toString();
|
|
287
|
+
}
|
|
288
|
+
if (surfaceStyle.blur !== undefined) {
|
|
289
|
+
surfaceStyleVars["--surface-blur"] = `${surfaceStyle.blur}px`;
|
|
290
|
+
}
|
|
291
|
+
if (surfaceStyle.borderColor !== undefined) {
|
|
292
|
+
surfaceStyleVars["--surface-border-color"] = surfaceStyle.borderColor;
|
|
293
|
+
}
|
|
294
|
+
if (surfaceStyle.shadow !== undefined) {
|
|
295
|
+
surfaceStyleVars["--surface-shadow"] = surfaceStyle.shadow;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const resolvedWidth = sliding
|
|
301
|
+
? clampDrawerWidth(drawerWidth ?? getInitialDrawerWidth(size) ?? PRESET_DRAWER_WIDTHS.md, resolvedMinWidth, normalizedMaxWidth)
|
|
302
|
+
: undefined;
|
|
303
|
+
|
|
304
|
+
const drawerStyle: React.CSSProperties = {
|
|
305
|
+
...(!sliding && !isPresetSize ? { width: size } : {}),
|
|
306
|
+
...(sliding ? { width: resolvedWidth, maxWidth: normalizedMaxWidth, minWidth: resolvedMinWidth } : {}),
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// Dragging measures from the viewport edge the drawer is anchored to.
|
|
310
|
+
const updateWidthFromPointer = (clientX: number) => {
|
|
311
|
+
const nextWidth = position === "right" ? window.innerWidth - clientX : clientX;
|
|
312
|
+
setDrawerWidth(clampDrawerWidth(nextWidth, resolvedMinWidth, normalizedMaxWidth));
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const handleResizePointerDown = (event: React.PointerEvent<HTMLButtonElement>) => {
|
|
316
|
+
if (!sliding) return;
|
|
317
|
+
|
|
318
|
+
event.preventDefault();
|
|
319
|
+
event.stopPropagation();
|
|
320
|
+
|
|
321
|
+
const target = event.currentTarget;
|
|
322
|
+
target.setPointerCapture(event.pointerId);
|
|
323
|
+
|
|
324
|
+
dragStateRef.current = { pointerId: event.pointerId, rafId: null };
|
|
325
|
+
|
|
326
|
+
// Use one animation frame per pointer tick burst to keep resize responsive without over-rendering.
|
|
327
|
+
const handlePointerMove = (moveEvent: PointerEvent) => {
|
|
328
|
+
if (!dragStateRef.current || moveEvent.pointerId !== dragStateRef.current.pointerId) return;
|
|
329
|
+
|
|
330
|
+
if (dragStateRef.current.rafId !== null) {
|
|
331
|
+
window.cancelAnimationFrame(dragStateRef.current.rafId);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
dragStateRef.current.rafId = window.requestAnimationFrame(() => {
|
|
335
|
+
updateWidthFromPointer(moveEvent.clientX);
|
|
336
|
+
if (dragStateRef.current) {
|
|
337
|
+
dragStateRef.current.rafId = null;
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const cleanupPointerSession = (pointerId: number) => {
|
|
343
|
+
const dragState = dragStateRef.current;
|
|
344
|
+
if (dragState?.rafId !== null && dragState?.rafId !== undefined) {
|
|
345
|
+
window.cancelAnimationFrame(dragState.rafId);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
dragStateRef.current = null;
|
|
349
|
+
target.releasePointerCapture(pointerId);
|
|
350
|
+
window.removeEventListener("pointermove", handlePointerMove);
|
|
351
|
+
window.removeEventListener("pointerup", handlePointerUp);
|
|
352
|
+
window.removeEventListener("pointercancel", handlePointerCancel);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const handlePointerUp = (upEvent: PointerEvent) => {
|
|
356
|
+
if (upEvent.pointerId !== event.pointerId) return;
|
|
357
|
+
cleanupPointerSession(upEvent.pointerId);
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const handlePointerCancel = (cancelEvent: PointerEvent) => {
|
|
361
|
+
if (cancelEvent.pointerId !== event.pointerId) return;
|
|
362
|
+
cleanupPointerSession(cancelEvent.pointerId);
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
window.addEventListener("pointermove", handlePointerMove);
|
|
366
|
+
window.addEventListener("pointerup", handlePointerUp);
|
|
367
|
+
window.addEventListener("pointercancel", handlePointerCancel);
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const content = (
|
|
371
|
+
<div
|
|
372
|
+
ref={drawerRef}
|
|
373
|
+
className={drawerClasses}
|
|
374
|
+
style={{ ...drawerStyle, ...surfaceStyleVars, ...style }}
|
|
375
|
+
role="dialog"
|
|
376
|
+
aria-modal={overlay ? "true" : undefined}
|
|
377
|
+
onClick={(event) => event.stopPropagation()}
|
|
378
|
+
{...surfaceAttributes}
|
|
379
|
+
{...props}
|
|
380
|
+
>
|
|
381
|
+
{sliding ? (
|
|
382
|
+
<button
|
|
383
|
+
type="button"
|
|
384
|
+
aria-label={`Resize ${position} drawer`}
|
|
385
|
+
className={classNames(
|
|
386
|
+
"solara-drawer__resize-handle",
|
|
387
|
+
position === "right"
|
|
388
|
+
? "solara-drawer__resize-handle--left"
|
|
389
|
+
: "solara-drawer__resize-handle--right"
|
|
390
|
+
)}
|
|
391
|
+
onPointerDown={handleResizePointerDown}
|
|
392
|
+
onClick={(event) => event.stopPropagation()}
|
|
393
|
+
>
|
|
394
|
+
<span aria-hidden="true" className="solara-drawer__resize-grip" />
|
|
395
|
+
</button>
|
|
396
|
+
) : null}
|
|
397
|
+
<DrawerContext.Provider value={contextValue}>{children}</DrawerContext.Provider>
|
|
398
|
+
</div>
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
if (!overlay) {
|
|
402
|
+
return createPortal(content, document.body);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return createPortal(
|
|
406
|
+
<div
|
|
407
|
+
className={classNames("solara-drawer__overlay", overlayClassName)}
|
|
408
|
+
onClick={closeOnOverlayClick ? onClose : undefined}
|
|
409
|
+
>
|
|
410
|
+
{content}
|
|
411
|
+
</div>,
|
|
412
|
+
document.body
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
DrawerComponent.displayName = "Drawer";
|
|
418
|
+
DrawerHeader.displayName = "Drawer.Header";
|
|
419
|
+
DrawerTitle.displayName = "Drawer.Title";
|
|
420
|
+
DrawerContent.displayName = "Drawer.Content";
|
|
421
|
+
DrawerFooter.displayName = "Drawer.Footer";
|
|
422
|
+
|
|
423
|
+
interface DrawerCompoundComponent
|
|
424
|
+
extends React.ForwardRefExoticComponent<DrawerProps & React.RefAttributes<HTMLDivElement>> {
|
|
425
|
+
Header: typeof DrawerHeader;
|
|
426
|
+
Title: typeof DrawerTitle;
|
|
427
|
+
Content: typeof DrawerContent;
|
|
428
|
+
Footer: typeof DrawerFooter;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const Drawer = Object.assign(DrawerComponent, {
|
|
432
|
+
Header: DrawerHeader,
|
|
433
|
+
Title: DrawerTitle,
|
|
434
|
+
Content: DrawerContent,
|
|
435
|
+
Footer: DrawerFooter,
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
export { Drawer, DrawerHeader, DrawerTitle, DrawerContent, DrawerFooter };
|
|
439
|
+
export type {
|
|
440
|
+
DrawerProps,
|
|
441
|
+
DrawerHeaderProps,
|
|
442
|
+
DrawerTitleProps,
|
|
443
|
+
DrawerContentProps,
|
|
444
|
+
DrawerFooterProps,
|
|
445
|
+
DrawerSize,
|
|
446
|
+
DrawerSide,
|
|
447
|
+
FooterButtonProps,
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
export default Drawer as DrawerCompoundComponent;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
|
|
3
|
+
export type DrawerSize = "sm" | "md" | "lg" | "xl";
|
|
4
|
+
export type DrawerSide = "left" | "right";
|
|
5
|
+
|
|
6
|
+
export type DrawerResizeConstraint = number | `${number}px`;
|
|
7
|
+
|
|
8
|
+
export type DrawerSurfaceStyle =
|
|
9
|
+
| "solid"
|
|
10
|
+
| "translucent"
|
|
11
|
+
| "glass"
|
|
12
|
+
| {
|
|
13
|
+
opacity?: number;
|
|
14
|
+
blur?: number;
|
|
15
|
+
borderColor?: string;
|
|
16
|
+
shadow?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export interface FooterButtonProps {
|
|
20
|
+
label: string;
|
|
21
|
+
variant?: "primary" | "secondary" | "danger";
|
|
22
|
+
onClick: (event?: React.MouseEvent<HTMLButtonElement>) => void;
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
loading?: boolean;
|
|
25
|
+
loadingText?: string;
|
|
26
|
+
fullWidth?: boolean;
|
|
27
|
+
radius?: "default" | "none" | "full";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DrawerHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
31
|
+
showCloseButton?: boolean;
|
|
32
|
+
closeButtonAriaLabel?: string;
|
|
33
|
+
rightContent?: React.ReactNode;
|
|
34
|
+
subheader?: React.ReactNode;
|
|
35
|
+
bordered?: boolean;
|
|
36
|
+
onClose: () => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface DrawerTitleProps extends React.HTMLAttributes<HTMLHeadingElement> { }
|
|
40
|
+
|
|
41
|
+
export interface DrawerContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
42
|
+
noPadding?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface DrawerFooterProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
46
|
+
primaryButton?: FooterButtonProps;
|
|
47
|
+
secondaryButton?: FooterButtonProps;
|
|
48
|
+
dangerButton?: FooterButtonProps;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface DrawerProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
52
|
+
isOpen: boolean;
|
|
53
|
+
onClose: () => void;
|
|
54
|
+
position?: DrawerSide;
|
|
55
|
+
overlay?: boolean;
|
|
56
|
+
closeOnOverlayClick?: boolean;
|
|
57
|
+
closeOnEscape?: boolean;
|
|
58
|
+
size?: DrawerSize | (string & {}) | number;
|
|
59
|
+
sliding?: boolean;
|
|
60
|
+
minWidth?: DrawerResizeConstraint;
|
|
61
|
+
maxWidth?: DrawerResizeConstraint;
|
|
62
|
+
overlayClassName?: string;
|
|
63
|
+
allowContentOverflow?: boolean;
|
|
64
|
+
children?: React.ReactNode;
|
|
65
|
+
floating?: boolean;
|
|
66
|
+
closeOnOutsideClick?: boolean;
|
|
67
|
+
/** Controls surface material treatment without changing layout tokens. */
|
|
68
|
+
surfaceStyle?: DrawerSurfaceStyle;
|
|
69
|
+
}
|