@atlaskit/drawer 13.1.1 → 13.3.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 +24 -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-top-layer.compiled.css +34 -0
- package/dist/cjs/drawer-panel/drawer-top-layer.js +271 -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-top-layer.compiled.css +34 -0
- package/dist/es2019/drawer-panel/drawer-top-layer.js +255 -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-top-layer.compiled.css +34 -0
- package/dist/esm/drawer-panel/drawer-top-layer.js +262 -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-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/drawer.docs.tsx +4 -4
- package/package.json +15 -7
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
4
|
+
Object.defineProperty(exports, "__esModule", {
|
|
5
|
+
value: true
|
|
6
|
+
});
|
|
7
|
+
exports.default = useDrawerStack;
|
|
8
|
+
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
|
|
9
|
+
var _react = require("react");
|
|
10
|
+
var _useLazyCallback = _interopRequireDefault(require("@atlaskit/ds-lib/use-lazy-callback"));
|
|
11
|
+
var _usePreviousValue = _interopRequireDefault(require("@atlaskit/ds-lib/use-previous-value"));
|
|
12
|
+
var _useStateRef3 = _interopRequireDefault(require("@atlaskit/ds-lib/use-state-ref"));
|
|
13
|
+
/**
|
|
14
|
+
* **Major versions will not know about each other**
|
|
15
|
+
*
|
|
16
|
+
* An array holding references to all currently open drawers. This only works
|
|
17
|
+
* for drawers of the same major version, because the reference differs between
|
|
18
|
+
* majors (for example V13 will not know about any from V14).
|
|
19
|
+
*
|
|
20
|
+
* Adapted from `useModalStack` in `@atlaskit/modal-dialog`. The top-layer drawer
|
|
21
|
+
* path uses this to know its position in the stack so that only the foreground
|
|
22
|
+
* drawer renders a visible `::backdrop` (this avoids cumulative darkening for
|
|
23
|
+
* nested or stacked drawers). Unlike the modal version, the register is timed
|
|
24
|
+
* off `isOpen` rather than `@atlaskit/motion`'s `useExitingPersistence`: the
|
|
25
|
+
* `Dialog` primitive owns its own exit lifecycle, so the top-layer drawer needs
|
|
26
|
+
* no `ExitingPersistence` wrapper.
|
|
27
|
+
*/
|
|
28
|
+
var drawerStackRegister = [];
|
|
29
|
+
/**
|
|
30
|
+
* Returns the position of the calling drawer in the drawer stack. A stack index
|
|
31
|
+
* of `0` is the foreground (top of the stack); higher numbers are further back.
|
|
32
|
+
*/
|
|
33
|
+
function useDrawerStack(_ref) {
|
|
34
|
+
var isOpen = _ref.isOpen,
|
|
35
|
+
onStackChange = _ref.onStackChange;
|
|
36
|
+
var _useStateRef = (0, _useStateRef3.default)(0),
|
|
37
|
+
_useStateRef2 = (0, _slicedToArray2.default)(_useStateRef, 2),
|
|
38
|
+
stackIndexRef = _useStateRef2[0],
|
|
39
|
+
setStackIndex = _useStateRef2[1];
|
|
40
|
+
var currentStackIndex = stackIndexRef.current;
|
|
41
|
+
var previousStackIndex = (0, _usePreviousValue.default)(stackIndexRef.current);
|
|
42
|
+
|
|
43
|
+
// Kept stable for the lifetime of the component so it can be the identity in
|
|
44
|
+
// the shared register. This is why it is a lazy callback and not a
|
|
45
|
+
// useMemo/useCallback value.
|
|
46
|
+
var updateStack = (0, _useLazyCallback.default)(function () {
|
|
47
|
+
var newStackIndex = drawerStackRegister.indexOf(updateStack);
|
|
48
|
+
// Read from the ref rather than state: this closure only ever sees the
|
|
49
|
+
// initial state value.
|
|
50
|
+
if (stackIndexRef.current !== newStackIndex) {
|
|
51
|
+
setStackIndex(newStackIndex);
|
|
52
|
+
stackIndexRef.current = newStackIndex;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
(0, _react.useEffect)(function () {
|
|
56
|
+
if (!isOpen) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Opening: join the front of the register (the newest open drawer is the
|
|
60
|
+
// foreground at index 0), then notify every open drawer to recompute.
|
|
61
|
+
if (drawerStackRegister.indexOf(updateStack) === -1) {
|
|
62
|
+
drawerStackRegister.unshift(updateStack);
|
|
63
|
+
}
|
|
64
|
+
drawerStackRegister.forEach(function (callback) {
|
|
65
|
+
return callback();
|
|
66
|
+
});
|
|
67
|
+
return function () {
|
|
68
|
+
// Closing or unmounting: leave the register and notify the rest.
|
|
69
|
+
var index = drawerStackRegister.indexOf(updateStack);
|
|
70
|
+
if (index !== -1) {
|
|
71
|
+
drawerStackRegister.splice(index, 1);
|
|
72
|
+
}
|
|
73
|
+
drawerStackRegister.forEach(function (callback) {
|
|
74
|
+
return callback();
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
}, [isOpen, updateStack]);
|
|
78
|
+
(0, _react.useEffect)(function () {
|
|
79
|
+
if (previousStackIndex === undefined) {
|
|
80
|
+
// Initial render: nothing to notify about.
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (previousStackIndex !== currentStackIndex) {
|
|
84
|
+
onStackChange === null || onStackChange === void 0 || onStackChange(currentStackIndex);
|
|
85
|
+
}
|
|
86
|
+
}, [onStackChange, previousStackIndex, currentStackIndex]);
|
|
87
|
+
return currentStackIndex;
|
|
88
|
+
}
|
|
@@ -5,6 +5,7 @@ import { ax, ix } from "@compiled/react/runtime";
|
|
|
5
5
|
import { forwardRef, useEffect, useRef, useState } from 'react';
|
|
6
6
|
import ScrollLock from 'react-scrolllock';
|
|
7
7
|
import { mergeRefs } from 'use-callback-ref';
|
|
8
|
+
import { fg } from '@atlaskit/platform-feature-flags';
|
|
8
9
|
import { useEnsureIsInsideDrawer } from '../use-ensure-is-inside-drawer';
|
|
9
10
|
import usePreventProgrammaticScroll from './hooks/use-prevent-programmatic-scroll';
|
|
10
11
|
const styles = {
|
|
@@ -48,8 +49,16 @@ export const DrawerContent = ({
|
|
|
48
49
|
}) => {
|
|
49
50
|
useEnsureIsInsideDrawer();
|
|
50
51
|
usePreventProgrammaticScroll();
|
|
51
|
-
|
|
52
|
+
const content = /*#__PURE__*/React.createElement(DrawerContentBase, {
|
|
52
53
|
scrollContentLabel: scrollContentLabel,
|
|
53
54
|
xcss: xcss
|
|
54
|
-
}, children)
|
|
55
|
+
}, children);
|
|
56
|
+
|
|
57
|
+
// Under the top-layer flag, background scroll is handled by `DialogScrollLock`
|
|
58
|
+
// (rendered by `DrawerTopLayer`), so we skip `react-scrolllock` here to avoid
|
|
59
|
+
// double-locking the body. See the top-layer migration scroll decision.
|
|
60
|
+
if (fg('platform-dst-top-layer')) {
|
|
61
|
+
return content;
|
|
62
|
+
}
|
|
63
|
+
return /*#__PURE__*/React.createElement(ScrollLock, null, content);
|
|
55
64
|
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
._13ku1fu8::-ms-backdrop{transition-timing-function:cubic-bezier(.15,1,.3,1)}
|
|
2
|
+
._13ku1fu8::backdrop{transition-timing-function:cubic-bezier(.15,1,.3,1)}
|
|
3
|
+
._18m915vq{overflow-y:hidden}
|
|
4
|
+
._1bsb1osq{width:100%}
|
|
5
|
+
._1e0c1txw{display:flex}
|
|
6
|
+
._1fsl1yx9[open]{transition-duration:.1s}
|
|
7
|
+
._1fyf1a5r::-ms-backdrop{transition-behavior:allow-discrete}
|
|
8
|
+
._1fyf1a5r::backdrop{transition-behavior:allow-discrete}
|
|
9
|
+
._1kdeglyw[open]{transform:none}
|
|
10
|
+
._1ltk1j28::-ms-backdrop{background-color:transparent}
|
|
11
|
+
._1ltk1j28::backdrop{background-color:transparent}
|
|
12
|
+
._1oec14ed{transition-duration:50ms}
|
|
13
|
+
._1q1l1bhr{--ds-elevation-surface-current:var(--ds-surface-overlay,#fff)}
|
|
14
|
+
._1reo15vq{overflow-x:hidden}
|
|
15
|
+
._1s9z1a5r{transition-behavior:allow-discrete}
|
|
16
|
+
._4t3i1osq{height:100%}
|
|
17
|
+
._4v7ipwmj[open]::-ms-backdrop{transition-duration:.7s}
|
|
18
|
+
._4v7ipwmj[open]::backdrop{transition-duration:.7s}
|
|
19
|
+
._6fl4dkwg{transition-timing-function:cubic-bezier(.2,0,0,1)}
|
|
20
|
+
._bfhk1bhr{background-color:var(--ds-surface-overlay,#fff)}
|
|
21
|
+
._d7fr1i5c[open]::-ms-backdrop{background-color:var(--ds-blanket,#050c1f75)}
|
|
22
|
+
._d7fr1i5c[open]::backdrop{background-color:var(--ds-blanket,#050c1f75)}
|
|
23
|
+
._ect4ttxp{font-family:var(--ds-font-family-body,"Atlassian Sans",ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",Ubuntu,"Helvetica Neue",sans-serif)}
|
|
24
|
+
._k8m05ji5{transition-property:transform,overlay,display}
|
|
25
|
+
._njwkivc9::-ms-backdrop{-ms-transition-property:background-color,overlay,display;transition-property:background-color,overlay,display}
|
|
26
|
+
._njwkivc9::backdrop{transition-property:background-color,overlay,display}
|
|
27
|
+
._t9ec3k1b{transform:translateY(100%)}
|
|
28
|
+
._t9ecjq3t{transform:translateX(-100%)}
|
|
29
|
+
._t9ecxwn4{transform:translateX(100%)}
|
|
30
|
+
._t9ecz6y5{transform:translateY(-100%)}
|
|
31
|
+
._yhpq1ttt::-ms-backdrop{transition-duration:.35s}
|
|
32
|
+
._yhpq1ttt::backdrop{transition-duration:.35s}
|
|
33
|
+
@media (prefers-reduced-motion:reduce){._156yru3m{transition-duration:0s}._aeldru3m[open]{transition-duration:0s}._1sbqru3m::-ms-backdrop{transition-duration:0s}._1sbqru3m::backdrop{transition-duration:0s}._1i6zru3m[open]::-ms-backdrop{transition-duration:0s}._1i6zru3m[open]::backdrop{transition-duration:0s}}
|
|
34
|
+
@starting-style{._1dar1j28[open]::-ms-backdrop{background-color:transparent}._1dar1j28[open]::backdrop{background-color:transparent}._1hqt3k1b[open]{transform:translateY(100%)}._1hqtjq3t[open]{transform:translateX(-100%)}._1hqtxwn4[open]{transform:translateX(100%)}._1hqtz6y5[open]{transform:translateY(-100%)}}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/* drawer-top-layer.tsx generated by @compiled/babel-plugin v0.39.1 */
|
|
2
|
+
import _extends from "@babel/runtime/helpers/extends";
|
|
3
|
+
import "./drawer-top-layer.compiled.css";
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import { ax, ix } from "@compiled/react/runtime";
|
|
6
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
7
|
+
import { cx } from '@compiled/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
|
+
const LOCAL_CURRENT_SURFACE_CSS_VAR = '--ds-elevation-surface-current';
|
|
16
|
+
|
|
17
|
+
// Legacy drawer panel uses `SlideIn duration="small"`, which resolves to
|
|
18
|
+
// 100ms on enter and 50ms on exit.
|
|
19
|
+
const slideEnterDurationMs = 100;
|
|
20
|
+
const slideExitDurationMs = 50;
|
|
21
|
+
|
|
22
|
+
// Legacy drawer blanket is a separate `FadeIn duration="large"`, which resolves
|
|
23
|
+
// to 700ms on enter and 350ms on exit. These intentionally differ from the panel
|
|
24
|
+
// slide timings to preserve the old visual behavior.
|
|
25
|
+
const fadeEnterDurationMs = 700;
|
|
26
|
+
const fadeExitDurationMs = 350;
|
|
27
|
+
const animate = {
|
|
28
|
+
kind: 'dialog',
|
|
29
|
+
name: 'custom'
|
|
30
|
+
};
|
|
31
|
+
const styles = {
|
|
32
|
+
root: "_k8m05ji5 _1oec14ed _6fl4dkwg _1s9z1a5r _1kdeglyw _1fsl1yx9 _1ltk1j28 _njwkivc9 _yhpq1ttt _13ku1fu8 _1fyf1a5r _d7fr1i5c _4v7ipwmj _1dar1j28 _156yru3m _aeldru3m _1sbqru3m _1i6zru3m",
|
|
33
|
+
surface: "_1reo15vq _18m915vq _1e0c1txw _1bsb1osq _4t3i1osq _bfhk1bhr _1q1l1bhr _ect4ttxp"
|
|
34
|
+
};
|
|
35
|
+
const drawerAnimationStyles = {
|
|
36
|
+
left: "_t9ecjq3t _1hqtjq3t",
|
|
37
|
+
right: "_t9ecxwn4 _1hqtxwn4",
|
|
38
|
+
top: "_t9ecz6y5 _1hqtz6y5",
|
|
39
|
+
bottom: "_t9ec3k1b _1hqt3k1b"
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Width presets mirror the legacy `DrawerPanel`. Applied to the `<dialog>` (the
|
|
43
|
+
// sized, edge-pinned element) and clamped to the viewport for mobile safety.
|
|
44
|
+
const WIDTH_MAP = {
|
|
45
|
+
narrow: '360px',
|
|
46
|
+
medium: '480px',
|
|
47
|
+
wide: '600px',
|
|
48
|
+
extended: '95vw',
|
|
49
|
+
full: '100vw'
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the native `<dialog>` accessible name props. Prefer the consumer's
|
|
54
|
+
* `label`, then their `titleId`. If neither is supplied (legacy allowed this),
|
|
55
|
+
* fall back to a generic name so the dialog is never unlabelled. Note:
|
|
56
|
+
* `@atlaskit/drawer` has no i18n setup, so this fallback is English only;
|
|
57
|
+
* consumers should pass a localised `label` or `titleId`.
|
|
58
|
+
*/
|
|
59
|
+
function getAccessibleName({
|
|
60
|
+
label,
|
|
61
|
+
titleId
|
|
62
|
+
}) {
|
|
63
|
+
if (label) {
|
|
64
|
+
return {
|
|
65
|
+
label
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (titleId) {
|
|
69
|
+
return {
|
|
70
|
+
labelledBy: titleId
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
label: 'Drawer'
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* **DrawerTopLayer**
|
|
80
|
+
*
|
|
81
|
+
* Top-layer (`platform-dst-top-layer`) implementation of `Drawer`. Renders a
|
|
82
|
+
* native `<dialog>` via `@atlaskit/top-layer`, replacing Portal, Blanket,
|
|
83
|
+
* react-focus-lock, react-scrolllock and `@atlaskit/layering` with native
|
|
84
|
+
* modality, `::backdrop`, focus trap and return, and `DialogScrollLock`.
|
|
85
|
+
*
|
|
86
|
+
* The `Dialog` primitive owns the entry and exit animation lifecycle (it keeps
|
|
87
|
+
* the host element mounted through the exit transition, then fires
|
|
88
|
+
* `onExitFinish`), so no `ExitingPersistence` wrapper is needed: the drawer
|
|
89
|
+
* renders `<Dialog isOpen={isOpen}>` directly. `useDrawerStack` tracks stack
|
|
90
|
+
* depth so only the foreground drawer shows a `::backdrop`.
|
|
91
|
+
*
|
|
92
|
+
* `isFocusLockEnabled` is intentionally unsupported: a native modal `<dialog>`
|
|
93
|
+
* always traps focus, so `isFocusLockEnabled={false}` is a no-op under this gate.
|
|
94
|
+
*/
|
|
95
|
+
export function DrawerTopLayer({
|
|
96
|
+
width = 'narrow',
|
|
97
|
+
isOpen,
|
|
98
|
+
shouldReturnFocus = true,
|
|
99
|
+
onKeyDown,
|
|
100
|
+
testId,
|
|
101
|
+
children,
|
|
102
|
+
onClose,
|
|
103
|
+
onCloseComplete,
|
|
104
|
+
onOpenComplete,
|
|
105
|
+
label,
|
|
106
|
+
titleId,
|
|
107
|
+
enterFrom = 'left'
|
|
108
|
+
}) {
|
|
109
|
+
const stackIndex = useDrawerStack({
|
|
110
|
+
isOpen
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Points to the panel surface. Passed to `onOpenComplete` / `onCloseComplete`.
|
|
115
|
+
*/
|
|
116
|
+
const contentRef = useRef(null);
|
|
117
|
+
// Cache the last content element so `onCloseComplete` still receives a node
|
|
118
|
+
// after children unmount (with reduced motion `contentRef` can clear before
|
|
119
|
+
// `onExitFinish` fires).
|
|
120
|
+
const lastContentElRef = useRef(null);
|
|
121
|
+
// Callback ref runs at commit (not during render), so both refs stay
|
|
122
|
+
// populated without a render-time side effect. `lastContentElRef` only
|
|
123
|
+
// overwrites with a non-null node, preserving it across the unmount.
|
|
124
|
+
const setContentEl = useCallback(el => {
|
|
125
|
+
contentRef.current = el;
|
|
126
|
+
if (el) {
|
|
127
|
+
lastContentElRef.current = el;
|
|
128
|
+
}
|
|
129
|
+
}, []);
|
|
130
|
+
|
|
131
|
+
// Analytics-wrapped close handlers, one per legacy trigger.
|
|
132
|
+
const handleEscapeClose = usePlatformLeafEventHandler({
|
|
133
|
+
fn: (evt, analyticsEvent) => onClose === null || onClose === void 0 ? void 0 : onClose(evt, analyticsEvent),
|
|
134
|
+
action: 'dismissed',
|
|
135
|
+
componentName: 'drawer',
|
|
136
|
+
packageName: "@atlaskit/drawer",
|
|
137
|
+
packageVersion: "13.2.0",
|
|
138
|
+
analyticsData: {
|
|
139
|
+
trigger: 'escKey'
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
const handleBlanketClose = usePlatformLeafEventHandler({
|
|
143
|
+
fn: (evt, analyticsEvent) => onClose === null || onClose === void 0 ? void 0 : onClose(evt, analyticsEvent),
|
|
144
|
+
action: 'dismissed',
|
|
145
|
+
componentName: 'drawer',
|
|
146
|
+
packageName: "@atlaskit/drawer",
|
|
147
|
+
packageVersion: "13.2.0",
|
|
148
|
+
analyticsData: {
|
|
149
|
+
trigger: 'blanket'
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
const handleBackButtonClose = usePlatformLeafEventHandler({
|
|
153
|
+
fn: (evt, analyticsEvent) => onClose === null || onClose === void 0 ? void 0 : onClose(evt, analyticsEvent),
|
|
154
|
+
action: 'dismissed',
|
|
155
|
+
componentName: 'drawer',
|
|
156
|
+
packageName: "@atlaskit/drawer",
|
|
157
|
+
packageVersion: "13.2.0",
|
|
158
|
+
analyticsData: {
|
|
159
|
+
trigger: 'backButton'
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Bridge the Dialog primitive's `onClose({ reason })` to the legacy
|
|
164
|
+
// `onClose(event, analyticsEvent)` contract via a synthetic event. Drawer
|
|
165
|
+
// has no `shouldCloseOn*` props, so both reasons always forward.
|
|
166
|
+
const handleDialogClose = useCallback(({
|
|
167
|
+
reason
|
|
168
|
+
}) => {
|
|
169
|
+
const event = createCloseEvent({
|
|
170
|
+
reason
|
|
171
|
+
});
|
|
172
|
+
if (reason === 'escape') {
|
|
173
|
+
handleEscapeClose(event);
|
|
174
|
+
} else {
|
|
175
|
+
handleBlanketClose(event);
|
|
176
|
+
}
|
|
177
|
+
}, [handleEscapeClose, handleBlanketClose]);
|
|
178
|
+
|
|
179
|
+
// Mirror the legacy window `keydown` listener so `onKeyDown` still fires
|
|
180
|
+
// while the drawer is open.
|
|
181
|
+
const handleKeyDown = useCallback(evt => {
|
|
182
|
+
onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(evt);
|
|
183
|
+
}, [onKeyDown]);
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
if (!isOpen) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
return bind(window, {
|
|
189
|
+
type: 'keydown',
|
|
190
|
+
listener: handleKeyDown
|
|
191
|
+
});
|
|
192
|
+
}, [isOpen, handleKeyDown]);
|
|
193
|
+
|
|
194
|
+
// `onOpenComplete` once the entry animation settles (via `Dialog`'s
|
|
195
|
+
// `onEnterFinish`, which the underlying hook fires for animated,
|
|
196
|
+
// non-animated and reduced-motion paths).
|
|
197
|
+
const handleEnterFinish = useCallback(() => {
|
|
198
|
+
onOpenComplete === null || onOpenComplete === void 0 ? void 0 : onOpenComplete(contentRef.current);
|
|
199
|
+
}, [onOpenComplete]);
|
|
200
|
+
|
|
201
|
+
// `onCloseComplete` once the exit animation settles (via `Dialog`'s
|
|
202
|
+
// `onExitFinish`). This is also where a custom `shouldReturnFocus={ref}` is
|
|
203
|
+
// honoured: native `<dialog>` restores focus to the trigger at the start of
|
|
204
|
+
// close, so the consumer's ref is focused now that the exit is done.
|
|
205
|
+
// `shouldReturnFocus={false}` is a documented best-effort limitation; native
|
|
206
|
+
// always restores focus to the trigger.
|
|
207
|
+
const handleExitFinish = useCallback(() => {
|
|
208
|
+
var _contentRef$current;
|
|
209
|
+
onCloseComplete === null || onCloseComplete === void 0 ? void 0 : onCloseComplete((_contentRef$current = contentRef.current) !== null && _contentRef$current !== void 0 ? _contentRef$current : lastContentElRef.current);
|
|
210
|
+
lastContentElRef.current = null;
|
|
211
|
+
if (typeof shouldReturnFocus === 'object' && shouldReturnFocus !== null && shouldReturnFocus !== void 0 && shouldReturnFocus.current) {
|
|
212
|
+
shouldReturnFocus.current.focus();
|
|
213
|
+
}
|
|
214
|
+
}, [onCloseComplete, shouldReturnFocus]);
|
|
215
|
+
|
|
216
|
+
// Pin the `<dialog>` full-height to the inline-start edge (overriding the
|
|
217
|
+
// primitive's centred `margin: auto`); width from the preset, clamped to the
|
|
218
|
+
// viewport. `enterFrom` only drives the slide animation, not the pin edge;
|
|
219
|
+
// it matches the legacy panel, which always pins `inset-inline-start`.
|
|
220
|
+
const dialogStyle = {
|
|
221
|
+
margin: 0,
|
|
222
|
+
insetBlockStart: 0,
|
|
223
|
+
insetInlineStart: 0,
|
|
224
|
+
insetInlineEnd: 'auto',
|
|
225
|
+
height: '100dvh',
|
|
226
|
+
maxHeight: '100dvh',
|
|
227
|
+
width: `min(${WIDTH_MAP[width]}, 100vw)`
|
|
228
|
+
};
|
|
229
|
+
const accessibleName = getAccessibleName({
|
|
230
|
+
label,
|
|
231
|
+
titleId
|
|
232
|
+
});
|
|
233
|
+
return /*#__PURE__*/React.createElement(Dialog, _extends({
|
|
234
|
+
isOpen: isOpen,
|
|
235
|
+
onClose: handleDialogClose,
|
|
236
|
+
onEnterFinish: handleEnterFinish,
|
|
237
|
+
onExitFinish: handleExitFinish,
|
|
238
|
+
animate: animate,
|
|
239
|
+
xcss: cx(styles.root, drawerAnimationStyles[enterFrom]),
|
|
240
|
+
shouldHideBackdrop: stackIndex > 0,
|
|
241
|
+
testId: testId
|
|
242
|
+
}, accessibleName, {
|
|
243
|
+
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop
|
|
244
|
+
style: dialogStyle
|
|
245
|
+
}), /*#__PURE__*/React.createElement(DialogScrollLock, {
|
|
246
|
+
isOpen: true
|
|
247
|
+
}), /*#__PURE__*/React.createElement("div", {
|
|
248
|
+
ref: setContentEl,
|
|
249
|
+
className: ax([styles.surface])
|
|
250
|
+
}, /*#__PURE__*/React.createElement(EnsureIsInsideDrawerContext.Provider, {
|
|
251
|
+
value: true
|
|
252
|
+
}, /*#__PURE__*/React.createElement(OnCloseContext.Provider, {
|
|
253
|
+
value: handleBackButtonClose
|
|
254
|
+
}, children))));
|
|
255
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useLayoutEffect, useState } from 'react';
|
|
2
2
|
import { bind } from 'bind-event-listener';
|
|
3
|
+
import { fg } from '@atlaskit/platform-feature-flags';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Returns how far the body is scrolled from the top of the viewport.
|
|
@@ -37,6 +38,11 @@ export default function usePreventProgrammaticScroll() {
|
|
|
37
38
|
}
|
|
38
39
|
}, [scrollTopOffset]);
|
|
39
40
|
useEffect(() => {
|
|
41
|
+
// The top-layer drawer relies on native modality + `DialogScrollLock`; the
|
|
42
|
+
// programmatic-scroll guard is intentionally dropped under the flag.
|
|
43
|
+
if (fg('platform-dst-top-layer')) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
40
46
|
return bind(window, {
|
|
41
47
|
type: 'scroll',
|
|
42
48
|
listener: onWindowScroll
|
package/dist/es2019/drawer.js
CHANGED
|
@@ -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
|
const EscapeCloseManager = ({
|
|
12
14
|
onClose
|
|
@@ -20,17 +22,7 @@ const EscapeCloseManager = ({
|
|
|
20
22
|
});
|
|
21
23
|
return /*#__PURE__*/React.createElement("span", null);
|
|
22
24
|
};
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* __Drawer__
|
|
26
|
-
*
|
|
27
|
-
* A drawer is a panel that slides in from the left side of the screen.
|
|
28
|
-
*
|
|
29
|
-
* - [Examples](https://atlassian.design/components/drawer/examples)
|
|
30
|
-
* - [Code](https://atlassian.design/components/drawer/code)
|
|
31
|
-
* - [Usage](https://atlassian.design/components/drawer/usage)
|
|
32
|
-
*/
|
|
33
|
-
export const Drawer = ({
|
|
25
|
+
const DrawerBase = ({
|
|
34
26
|
width = 'narrow',
|
|
35
27
|
isOpen,
|
|
36
28
|
isFocusLockEnabled = true,
|
|
@@ -52,7 +44,7 @@ export const Drawer = ({
|
|
|
52
44
|
action: 'dismissed',
|
|
53
45
|
componentName: 'drawer',
|
|
54
46
|
packageName: "@atlaskit/drawer",
|
|
55
|
-
packageVersion: "13.
|
|
47
|
+
packageVersion: "13.2.0",
|
|
56
48
|
analyticsData: {
|
|
57
49
|
trigger: 'escKey'
|
|
58
50
|
}
|
|
@@ -73,7 +65,7 @@ export const Drawer = ({
|
|
|
73
65
|
action: 'dismissed',
|
|
74
66
|
componentName: 'drawer',
|
|
75
67
|
packageName: "@atlaskit/drawer",
|
|
76
|
-
packageVersion: "13.
|
|
68
|
+
packageVersion: "13.2.0",
|
|
77
69
|
analyticsData: {
|
|
78
70
|
trigger: 'blanket'
|
|
79
71
|
}
|
|
@@ -83,7 +75,7 @@ export const Drawer = ({
|
|
|
83
75
|
action: 'dismissed',
|
|
84
76
|
componentName: 'drawer',
|
|
85
77
|
packageName: "@atlaskit/drawer",
|
|
86
|
-
packageVersion: "13.
|
|
78
|
+
packageVersion: "13.2.0",
|
|
87
79
|
analyticsData: {
|
|
88
80
|
trigger: 'backButton'
|
|
89
81
|
}
|
|
@@ -116,4 +108,23 @@ export const Drawer = ({
|
|
|
116
108
|
}, children, /*#__PURE__*/React.createElement(EscapeCloseManager, {
|
|
117
109
|
onClose: handleClose
|
|
118
110
|
})) : children));
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* __Drawer__
|
|
115
|
+
*
|
|
116
|
+
* A drawer is a panel that slides in from the left side of the screen.
|
|
117
|
+
*
|
|
118
|
+
* - [Examples](https://atlassian.design/components/drawer/examples)
|
|
119
|
+
* - [Code](https://atlassian.design/components/drawer/code)
|
|
120
|
+
* - [Usage](https://atlassian.design/components/drawer/usage)
|
|
121
|
+
*/
|
|
122
|
+
export const Drawer = props => {
|
|
123
|
+
if (fg('platform-dst-top-layer')) {
|
|
124
|
+
// eslint-disable-next-line @repo/internal/react/no-unsafe-spread-props -- internal implementation component takes the same DrawerProps
|
|
125
|
+
return /*#__PURE__*/React.createElement(DrawerTopLayer, props);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// eslint-disable-next-line @repo/internal/react/no-unsafe-spread-props -- internal implementation component takes the same DrawerProps
|
|
129
|
+
return /*#__PURE__*/React.createElement(DrawerBase, props);
|
|
119
130
|
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import useLazyCallback from '@atlaskit/ds-lib/use-lazy-callback';
|
|
3
|
+
import usePreviousValue from '@atlaskit/ds-lib/use-previous-value';
|
|
4
|
+
import useStateRef from '@atlaskit/ds-lib/use-state-ref';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* **Major versions will not know about each other**
|
|
8
|
+
*
|
|
9
|
+
* An array holding references to all currently open drawers. This only works
|
|
10
|
+
* for drawers of the same major version, because the reference differs between
|
|
11
|
+
* majors (for example V13 will not know about any from V14).
|
|
12
|
+
*
|
|
13
|
+
* Adapted from `useModalStack` in `@atlaskit/modal-dialog`. The top-layer drawer
|
|
14
|
+
* path uses this to know its position in the stack so that only the foreground
|
|
15
|
+
* drawer renders a visible `::backdrop` (this avoids cumulative darkening for
|
|
16
|
+
* nested or stacked drawers). Unlike the modal version, the register is timed
|
|
17
|
+
* off `isOpen` rather than `@atlaskit/motion`'s `useExitingPersistence`: the
|
|
18
|
+
* `Dialog` primitive owns its own exit lifecycle, so the top-layer drawer needs
|
|
19
|
+
* no `ExitingPersistence` wrapper.
|
|
20
|
+
*/
|
|
21
|
+
const drawerStackRegister = [];
|
|
22
|
+
/**
|
|
23
|
+
* Returns the position of the calling drawer in the drawer stack. A stack index
|
|
24
|
+
* of `0` is the foreground (top of the stack); higher numbers are further back.
|
|
25
|
+
*/
|
|
26
|
+
export default function useDrawerStack({
|
|
27
|
+
isOpen,
|
|
28
|
+
onStackChange
|
|
29
|
+
}) {
|
|
30
|
+
const [stackIndexRef, setStackIndex] = useStateRef(0);
|
|
31
|
+
const currentStackIndex = stackIndexRef.current;
|
|
32
|
+
const previousStackIndex = usePreviousValue(stackIndexRef.current);
|
|
33
|
+
|
|
34
|
+
// Kept stable for the lifetime of the component so it can be the identity in
|
|
35
|
+
// the shared register. This is why it is a lazy callback and not a
|
|
36
|
+
// useMemo/useCallback value.
|
|
37
|
+
const updateStack = useLazyCallback(() => {
|
|
38
|
+
const newStackIndex = drawerStackRegister.indexOf(updateStack);
|
|
39
|
+
// Read from the ref rather than state: this closure only ever sees the
|
|
40
|
+
// initial state value.
|
|
41
|
+
if (stackIndexRef.current !== newStackIndex) {
|
|
42
|
+
setStackIndex(newStackIndex);
|
|
43
|
+
stackIndexRef.current = newStackIndex;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (!isOpen) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// Opening: join the front of the register (the newest open drawer is the
|
|
51
|
+
// foreground at index 0), then notify every open drawer to recompute.
|
|
52
|
+
if (drawerStackRegister.indexOf(updateStack) === -1) {
|
|
53
|
+
drawerStackRegister.unshift(updateStack);
|
|
54
|
+
}
|
|
55
|
+
drawerStackRegister.forEach(callback => callback());
|
|
56
|
+
return () => {
|
|
57
|
+
// Closing or unmounting: leave the register and notify the rest.
|
|
58
|
+
const index = drawerStackRegister.indexOf(updateStack);
|
|
59
|
+
if (index !== -1) {
|
|
60
|
+
drawerStackRegister.splice(index, 1);
|
|
61
|
+
}
|
|
62
|
+
drawerStackRegister.forEach(callback => callback());
|
|
63
|
+
};
|
|
64
|
+
}, [isOpen, updateStack]);
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (previousStackIndex === undefined) {
|
|
67
|
+
// Initial render: nothing to notify about.
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (previousStackIndex !== currentStackIndex) {
|
|
71
|
+
onStackChange === null || onStackChange === void 0 ? void 0 : onStackChange(currentStackIndex);
|
|
72
|
+
}
|
|
73
|
+
}, [onStackChange, previousStackIndex, currentStackIndex]);
|
|
74
|
+
return currentStackIndex;
|
|
75
|
+
}
|
|
@@ -6,6 +6,7 @@ import { ax, ix } from "@compiled/react/runtime";
|
|
|
6
6
|
import { forwardRef, useEffect, useRef, useState } from 'react';
|
|
7
7
|
import ScrollLock from 'react-scrolllock';
|
|
8
8
|
import { mergeRefs } from 'use-callback-ref';
|
|
9
|
+
import { fg } from '@atlaskit/platform-feature-flags';
|
|
9
10
|
import { useEnsureIsInsideDrawer } from '../use-ensure-is-inside-drawer';
|
|
10
11
|
import usePreventProgrammaticScroll from './hooks/use-prevent-programmatic-scroll';
|
|
11
12
|
var styles = {
|
|
@@ -51,8 +52,16 @@ export var DrawerContent = function DrawerContent(_ref2) {
|
|
|
51
52
|
xcss = _ref2.xcss;
|
|
52
53
|
useEnsureIsInsideDrawer();
|
|
53
54
|
usePreventProgrammaticScroll();
|
|
54
|
-
|
|
55
|
+
var content = /*#__PURE__*/React.createElement(DrawerContentBase, {
|
|
55
56
|
scrollContentLabel: scrollContentLabel,
|
|
56
57
|
xcss: xcss
|
|
57
|
-
}, children)
|
|
58
|
+
}, children);
|
|
59
|
+
|
|
60
|
+
// Under the top-layer flag, background scroll is handled by `DialogScrollLock`
|
|
61
|
+
// (rendered by `DrawerTopLayer`), so we skip `react-scrolllock` here to avoid
|
|
62
|
+
// double-locking the body. See the top-layer migration scroll decision.
|
|
63
|
+
if (fg('platform-dst-top-layer')) {
|
|
64
|
+
return content;
|
|
65
|
+
}
|
|
66
|
+
return /*#__PURE__*/React.createElement(ScrollLock, null, content);
|
|
58
67
|
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
._13ku1fu8::-ms-backdrop{transition-timing-function:cubic-bezier(.15,1,.3,1)}
|
|
2
|
+
._13ku1fu8::backdrop{transition-timing-function:cubic-bezier(.15,1,.3,1)}
|
|
3
|
+
._18m915vq{overflow-y:hidden}
|
|
4
|
+
._1bsb1osq{width:100%}
|
|
5
|
+
._1e0c1txw{display:flex}
|
|
6
|
+
._1fsl1yx9[open]{transition-duration:.1s}
|
|
7
|
+
._1fyf1a5r::-ms-backdrop{transition-behavior:allow-discrete}
|
|
8
|
+
._1fyf1a5r::backdrop{transition-behavior:allow-discrete}
|
|
9
|
+
._1kdeglyw[open]{transform:none}
|
|
10
|
+
._1ltk1j28::-ms-backdrop{background-color:transparent}
|
|
11
|
+
._1ltk1j28::backdrop{background-color:transparent}
|
|
12
|
+
._1oec14ed{transition-duration:50ms}
|
|
13
|
+
._1q1l1bhr{--ds-elevation-surface-current:var(--ds-surface-overlay,#fff)}
|
|
14
|
+
._1reo15vq{overflow-x:hidden}
|
|
15
|
+
._1s9z1a5r{transition-behavior:allow-discrete}
|
|
16
|
+
._4t3i1osq{height:100%}
|
|
17
|
+
._4v7ipwmj[open]::-ms-backdrop{transition-duration:.7s}
|
|
18
|
+
._4v7ipwmj[open]::backdrop{transition-duration:.7s}
|
|
19
|
+
._6fl4dkwg{transition-timing-function:cubic-bezier(.2,0,0,1)}
|
|
20
|
+
._bfhk1bhr{background-color:var(--ds-surface-overlay,#fff)}
|
|
21
|
+
._d7fr1i5c[open]::-ms-backdrop{background-color:var(--ds-blanket,#050c1f75)}
|
|
22
|
+
._d7fr1i5c[open]::backdrop{background-color:var(--ds-blanket,#050c1f75)}
|
|
23
|
+
._ect4ttxp{font-family:var(--ds-font-family-body,"Atlassian Sans",ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",Ubuntu,"Helvetica Neue",sans-serif)}
|
|
24
|
+
._k8m05ji5{transition-property:transform,overlay,display}
|
|
25
|
+
._njwkivc9::-ms-backdrop{-ms-transition-property:background-color,overlay,display;transition-property:background-color,overlay,display}
|
|
26
|
+
._njwkivc9::backdrop{transition-property:background-color,overlay,display}
|
|
27
|
+
._t9ec3k1b{transform:translateY(100%)}
|
|
28
|
+
._t9ecjq3t{transform:translateX(-100%)}
|
|
29
|
+
._t9ecxwn4{transform:translateX(100%)}
|
|
30
|
+
._t9ecz6y5{transform:translateY(-100%)}
|
|
31
|
+
._yhpq1ttt::-ms-backdrop{transition-duration:.35s}
|
|
32
|
+
._yhpq1ttt::backdrop{transition-duration:.35s}
|
|
33
|
+
@media (prefers-reduced-motion:reduce){._156yru3m{transition-duration:0s}._aeldru3m[open]{transition-duration:0s}._1sbqru3m::-ms-backdrop{transition-duration:0s}._1sbqru3m::backdrop{transition-duration:0s}._1i6zru3m[open]::-ms-backdrop{transition-duration:0s}._1i6zru3m[open]::backdrop{transition-duration:0s}}
|
|
34
|
+
@starting-style{._1dar1j28[open]::-ms-backdrop{background-color:transparent}._1dar1j28[open]::backdrop{background-color:transparent}._1hqt3k1b[open]{transform:translateY(100%)}._1hqtjq3t[open]{transform:translateX(-100%)}._1hqtxwn4[open]{transform:translateX(100%)}._1hqtz6y5[open]{transform:translateY(-100%)}}
|