@astryxdesign/lab 0.4.5-canary.a7ba3a0 → 0.4.5-canary.c2bb31b
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/Drawer/Drawer.d.ts +46 -44
- package/dist/Drawer/Drawer.d.ts.map +1 -1
- package/dist/Drawer/Drawer.js +85 -170
- package/dist/lab.css +7 -15
- package/package.json +3 -3
- package/src/Drawer/Drawer.doc.mjs +171 -59
- package/src/Drawer/Drawer.test.tsx +138 -196
- package/src/Drawer/Drawer.tsx +138 -273
package/dist/Drawer/Drawer.d.ts
CHANGED
|
@@ -4,10 +4,17 @@
|
|
|
4
4
|
* @output Exports Drawer component and DrawerProps
|
|
5
5
|
* @position Lab implementation; consumed by index.ts, tested by Drawer.test.tsx, demonstrated in Storybook
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* the
|
|
10
|
-
* (
|
|
7
|
+
* Overlay panel for inspectors and detail views — the "click a table row,
|
|
8
|
+
* see its details" pattern. Slides in from the inline start or end edge and
|
|
9
|
+
* floats above the page content: unlike a docked panel it never reflows the
|
|
10
|
+
* layout underneath, it overlays it (with or without a scrim).
|
|
11
|
+
*
|
|
12
|
+
* Inline axis only (start/end). Block-axis sheets are BottomSheet's job;
|
|
13
|
+
* a drawer is always a full-height side panel.
|
|
14
|
+
*
|
|
15
|
+
* Sizing is viewport-aware: `width` is the desktop budget, and below
|
|
16
|
+
* the mobile breakpoint it preserves a 56px reveal of the page behind, capped
|
|
17
|
+
* by the requested width (or fills the viewport with `isFullWidthOnMobile`).
|
|
11
18
|
*
|
|
12
19
|
* Uses the native `<dialog>` element (same precedent as Dialog/MobileNav):
|
|
13
20
|
* - `showModal()` when `hasScrim` (default) — top-layer rendering, focus
|
|
@@ -36,36 +43,42 @@ export interface DrawerProps extends BaseProps<HTMLDialogElement> {
|
|
|
36
43
|
/** Ref forwarded to the root <dialog> element */
|
|
37
44
|
ref?: React.Ref<HTMLDialogElement>;
|
|
38
45
|
/**
|
|
39
|
-
* Whether the drawer is open. Fully controlled — pair with `
|
|
46
|
+
* Whether the drawer is open. Fully controlled — pair with `onOpenChange`.
|
|
40
47
|
*/
|
|
41
48
|
isOpen: boolean;
|
|
42
49
|
/**
|
|
43
|
-
* Called when the drawer requests
|
|
44
|
-
*
|
|
45
|
-
* open state. When sibling drawers are open, Escape only closes the
|
|
46
|
-
*
|
|
50
|
+
* Called when the drawer requests an open-state change. Escape, scrim
|
|
51
|
+
* click, and the built-in close button call it with `false`. The caller owns
|
|
52
|
+
* the open state. When sibling drawers are open, Escape only closes the top
|
|
53
|
+
* (last-opened) drawer.
|
|
47
54
|
*/
|
|
48
|
-
|
|
55
|
+
onOpenChange: (isOpen: boolean) => void;
|
|
49
56
|
/**
|
|
50
57
|
* Which edge the drawer slides from.
|
|
51
58
|
* - `'end'` — inline-end edge (right in LTR) — the inspector convention
|
|
52
59
|
* - `'start'` — inline-start edge (left in LTR)
|
|
53
|
-
* - `'top'` / `'bottom'` — full-width sheets on the block axis
|
|
54
60
|
* @default 'end'
|
|
55
61
|
*/
|
|
56
|
-
side?: 'start' | 'end'
|
|
62
|
+
side?: 'start' | 'end';
|
|
57
63
|
/**
|
|
58
|
-
*
|
|
59
|
-
* `
|
|
60
|
-
*
|
|
61
|
-
*
|
|
64
|
+
* Desktop width budget. A number is pixels; a string is any CSS length
|
|
65
|
+
* (`'50%'`, `'32rem'`). Below the mobile breakpoint (640px), this
|
|
66
|
+
* remains the maximum while the drawer preserves a 56px reveal of the page
|
|
67
|
+
* behind — see `isFullWidthOnMobile`.
|
|
62
68
|
* @default 400
|
|
63
69
|
*/
|
|
64
|
-
|
|
70
|
+
width?: number | string;
|
|
71
|
+
/**
|
|
72
|
+
* Whether the drawer covers the full viewport width on mobile
|
|
73
|
+
* (below 640px) instead of preserving the default 56px reveal of the page
|
|
74
|
+
* behind. The reveal makes the drawer read as an overlay rather than a
|
|
75
|
+
* navigation.
|
|
76
|
+
* @default false
|
|
77
|
+
*/
|
|
78
|
+
isFullWidthOnMobile?: boolean;
|
|
65
79
|
/**
|
|
66
80
|
* Accessible label for the drawer (required — the drawer has no
|
|
67
|
-
* built-in heading to derive a name from).
|
|
68
|
-
* collapse/expand affordances.
|
|
81
|
+
* built-in heading to derive a name from).
|
|
69
82
|
*/
|
|
70
83
|
label: string;
|
|
71
84
|
/**
|
|
@@ -79,24 +92,11 @@ export interface DrawerProps extends BaseProps<HTMLDialogElement> {
|
|
|
79
92
|
hasScrim?: boolean;
|
|
80
93
|
/**
|
|
81
94
|
* Whether to render the built-in close button in the top-trailing
|
|
82
|
-
* corner.
|
|
83
|
-
*
|
|
84
|
-
* @default
|
|
95
|
+
* corner. Enabled by default for both modal and non-modal drawers so every
|
|
96
|
+
* overlay has an obvious dismissal affordance.
|
|
97
|
+
* @default true
|
|
85
98
|
*/
|
|
86
99
|
hasCloseButton?: boolean;
|
|
87
|
-
/**
|
|
88
|
-
* Collapse the drawer to a narrow click-to-expand rail. Only supported
|
|
89
|
-
* for non-modal (`hasScrim={false}`) drawers with `side="start"/"end"`;
|
|
90
|
-
* ignored (with a dev warning) otherwise. Controlled — pair with
|
|
91
|
-
* `onCollapsedChange`.
|
|
92
|
-
*/
|
|
93
|
-
isCollapsed?: boolean;
|
|
94
|
-
/**
|
|
95
|
-
* Called when the built-in collapse/expand affordances are used.
|
|
96
|
-
* Providing it renders a collapse toggle next to the close button while
|
|
97
|
-
* expanded; the collapsed rail always expands on click.
|
|
98
|
-
*/
|
|
99
|
-
onCollapsedChange?: (collapsed: boolean) => void;
|
|
100
100
|
/**
|
|
101
101
|
* Drawer content. Rendered inside a full-height scrollable area.
|
|
102
102
|
* Focus the element with `data-autofocus` on open, if present.
|
|
@@ -108,27 +108,29 @@ export interface DrawerProps extends BaseProps<HTMLDialogElement> {
|
|
|
108
108
|
'data-testid'?: string;
|
|
109
109
|
}
|
|
110
110
|
/**
|
|
111
|
-
* An
|
|
111
|
+
* An overlay panel for inspectors and detail views.
|
|
112
112
|
*
|
|
113
|
-
* Slides in from the logical start
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* `
|
|
113
|
+
* Slides in from the logical start or end edge and floats above the page
|
|
114
|
+
* using the native `<dialog>` element: modal with a scrim by default, or a
|
|
115
|
+
* non-modal overlay with `hasScrim={false}` that leaves the page behind
|
|
116
|
+
* interactive. `width` is the desktop budget; below 640px the panel preserves
|
|
117
|
+
* a 56px page reveal without exceeding that budget (or fills the viewport
|
|
118
|
+
* with `isFullWidthOnMobile`). Escape
|
|
119
|
+
* closes the top-most open drawer; focus returns to the element that
|
|
120
|
+
* opened it.
|
|
119
121
|
*
|
|
120
122
|
* @example
|
|
121
123
|
* ```
|
|
122
124
|
* const [selected, setSelected] = useState(null);
|
|
123
125
|
* <Drawer
|
|
124
126
|
* isOpen={selected != null}
|
|
125
|
-
*
|
|
127
|
+
* onOpenChange={isOpen => !isOpen && setSelected(null)}
|
|
126
128
|
* label={`Details: ${selected?.name}`}>
|
|
127
129
|
* <HostDetails host={selected} />
|
|
128
130
|
* </Drawer>
|
|
129
131
|
* ```
|
|
130
132
|
*/
|
|
131
|
-
export declare function Drawer({ isOpen,
|
|
133
|
+
export declare function Drawer({ isOpen, onOpenChange, side, width, isFullWidthOnMobile, label, hasScrim, hasCloseButton, children, xstyle, className, style, onClick: onClickProp, onKeyDown: onKeyDownProp, ref, ...props }: DrawerProps): import("react").JSX.Element;
|
|
132
134
|
export declare namespace Drawer {
|
|
133
135
|
var displayName: string;
|
|
134
136
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Drawer.d.ts","sourceRoot":"","sources":["../../src/Drawer/Drawer.tsx"],"names":[],"mappings":"AAIA
|
|
1
|
+
{"version":3,"file":"Drawer.d.ts","sourceRoot":"","sources":["../../src/Drawer/Drawer.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,OAAO,EAML,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AAsOlD,MAAM,WAAW,WAAY,SAAQ,SAAS,CAAC,iBAAiB,CAAC;IAC/D,iDAAiD;IACjD,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IAEnC;;OAEG;IACH,MAAM,EAAE,OAAO,CAAC;IAEhB;;;;;OAKG;IACH,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAExC;;;;;OAKG;IACH,IAAI,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC;IAEvB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAExB;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAE9B;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB;;;OAGG;IACH,QAAQ,EAAE,SAAS,CAAC;IAEpB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAMD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,MAAM,CAAC,EACrB,MAAM,EACN,YAAY,EACZ,IAAY,EACZ,KAAW,EACX,mBAA2B,EAC3B,KAAK,EACL,QAAe,EACf,cAAqB,EACrB,QAAQ,EACR,MAAM,EACN,SAAS,EACT,KAAK,EACL,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,aAAa,EACxB,GAAG,EACH,GAAG,KAAK,EACT,EAAE,WAAW,+BAmMb;yBApNe,MAAM"}
|
package/dist/Drawer/Drawer.js
CHANGED
|
@@ -8,10 +8,17 @@
|
|
|
8
8
|
* @output Exports Drawer component and DrawerProps
|
|
9
9
|
* @position Lab implementation; consumed by index.ts, tested by Drawer.test.tsx, demonstrated in Storybook
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* the
|
|
14
|
-
* (
|
|
11
|
+
* Overlay panel for inspectors and detail views — the "click a table row,
|
|
12
|
+
* see its details" pattern. Slides in from the inline start or end edge and
|
|
13
|
+
* floats above the page content: unlike a docked panel it never reflows the
|
|
14
|
+
* layout underneath, it overlays it (with or without a scrim).
|
|
15
|
+
*
|
|
16
|
+
* Inline axis only (start/end). Block-axis sheets are BottomSheet's job;
|
|
17
|
+
* a drawer is always a full-height side panel.
|
|
18
|
+
*
|
|
19
|
+
* Sizing is viewport-aware: `width` is the desktop budget, and below
|
|
20
|
+
* the mobile breakpoint it preserves a 56px reveal of the page behind, capped
|
|
21
|
+
* by the requested width (or fills the viewport with `isFullWidthOnMobile`).
|
|
15
22
|
*
|
|
16
23
|
* Uses the native `<dialog>` element (same precedent as Dialog/MobileNav):
|
|
17
24
|
* - `showModal()` when `hasScrim` (default) — top-layer rendering, focus
|
|
@@ -37,11 +44,11 @@
|
|
|
37
44
|
import { useCallback, useEffect, useId, useRef, useState } from 'react';
|
|
38
45
|
import * as stylex from '@stylexjs/stylex';
|
|
39
46
|
import '@astryxdesign/core/theme/tokens.stylex';
|
|
40
|
-
import { borderVars, colorVars, durationVars, easeVars, shadowVars, spacingVars
|
|
47
|
+
import { borderVars, colorVars, durationVars, easeVars, shadowVars, spacingVars } from '@astryxdesign/core/theme/tokens.stylex';
|
|
41
48
|
import { Icon } from '@astryxdesign/core/Icon';
|
|
42
49
|
import { IconButton } from '@astryxdesign/core/IconButton';
|
|
43
|
-
import { useScrollLock
|
|
44
|
-
import { mergeProps, mergeRefs, themeProps } from '@astryxdesign/core/utils';
|
|
50
|
+
import { useScrollLock } from '@astryxdesign/core/hooks';
|
|
51
|
+
import { composeEventHandlers, mergeProps, mergeRefs, themeProps } from '@astryxdesign/core/utils';
|
|
45
52
|
import { overlayPaddingReset } from '@astryxdesign/core/Layout';
|
|
46
53
|
|
|
47
54
|
// =============================================================================
|
|
@@ -86,14 +93,22 @@ function isTopDrawer(id) {
|
|
|
86
93
|
// Styles
|
|
87
94
|
// =============================================================================
|
|
88
95
|
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
|
|
96
|
+
// Below this viewport width the drawer preserves a fixed reveal of the page
|
|
97
|
+
// behind instead of growing proportionally with the viewport. 640px is the
|
|
98
|
+
// repo's mobile breakpoint.
|
|
99
|
+
const MOBILE_BREAKPOINT = 640;
|
|
100
|
+
|
|
101
|
+
// Material's established mobile drawer pattern leaves a 56dp reveal. Using
|
|
102
|
+
// the same value in CSS pixels gives the overlay a stable visual relationship
|
|
103
|
+
// to the page behind while the requested width remains an upper bound.
|
|
104
|
+
const MOBILE_PAGE_REVEAL = 56;
|
|
105
|
+
const MOBILE_WIDTH_FULL = '100dvw';
|
|
92
106
|
const styles = {
|
|
93
107
|
dialog: {
|
|
94
108
|
kVAEAm: "xixxii4",
|
|
95
109
|
kogj98: "x1ghz6dp",
|
|
96
110
|
kmVPX3: "x1717udv",
|
|
111
|
+
kaIpWk: "x2u8bby",
|
|
97
112
|
ks0D6T: "x1x1rfll",
|
|
98
113
|
kskxy: "x7ab17h",
|
|
99
114
|
kB7OPa: "x9f619",
|
|
@@ -103,8 +118,11 @@ const styles = {
|
|
|
103
118
|
kVQacm: "xb3r6kr",
|
|
104
119
|
kZeWKH: "xish69e",
|
|
105
120
|
kI3sdo: "x1a2a7pz",
|
|
121
|
+
k87sOh: "x13vifvy",
|
|
122
|
+
krVfgx: "x1ey2m1c",
|
|
123
|
+
kZKoxP: "xtdtrs8",
|
|
106
124
|
k1xSpc: "x1s85apg",
|
|
107
|
-
k1ekBW: "
|
|
125
|
+
k1ekBW: "xayqnl7",
|
|
108
126
|
kIyJzY: "x80gvsz",
|
|
109
127
|
kAMwcw: "xlr8y92",
|
|
110
128
|
kzIqYQ: "xd00j3c",
|
|
@@ -116,11 +134,8 @@ const styles = {
|
|
|
116
134
|
$$css: true
|
|
117
135
|
},
|
|
118
136
|
end: {
|
|
119
|
-
k87sOh: "x13vifvy",
|
|
120
|
-
krVfgx: "x1ey2m1c",
|
|
121
137
|
kt4wiu: "xtijo5x",
|
|
122
138
|
kLqNvP: "xhi6v0a",
|
|
123
|
-
kZKoxP: "xtdtrs8",
|
|
124
139
|
k2ei4v: "xgbv0en",
|
|
125
140
|
kVhnKS: "x1t7ytsu",
|
|
126
141
|
kGJrpR: "x1j92z86",
|
|
@@ -132,11 +147,8 @@ const styles = {
|
|
|
132
147
|
$$css: true
|
|
133
148
|
},
|
|
134
149
|
start: {
|
|
135
|
-
k87sOh: "x13vifvy",
|
|
136
|
-
krVfgx: "x1ey2m1c",
|
|
137
150
|
kLqNvP: "x1o0tod",
|
|
138
151
|
kt4wiu: "x1woyocn",
|
|
139
|
-
kZKoxP: "xtdtrs8",
|
|
140
152
|
ke9TFa: "xw8tdv1",
|
|
141
153
|
k8ry5P: "x18b5jzi",
|
|
142
154
|
kBCPoo: "x1gejf6u",
|
|
@@ -147,38 +159,6 @@ const styles = {
|
|
|
147
159
|
k3aq6I: "xbryuvx x6mt36l x14gflnl",
|
|
148
160
|
$$css: true
|
|
149
161
|
},
|
|
150
|
-
top: {
|
|
151
|
-
kzqmXN: "x1o6l61p",
|
|
152
|
-
kLqNvP: "x1o0tod",
|
|
153
|
-
kt4wiu: "xtijo5x",
|
|
154
|
-
k87sOh: "x13vifvy",
|
|
155
|
-
krVfgx: "xdd4er5",
|
|
156
|
-
kt9PQ7: "x92x3c3",
|
|
157
|
-
kfdmCh: "x1q0q8m5",
|
|
158
|
-
kL6WhQ: "xw8gpjh",
|
|
159
|
-
k3aq6I: "x105ttfm",
|
|
160
|
-
$$css: true
|
|
161
|
-
},
|
|
162
|
-
topOpen: {
|
|
163
|
-
k3aq6I: "xnn1q72 xub2912",
|
|
164
|
-
$$css: true
|
|
165
|
-
},
|
|
166
|
-
bottom: {
|
|
167
|
-
kzqmXN: "x1o6l61p",
|
|
168
|
-
kLqNvP: "x1o0tod",
|
|
169
|
-
kt4wiu: "xtijo5x",
|
|
170
|
-
krVfgx: "x1ey2m1c",
|
|
171
|
-
k87sOh: "x80663w",
|
|
172
|
-
kEafiO: "x11xkdxz",
|
|
173
|
-
kPef9Z: "x13fuv20",
|
|
174
|
-
kLZC3w: "x1pc3f07",
|
|
175
|
-
k3aq6I: "x1weeur4",
|
|
176
|
-
$$css: true
|
|
177
|
-
},
|
|
178
|
-
bottomOpen: {
|
|
179
|
-
k3aq6I: "xnn1q72 xhbqy3z",
|
|
180
|
-
$$css: true
|
|
181
|
-
},
|
|
182
162
|
scrim: {
|
|
183
163
|
kGyWv1: "xnixb3f",
|
|
184
164
|
kba3nw: "x1abwkk1",
|
|
@@ -192,32 +172,19 @@ const styles = {
|
|
|
192
172
|
scrimOpen: {
|
|
193
173
|
k5sjJv: "xb3n6bw xxiuuzi",
|
|
194
174
|
$$css: true
|
|
195
|
-
},
|
|
196
|
-
collapsedRail: {
|
|
197
|
-
ks0D6T: "x1k2d6hx",
|
|
198
|
-
$$css: true
|
|
199
175
|
}
|
|
200
176
|
};
|
|
201
177
|
const _temp = {
|
|
202
|
-
|
|
203
|
-
"$$css": true
|
|
204
|
-
};
|
|
205
|
-
const _temp2 = {
|
|
206
|
-
kZKoxP: "xtdtrs8",
|
|
178
|
+
ks0D6T: "xlbgzzq",
|
|
207
179
|
"$$css": true
|
|
208
180
|
};
|
|
209
181
|
const dynamicStyles = {
|
|
210
|
-
inlineSize:
|
|
211
|
-
|
|
182
|
+
inlineSize: (desktopWidth, mobileWidth) => [_temp, {
|
|
183
|
+
kzqmXN: (desktopWidth != null ? "x11ymkkh " : desktopWidth) + (mobileWidth != null ? "xci7e5a" : mobileWidth),
|
|
212
184
|
$$css: true
|
|
213
185
|
}, {
|
|
214
|
-
"--x-
|
|
215
|
-
|
|
216
|
-
blockSize: s => [_temp2, {
|
|
217
|
-
kskxy: s != null ? "x1jols5v" : s,
|
|
218
|
-
$$css: true
|
|
219
|
-
}, {
|
|
220
|
-
"--x-maxHeight": (val => typeof val === "number" ? val + "px" : val != null ? val : undefined)(s)
|
|
186
|
+
"--x-1xmrurk": (val => typeof val === "number" ? val + "px" : val != null ? val : undefined)(desktopWidth),
|
|
187
|
+
"--x-fqzwsx": (val => typeof val === "number" ? val + "px" : val != null ? val : undefined)(mobileWidth)
|
|
221
188
|
}],
|
|
222
189
|
stackZ: z => [{
|
|
223
190
|
kY2c9j: z != null ? "xr3buco" : z,
|
|
@@ -236,21 +203,23 @@ const dynamicStyles = {
|
|
|
236
203
|
// =============================================================================
|
|
237
204
|
|
|
238
205
|
/**
|
|
239
|
-
* An
|
|
206
|
+
* An overlay panel for inspectors and detail views.
|
|
240
207
|
*
|
|
241
|
-
* Slides in from the logical start
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
* `
|
|
208
|
+
* Slides in from the logical start or end edge and floats above the page
|
|
209
|
+
* using the native `<dialog>` element: modal with a scrim by default, or a
|
|
210
|
+
* non-modal overlay with `hasScrim={false}` that leaves the page behind
|
|
211
|
+
* interactive. `width` is the desktop budget; below 640px the panel preserves
|
|
212
|
+
* a 56px page reveal without exceeding that budget (or fills the viewport
|
|
213
|
+
* with `isFullWidthOnMobile`). Escape
|
|
214
|
+
* closes the top-most open drawer; focus returns to the element that
|
|
215
|
+
* opened it.
|
|
247
216
|
*
|
|
248
217
|
* @example
|
|
249
218
|
* ```
|
|
250
219
|
* const [selected, setSelected] = useState(null);
|
|
251
220
|
* <Drawer
|
|
252
221
|
* isOpen={selected != null}
|
|
253
|
-
*
|
|
222
|
+
* onOpenChange={isOpen => !isOpen && setSelected(null)}
|
|
254
223
|
* label={`Details: ${selected?.name}`}>
|
|
255
224
|
* <HostDetails host={selected} />
|
|
256
225
|
* </Drawer>
|
|
@@ -258,18 +227,19 @@ const dynamicStyles = {
|
|
|
258
227
|
*/
|
|
259
228
|
export function Drawer({
|
|
260
229
|
isOpen,
|
|
261
|
-
|
|
230
|
+
onOpenChange,
|
|
262
231
|
side = 'end',
|
|
263
|
-
|
|
232
|
+
width = 400,
|
|
233
|
+
isFullWidthOnMobile = false,
|
|
264
234
|
label,
|
|
265
235
|
hasScrim = true,
|
|
266
|
-
hasCloseButton,
|
|
267
|
-
isCollapsed,
|
|
268
|
-
onCollapsedChange,
|
|
236
|
+
hasCloseButton = true,
|
|
269
237
|
children,
|
|
270
238
|
xstyle,
|
|
271
239
|
className,
|
|
272
240
|
style,
|
|
241
|
+
onClick: onClickProp,
|
|
242
|
+
onKeyDown: onKeyDownProp,
|
|
273
243
|
ref,
|
|
274
244
|
...props
|
|
275
245
|
}) {
|
|
@@ -277,23 +247,15 @@ export function Drawer({
|
|
|
277
247
|
const closeTimeoutRef = useRef(null);
|
|
278
248
|
// Element focused when the drawer opened — restored on close.
|
|
279
249
|
const triggerElementRef = useRef(null);
|
|
280
|
-
// Registry identity + latest
|
|
281
|
-
// registration effect doesn't churn on every
|
|
250
|
+
// Registry identity + latest onOpenChange (stable across re-renders so the
|
|
251
|
+
// registration effect doesn't churn on every onOpenChange identity change).
|
|
282
252
|
const drawerId = useId();
|
|
283
|
-
const
|
|
253
|
+
const onOpenChangeRef = useRef(onOpenChange);
|
|
284
254
|
useEffect(() => {
|
|
285
|
-
|
|
286
|
-
}, [
|
|
255
|
+
onOpenChangeRef.current = onOpenChange;
|
|
256
|
+
}, [onOpenChange]);
|
|
287
257
|
// z-index assigned by the registry on open (non-modal stacking only).
|
|
288
258
|
const [stackZ, setStackZ] = useState(NON_MODAL_BASE_Z);
|
|
289
|
-
const isSheet = side === 'top' || side === 'bottom';
|
|
290
|
-
// Collapse only makes sense for a persistent (non-modal) side panel.
|
|
291
|
-
const canCollapse = !hasScrim && !isSheet;
|
|
292
|
-
const collapsed = canCollapse && isCollapsed === true;
|
|
293
|
-
const showCloseButton = hasCloseButton ?? hasScrim;
|
|
294
|
-
|
|
295
|
-
// Dev warning for unsupported collapse combinations.
|
|
296
|
-
useDevWarning('Drawer', '`isCollapsed` is only supported for non-modal drawers ' + '(hasScrim={false}) with side="start" or side="end". The prop is ' + 'ignored.', isCollapsed != null && !canCollapse);
|
|
297
259
|
|
|
298
260
|
// Open/close the native dialog. close() is delayed so the slide-out
|
|
299
261
|
// transition can play; focus restore happens after close() because a
|
|
@@ -365,7 +327,7 @@ export function Drawer({
|
|
|
365
327
|
if (!isOpen) {
|
|
366
328
|
return;
|
|
367
329
|
}
|
|
368
|
-
const z = registerDrawer(drawerId, () =>
|
|
330
|
+
const z = registerDrawer(drawerId, () => onOpenChangeRef.current(false));
|
|
369
331
|
setStackZ(z);
|
|
370
332
|
return () => unregisterDrawer(drawerId);
|
|
371
333
|
}, [isOpen, drawerId]);
|
|
@@ -374,57 +336,40 @@ export function Drawer({
|
|
|
374
336
|
useScrollLock(isOpen && hasScrim);
|
|
375
337
|
|
|
376
338
|
// Escape closes. The native `cancel` event only fires for showModal();
|
|
377
|
-
// this keydown handler covers the non-modal show() path too. Only the
|
|
339
|
+
// this React keydown handler covers the non-modal show() path too. Only the
|
|
378
340
|
// top of the drawer stack closes, so stacked siblings peel off
|
|
379
341
|
// innermost-first.
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const handleKeyDown = event => {
|
|
386
|
-
if (event.key === 'Escape') {
|
|
387
|
-
event.preventDefault();
|
|
388
|
-
if (isTopDrawer(drawerId)) {
|
|
389
|
-
onClose();
|
|
390
|
-
}
|
|
342
|
+
const handleKeyDown = useCallback(event => {
|
|
343
|
+
if (event.key === 'Escape') {
|
|
344
|
+
event.preventDefault();
|
|
345
|
+
if (isTopDrawer(drawerId)) {
|
|
346
|
+
onOpenChange(false);
|
|
391
347
|
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
return () => dialog.removeEventListener('keydown', handleKeyDown);
|
|
395
|
-
}, [isOpen, onClose, drawerId]);
|
|
348
|
+
}
|
|
349
|
+
}, [onOpenChange, drawerId]);
|
|
396
350
|
|
|
397
351
|
// Native cancel event (browser Escape handling) — prevent the browser
|
|
398
|
-
// from closing the dialog directly and route through
|
|
352
|
+
// from closing the dialog directly and route through onOpenChange so the
|
|
399
353
|
// caller's state stays the source of truth. Same top-of-stack rule as
|
|
400
354
|
// the keydown path.
|
|
401
355
|
const handleCancel = useCallback(event => {
|
|
402
356
|
event.preventDefault();
|
|
403
357
|
if (isTopDrawer(drawerId)) {
|
|
404
|
-
|
|
358
|
+
onOpenChange(false);
|
|
405
359
|
}
|
|
406
|
-
}, [
|
|
360
|
+
}, [onOpenChange, drawerId]);
|
|
407
361
|
|
|
408
362
|
// Clicks on the ::backdrop target the <dialog> element itself; clicks on
|
|
409
363
|
// drawer content always target a child (the content area fills the panel).
|
|
410
364
|
const handleClick = useCallback(event => {
|
|
411
365
|
if (event.target === event.currentTarget && hasScrim) {
|
|
412
|
-
|
|
366
|
+
onOpenChange(false);
|
|
413
367
|
}
|
|
414
|
-
}, [hasScrim,
|
|
415
|
-
const
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
top: styles.top,
|
|
420
|
-
bottom: styles.bottom
|
|
421
|
-
}[side];
|
|
422
|
-
const sideOpenStyle = {
|
|
423
|
-
start: styles.startOpen,
|
|
424
|
-
end: styles.endOpen,
|
|
425
|
-
top: styles.topOpen,
|
|
426
|
-
bottom: styles.bottomOpen
|
|
427
|
-
}[side];
|
|
368
|
+
}, [hasScrim, onOpenChange]);
|
|
369
|
+
const widthValue = typeof width === 'number' ? `${width}px` : width;
|
|
370
|
+
const mobileWidth = isFullWidthOnMobile ? MOBILE_WIDTH_FULL : `min(${widthValue}, calc(100dvw - ${MOBILE_PAGE_REVEAL}px))`;
|
|
371
|
+
const sideStyle = side === 'start' ? styles.start : styles.end;
|
|
372
|
+
const sideOpenStyle = side === 'start' ? styles.startOpen : styles.endOpen;
|
|
428
373
|
|
|
429
374
|
// Filter out native `open` to prevent InvalidStateError when passed
|
|
430
375
|
const {
|
|
@@ -433,56 +378,26 @@ export function Drawer({
|
|
|
433
378
|
} = props;
|
|
434
379
|
return /*#__PURE__*/_jsxs("dialog", {
|
|
435
380
|
ref: mergeRefs(ref, dialogRef),
|
|
436
|
-
"aria-label": label,
|
|
437
|
-
"aria-modal": hasScrim ? 'true' : undefined,
|
|
438
|
-
onClick: handleClick,
|
|
439
|
-
onCancel: handleCancel,
|
|
440
381
|
...mergeProps(themeProps('drawer', {
|
|
441
382
|
side
|
|
442
|
-
}), stylex.props(styles.dialog, overlayPaddingReset.reset, sideStyle,
|
|
383
|
+
}), stylex.props(styles.dialog, overlayPaddingReset.reset, sideStyle, dynamicStyles.inlineSize(widthValue, mobileWidth), isOpen && styles.open, isOpen && sideOpenStyle, hasScrim ? styles.scrim : dynamicStyles.stackZ(stackZ), hasScrim && isOpen && styles.scrimOpen, xstyle), className, style),
|
|
443
384
|
...safeProps,
|
|
385
|
+
"aria-label": label,
|
|
386
|
+
"aria-modal": hasScrim ? 'true' : undefined,
|
|
387
|
+
onClick: composeEventHandlers(onClickProp, handleClick),
|
|
388
|
+
onKeyDown: composeEventHandlers(onKeyDownProp, handleKeyDown),
|
|
389
|
+
onCancel: handleCancel,
|
|
444
390
|
children: [/*#__PURE__*/_jsx("div", {
|
|
445
391
|
tabIndex: -1,
|
|
446
392
|
...{
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
},
|
|
450
|
-
1: {
|
|
451
|
-
className: "x1iyjqo2 x2lwn1j xh8yej3 x1odjw0f x6ikm8r xish69e xx69xxh x1a2a7pz x1s85apg"
|
|
452
|
-
}
|
|
453
|
-
}[!!collapsed << 0],
|
|
393
|
+
className: "x1iyjqo2 x2lwn1j xh8yej3 x1odjw0f x6ikm8r xish69e xx69xxh x1a148e8 x1a2a7pz"
|
|
394
|
+
},
|
|
454
395
|
children: children
|
|
455
|
-
}),
|
|
456
|
-
/*#__PURE__*/
|
|
457
|
-
// Collapsed rail: one full-size expand button with the label
|
|
458
|
-
// reading vertically.
|
|
459
|
-
_jsx("button", {
|
|
460
|
-
type: "button",
|
|
461
|
-
"aria-label": `Expand ${label}`,
|
|
462
|
-
onClick: () => onCollapsedChange?.(false),
|
|
463
|
-
...{
|
|
464
|
-
0: {
|
|
465
|
-
className: "xjyslct xng3xce x1ghz6dp x8o8v82 x7a5moj x1iyjqo2 xh8yej3 x2lwn1j x78zum5 x6s0dn4 xl56j7k x1ypdohk xjbqb8w xtao9ic xv1l7n4 xjb2p0i xcr08ib xmhvcl5 x1kq96og xuxw1ft xb3r6kr xlyipyv x98t3bc x1a2a7pz"
|
|
466
|
-
},
|
|
467
|
-
1: {
|
|
468
|
-
className: "xjyslct xng3xce x1ghz6dp x8o8v82 x7a5moj x1iyjqo2 xh8yej3 x2lwn1j x78zum5 x6s0dn4 xl56j7k x1ypdohk xjbqb8w xtao9ic xv1l7n4 xjb2p0i xcr08ib xmhvcl5 x1kq96og xuxw1ft xb3r6kr xlyipyv x98t3bc x1a2a7pz x19jd1h0"
|
|
469
|
-
}
|
|
470
|
-
}[!!(side === 'start') << 0],
|
|
471
|
-
children: label
|
|
472
|
-
}) : (showCloseButton || canCollapse && onCollapsedChange != null) && /*#__PURE__*/_jsxs("div", {
|
|
396
|
+
}), hasCloseButton && /*#__PURE__*/_jsx("div", {
|
|
473
397
|
...{
|
|
474
398
|
className: "x10l6tqk xctzyg x72tfeb x78zum5 xzye2dw x1vjfegm"
|
|
475
399
|
},
|
|
476
|
-
children:
|
|
477
|
-
icon: /*#__PURE__*/_jsx(Icon, {
|
|
478
|
-
icon: side === 'start' ? 'chevronLeft' : 'chevronRight',
|
|
479
|
-
size: "sm",
|
|
480
|
-
color: "inherit"
|
|
481
|
-
}),
|
|
482
|
-
label: `Collapse ${label}`,
|
|
483
|
-
variant: "ghost",
|
|
484
|
-
onClick: () => onCollapsedChange(true)
|
|
485
|
-
}), showCloseButton && /*#__PURE__*/_jsx(IconButton, {
|
|
400
|
+
children: /*#__PURE__*/_jsx(IconButton, {
|
|
486
401
|
icon: /*#__PURE__*/_jsx(Icon, {
|
|
487
402
|
icon: "close",
|
|
488
403
|
size: "sm",
|
|
@@ -490,8 +405,8 @@ export function Drawer({
|
|
|
490
405
|
}),
|
|
491
406
|
label: "Close",
|
|
492
407
|
variant: "ghost",
|
|
493
|
-
onClick:
|
|
494
|
-
})
|
|
408
|
+
onClick: () => onOpenChange(false)
|
|
409
|
+
})
|
|
495
410
|
})]
|
|
496
411
|
});
|
|
497
412
|
}
|