@atlaskit/drawer 13.1.0 → 13.2.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.
Files changed (29) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/__tests__/playwright/top-layer-focus.spec.tsx +148 -0
  3. package/compass.yml +38 -0
  4. package/dist/cjs/drawer-panel/drawer-content.js +11 -2
  5. package/dist/cjs/drawer-panel/drawer-slide-in.js +67 -0
  6. package/dist/cjs/drawer-panel/drawer-top-layer.compiled.css +8 -0
  7. package/dist/cjs/drawer-panel/drawer-top-layer.js +256 -0
  8. package/dist/cjs/drawer-panel/hooks/use-prevent-programmatic-scroll.js +6 -0
  9. package/dist/cjs/drawer.js +25 -14
  10. package/dist/cjs/use-drawer-stack.js +88 -0
  11. package/dist/es2019/drawer-panel/drawer-content.js +11 -2
  12. package/dist/es2019/drawer-panel/drawer-slide-in.js +107 -0
  13. package/dist/es2019/drawer-panel/drawer-top-layer.compiled.css +8 -0
  14. package/dist/es2019/drawer-panel/drawer-top-layer.js +238 -0
  15. package/dist/es2019/drawer-panel/hooks/use-prevent-programmatic-scroll.js +6 -0
  16. package/dist/es2019/drawer.js +25 -14
  17. package/dist/es2019/use-drawer-stack.js +75 -0
  18. package/dist/esm/drawer-panel/drawer-content.js +11 -2
  19. package/dist/esm/drawer-panel/drawer-slide-in.js +61 -0
  20. package/dist/esm/drawer-panel/drawer-top-layer.compiled.css +8 -0
  21. package/dist/esm/drawer-panel/drawer-top-layer.js +247 -0
  22. package/dist/esm/drawer-panel/hooks/use-prevent-programmatic-scroll.js +6 -0
  23. package/dist/esm/drawer.js +25 -14
  24. package/dist/esm/use-drawer-stack.js +82 -0
  25. package/dist/types/drawer-panel/drawer-slide-in.d.ts +23 -0
  26. package/dist/types/drawer-panel/drawer-top-layer.d.ts +24 -0
  27. package/dist/types/drawer.d.ts +1 -1
  28. package/dist/types/use-drawer-stack.d.ts +17 -0
  29. package/package.json +14 -6
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Off-screen transform per entry edge. A drawer slides in from the edge it is
3
+ * pinned to and exits back to the same edge (legacy `SlideIn` used
4
+ * `exitTo={enterFrom}`), so enter and exit are symmetric.
5
+ */
6
+ var HIDDEN_TRANSFORM = {
7
+ left: 'translateX(-100%)',
8
+ right: 'translateX(100%)',
9
+ top: 'translateY(-100%)',
10
+ bottom: 'translateY(100%)'
11
+ };
12
+
13
+ // Match the legacy `CustomSlideIn`: `duration="small"` (100ms) on enter,
14
+ // `small * 0.5` (50ms) on exit, `ease-out` timing. Drawer slides with no panel
15
+ // fade (`fade="none"`); only the backdrop fades.
16
+ var ENTER_MS = 100;
17
+ var EXIT_MS = 50;
18
+ var EASE = 'cubic-bezier(0.2,0,0,1)'; // @atlaskit/motion `easeOut`
19
+
20
+ /**
21
+ * Builds the slide CSS for a single entry edge, scoped to
22
+ * `[data-ds-dialog-drawer-slide-{from}]` (the `Dialog` primitive stamps
23
+ * `data-ds-dialog-{name}` on the `<dialog>` element).
24
+ *
25
+ * - base selector → hidden/exit state + exit duration
26
+ * - `[open]` → visible state + enter duration
27
+ * - `@starting-style` → initial (pre-paint) state so the entry transition runs
28
+ *
29
+ * The `::backdrop` fades its blanket colour in step with the panel. Stacked
30
+ * drawers hide their backdrop via the `Dialog` `shouldHideBackdrop` prop (an
31
+ * ID-scoped `<style>` whose specificity beats this rule).
32
+ */
33
+ function buildCss(name, hidden) {
34
+ var sel = "[data-ds-dialog-".concat(name, "]");
35
+ return "\n".concat(sel, " {\n transform: ").concat(hidden, ";\n transition:\n transform ").concat(EXIT_MS, "ms ").concat(EASE, ",\n overlay ").concat(EXIT_MS, "ms allow-discrete,\n display ").concat(EXIT_MS, "ms allow-discrete;\n}\n\n").concat(sel, "[open] {\n transform: none;\n transition-duration: ").concat(ENTER_MS, "ms;\n}\n\n@starting-style {\n ").concat(sel, "[open] {\n transform: ").concat(hidden, ";\n }\n}\n\n").concat(sel, "::backdrop {\n background-color: transparent;\n transition:\n background-color ").concat(EXIT_MS, "ms ").concat(EASE, ",\n overlay ").concat(EXIT_MS, "ms allow-discrete,\n display ").concat(EXIT_MS, "ms allow-discrete;\n}\n\n").concat(sel, "[open]::backdrop {\n background-color: var(--ds-blanket, #050C1F75);\n transition-duration: ").concat(ENTER_MS, "ms;\n}\n\n@starting-style {\n ").concat(sel, "[open]::backdrop {\n background-color: transparent;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n ").concat(sel, ",\n ").concat(sel, "[open],\n ").concat(sel, "::backdrop,\n ").concat(sel, "[open]::backdrop {\n transition-duration: 0s;\n }\n}\n");
36
+ }
37
+ /**
38
+ * Directional slide animation for the top-layer `Drawer`, passed to the
39
+ * `Dialog` primitive's `animate` prop. Local to `@atlaskit/drawer` (not a
40
+ * shared top-layer preset). The panel slides in from its pinned viewport edge
41
+ * while the `::backdrop` fades.
42
+ *
43
+ * @example
44
+ * ```tsx
45
+ * <Dialog animate={drawerSlideIn({ from: 'left' })} isOpen={isOpen} onClose={onClose} label="...">
46
+ * ...
47
+ * </Dialog>
48
+ * ```
49
+ */
50
+ export function drawerSlideIn() {
51
+ var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
52
+ _ref$from = _ref.from,
53
+ from = _ref$from === void 0 ? 'left' : _ref$from;
54
+ var name = "drawer-slide-".concat(from);
55
+ return {
56
+ name: name,
57
+ css: buildCss(name, HIDDEN_TRANSFORM[from]),
58
+ enterDurationMs: ENTER_MS,
59
+ exitDurationMs: EXIT_MS
60
+ };
61
+ }
@@ -0,0 +1,8 @@
1
+ ._18m915vq{overflow-y:hidden}
2
+ ._1bsb1osq{width:100%}
3
+ ._1e0c1txw{display:flex}
4
+ ._1q1l1bhr{--ds-elevation-surface-current:var(--ds-surface-overlay,#fff)}
5
+ ._1reo15vq{overflow-x:hidden}
6
+ ._4t3i1osq{height:100%}
7
+ ._bfhk1bhr{background-color:var(--ds-surface-overlay,#fff)}
8
+ ._ect4ttxp{font-family:var(--ds-font-family-body,"Atlassian Sans",ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",Ubuntu,"Helvetica Neue",sans-serif)}
@@ -0,0 +1,247 @@
1
+ /* drawer-top-layer.tsx generated by @compiled/babel-plugin v0.39.1 */
2
+ import _extends from "@babel/runtime/helpers/extends";
3
+ import _typeof from "@babel/runtime/helpers/typeof";
4
+ import "./drawer-top-layer.compiled.css";
5
+ import * as React from 'react';
6
+ import { ax, ix } from "@compiled/react/runtime";
7
+ import { useCallback, useEffect, useMemo, useRef } from 'react';
8
+ import { bind } from 'bind-event-listener';
9
+ import { usePlatformLeafEventHandler } from '@atlaskit/analytics-next/usePlatformLeafEventHandler';
10
+ import { createCloseEvent, Dialog } from '@atlaskit/top-layer/dialog';
11
+ import { DialogScrollLock } from '@atlaskit/top-layer/dialog-scroll-lock';
12
+ import { EnsureIsInsideDrawerContext } from '../ensure-is-inside-drawer-context';
13
+ import { OnCloseContext } from '../on-close-context';
14
+ import useDrawerStack from '../use-drawer-stack';
15
+ import { drawerSlideIn } from './drawer-slide-in';
16
+ var LOCAL_CURRENT_SURFACE_CSS_VAR = '--ds-elevation-surface-current';
17
+ var styles = {
18
+ surface: "_1reo15vq _18m915vq _1e0c1txw _1bsb1osq _4t3i1osq _bfhk1bhr _1q1l1bhr _ect4ttxp"
19
+ };
20
+
21
+ // Width presets mirror the legacy `DrawerPanel`. Applied to the `<dialog>` (the
22
+ // sized, edge-pinned element) and clamped to the viewport for mobile safety.
23
+ var WIDTH_MAP = {
24
+ narrow: '360px',
25
+ medium: '480px',
26
+ wide: '600px',
27
+ extended: '95vw',
28
+ full: '100vw'
29
+ };
30
+
31
+ /**
32
+ * Resolve the native `<dialog>` accessible name props. Prefer the consumer's
33
+ * `label`, then their `titleId`. If neither is supplied (legacy allowed this),
34
+ * fall back to a generic name so the dialog is never unlabelled. Note:
35
+ * `@atlaskit/drawer` has no i18n setup, so this fallback is English only;
36
+ * consumers should pass a localised `label` or `titleId`.
37
+ */
38
+ function getAccessibleName(_ref) {
39
+ var label = _ref.label,
40
+ titleId = _ref.titleId;
41
+ if (label) {
42
+ return {
43
+ label: label
44
+ };
45
+ }
46
+ if (titleId) {
47
+ return {
48
+ labelledBy: titleId
49
+ };
50
+ }
51
+ return {
52
+ label: 'Drawer'
53
+ };
54
+ }
55
+
56
+ /**
57
+ * **DrawerTopLayer**
58
+ *
59
+ * Top-layer (`platform-dst-top-layer`) implementation of `Drawer`. Renders a
60
+ * native `<dialog>` via `@atlaskit/top-layer`, replacing Portal, Blanket,
61
+ * react-focus-lock, react-scrolllock and `@atlaskit/layering` with native
62
+ * modality, `::backdrop`, focus trap and return, and `DialogScrollLock`.
63
+ *
64
+ * The `Dialog` primitive owns the entry and exit animation lifecycle (it keeps
65
+ * the host element mounted through the exit transition, then fires
66
+ * `onExitFinish`), so no `ExitingPersistence` wrapper is needed: the drawer
67
+ * renders `<Dialog isOpen={isOpen}>` directly. `useDrawerStack` tracks stack
68
+ * depth so only the foreground drawer shows a `::backdrop`.
69
+ *
70
+ * `isFocusLockEnabled` is intentionally unsupported: a native modal `<dialog>`
71
+ * always traps focus, so `isFocusLockEnabled={false}` is a no-op under this gate.
72
+ */
73
+ export function DrawerTopLayer(_ref2) {
74
+ var _ref2$width = _ref2.width,
75
+ width = _ref2$width === void 0 ? 'narrow' : _ref2$width,
76
+ isOpen = _ref2.isOpen,
77
+ _ref2$shouldReturnFoc = _ref2.shouldReturnFocus,
78
+ shouldReturnFocus = _ref2$shouldReturnFoc === void 0 ? true : _ref2$shouldReturnFoc,
79
+ onKeyDown = _ref2.onKeyDown,
80
+ testId = _ref2.testId,
81
+ children = _ref2.children,
82
+ onClose = _ref2.onClose,
83
+ onCloseComplete = _ref2.onCloseComplete,
84
+ onOpenComplete = _ref2.onOpenComplete,
85
+ label = _ref2.label,
86
+ titleId = _ref2.titleId,
87
+ _ref2$enterFrom = _ref2.enterFrom,
88
+ enterFrom = _ref2$enterFrom === void 0 ? 'left' : _ref2$enterFrom;
89
+ var stackIndex = useDrawerStack({
90
+ isOpen: isOpen
91
+ });
92
+
93
+ /**
94
+ * Points to the panel surface. Passed to `onOpenComplete` / `onCloseComplete`.
95
+ */
96
+ var contentRef = useRef(null);
97
+ // Cache the last content element so `onCloseComplete` still receives a node
98
+ // after children unmount (with reduced motion `contentRef` can clear before
99
+ // `onExitFinish` fires).
100
+ var lastContentElRef = useRef(null);
101
+ // Callback ref runs at commit (not during render), so both refs stay
102
+ // populated without a render-time side effect. `lastContentElRef` only
103
+ // overwrites with a non-null node, preserving it across the unmount.
104
+ var setContentEl = useCallback(function (el) {
105
+ contentRef.current = el;
106
+ if (el) {
107
+ lastContentElRef.current = el;
108
+ }
109
+ }, []);
110
+
111
+ // Analytics-wrapped close handlers, one per legacy trigger.
112
+ var handleEscapeClose = usePlatformLeafEventHandler({
113
+ fn: function fn(evt, analyticsEvent) {
114
+ return onClose === null || onClose === void 0 ? void 0 : onClose(evt, analyticsEvent);
115
+ },
116
+ action: 'dismissed',
117
+ componentName: 'drawer',
118
+ packageName: "@atlaskit/drawer",
119
+ packageVersion: "13.1.1",
120
+ analyticsData: {
121
+ trigger: 'escKey'
122
+ }
123
+ });
124
+ var handleBlanketClose = usePlatformLeafEventHandler({
125
+ fn: function fn(evt, analyticsEvent) {
126
+ return onClose === null || onClose === void 0 ? void 0 : onClose(evt, analyticsEvent);
127
+ },
128
+ action: 'dismissed',
129
+ componentName: 'drawer',
130
+ packageName: "@atlaskit/drawer",
131
+ packageVersion: "13.1.1",
132
+ analyticsData: {
133
+ trigger: 'blanket'
134
+ }
135
+ });
136
+ var handleBackButtonClose = usePlatformLeafEventHandler({
137
+ fn: function fn(evt, analyticsEvent) {
138
+ return onClose === null || onClose === void 0 ? void 0 : onClose(evt, analyticsEvent);
139
+ },
140
+ action: 'dismissed',
141
+ componentName: 'drawer',
142
+ packageName: "@atlaskit/drawer",
143
+ packageVersion: "13.1.1",
144
+ analyticsData: {
145
+ trigger: 'backButton'
146
+ }
147
+ });
148
+
149
+ // Bridge the Dialog primitive's `onClose({ reason })` to the legacy
150
+ // `onClose(event, analyticsEvent)` contract via a synthetic event. Drawer
151
+ // has no `shouldCloseOn*` props, so both reasons always forward.
152
+ var handleDialogClose = useCallback(function (_ref3) {
153
+ var reason = _ref3.reason;
154
+ var event = createCloseEvent({
155
+ reason: reason
156
+ });
157
+ if (reason === 'escape') {
158
+ handleEscapeClose(event);
159
+ } else {
160
+ handleBlanketClose(event);
161
+ }
162
+ }, [handleEscapeClose, handleBlanketClose]);
163
+
164
+ // Mirror the legacy window `keydown` listener so `onKeyDown` still fires
165
+ // while the drawer is open.
166
+ var handleKeyDown = useCallback(function (evt) {
167
+ onKeyDown === null || onKeyDown === void 0 || onKeyDown(evt);
168
+ }, [onKeyDown]);
169
+ useEffect(function () {
170
+ if (!isOpen) {
171
+ return;
172
+ }
173
+ return bind(window, {
174
+ type: 'keydown',
175
+ listener: handleKeyDown
176
+ });
177
+ }, [isOpen, handleKeyDown]);
178
+
179
+ // `onOpenComplete` once the entry animation settles (via `Dialog`'s
180
+ // `onEnterFinish`, which the underlying hook fires for animated,
181
+ // non-animated and reduced-motion paths).
182
+ var handleEnterFinish = useCallback(function () {
183
+ onOpenComplete === null || onOpenComplete === void 0 || onOpenComplete(contentRef.current);
184
+ }, [onOpenComplete]);
185
+
186
+ // `onCloseComplete` once the exit animation settles (via `Dialog`'s
187
+ // `onExitFinish`). This is also where a custom `shouldReturnFocus={ref}` is
188
+ // honoured: native `<dialog>` restores focus to the trigger at the start of
189
+ // close, so the consumer's ref is focused now that the exit is done.
190
+ // `shouldReturnFocus={false}` is a documented best-effort limitation; native
191
+ // always restores focus to the trigger.
192
+ var handleExitFinish = useCallback(function () {
193
+ var _contentRef$current;
194
+ onCloseComplete === null || onCloseComplete === void 0 || onCloseComplete((_contentRef$current = contentRef.current) !== null && _contentRef$current !== void 0 ? _contentRef$current : lastContentElRef.current);
195
+ lastContentElRef.current = null;
196
+ if (_typeof(shouldReturnFocus) === 'object' && shouldReturnFocus !== null && shouldReturnFocus !== void 0 && shouldReturnFocus.current) {
197
+ shouldReturnFocus.current.focus();
198
+ }
199
+ }, [onCloseComplete, shouldReturnFocus]);
200
+
201
+ // Pin the `<dialog>` full-height to the inline-start edge (overriding the
202
+ // primitive's centred `margin: auto`); width from the preset, clamped to the
203
+ // viewport. `enterFrom` only drives the slide animation, not the pin edge;
204
+ // it matches the legacy panel, which always pins `inset-inline-start`.
205
+ var dialogStyle = {
206
+ margin: 0,
207
+ insetBlockStart: 0,
208
+ insetInlineStart: 0,
209
+ insetInlineEnd: 'auto',
210
+ height: '100dvh',
211
+ maxHeight: '100dvh',
212
+ width: "min(".concat(WIDTH_MAP[width], ", 100vw)")
213
+ };
214
+
215
+ // Memoized so the preset object (and its CSS string) is rebuilt only when
216
+ // `enterFrom` changes, not on every render.
217
+ var animate = useMemo(function () {
218
+ return drawerSlideIn({
219
+ from: enterFrom
220
+ });
221
+ }, [enterFrom]);
222
+ var accessibleName = getAccessibleName({
223
+ label: label,
224
+ titleId: titleId
225
+ });
226
+ return /*#__PURE__*/React.createElement(Dialog, _extends({
227
+ isOpen: isOpen,
228
+ onClose: handleDialogClose,
229
+ onEnterFinish: handleEnterFinish,
230
+ onExitFinish: handleExitFinish,
231
+ animate: animate,
232
+ shouldHideBackdrop: stackIndex > 0,
233
+ testId: testId
234
+ }, accessibleName, {
235
+ // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop
236
+ style: dialogStyle
237
+ }), /*#__PURE__*/React.createElement(DialogScrollLock, {
238
+ isOpen: true
239
+ }), /*#__PURE__*/React.createElement("div", {
240
+ ref: setContentEl,
241
+ className: ax([styles.surface])
242
+ }, /*#__PURE__*/React.createElement(EnsureIsInsideDrawerContext.Provider, {
243
+ value: true
244
+ }, /*#__PURE__*/React.createElement(OnCloseContext.Provider, {
245
+ value: handleBackButtonClose
246
+ }, children))));
247
+ }
@@ -1,6 +1,7 @@
1
1
  import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
2
2
  import { useCallback, useEffect, useLayoutEffect, useState } from 'react';
3
3
  import { bind } from 'bind-event-listener';
4
+ import { fg } from '@atlaskit/platform-feature-flags';
4
5
 
5
6
  /**
6
7
  * Returns how far the body is scrolled from the top of the viewport.
@@ -41,6 +42,11 @@ export default function usePreventProgrammaticScroll() {
41
42
  }
42
43
  }, [scrollTopOffset]);
43
44
  useEffect(function () {
45
+ // The top-layer drawer relies on native modality + `DialogScrollLock`; the
46
+ // programmatic-scroll guard is intentionally dropped under the flag.
47
+ if (fg('platform-dst-top-layer')) {
48
+ return;
49
+ }
44
50
  return bind(window, {
45
51
  type: 'scroll',
46
52
  listener: onWindowScroll
@@ -4,9 +4,11 @@ import React, { useCallback, useEffect } from 'react';
4
4
  import { canUseDOM } from 'exenv';
5
5
  import { usePlatformLeafEventHandler } from '@atlaskit/analytics-next';
6
6
  import { Layering, useCloseOnEscapePress } from '@atlaskit/layering';
7
+ import { fg } from '@atlaskit/platform-feature-flags';
7
8
  import Portal from '@atlaskit/portal';
8
9
  import Blanket from './blanket';
9
10
  import { DrawerPanel } from './drawer-panel/drawer-panel';
11
+ import { DrawerTopLayer } from './drawer-panel/drawer-top-layer';
10
12
  // escape close manager for layering
11
13
  var EscapeCloseManager = function EscapeCloseManager(_ref) {
12
14
  var onClose = _ref.onClose;
@@ -19,17 +21,7 @@ var EscapeCloseManager = function EscapeCloseManager(_ref) {
19
21
  });
20
22
  return /*#__PURE__*/React.createElement("span", null);
21
23
  };
22
-
23
- /**
24
- * __Drawer__
25
- *
26
- * A drawer is a panel that slides in from the left side of the screen.
27
- *
28
- * - [Examples](https://atlassian.design/components/drawer/examples)
29
- * - [Code](https://atlassian.design/components/drawer/code)
30
- * - [Usage](https://atlassian.design/components/drawer/usage)
31
- */
32
- export var Drawer = function Drawer(_ref2) {
24
+ var DrawerBase = function DrawerBase(_ref2) {
33
25
  var _ref2$width = _ref2.width,
34
26
  width = _ref2$width === void 0 ? 'narrow' : _ref2$width,
35
27
  isOpen = _ref2.isOpen,
@@ -57,7 +49,7 @@ export var Drawer = function Drawer(_ref2) {
57
49
  action: 'dismissed',
58
50
  componentName: 'drawer',
59
51
  packageName: "@atlaskit/drawer",
60
- packageVersion: "13.0.1",
52
+ packageVersion: "13.1.1",
61
53
  analyticsData: {
62
54
  trigger: 'escKey'
63
55
  }
@@ -80,7 +72,7 @@ export var Drawer = function Drawer(_ref2) {
80
72
  action: 'dismissed',
81
73
  componentName: 'drawer',
82
74
  packageName: "@atlaskit/drawer",
83
- packageVersion: "13.0.1",
75
+ packageVersion: "13.1.1",
84
76
  analyticsData: {
85
77
  trigger: 'blanket'
86
78
  }
@@ -92,7 +84,7 @@ export var Drawer = function Drawer(_ref2) {
92
84
  action: 'dismissed',
93
85
  componentName: 'drawer',
94
86
  packageName: "@atlaskit/drawer",
95
- packageVersion: "13.0.1",
87
+ packageVersion: "13.1.1",
96
88
  analyticsData: {
97
89
  trigger: 'backButton'
98
90
  }
@@ -125,4 +117,23 @@ export var Drawer = function Drawer(_ref2) {
125
117
  }, children, /*#__PURE__*/React.createElement(EscapeCloseManager, {
126
118
  onClose: handleClose
127
119
  })) : children));
120
+ };
121
+
122
+ /**
123
+ * __Drawer__
124
+ *
125
+ * A drawer is a panel that slides in from the left side of the screen.
126
+ *
127
+ * - [Examples](https://atlassian.design/components/drawer/examples)
128
+ * - [Code](https://atlassian.design/components/drawer/code)
129
+ * - [Usage](https://atlassian.design/components/drawer/usage)
130
+ */
131
+ export var Drawer = function Drawer(props) {
132
+ if (fg('platform-dst-top-layer')) {
133
+ // eslint-disable-next-line @repo/internal/react/no-unsafe-spread-props -- internal implementation component takes the same DrawerProps
134
+ return /*#__PURE__*/React.createElement(DrawerTopLayer, props);
135
+ }
136
+
137
+ // eslint-disable-next-line @repo/internal/react/no-unsafe-spread-props -- internal implementation component takes the same DrawerProps
138
+ return /*#__PURE__*/React.createElement(DrawerBase, props);
128
139
  };
@@ -0,0 +1,82 @@
1
+ import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
2
+ import { useEffect } from 'react';
3
+ import useLazyCallback from '@atlaskit/ds-lib/use-lazy-callback';
4
+ import usePreviousValue from '@atlaskit/ds-lib/use-previous-value';
5
+ import useStateRef from '@atlaskit/ds-lib/use-state-ref';
6
+
7
+ /**
8
+ * **Major versions will not know about each other**
9
+ *
10
+ * An array holding references to all currently open drawers. This only works
11
+ * for drawers of the same major version, because the reference differs between
12
+ * majors (for example V13 will not know about any from V14).
13
+ *
14
+ * Adapted from `useModalStack` in `@atlaskit/modal-dialog`. The top-layer drawer
15
+ * path uses this to know its position in the stack so that only the foreground
16
+ * drawer renders a visible `::backdrop` (this avoids cumulative darkening for
17
+ * nested or stacked drawers). Unlike the modal version, the register is timed
18
+ * off `isOpen` rather than `@atlaskit/motion`'s `useExitingPersistence`: the
19
+ * `Dialog` primitive owns its own exit lifecycle, so the top-layer drawer needs
20
+ * no `ExitingPersistence` wrapper.
21
+ */
22
+ var drawerStackRegister = [];
23
+ /**
24
+ * Returns the position of the calling drawer in the drawer stack. A stack index
25
+ * of `0` is the foreground (top of the stack); higher numbers are further back.
26
+ */
27
+ export default function useDrawerStack(_ref) {
28
+ var isOpen = _ref.isOpen,
29
+ onStackChange = _ref.onStackChange;
30
+ var _useStateRef = useStateRef(0),
31
+ _useStateRef2 = _slicedToArray(_useStateRef, 2),
32
+ stackIndexRef = _useStateRef2[0],
33
+ setStackIndex = _useStateRef2[1];
34
+ var currentStackIndex = stackIndexRef.current;
35
+ var previousStackIndex = usePreviousValue(stackIndexRef.current);
36
+
37
+ // Kept stable for the lifetime of the component so it can be the identity in
38
+ // the shared register. This is why it is a lazy callback and not a
39
+ // useMemo/useCallback value.
40
+ var updateStack = useLazyCallback(function () {
41
+ var newStackIndex = drawerStackRegister.indexOf(updateStack);
42
+ // Read from the ref rather than state: this closure only ever sees the
43
+ // initial state value.
44
+ if (stackIndexRef.current !== newStackIndex) {
45
+ setStackIndex(newStackIndex);
46
+ stackIndexRef.current = newStackIndex;
47
+ }
48
+ });
49
+ useEffect(function () {
50
+ if (!isOpen) {
51
+ return;
52
+ }
53
+ // Opening: join the front of the register (the newest open drawer is the
54
+ // foreground at index 0), then notify every open drawer to recompute.
55
+ if (drawerStackRegister.indexOf(updateStack) === -1) {
56
+ drawerStackRegister.unshift(updateStack);
57
+ }
58
+ drawerStackRegister.forEach(function (callback) {
59
+ return callback();
60
+ });
61
+ return function () {
62
+ // Closing or unmounting: leave the register and notify the rest.
63
+ var index = drawerStackRegister.indexOf(updateStack);
64
+ if (index !== -1) {
65
+ drawerStackRegister.splice(index, 1);
66
+ }
67
+ drawerStackRegister.forEach(function (callback) {
68
+ return callback();
69
+ });
70
+ };
71
+ }, [isOpen, updateStack]);
72
+ useEffect(function () {
73
+ if (previousStackIndex === undefined) {
74
+ // Initial render: nothing to notify about.
75
+ return;
76
+ }
77
+ if (previousStackIndex !== currentStackIndex) {
78
+ onStackChange === null || onStackChange === void 0 || onStackChange(currentStackIndex);
79
+ }
80
+ }, [onStackChange, previousStackIndex, currentStackIndex]);
81
+ return currentStackIndex;
82
+ }
@@ -0,0 +1,23 @@
1
+ import { type Direction } from '@atlaskit/motion/types';
2
+ import { type TAnimationPreset } from '@atlaskit/top-layer/animations';
3
+ type TDrawerSlideInOptions = {
4
+ /**
5
+ * The edge the drawer slides in from. Defaults to `left`.
6
+ */
7
+ from?: Direction;
8
+ };
9
+ /**
10
+ * Directional slide animation for the top-layer `Drawer`, passed to the
11
+ * `Dialog` primitive's `animate` prop. Local to `@atlaskit/drawer` (not a
12
+ * shared top-layer preset). The panel slides in from its pinned viewport edge
13
+ * while the `::backdrop` fades.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * <Dialog animate={drawerSlideIn({ from: 'left' })} isOpen={isOpen} onClose={onClose} label="...">
18
+ * ...
19
+ * </Dialog>
20
+ * ```
21
+ */
22
+ export declare function drawerSlideIn({ from }?: TDrawerSlideInOptions): TAnimationPreset;
23
+ export {};
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @jsxRuntime classic
3
+ * @jsx jsx
4
+ */
5
+ import { type ReactNode } from 'react';
6
+ import { type DrawerProps } from '../types';
7
+ /**
8
+ * **DrawerTopLayer**
9
+ *
10
+ * Top-layer (`platform-dst-top-layer`) implementation of `Drawer`. Renders a
11
+ * native `<dialog>` via `@atlaskit/top-layer`, replacing Portal, Blanket,
12
+ * react-focus-lock, react-scrolllock and `@atlaskit/layering` with native
13
+ * modality, `::backdrop`, focus trap and return, and `DialogScrollLock`.
14
+ *
15
+ * The `Dialog` primitive owns the entry and exit animation lifecycle (it keeps
16
+ * the host element mounted through the exit transition, then fires
17
+ * `onExitFinish`), so no `ExitingPersistence` wrapper is needed: the drawer
18
+ * renders `<Dialog isOpen={isOpen}>` directly. `useDrawerStack` tracks stack
19
+ * depth so only the foreground drawer shows a `::backdrop`.
20
+ *
21
+ * `isFocusLockEnabled` is intentionally unsupported: a native modal `<dialog>`
22
+ * always traps focus, so `isFocusLockEnabled={false}` is a no-op under this gate.
23
+ */
24
+ export declare function DrawerTopLayer({ width, isOpen, shouldReturnFocus, onKeyDown, testId, children, onClose, onCloseComplete, onOpenComplete, label, titleId, enterFrom, }: DrawerProps): ReactNode;
@@ -9,4 +9,4 @@ import type { DrawerProps } from './types';
9
9
  * - [Code](https://atlassian.design/components/drawer/code)
10
10
  * - [Usage](https://atlassian.design/components/drawer/usage)
11
11
  */
12
- export declare const Drawer: ({ width, isOpen, isFocusLockEnabled, shouldReturnFocus, autoFocusFirstElem, onKeyDown, testId, children, onClose, onCloseComplete, onOpenComplete, zIndex, label, titleId, enterFrom, }: DrawerProps) => React.JSX.Element | null;
12
+ export declare const Drawer: (props: DrawerProps) => React.JSX.Element | null;
@@ -0,0 +1,17 @@
1
+ type TDrawerStackOpts = {
2
+ /**
3
+ * Whether the calling drawer is open. The drawer joins the stack while open
4
+ * and leaves it on close or unmount.
5
+ */
6
+ isOpen: boolean;
7
+ /**
8
+ * Fired when the calling drawer's position in the stack changes.
9
+ */
10
+ onStackChange?: (newStackIndex: number) => void;
11
+ };
12
+ /**
13
+ * Returns the position of the calling drawer in the drawer stack. A stack index
14
+ * of `0` is the foreground (top of the stack); higher numbers are further back.
15
+ */
16
+ export default function useDrawerStack({ isOpen, onStackChange }: TDrawerStackOpts): number;
17
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/drawer",
3
- "version": "13.1.0",
3
+ "version": "13.2.0",
4
4
  "description": "A drawer is a panel that slides in from the left side of the screen.",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/"
@@ -16,6 +16,11 @@
16
16
  "**/*.compiled.css"
17
17
  ],
18
18
  "atlaskit:src": "src/index.tsx",
19
+ "platform-feature-flags": {
20
+ "platform-dst-top-layer": {
21
+ "type": "boolean"
22
+ }
23
+ },
19
24
  "homepage": "https://atlassian.design/components/drawer",
20
25
  "atlassian": {
21
26
  "react-compiler": {
@@ -36,17 +41,20 @@
36
41
  }
37
42
  },
38
43
  "dependencies": {
39
- "@atlaskit/analytics-next": "^12.1.0",
44
+ "@atlaskit/analytics-next": "^12.2.0",
40
45
  "@atlaskit/blanket": "^16.1.0",
41
- "@atlaskit/button": "^24.2.0",
46
+ "@atlaskit/button": "^24.3.0",
42
47
  "@atlaskit/css": "^1.0.0",
48
+ "@atlaskit/ds-lib": "^8.0.0",
43
49
  "@atlaskit/icon": "^36.1.0",
44
50
  "@atlaskit/layering": "^4.1.0",
45
51
  "@atlaskit/motion": "^7.2.0",
52
+ "@atlaskit/platform-feature-flags": "^2.0.0",
46
53
  "@atlaskit/portal": "^6.1.0",
47
54
  "@atlaskit/react-compiler-gating": "^0.2.0",
48
55
  "@atlaskit/theme": "^26.1.0",
49
- "@atlaskit/tokens": "^15.1.0",
56
+ "@atlaskit/tokens": "^15.4.0",
57
+ "@atlaskit/top-layer": "^1.3.0",
50
58
  "@babel/runtime": "^7.0.0",
51
59
  "@compiled/react": "^0.20.0",
52
60
  "bind-event-listener": "^3.0.0",
@@ -66,14 +74,14 @@
66
74
  "@atlaskit/code": "^18.2.0",
67
75
  "@atlaskit/docs": "^12.0.0",
68
76
  "@atlaskit/dropdown-menu": "^17.1.0",
69
- "@atlaskit/ds-lib": "^8.0.0",
70
77
  "@atlaskit/form": "^16.1.0",
71
78
  "@atlaskit/inline-message": "^16.1.0",
72
79
  "@atlaskit/link": "^4.1.0",
73
80
  "@atlaskit/menu": "^9.1.0",
74
81
  "@atlaskit/modal-dialog": "^16.1.0",
75
- "@atlaskit/primitives": "^20.1.0",
82
+ "@atlaskit/primitives": "^20.3.0",
76
83
  "@atlaskit/section-message": "^9.2.0",
84
+ "@atlassian/feature-flags-test-utils": "^1.1.0",
77
85
  "@atlassian/ssr-tests": "workspace:^",
78
86
  "@atlassian/structured-docs-types": "workspace:^",
79
87
  "@testing-library/react": "^16.3.0",