@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.
@@ -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
- * Edge-anchored overlay panel for inspectors, detail views, and sheets —
8
- * the "click a table row, see its details" pattern. Slides in from any of
9
- * the four edges: inline start/end (side panels) or block top/bottom
10
- * (full-width sheets).
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 `onClose`.
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 to be closed
44
- * (Escape key, scrim click, built-in close button). The caller owns the
45
- * open state. When sibling drawers are open, Escape only closes the
46
- * top (last-opened) drawer.
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
- onClose: () => void;
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' | 'top' | 'bottom';
62
+ side?: 'start' | 'end';
57
63
  /**
58
- * Size budget of the panel along its slide axis: width for
59
- * `side="start"/"end"`, height for `side="top"/"bottom"`. A number is
60
- * pixels; a string is any CSS length (`'50%'`, `'40dvh'`). On viewports
61
- * smaller than the budget the drawer fills the axis.
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
- size?: number | string;
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). Also names the built-in
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. Defaults to the `hasScrim` value: modal drawers get a close
83
- * button, non-modal drawers don't.
84
- * @default hasScrim
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 edge-anchored overlay panel for inspectors, detail views, and sheets.
111
+ * An overlay panel for inspectors and detail views.
112
112
  *
113
- * Slides in from the logical start/end edge (side panel) or the top/bottom
114
- * edge (full-width sheet) using the native `<dialog>` element: modal with a
115
- * scrim by default, or a non-modal inline overlay with `hasScrim={false}`.
116
- * Escape closes the top-most open drawer; focus returns to the element that
117
- * opened the drawer. Non-modal side drawers can collapse to a rail via
118
- * `isCollapsed`/`onCollapsedChange`.
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
- * onClose={() => setSelected(null)}
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, onClose, side, size, label, hasScrim, hasCloseButton, isCollapsed, onCollapsedChange, children, xstyle, className, style, ref, ...props }: DrawerProps): import("react").JSX.Element;
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,EAML,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AA8SlD,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,OAAO,EAAE,MAAM,IAAI,CAAC;IAEpB;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,KAAK,GAAG,QAAQ,CAAC;IAE1C;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAEvB;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,CAAC;IAEjD;;;OAGG;IACH,QAAQ,EAAE,SAAS,CAAC;IAEpB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAMD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,MAAM,CAAC,EACrB,MAAM,EACN,OAAO,EACP,IAAY,EACZ,IAAU,EACV,KAAK,EACL,QAAe,EACf,cAAc,EACd,WAAW,EACX,iBAAiB,EACjB,QAAQ,EACR,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,GAAG,KAAK,EACT,EAAE,WAAW,+BAoQb;yBApRe,MAAM"}
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"}
@@ -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
- * Edge-anchored overlay panel for inspectors, detail views, and sheets —
12
- * the "click a table row, see its details" pattern. Slides in from any of
13
- * the four edges: inline start/end (side panels) or block top/bottom
14
- * (full-width sheets).
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, typeScaleVars } from '@astryxdesign/core/theme/tokens.stylex';
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, useDevWarning } from '@astryxdesign/core/hooks';
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
- // Structural rail width when collapsed — wide enough for a 44px touch
90
- // target, matching the raw-number convention used for the size default.
91
- const RAIL_WIDTH = 44;
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: "x1yw18vd",
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
- kzqmXN: "x1o6l61p",
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: s => [_temp, {
211
- ks0D6T: s != null ? "xf68679" : s,
182
+ inlineSize: (desktopWidth, mobileWidth) => [_temp, {
183
+ kzqmXN: (desktopWidth != null ? "x11ymkkh " : desktopWidth) + (mobileWidth != null ? "xci7e5a" : mobileWidth),
212
184
  $$css: true
213
185
  }, {
214
- "--x-maxWidth": (val => typeof val === "number" ? val + "px" : val != null ? val : undefined)(s)
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 edge-anchored overlay panel for inspectors, detail views, and sheets.
206
+ * An overlay panel for inspectors and detail views.
240
207
  *
241
- * Slides in from the logical start/end edge (side panel) or the top/bottom
242
- * edge (full-width sheet) using the native `<dialog>` element: modal with a
243
- * scrim by default, or a non-modal inline overlay with `hasScrim={false}`.
244
- * Escape closes the top-most open drawer; focus returns to the element that
245
- * opened the drawer. Non-modal side drawers can collapse to a rail via
246
- * `isCollapsed`/`onCollapsedChange`.
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
- * onClose={() => setSelected(null)}
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
- onClose,
230
+ onOpenChange,
262
231
  side = 'end',
263
- size = 400,
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 onClose (stable across re-renders so the
281
- // registration effect doesn't churn on every onClose identity change).
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 onCloseRef = useRef(onClose);
253
+ const onOpenChangeRef = useRef(onOpenChange);
284
254
  useEffect(() => {
285
- onCloseRef.current = onClose;
286
- }, [onClose]);
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, () => onCloseRef.current());
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
- useEffect(() => {
381
- const dialog = dialogRef.current;
382
- if (!dialog || !isOpen) {
383
- return;
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
- dialog.addEventListener('keydown', handleKeyDown);
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 onClose so the
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
- onClose();
358
+ onOpenChange(false);
405
359
  }
406
- }, [onClose, drawerId]);
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
- onClose();
366
+ onOpenChange(false);
413
367
  }
414
- }, [hasScrim, onClose]);
415
- const sizeValue = typeof size === 'number' ? `${size}px` : size;
416
- const sideStyle = {
417
- start: styles.start,
418
- end: styles.end,
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, isSheet ? dynamicStyles.blockSize(sizeValue) : dynamicStyles.inlineSize(sizeValue), isOpen && styles.open, isOpen && sideOpenStyle, hasScrim ? styles.scrim : dynamicStyles.stackZ(stackZ), hasScrim && isOpen && styles.scrimOpen, collapsed && styles.collapsedRail, xstyle), className, style),
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
- 0: {
448
- className: "x1iyjqo2 x2lwn1j xh8yej3 x1odjw0f x6ikm8r xish69e xx69xxh x1a2a7pz"
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
- }), collapsed ?
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: [canCollapse && onCollapsedChange != null && /*#__PURE__*/_jsx(IconButton, {
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: onClose
494
- })]
408
+ onClick: () => onOpenChange(false)
409
+ })
495
410
  })]
496
411
  });
497
412
  }