@atlaskit/drawer
Version:
A drawer is a panel that slides in from the left side of the screen.
221 lines (214 loc) • 8.17 kB
JavaScript
/* drawer-top-layer.tsx generated by @compiled/babel-plugin v3.0.2 */
import _extends from "@babel/runtime/helpers/extends";
import "./drawer-top-layer.compiled.css";
import * as React from 'react';
import { ax, ix } from "@compiled/react/runtime";
import { useCallback, useEffect, useRef } from 'react';
import { cx } from '@compiled/react';
import { bind } from 'bind-event-listener';
import { usePlatformLeafEventHandler } from '@atlaskit/analytics-next/usePlatformLeafEventHandler';
import { createCloseEvent } from '@atlaskit/top-layer/create-close-event';
import { Dialog } from '@atlaskit/top-layer/dialog-content';
import { DialogScrollLock } from '@atlaskit/top-layer/dialog-scroll-lock';
import { EnsureIsInsideDrawerContext } from '../ensure-is-inside-drawer-context';
import { OnCloseContext } from '../on-close-context';
const LOCAL_CURRENT_SURFACE_CSS_VAR = '--ds-elevation-surface-current';
const slideInKeyframes = null;
const slideOutKeyframes = null;
const styles = {
root: "_19pkidpf _2hwxidpf _otyridpf _18u0idpf _152tidpf _1e02idpf _rjxp1wug _4t3i1496 _p12fauwl",
enter: "_j7hqntq4",
exit: "_j7hqcljl",
surface: "_1reo15vq _18m915vq _1e0c1txw _1bsb1osq _4t3i1osq _bfhk1bhr _1q1l1bhr _ect4ttxp"
};
const widthStyles = {
narrow: "_1bsb1gwv",
medium: "_1bsb1y37",
wide: "_1bsb1ogm",
extended: "_1bsb1mwr",
full: "_1bsbauwl"
};
const enterFromStyles = {
left: "_1s8jpwvu _1c2rf1ug",
right: "_1s8j1osq _1c2rf1ug",
top: "_1s8jf1ug _1c2rpwvu",
bottom: "_1s8jf1ug _1c2r1osq"
};
/**
* Resolve the native `<dialog>` accessible name props. Prefer the consumer's
* `label`, then their `titleId`. If neither is supplied (legacy allowed this),
* fall back to a generic name so the dialog is never unlabelled. Note:
* `@atlaskit/drawer` has no i18n setup, so this fallback is English only;
* consumers should pass a localised `label` or `titleId`.
*/
function getAccessibleName({
label,
titleId
}) {
if (label) {
return {
label
};
}
if (titleId) {
return {
labelledBy: titleId
};
}
return {
label: 'Drawer'
};
}
/**
* **DrawerTopLayer**
*
* Top-layer (`platform-dst-top-layer`) implementation of `Drawer`. Renders a
* native `<dialog>` via `@atlaskit/top-layer`, replacing Portal, Blanket,
* react-focus-lock, react-scrolllock and `@atlaskit/layering` with native
* modality, `::backdrop`, focus trap and return, and `DialogScrollLock`.
*
* The `Dialog` primitive owns the entry and exit animation lifecycle (it keeps
* the host element mounted through the exit transition, then fires
* `onExitFinish`), so no `ExitingPersistence` wrapper is needed: the drawer
* renders `<Dialog isOpen={isOpen}>` directly. `useDrawerStack` tracks stack
* depth so only the foreground drawer shows a `::backdrop`.
*
* `isFocusLockEnabled` is intentionally unsupported: a native modal `<dialog>`
* always traps focus, so `isFocusLockEnabled={false}` is a no-op under this gate.
*/
export function DrawerTopLayer({
width = 'narrow',
isOpen,
shouldReturnFocus = true,
onKeyDown,
testId,
children,
onClose,
onCloseComplete,
onOpenComplete,
label,
titleId,
enterFrom = 'left'
}) {
/**
* Points to the panel surface. Passed to `onOpenComplete` / `onCloseComplete`.
*/
const contentRef = useRef(null);
// Cache the last content element so `onCloseComplete` still receives a node
// after children unmount (with reduced motion `contentRef` can clear before
// `onExitFinish` fires).
const lastContentElRef = useRef(null);
// Callback ref runs at commit (not during render), so both refs stay
// populated without a render-time side effect. `lastContentElRef` only
// overwrites with a non-null node, preserving it across the unmount.
const setContentEl = useCallback(el => {
contentRef.current = el;
if (el) {
lastContentElRef.current = el;
}
}, []);
// Analytics-wrapped close handlers, one per legacy trigger.
const handleEscapeClose = usePlatformLeafEventHandler({
fn: (evt, analyticsEvent) => onClose === null || onClose === void 0 ? void 0 : onClose(evt, analyticsEvent),
action: 'dismissed',
componentName: 'drawer',
packageName: "@atlaskit/drawer",
packageVersion: "14.3.5",
analyticsData: {
trigger: 'escKey'
}
});
const handleBlanketClose = usePlatformLeafEventHandler({
fn: (evt, analyticsEvent) => onClose === null || onClose === void 0 ? void 0 : onClose(evt, analyticsEvent),
action: 'dismissed',
componentName: 'drawer',
packageName: "@atlaskit/drawer",
packageVersion: "14.3.5",
analyticsData: {
trigger: 'blanket'
}
});
const handleBackButtonClose = usePlatformLeafEventHandler({
fn: (evt, analyticsEvent) => onClose === null || onClose === void 0 ? void 0 : onClose(evt, analyticsEvent),
action: 'dismissed',
componentName: 'drawer',
packageName: "@atlaskit/drawer",
packageVersion: "14.3.5",
analyticsData: {
trigger: 'backButton'
}
});
// Bridge the Dialog primitive's `onClose({ reason })` to the legacy
// `onClose(event, analyticsEvent)` contract via a synthetic event. Drawer
// has no `shouldCloseOn*` props, so both reasons always forward.
const handleDialogClose = useCallback(({
reason
}) => {
const event = createCloseEvent({
reason
});
if (reason === 'escape') {
handleEscapeClose(event);
} else {
handleBlanketClose(event);
}
}, [handleEscapeClose, handleBlanketClose]);
// Mirror the legacy window `keydown` listener so `onKeyDown` still fires
// while the drawer is open.
const handleKeyDown = useCallback(evt => {
onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(evt);
}, [onKeyDown]);
useEffect(() => {
if (!isOpen) {
return;
}
return bind(window, {
type: 'keydown',
listener: handleKeyDown
});
}, [isOpen, handleKeyDown]);
// `onOpenComplete` once the entry animation settles (via `Dialog`'s
// `onEnterFinish`, which the underlying hook fires for animated,
// non-animated and reduced-motion paths).
const handleEnterFinish = useCallback(() => {
onOpenComplete === null || onOpenComplete === void 0 ? void 0 : onOpenComplete(contentRef.current);
}, [onOpenComplete]);
// `onCloseComplete` once the exit animation settles (via `Dialog`'s
// `onExitFinish`). This is also where a custom `shouldReturnFocus={ref}` is
// honoured: native `<dialog>` restores focus to the trigger at the start of
// close, so the consumer's ref is focused now that the exit is done.
// `shouldReturnFocus={false}` is a documented best-effort limitation; native
// always restores focus to the trigger.
const handleExitFinish = useCallback(() => {
var _contentRef$current;
onCloseComplete === null || onCloseComplete === void 0 ? void 0 : onCloseComplete((_contentRef$current = contentRef.current) !== null && _contentRef$current !== void 0 ? _contentRef$current : lastContentElRef.current);
lastContentElRef.current = null;
if (typeof shouldReturnFocus === 'object' && shouldReturnFocus !== null && shouldReturnFocus !== void 0 && shouldReturnFocus.current) {
shouldReturnFocus.current.focus();
}
}, [onCloseComplete, shouldReturnFocus]);
const accessibleName = getAccessibleName({
label,
titleId
});
return /*#__PURE__*/React.createElement(Dialog, _extends({
isOpen: isOpen,
onClose: handleDialogClose,
onEnterFinish: handleEnterFinish,
onExitFinish: handleExitFinish,
shouldAnimate: true,
xcss: cx(styles.root, widthStyles[width], enterFromStyles[enterFrom]),
enteringAnimationXcss: styles.enter,
exitingAnimationXcss: styles.exit,
testId: testId
}, accessibleName), /*#__PURE__*/React.createElement(DialogScrollLock, {
isOpen: true
}), /*#__PURE__*/React.createElement("div", {
ref: setContentEl,
className: ax([styles.surface])
}, /*#__PURE__*/React.createElement(EnsureIsInsideDrawerContext.Provider, {
value: true
}, /*#__PURE__*/React.createElement(OnCloseContext.Provider, {
value: handleBackButtonClose
}, children))));
}