@kiwicom/orbit-components
Version:
Orbit-components is a React component library which provides developers with the easiest possible way of building Kiwi.com's products.
236 lines (232 loc) • 12.9 kB
JavaScript
;
"use client";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
exports.__esModule = true;
exports.default = void 0;
var React = _interopRequireWildcard(require("react"));
var _clsx = _interopRequireDefault(require("clsx"));
var _useFocusTrap = _interopRequireDefault(require("../hooks/useFocusTrap"));
var _useLockScrolling = _interopRequireDefault(require("../hooks/useLockScrolling"));
var _DrawerClose = _interopRequireDefault(require("./components/DrawerClose"));
var _consts = _interopRequireDefault(require("./consts"));
var _Stack = _interopRequireDefault(require("../Stack"));
var _Heading = _interopRequireDefault(require("../Heading"));
var _defaultTheme = _interopRequireDefault(require("../defaultTheme"));
var _useStateWithTimeout = _interopRequireDefault(require("../hooks/useStateWithTimeout"));
var _useClickOutside = _interopRequireDefault(require("../hooks/useClickOutside"));
var _consts2 = _interopRequireDefault(require("../hooks/useFocusTrap/consts"));
const getTransitionClasses = (shown, position) => {
if (shown) return "translate-x-0 visible";
return position === _consts.default.RIGHT ? "ltr:lm:translate-x-[var(--lm-drawer-width)] rtl:lm:-translate-x-[var(--lm-drawer-width)] ltr:translate-x-full rtl:-translate-x-full invisible w-0" : "ltr:lm:-translate-x-[var(--lm-drawer-width)] rtl:lm:translate-x-[var(--lm-drawer-width)] ltr:-translate-x-full rtl:translate-x-full invisible w-0";
};
/**
* @orbit-doc-start
* README
* ----------
* # Drawer
*
* To implement Drawer component into your project you'll need to add the import:
*
* ```jsx
* import Drawer from "@kiwicom/orbit-components/lib/Drawer";
* ```
*
* After adding import into your project you can use it simply like:
*
* ```jsx
* <Drawer shown>Content to show</Drawer>
* ```
*
* ## Props
*
* Table below contains all types of the props available in the Drawer component.
*
* | Name | Type | Default | Description |
* | :------------ | :---------------------- | :-------- | :------------------------------------------------------------------------------------------------------------------------------------ |
* | actions | `React.Node` | | Actions, especially Buttons, that will be rendered in the Drawer's header. |
* | **children** | `React.Node` | | The content of the Drawer. |
* | dataTest | `string` | | Optional prop for testing purposes. |
* | id | `string` | | Sets the `id` attribute for the `Drawer`. |
* | noPadding | `boolean` | `false` | If `true`, the wrapper won't have any inner padding. |
* | onClose | `() => void \| Promise` | | Function for handling the onClose event. |
* | position | [`enum`](#enum) | `"right"` | The side on which the Drawer should appear. |
* | shown | `boolean` | `"true"` | If `true`, the Drawer will be visible; otherwise, it will be visually hidden but will stay in the DOM. |
* | suppressed | `boolean` | `false` | If `true`, the Drawer will have a cloudy background. |
* | title | `string` | | Title of the Drawer that will be rendered in the Drawer's header. If `ariaLabel` is undefined, this will be used as `aria-label`. |
* | width | `string` | `"320px"` | The width of the Drawer. |
* | lockScrolling | `boolean` | `true` | Whether to prevent scrolling of the rest of the page while Drawer is open. This is on by default to provide a better user experience. |
* | fixedHeader | `boolean` | | If `true`, the DrawerHeader will be fixed to the top. |
* | labelHide | `string` | `Hide` | Label for the close button. |
* | ariaLabel | `string` | | Optional prop for `aria-label`. |
*
* ### enum
*
* | position |
* | :-------- |
* | `"right"` |
* | `"left"` |
*
*
* Accessibility
* -------------
* ## Accessibility
*
* ## Drawer
*
* The Drawer component has been designed with accessibility in mind.
*
* To ease keyboard navigation, when opening a drawer, the focus is moved to the drawer.
*
* It should not be possible to focus anything outside of the drawer while it is open, ensuring a focused user experience.
*
* When closing the drawer, **the focus should be moved back to the element that triggered the drawer**.
* This is not handled automatically, so make sure to implement this behavior in your application by managing focus properly.
*
* ### Keyboard interaction
*
* The Drawer component handles the following keyboard interactions:
*
* - **Escape** key closes the drawer
* - **Tab** key cycles through focusable elements within the drawer
* - **Shift+Tab** navigates backward through focusable elements
*
* ### ARIA attributes
*
* The Drawer component accepts ARIA attributes to ensure it's accessible to users of assistive technologies. You can provide these attributes as described below:
*
* | Name | Type | Description |
* | :-------- | :------- | :-------------------------------------------------------------------------------------------------------------------------- |
* | ariaLabel | `string` | Text that labels the drawer content. Think of it as the title of the drawer. This should be used if `title` is not defined. |
*
* If you provide a `title` prop, it is automatically used as the drawer's `aria-label`.
* However, if you also provide a `ariaLabel` prop, it will take precedence over the `title` prop.
*
* ### Close button
*
* The Drawer component includes a close button that can be displayed in the header. It's important to use the `labelHide` prop to provide an accessible label for this button.
*
* The default value is "Hide", but you should consider providing a more descriptive label, especially for internationalization purposes.
*
* ### Toggle element
*
* When implementing a toggle element to open and close the drawer, it's essential that the element uses the `aria-expanded` attribute to indicate whether the drawer is open (`true`) or closed (`false`).
* This informs assistive technologies about the current state of the drawer.
*
* Additionally, the toggle element should use the `aria-controls` attribute with the value matching the drawer's ID.
* This creates a programmatic association between the toggle and the drawer it controls, helping assistive technologies understand this relationship.
*
*
* @orbit-doc-end
*/
const Drawer = ({
children,
onClose,
lockScrolling = true,
fixedHeader,
labelHide = "Hide",
shown = true,
width = "320px",
position = _consts.default.RIGHT,
dataTest,
id,
noPadding,
suppressed,
title,
actions,
ariaLabel,
triggerRef
}) => {
const overlayRef = React.useRef(null);
const drawerRef = React.useRef(null);
const focusableElements = React.useRef([]);
const [overlayShown, setOverlayShown, setOverlayShownWithTimeout] = (0, _useStateWithTimeout.default)(shown, parseFloat(_defaultTheme.default.orbit.durationNormal) * 1000);
(0, _useFocusTrap.default)(drawerRef, true);
(0, _useLockScrolling.default)(drawerRef, lockScrolling && overlayShown);
React.useEffect(() => {
if (overlayShown !== shown) {
if (shown) {
setOverlayShown(true);
} else if (!shown) {
setOverlayShownWithTimeout(false);
}
}
}, [overlayShown, setOverlayShown, shown, setOverlayShownWithTimeout]);
React.useEffect(() => {
const findFocusableElements = () => {
return Array.from(drawerRef.current?.querySelectorAll(_consts2.default) || []);
};
if (!shown || !drawerRef.current) return undefined;
// Find all focusable elements within the drawer
focusableElements.current = findFocusableElements();
if (focusableElements.current.length) {
focusableElements.current[0].focus();
}
const observer = new MutationObserver(() => {
focusableElements.current = findFocusableElements();
});
// Start observing the drawer content for DOM changes
observer.observe(drawerRef.current, {
childList: true,
// Watch for added/removed nodes
subtree: true // Watch all descendants, not just direct children
});
return () => {
observer.disconnect();
};
}, [shown]);
React.useEffect(() => {
const handleKeyDown = event => {
if (shown && event.key === "Escape" && onClose) {
onClose();
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [onClose, shown]);
const handleClickOutside = React.useCallback(() => {
if (shown && onClose) onClose();
}, [shown, onClose]);
const vars = {
"--lm-drawer-width": width
};
const varClasses = [vars["--lm-drawer-width"] != null && "lm:max-w-[var(--lm-drawer-width)]"];
const onlyIcon = !title && !actions;
const bordered = !!(title || actions);
(0, _useClickOutside.default)(drawerRef, handleClickOutside);
React.useEffect(() => {
return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
triggerRef?.current?.focus();
};
}, [triggerRef]);
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
className: (0, _clsx.default)("orbit-drawer", "flex", "fixed inset-0", "size-full", "z-drawer", "duration-fast transition-[background-color,visibility] ease-in-out", overlayShown ? "visible" : "invisible", shown ? "bg-drawer-overlay-background" : "bg-transparent"),
id: id,
ref: overlayRef,
"aria-hidden": "true"
}), /*#__PURE__*/React.createElement("div", {
className: (0, _clsx.default)("box-border block", "fixed inset-y-0", "size-full", "font-base", "z-drawer", "overflow-y-auto", "overflow-x-hidden", "shadow-level3", "duration-normal transform-gpu transition-[transform,visibility,width] ease-in-out", getTransitionClasses(shown, position), suppressed ? "bg-cloud-light" : "bg-white-normal", position === _consts.default.RIGHT ? "end-0" : "start-0", ...varClasses),
style: vars,
ref: drawerRef,
role: "dialog",
"aria-modal": "true",
"aria-label": ariaLabel || title,
"data-test": dataTest
}, (title || actions || onClose) && /*#__PURE__*/React.createElement("div", {
className: (0, _clsx.default)("flex", "items-center", "h-1600", "box-border", suppressed && !bordered ? "bg-cloud-light" : "bg-white-normal", fixedHeader && "z-sticky sticky top-0", onlyIcon ? "justify-end" : "justify-between", bordered && "border-cloud-normal border-x-0 border-b border-t-0 border-solid", "px-400 lm:ps-800 lm:pe-600 py-0")
}, title && /*#__PURE__*/React.createElement(_Heading.default, {
type: "title2"
}, title), actions && /*#__PURE__*/React.createElement(_Stack.default, {
spacing: "none",
justify: "end",
flex: true,
shrink: true
}, actions), onClose && /*#__PURE__*/React.createElement(_DrawerClose.default, {
onClick: onClose,
title: labelHide
})), /*#__PURE__*/React.createElement("div", {
className: (0, _clsx.default)(!onClose && noPadding && "mt-600", noPadding && "mb-600", !noPadding && (bordered ? "p-400 lm:p-800" : "px-400 pb-400 lm:px-800 lm:pb-800"))
}, children)));
};
var _default = exports.default = Drawer;