@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.
- package/CHANGELOG.md +20 -0
- package/__tests__/playwright/top-layer-focus.spec.tsx +148 -0
- package/compass.yml +38 -0
- package/dist/cjs/drawer-panel/drawer-content.js +11 -2
- package/dist/cjs/drawer-panel/drawer-slide-in.js +67 -0
- package/dist/cjs/drawer-panel/drawer-top-layer.compiled.css +8 -0
- package/dist/cjs/drawer-panel/drawer-top-layer.js +256 -0
- package/dist/cjs/drawer-panel/hooks/use-prevent-programmatic-scroll.js +6 -0
- package/dist/cjs/drawer.js +25 -14
- package/dist/cjs/use-drawer-stack.js +88 -0
- package/dist/es2019/drawer-panel/drawer-content.js +11 -2
- package/dist/es2019/drawer-panel/drawer-slide-in.js +107 -0
- package/dist/es2019/drawer-panel/drawer-top-layer.compiled.css +8 -0
- package/dist/es2019/drawer-panel/drawer-top-layer.js +238 -0
- package/dist/es2019/drawer-panel/hooks/use-prevent-programmatic-scroll.js +6 -0
- package/dist/es2019/drawer.js +25 -14
- package/dist/es2019/use-drawer-stack.js +75 -0
- package/dist/esm/drawer-panel/drawer-content.js +11 -2
- package/dist/esm/drawer-panel/drawer-slide-in.js +61 -0
- package/dist/esm/drawer-panel/drawer-top-layer.compiled.css +8 -0
- package/dist/esm/drawer-panel/drawer-top-layer.js +247 -0
- package/dist/esm/drawer-panel/hooks/use-prevent-programmatic-scroll.js +6 -0
- package/dist/esm/drawer.js +25 -14
- package/dist/esm/use-drawer-stack.js +82 -0
- package/dist/types/drawer-panel/drawer-slide-in.d.ts +23 -0
- package/dist/types/drawer-panel/drawer-top-layer.d.ts +24 -0
- package/dist/types/drawer.d.ts +1 -1
- package/dist/types/use-drawer-stack.d.ts +17 -0
- package/package.json +14 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @atlaskit/drawer
|
|
2
2
|
|
|
3
|
+
## 13.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`e297058763bc0`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/e297058763bc0) -
|
|
8
|
+
Add a top-layer rendering path for `Drawer` behind the `platform-dst-top-layer` feature gate. Some
|
|
9
|
+
focus-management and labelling props behave slightly differently on the native dialog path.
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Updated dependencies
|
|
14
|
+
|
|
15
|
+
## 13.1.1
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- [`820f00bd7db09`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/820f00bd7db09) -
|
|
20
|
+
Enable platform-dst-motion-uplift-button by default in constellation examples so button motion is
|
|
21
|
+
previewed in component demos. Docs/examples-only; no change to shipped component behaviour.
|
|
22
|
+
|
|
3
23
|
## 13.1.0
|
|
4
24
|
|
|
5
25
|
### Minor Changes
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { expect, test } from '@af/integration-testing';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Drawer: focus contract on the top-layer code path.
|
|
5
|
+
*
|
|
6
|
+
* The top-layer `Drawer` renders as a native modal `<dialog>` (via the
|
|
7
|
+
* `@atlaskit/top-layer` `Dialog` primitive). Per WCAG 2.4.3 (Focus Order) and
|
|
8
|
+
* the top-layer focus rules:
|
|
9
|
+
*
|
|
10
|
+
* 1. Initial focus moves to the first focusable element on open (or to the
|
|
11
|
+
* element marked with the native HTML `autofocus` attribute when present).
|
|
12
|
+
* 2. Closing the drawer (Escape) restores focus to the trigger.
|
|
13
|
+
* 3. Tab / Shift+Tab cycle focus within the drawer (focus does not escape to
|
|
14
|
+
* elements behind the inert modal surface).
|
|
15
|
+
*
|
|
16
|
+
* `Drawer` has no `autoFocus` ref prop (unlike `ModalDialog`); its consumer
|
|
17
|
+
* focus override is `shouldReturnFocus`, exercised in the top-layer
|
|
18
|
+
* `drawer.spec.tsx`.
|
|
19
|
+
*
|
|
20
|
+
* See: `platform/packages/design-system/top-layer/notes/architecture/focus.md`.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const featureFlag = 'platform-dst-top-layer';
|
|
24
|
+
|
|
25
|
+
test.describe('Drawer: top-layer focus contract', () => {
|
|
26
|
+
test('initial focus: focus moves to the first focusable element on open', async ({ page }) => {
|
|
27
|
+
await page.visitExample<typeof import('../../examples/98-testing-initial-focus-matrix.tsx')>(
|
|
28
|
+
'design-system',
|
|
29
|
+
'drawer',
|
|
30
|
+
'testing-initial-focus-matrix',
|
|
31
|
+
{ featureFlag },
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
await page.getByTestId('default-drawer-trigger').click();
|
|
35
|
+
|
|
36
|
+
const dialog = page.getByTestId('default-drawer');
|
|
37
|
+
await expect(dialog).toBeVisible();
|
|
38
|
+
|
|
39
|
+
// The first focusable inside the drawer is the sidebar close button.
|
|
40
|
+
await expect(page.getByTestId('DrawerCloseButton')).toBeFocused();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('focus restoration: Escape restores focus to the trigger', async ({ page }) => {
|
|
44
|
+
await page.visitExample<typeof import('../../examples/98-testing-initial-focus-matrix.tsx')>(
|
|
45
|
+
'design-system',
|
|
46
|
+
'drawer',
|
|
47
|
+
'testing-initial-focus-matrix',
|
|
48
|
+
{ featureFlag },
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const trigger = page.getByTestId('default-drawer-trigger');
|
|
52
|
+
await trigger.click();
|
|
53
|
+
await expect(page.getByTestId('default-drawer')).toBeVisible();
|
|
54
|
+
|
|
55
|
+
await page.keyboard.press('Escape');
|
|
56
|
+
await expect(page.getByTestId('default-drawer')).toBeHidden();
|
|
57
|
+
await expect(trigger).toBeFocused();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// WCAG 2.4.3 Focus Order + HTML `<dialog>` focusing steps
|
|
61
|
+
// (https://html.spec.whatwg.org/multipage/interactive-elements.html#dialog-focusing-steps).
|
|
62
|
+
// When a descendant of the dialog carries the native HTML `autofocus`
|
|
63
|
+
// attribute, focus must land on that element instead of the first
|
|
64
|
+
// focusable. This matches both `<dialog>.showModal()` and the WAI-ARIA APG
|
|
65
|
+
// Dialog pattern.
|
|
66
|
+
test('initial focus: native [autofocus] element wins over the first focusable element', async ({
|
|
67
|
+
page,
|
|
68
|
+
}) => {
|
|
69
|
+
await page.visitExample<typeof import('../../examples/98-testing-initial-focus-matrix.tsx')>(
|
|
70
|
+
'design-system',
|
|
71
|
+
'drawer',
|
|
72
|
+
'testing-initial-focus-matrix',
|
|
73
|
+
{ featureFlag },
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
await page.getByTestId('native-autofocus-drawer-trigger').click();
|
|
77
|
+
|
|
78
|
+
const dialog = page.getByTestId('native-autofocus-drawer');
|
|
79
|
+
await expect(dialog).toBeVisible();
|
|
80
|
+
|
|
81
|
+
// The first focusable inside the drawer is the sidebar close button, but
|
|
82
|
+
// the body input carries `autofocus`, so focus must land on the input.
|
|
83
|
+
await expect(page.getByTestId('native-autofocus-input')).toBeFocused();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('focus movement: Tab cycles focus forward within the drawer', async ({ page }) => {
|
|
87
|
+
await page.visitExample<typeof import('../../examples/98-testing-initial-focus-matrix.tsx')>(
|
|
88
|
+
'design-system',
|
|
89
|
+
'drawer',
|
|
90
|
+
'testing-initial-focus-matrix',
|
|
91
|
+
{ featureFlag },
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
await page.getByTestId('multi-focusable-drawer-trigger').click();
|
|
95
|
+
const dialog = page.getByTestId('multi-focusable-drawer');
|
|
96
|
+
await expect(dialog).toBeVisible();
|
|
97
|
+
|
|
98
|
+
// Walk forward through every focusable element in the drawer and verify
|
|
99
|
+
// focus never escapes the dialog surface.
|
|
100
|
+
const focusables = [
|
|
101
|
+
page.getByTestId('DrawerCloseButton'),
|
|
102
|
+
page.getByTestId('focusable-1'),
|
|
103
|
+
page.getByTestId('focusable-2'),
|
|
104
|
+
page.getByTestId('focusable-3'),
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
for (const target of focusables) {
|
|
108
|
+
await expect(target).toBeFocused();
|
|
109
|
+
await page.keyboard.press('Tab');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// After cycling past the last focusable, focus must remain inside the
|
|
113
|
+
// drawer (focus wrap), never on a node behind it.
|
|
114
|
+
const activeWithinDialog = await dialog.evaluate(
|
|
115
|
+
(dialogElement) =>
|
|
116
|
+
document.activeElement !== null && dialogElement.contains(document.activeElement),
|
|
117
|
+
);
|
|
118
|
+
expect(activeWithinDialog).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('focus movement: Shift+Tab cycles focus backward within the drawer', async ({ page }) => {
|
|
122
|
+
await page.visitExample<typeof import('../../examples/98-testing-initial-focus-matrix.tsx')>(
|
|
123
|
+
'design-system',
|
|
124
|
+
'drawer',
|
|
125
|
+
'testing-initial-focus-matrix',
|
|
126
|
+
{ featureFlag },
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
await page.getByTestId('multi-focusable-drawer-trigger').click();
|
|
130
|
+
const dialog = page.getByTestId('multi-focusable-drawer');
|
|
131
|
+
await expect(dialog).toBeVisible();
|
|
132
|
+
await expect(page.getByTestId('DrawerCloseButton')).toBeFocused();
|
|
133
|
+
|
|
134
|
+
// From the first focusable, Shift+Tab wraps to the last, then walks back
|
|
135
|
+
// up. Focus stays trapped within the drawer throughout.
|
|
136
|
+
const backwardOrder = [
|
|
137
|
+
page.getByTestId('focusable-3'),
|
|
138
|
+
page.getByTestId('focusable-2'),
|
|
139
|
+
page.getByTestId('focusable-1'),
|
|
140
|
+
page.getByTestId('DrawerCloseButton'),
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
for (const target of backwardOrder) {
|
|
144
|
+
await page.keyboard.press('Shift+Tab');
|
|
145
|
+
await expect(target).toBeFocused();
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
});
|
package/compass.yml
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
configVersion: 1
|
|
2
|
+
id: ari:cloud:compass:a436116f-02ce-4520-8fbb-7301462a1674:component/c5751cc6-3513-4070-9deb-af31e86aed34/77224ba7-72d9-4d2b-a3f6-a78dcc8067ee
|
|
3
|
+
name: '@atlaskit/drawer'
|
|
4
|
+
ownerId: ari:cloud:identity::team/be19136e-6275-419f-be0c-fcee844b8684 # Design System Engineers
|
|
5
|
+
labels:
|
|
6
|
+
- platform-code
|
|
7
|
+
- platform-afm
|
|
8
|
+
typeId: OTHER
|
|
9
|
+
fields:
|
|
10
|
+
tier: 4
|
|
11
|
+
lifecycle: Active
|
|
12
|
+
isMonorepoProject: true
|
|
13
|
+
links:
|
|
14
|
+
- name: Root Repository
|
|
15
|
+
type: REPOSITORY
|
|
16
|
+
url: https://bitbucket.org/atlassian/atlassian-frontend-monorepo/src/master
|
|
17
|
+
- name: Slack Channel
|
|
18
|
+
type: CHAT_CHANNEL
|
|
19
|
+
url: https://atlassian.enterprise.slack.com/archives/CFJ9DU39U # #help-design-system
|
|
20
|
+
- name: Drawer
|
|
21
|
+
type: REPOSITORY
|
|
22
|
+
url: https://bitbucket.org/atlassian/atlassian-frontend-monorepo/src/master/platform/packages/design-system/drawer
|
|
23
|
+
customFields:
|
|
24
|
+
- name: Department
|
|
25
|
+
type: text
|
|
26
|
+
value: Eng - Design System Fundamentals
|
|
27
|
+
- name: Technical Owner
|
|
28
|
+
type: user
|
|
29
|
+
value: ari:cloud:identity::user/557058:86a9b692-7997-49cb-9984-080801b1de91 # Jared Crowe
|
|
30
|
+
- name: Required Reviewers Opt In
|
|
31
|
+
type: boolean
|
|
32
|
+
value: true
|
|
33
|
+
- name: Reviewer Selection Mechanism
|
|
34
|
+
type: text
|
|
35
|
+
value: random(3)
|
|
36
|
+
- name: Required Reviewer Approvals
|
|
37
|
+
type: number
|
|
38
|
+
value: 1
|
|
@@ -14,6 +14,7 @@ var _runtime = require("@compiled/react/runtime");
|
|
|
14
14
|
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
|
|
15
15
|
var _reactScrolllock = _interopRequireDefault(require("react-scrolllock"));
|
|
16
16
|
var _useCallbackRef = require("use-callback-ref");
|
|
17
|
+
var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
|
|
17
18
|
var _useEnsureIsInsideDrawer = require("../use-ensure-is-inside-drawer");
|
|
18
19
|
var _usePreventProgrammaticScroll = _interopRequireDefault(require("./hooks/use-prevent-programmatic-scroll"));
|
|
19
20
|
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); }
|
|
@@ -60,8 +61,16 @@ var DrawerContent = exports.DrawerContent = function DrawerContent(_ref2) {
|
|
|
60
61
|
xcss = _ref2.xcss;
|
|
61
62
|
(0, _useEnsureIsInsideDrawer.useEnsureIsInsideDrawer)();
|
|
62
63
|
(0, _usePreventProgrammaticScroll.default)();
|
|
63
|
-
|
|
64
|
+
var content = /*#__PURE__*/React.createElement(DrawerContentBase, {
|
|
64
65
|
scrollContentLabel: scrollContentLabel,
|
|
65
66
|
xcss: xcss
|
|
66
|
-
}, children)
|
|
67
|
+
}, children);
|
|
68
|
+
|
|
69
|
+
// Under the top-layer flag, background scroll is handled by `DialogScrollLock`
|
|
70
|
+
// (rendered by `DrawerTopLayer`), so we skip `react-scrolllock` here to avoid
|
|
71
|
+
// double-locking the body. See the top-layer migration scroll decision.
|
|
72
|
+
if ((0, _platformFeatureFlags.fg)('platform-dst-top-layer')) {
|
|
73
|
+
return content;
|
|
74
|
+
}
|
|
75
|
+
return /*#__PURE__*/React.createElement(_reactScrolllock.default, null, content);
|
|
67
76
|
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.drawerSlideIn = drawerSlideIn;
|
|
7
|
+
/**
|
|
8
|
+
* Off-screen transform per entry edge. A drawer slides in from the edge it is
|
|
9
|
+
* pinned to and exits back to the same edge (legacy `SlideIn` used
|
|
10
|
+
* `exitTo={enterFrom}`), so enter and exit are symmetric.
|
|
11
|
+
*/
|
|
12
|
+
var HIDDEN_TRANSFORM = {
|
|
13
|
+
left: 'translateX(-100%)',
|
|
14
|
+
right: 'translateX(100%)',
|
|
15
|
+
top: 'translateY(-100%)',
|
|
16
|
+
bottom: 'translateY(100%)'
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Match the legacy `CustomSlideIn`: `duration="small"` (100ms) on enter,
|
|
20
|
+
// `small * 0.5` (50ms) on exit, `ease-out` timing. Drawer slides with no panel
|
|
21
|
+
// fade (`fade="none"`); only the backdrop fades.
|
|
22
|
+
var ENTER_MS = 100;
|
|
23
|
+
var EXIT_MS = 50;
|
|
24
|
+
var EASE = 'cubic-bezier(0.2,0,0,1)'; // @atlaskit/motion `easeOut`
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Builds the slide CSS for a single entry edge, scoped to
|
|
28
|
+
* `[data-ds-dialog-drawer-slide-{from}]` (the `Dialog` primitive stamps
|
|
29
|
+
* `data-ds-dialog-{name}` on the `<dialog>` element).
|
|
30
|
+
*
|
|
31
|
+
* - base selector → hidden/exit state + exit duration
|
|
32
|
+
* - `[open]` → visible state + enter duration
|
|
33
|
+
* - `@starting-style` → initial (pre-paint) state so the entry transition runs
|
|
34
|
+
*
|
|
35
|
+
* The `::backdrop` fades its blanket colour in step with the panel. Stacked
|
|
36
|
+
* drawers hide their backdrop via the `Dialog` `shouldHideBackdrop` prop (an
|
|
37
|
+
* ID-scoped `<style>` whose specificity beats this rule).
|
|
38
|
+
*/
|
|
39
|
+
function buildCss(name, hidden) {
|
|
40
|
+
var sel = "[data-ds-dialog-".concat(name, "]");
|
|
41
|
+
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");
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Directional slide animation for the top-layer `Drawer`, passed to the
|
|
45
|
+
* `Dialog` primitive's `animate` prop. Local to `@atlaskit/drawer` (not a
|
|
46
|
+
* shared top-layer preset). The panel slides in from its pinned viewport edge
|
|
47
|
+
* while the `::backdrop` fades.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```tsx
|
|
51
|
+
* <Dialog animate={drawerSlideIn({ from: 'left' })} isOpen={isOpen} onClose={onClose} label="...">
|
|
52
|
+
* ...
|
|
53
|
+
* </Dialog>
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
function drawerSlideIn() {
|
|
57
|
+
var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
|
|
58
|
+
_ref$from = _ref.from,
|
|
59
|
+
from = _ref$from === void 0 ? 'left' : _ref$from;
|
|
60
|
+
var name = "drawer-slide-".concat(from);
|
|
61
|
+
return {
|
|
62
|
+
name: name,
|
|
63
|
+
css: buildCss(name, HIDDEN_TRANSFORM[from]),
|
|
64
|
+
enterDurationMs: ENTER_MS,
|
|
65
|
+
exitDurationMs: EXIT_MS
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -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,256 @@
|
|
|
1
|
+
/* drawer-top-layer.tsx generated by @compiled/babel-plugin v0.39.1 */
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
5
|
+
var _typeof3 = require("@babel/runtime/helpers/typeof");
|
|
6
|
+
Object.defineProperty(exports, "__esModule", {
|
|
7
|
+
value: true
|
|
8
|
+
});
|
|
9
|
+
exports.DrawerTopLayer = DrawerTopLayer;
|
|
10
|
+
require("./drawer-top-layer.compiled.css");
|
|
11
|
+
var _react = _interopRequireWildcard(require("react"));
|
|
12
|
+
var React = _react;
|
|
13
|
+
var _runtime = require("@compiled/react/runtime");
|
|
14
|
+
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
|
|
15
|
+
var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof"));
|
|
16
|
+
var _bindEventListener = require("bind-event-listener");
|
|
17
|
+
var _usePlatformLeafEventHandler = require("@atlaskit/analytics-next/usePlatformLeafEventHandler");
|
|
18
|
+
var _dialog = require("@atlaskit/top-layer/dialog");
|
|
19
|
+
var _dialogScrollLock = require("@atlaskit/top-layer/dialog-scroll-lock");
|
|
20
|
+
var _ensureIsInsideDrawerContext = require("../ensure-is-inside-drawer-context");
|
|
21
|
+
var _onCloseContext = require("../on-close-context");
|
|
22
|
+
var _useDrawerStack = _interopRequireDefault(require("../use-drawer-stack"));
|
|
23
|
+
var _drawerSlideIn = require("./drawer-slide-in");
|
|
24
|
+
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != _typeof3(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); }
|
|
25
|
+
var LOCAL_CURRENT_SURFACE_CSS_VAR = '--ds-elevation-surface-current';
|
|
26
|
+
var styles = {
|
|
27
|
+
surface: "_1reo15vq _18m915vq _1e0c1txw _1bsb1osq _4t3i1osq _bfhk1bhr _1q1l1bhr _ect4ttxp"
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Width presets mirror the legacy `DrawerPanel`. Applied to the `<dialog>` (the
|
|
31
|
+
// sized, edge-pinned element) and clamped to the viewport for mobile safety.
|
|
32
|
+
var WIDTH_MAP = {
|
|
33
|
+
narrow: '360px',
|
|
34
|
+
medium: '480px',
|
|
35
|
+
wide: '600px',
|
|
36
|
+
extended: '95vw',
|
|
37
|
+
full: '100vw'
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the native `<dialog>` accessible name props. Prefer the consumer's
|
|
42
|
+
* `label`, then their `titleId`. If neither is supplied (legacy allowed this),
|
|
43
|
+
* fall back to a generic name so the dialog is never unlabelled. Note:
|
|
44
|
+
* `@atlaskit/drawer` has no i18n setup, so this fallback is English only;
|
|
45
|
+
* consumers should pass a localised `label` or `titleId`.
|
|
46
|
+
*/
|
|
47
|
+
function getAccessibleName(_ref) {
|
|
48
|
+
var label = _ref.label,
|
|
49
|
+
titleId = _ref.titleId;
|
|
50
|
+
if (label) {
|
|
51
|
+
return {
|
|
52
|
+
label: label
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (titleId) {
|
|
56
|
+
return {
|
|
57
|
+
labelledBy: titleId
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
label: 'Drawer'
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* **DrawerTopLayer**
|
|
67
|
+
*
|
|
68
|
+
* Top-layer (`platform-dst-top-layer`) implementation of `Drawer`. Renders a
|
|
69
|
+
* native `<dialog>` via `@atlaskit/top-layer`, replacing Portal, Blanket,
|
|
70
|
+
* react-focus-lock, react-scrolllock and `@atlaskit/layering` with native
|
|
71
|
+
* modality, `::backdrop`, focus trap and return, and `DialogScrollLock`.
|
|
72
|
+
*
|
|
73
|
+
* The `Dialog` primitive owns the entry and exit animation lifecycle (it keeps
|
|
74
|
+
* the host element mounted through the exit transition, then fires
|
|
75
|
+
* `onExitFinish`), so no `ExitingPersistence` wrapper is needed: the drawer
|
|
76
|
+
* renders `<Dialog isOpen={isOpen}>` directly. `useDrawerStack` tracks stack
|
|
77
|
+
* depth so only the foreground drawer shows a `::backdrop`.
|
|
78
|
+
*
|
|
79
|
+
* `isFocusLockEnabled` is intentionally unsupported: a native modal `<dialog>`
|
|
80
|
+
* always traps focus, so `isFocusLockEnabled={false}` is a no-op under this gate.
|
|
81
|
+
*/
|
|
82
|
+
function DrawerTopLayer(_ref2) {
|
|
83
|
+
var _ref2$width = _ref2.width,
|
|
84
|
+
width = _ref2$width === void 0 ? 'narrow' : _ref2$width,
|
|
85
|
+
isOpen = _ref2.isOpen,
|
|
86
|
+
_ref2$shouldReturnFoc = _ref2.shouldReturnFocus,
|
|
87
|
+
shouldReturnFocus = _ref2$shouldReturnFoc === void 0 ? true : _ref2$shouldReturnFoc,
|
|
88
|
+
onKeyDown = _ref2.onKeyDown,
|
|
89
|
+
testId = _ref2.testId,
|
|
90
|
+
children = _ref2.children,
|
|
91
|
+
onClose = _ref2.onClose,
|
|
92
|
+
onCloseComplete = _ref2.onCloseComplete,
|
|
93
|
+
onOpenComplete = _ref2.onOpenComplete,
|
|
94
|
+
label = _ref2.label,
|
|
95
|
+
titleId = _ref2.titleId,
|
|
96
|
+
_ref2$enterFrom = _ref2.enterFrom,
|
|
97
|
+
enterFrom = _ref2$enterFrom === void 0 ? 'left' : _ref2$enterFrom;
|
|
98
|
+
var stackIndex = (0, _useDrawerStack.default)({
|
|
99
|
+
isOpen: isOpen
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Points to the panel surface. Passed to `onOpenComplete` / `onCloseComplete`.
|
|
104
|
+
*/
|
|
105
|
+
var contentRef = (0, _react.useRef)(null);
|
|
106
|
+
// Cache the last content element so `onCloseComplete` still receives a node
|
|
107
|
+
// after children unmount (with reduced motion `contentRef` can clear before
|
|
108
|
+
// `onExitFinish` fires).
|
|
109
|
+
var lastContentElRef = (0, _react.useRef)(null);
|
|
110
|
+
// Callback ref runs at commit (not during render), so both refs stay
|
|
111
|
+
// populated without a render-time side effect. `lastContentElRef` only
|
|
112
|
+
// overwrites with a non-null node, preserving it across the unmount.
|
|
113
|
+
var setContentEl = (0, _react.useCallback)(function (el) {
|
|
114
|
+
contentRef.current = el;
|
|
115
|
+
if (el) {
|
|
116
|
+
lastContentElRef.current = el;
|
|
117
|
+
}
|
|
118
|
+
}, []);
|
|
119
|
+
|
|
120
|
+
// Analytics-wrapped close handlers, one per legacy trigger.
|
|
121
|
+
var handleEscapeClose = (0, _usePlatformLeafEventHandler.usePlatformLeafEventHandler)({
|
|
122
|
+
fn: function fn(evt, analyticsEvent) {
|
|
123
|
+
return onClose === null || onClose === void 0 ? void 0 : onClose(evt, analyticsEvent);
|
|
124
|
+
},
|
|
125
|
+
action: 'dismissed',
|
|
126
|
+
componentName: 'drawer',
|
|
127
|
+
packageName: "@atlaskit/drawer",
|
|
128
|
+
packageVersion: "13.1.1",
|
|
129
|
+
analyticsData: {
|
|
130
|
+
trigger: 'escKey'
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
var handleBlanketClose = (0, _usePlatformLeafEventHandler.usePlatformLeafEventHandler)({
|
|
134
|
+
fn: function fn(evt, analyticsEvent) {
|
|
135
|
+
return onClose === null || onClose === void 0 ? void 0 : onClose(evt, analyticsEvent);
|
|
136
|
+
},
|
|
137
|
+
action: 'dismissed',
|
|
138
|
+
componentName: 'drawer',
|
|
139
|
+
packageName: "@atlaskit/drawer",
|
|
140
|
+
packageVersion: "13.1.1",
|
|
141
|
+
analyticsData: {
|
|
142
|
+
trigger: 'blanket'
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
var handleBackButtonClose = (0, _usePlatformLeafEventHandler.usePlatformLeafEventHandler)({
|
|
146
|
+
fn: function fn(evt, analyticsEvent) {
|
|
147
|
+
return onClose === null || onClose === void 0 ? void 0 : onClose(evt, analyticsEvent);
|
|
148
|
+
},
|
|
149
|
+
action: 'dismissed',
|
|
150
|
+
componentName: 'drawer',
|
|
151
|
+
packageName: "@atlaskit/drawer",
|
|
152
|
+
packageVersion: "13.1.1",
|
|
153
|
+
analyticsData: {
|
|
154
|
+
trigger: 'backButton'
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Bridge the Dialog primitive's `onClose({ reason })` to the legacy
|
|
159
|
+
// `onClose(event, analyticsEvent)` contract via a synthetic event. Drawer
|
|
160
|
+
// has no `shouldCloseOn*` props, so both reasons always forward.
|
|
161
|
+
var handleDialogClose = (0, _react.useCallback)(function (_ref3) {
|
|
162
|
+
var reason = _ref3.reason;
|
|
163
|
+
var event = (0, _dialog.createCloseEvent)({
|
|
164
|
+
reason: reason
|
|
165
|
+
});
|
|
166
|
+
if (reason === 'escape') {
|
|
167
|
+
handleEscapeClose(event);
|
|
168
|
+
} else {
|
|
169
|
+
handleBlanketClose(event);
|
|
170
|
+
}
|
|
171
|
+
}, [handleEscapeClose, handleBlanketClose]);
|
|
172
|
+
|
|
173
|
+
// Mirror the legacy window `keydown` listener so `onKeyDown` still fires
|
|
174
|
+
// while the drawer is open.
|
|
175
|
+
var handleKeyDown = (0, _react.useCallback)(function (evt) {
|
|
176
|
+
onKeyDown === null || onKeyDown === void 0 || onKeyDown(evt);
|
|
177
|
+
}, [onKeyDown]);
|
|
178
|
+
(0, _react.useEffect)(function () {
|
|
179
|
+
if (!isOpen) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
return (0, _bindEventListener.bind)(window, {
|
|
183
|
+
type: 'keydown',
|
|
184
|
+
listener: handleKeyDown
|
|
185
|
+
});
|
|
186
|
+
}, [isOpen, handleKeyDown]);
|
|
187
|
+
|
|
188
|
+
// `onOpenComplete` once the entry animation settles (via `Dialog`'s
|
|
189
|
+
// `onEnterFinish`, which the underlying hook fires for animated,
|
|
190
|
+
// non-animated and reduced-motion paths).
|
|
191
|
+
var handleEnterFinish = (0, _react.useCallback)(function () {
|
|
192
|
+
onOpenComplete === null || onOpenComplete === void 0 || onOpenComplete(contentRef.current);
|
|
193
|
+
}, [onOpenComplete]);
|
|
194
|
+
|
|
195
|
+
// `onCloseComplete` once the exit animation settles (via `Dialog`'s
|
|
196
|
+
// `onExitFinish`). This is also where a custom `shouldReturnFocus={ref}` is
|
|
197
|
+
// honoured: native `<dialog>` restores focus to the trigger at the start of
|
|
198
|
+
// close, so the consumer's ref is focused now that the exit is done.
|
|
199
|
+
// `shouldReturnFocus={false}` is a documented best-effort limitation; native
|
|
200
|
+
// always restores focus to the trigger.
|
|
201
|
+
var handleExitFinish = (0, _react.useCallback)(function () {
|
|
202
|
+
var _contentRef$current;
|
|
203
|
+
onCloseComplete === null || onCloseComplete === void 0 || onCloseComplete((_contentRef$current = contentRef.current) !== null && _contentRef$current !== void 0 ? _contentRef$current : lastContentElRef.current);
|
|
204
|
+
lastContentElRef.current = null;
|
|
205
|
+
if ((0, _typeof2.default)(shouldReturnFocus) === 'object' && shouldReturnFocus !== null && shouldReturnFocus !== void 0 && shouldReturnFocus.current) {
|
|
206
|
+
shouldReturnFocus.current.focus();
|
|
207
|
+
}
|
|
208
|
+
}, [onCloseComplete, shouldReturnFocus]);
|
|
209
|
+
|
|
210
|
+
// Pin the `<dialog>` full-height to the inline-start edge (overriding the
|
|
211
|
+
// primitive's centred `margin: auto`); width from the preset, clamped to the
|
|
212
|
+
// viewport. `enterFrom` only drives the slide animation, not the pin edge;
|
|
213
|
+
// it matches the legacy panel, which always pins `inset-inline-start`.
|
|
214
|
+
var dialogStyle = {
|
|
215
|
+
margin: 0,
|
|
216
|
+
insetBlockStart: 0,
|
|
217
|
+
insetInlineStart: 0,
|
|
218
|
+
insetInlineEnd: 'auto',
|
|
219
|
+
height: '100dvh',
|
|
220
|
+
maxHeight: '100dvh',
|
|
221
|
+
width: "min(".concat(WIDTH_MAP[width], ", 100vw)")
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// Memoized so the preset object (and its CSS string) is rebuilt only when
|
|
225
|
+
// `enterFrom` changes, not on every render.
|
|
226
|
+
var animate = (0, _react.useMemo)(function () {
|
|
227
|
+
return (0, _drawerSlideIn.drawerSlideIn)({
|
|
228
|
+
from: enterFrom
|
|
229
|
+
});
|
|
230
|
+
}, [enterFrom]);
|
|
231
|
+
var accessibleName = getAccessibleName({
|
|
232
|
+
label: label,
|
|
233
|
+
titleId: titleId
|
|
234
|
+
});
|
|
235
|
+
return /*#__PURE__*/React.createElement(_dialog.Dialog, (0, _extends2.default)({
|
|
236
|
+
isOpen: isOpen,
|
|
237
|
+
onClose: handleDialogClose,
|
|
238
|
+
onEnterFinish: handleEnterFinish,
|
|
239
|
+
onExitFinish: handleExitFinish,
|
|
240
|
+
animate: animate,
|
|
241
|
+
shouldHideBackdrop: stackIndex > 0,
|
|
242
|
+
testId: testId
|
|
243
|
+
}, accessibleName, {
|
|
244
|
+
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop
|
|
245
|
+
style: dialogStyle
|
|
246
|
+
}), /*#__PURE__*/React.createElement(_dialogScrollLock.DialogScrollLock, {
|
|
247
|
+
isOpen: true
|
|
248
|
+
}), /*#__PURE__*/React.createElement("div", {
|
|
249
|
+
ref: setContentEl,
|
|
250
|
+
className: (0, _runtime.ax)([styles.surface])
|
|
251
|
+
}, /*#__PURE__*/React.createElement(_ensureIsInsideDrawerContext.EnsureIsInsideDrawerContext.Provider, {
|
|
252
|
+
value: true
|
|
253
|
+
}, /*#__PURE__*/React.createElement(_onCloseContext.OnCloseContext.Provider, {
|
|
254
|
+
value: handleBackButtonClose
|
|
255
|
+
}, children))));
|
|
256
|
+
}
|
|
@@ -8,6 +8,7 @@ exports.default = usePreventProgrammaticScroll;
|
|
|
8
8
|
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
|
|
9
9
|
var _react = require("react");
|
|
10
10
|
var _bindEventListener = require("bind-event-listener");
|
|
11
|
+
var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
|
|
11
12
|
/**
|
|
12
13
|
* Returns how far the body is scrolled from the top of the viewport.
|
|
13
14
|
*
|
|
@@ -47,6 +48,11 @@ function usePreventProgrammaticScroll() {
|
|
|
47
48
|
}
|
|
48
49
|
}, [scrollTopOffset]);
|
|
49
50
|
(0, _react.useEffect)(function () {
|
|
51
|
+
// The top-layer drawer relies on native modality + `DialogScrollLock`; the
|
|
52
|
+
// programmatic-scroll guard is intentionally dropped under the flag.
|
|
53
|
+
if ((0, _platformFeatureFlags.fg)('platform-dst-top-layer')) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
50
56
|
return (0, _bindEventListener.bind)(window, {
|
|
51
57
|
type: 'scroll',
|
|
52
58
|
listener: onWindowScroll
|
package/dist/cjs/drawer.js
CHANGED
|
@@ -10,9 +10,11 @@ var _react = _interopRequireWildcard(require("react"));
|
|
|
10
10
|
var _exenv = require("exenv");
|
|
11
11
|
var _analyticsNext = require("@atlaskit/analytics-next");
|
|
12
12
|
var _layering = require("@atlaskit/layering");
|
|
13
|
+
var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
|
|
13
14
|
var _portal = _interopRequireDefault(require("@atlaskit/portal"));
|
|
14
15
|
var _blanket = _interopRequireDefault(require("./blanket"));
|
|
15
16
|
var _drawerPanel = require("./drawer-panel/drawer-panel");
|
|
17
|
+
var _drawerTopLayer = require("./drawer-panel/drawer-top-layer");
|
|
16
18
|
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); }
|
|
17
19
|
/// <reference types="node" />
|
|
18
20
|
/* eslint-disable @repo/internal/dom-events/no-unsafe-event-listeners */
|
|
@@ -29,17 +31,7 @@ var EscapeCloseManager = function EscapeCloseManager(_ref) {
|
|
|
29
31
|
});
|
|
30
32
|
return /*#__PURE__*/_react.default.createElement("span", null);
|
|
31
33
|
};
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* __Drawer__
|
|
35
|
-
*
|
|
36
|
-
* A drawer is a panel that slides in from the left side of the screen.
|
|
37
|
-
*
|
|
38
|
-
* - [Examples](https://atlassian.design/components/drawer/examples)
|
|
39
|
-
* - [Code](https://atlassian.design/components/drawer/code)
|
|
40
|
-
* - [Usage](https://atlassian.design/components/drawer/usage)
|
|
41
|
-
*/
|
|
42
|
-
var Drawer = exports.Drawer = function Drawer(_ref2) {
|
|
34
|
+
var DrawerBase = function DrawerBase(_ref2) {
|
|
43
35
|
var _ref2$width = _ref2.width,
|
|
44
36
|
width = _ref2$width === void 0 ? 'narrow' : _ref2$width,
|
|
45
37
|
isOpen = _ref2.isOpen,
|
|
@@ -67,7 +59,7 @@ var Drawer = exports.Drawer = function Drawer(_ref2) {
|
|
|
67
59
|
action: 'dismissed',
|
|
68
60
|
componentName: 'drawer',
|
|
69
61
|
packageName: "@atlaskit/drawer",
|
|
70
|
-
packageVersion: "13.
|
|
62
|
+
packageVersion: "13.1.1",
|
|
71
63
|
analyticsData: {
|
|
72
64
|
trigger: 'escKey'
|
|
73
65
|
}
|
|
@@ -90,7 +82,7 @@ var Drawer = exports.Drawer = function Drawer(_ref2) {
|
|
|
90
82
|
action: 'dismissed',
|
|
91
83
|
componentName: 'drawer',
|
|
92
84
|
packageName: "@atlaskit/drawer",
|
|
93
|
-
packageVersion: "13.
|
|
85
|
+
packageVersion: "13.1.1",
|
|
94
86
|
analyticsData: {
|
|
95
87
|
trigger: 'blanket'
|
|
96
88
|
}
|
|
@@ -102,7 +94,7 @@ var Drawer = exports.Drawer = function Drawer(_ref2) {
|
|
|
102
94
|
action: 'dismissed',
|
|
103
95
|
componentName: 'drawer',
|
|
104
96
|
packageName: "@atlaskit/drawer",
|
|
105
|
-
packageVersion: "13.
|
|
97
|
+
packageVersion: "13.1.1",
|
|
106
98
|
analyticsData: {
|
|
107
99
|
trigger: 'backButton'
|
|
108
100
|
}
|
|
@@ -135,4 +127,23 @@ var Drawer = exports.Drawer = function Drawer(_ref2) {
|
|
|
135
127
|
}, children, /*#__PURE__*/_react.default.createElement(EscapeCloseManager, {
|
|
136
128
|
onClose: handleClose
|
|
137
129
|
})) : children));
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* __Drawer__
|
|
134
|
+
*
|
|
135
|
+
* A drawer is a panel that slides in from the left side of the screen.
|
|
136
|
+
*
|
|
137
|
+
* - [Examples](https://atlassian.design/components/drawer/examples)
|
|
138
|
+
* - [Code](https://atlassian.design/components/drawer/code)
|
|
139
|
+
* - [Usage](https://atlassian.design/components/drawer/usage)
|
|
140
|
+
*/
|
|
141
|
+
var Drawer = exports.Drawer = function Drawer(props) {
|
|
142
|
+
if ((0, _platformFeatureFlags.fg)('platform-dst-top-layer')) {
|
|
143
|
+
// eslint-disable-next-line @repo/internal/react/no-unsafe-spread-props -- internal implementation component takes the same DrawerProps
|
|
144
|
+
return /*#__PURE__*/_react.default.createElement(_drawerTopLayer.DrawerTopLayer, props);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// eslint-disable-next-line @repo/internal/react/no-unsafe-spread-props -- internal implementation component takes the same DrawerProps
|
|
148
|
+
return /*#__PURE__*/_react.default.createElement(DrawerBase, props);
|
|
138
149
|
};
|