UNPKG

@grafana/ui

Version:
293 lines (290 loc) • 9.39 kB
import { jsx, jsxs } from 'react/jsx-runtime'; import { cx, css } from '@emotion/css'; import { useDialog } from '@react-aria/dialog'; import { FocusScope } from '@react-aria/focus'; import { useOverlay } from '@react-aria/overlays'; import RcDrawer from 'rc-drawer'; import * as React from 'react'; import { useState, useCallback, useEffect } from 'react'; import { selectors } from '@grafana/e2e-selectors'; import { t } from '@grafana/i18n'; import { useStyles2 } from '../../themes/ThemeContext.mjs'; import { getDragStyles } from '../DragHandle/DragHandle.mjs'; import { IconButton } from '../IconButton/IconButton.mjs'; import { ScrollContainer } from '../ScrollContainer/ScrollContainer.mjs'; import { Text } from '../Text/Text.mjs'; import 'rc-drawer/assets/index.css'; const drawerSizes = { sm: { width: "25vw", minWidth: 384 }, md: { width: "50vw", minWidth: 568 }, lg: { width: "75vw", minWidth: 744 } }; function Drawer({ children, onClose, closeOnMaskClick = true, scrollableContent = true, title, subtitle, width, size = "md", tabs }) { var _a; const [drawerWidth, onMouseDown, onTouchStart] = useResizebleDrawer(); const styles = useStyles2(getStyles); const wrapperStyles = useStyles2(getWrapperStyles, size); const dragStyles = useStyles2(getDragStyles); const overlayRef = React.useRef(null); const { dialogProps, titleProps } = useDialog({}, overlayRef); const { overlayProps } = useOverlay( { isDismissable: false, isOpen: true, onClose }, overlayRef ); useBodyClassWhileOpen(); const content = /* @__PURE__ */ jsx("div", { className: styles.content, children }); const overrideWidth = (_a = drawerWidth != null ? drawerWidth : width) != null ? _a : drawerSizes[size].width; const minWidth = drawerSizes[size].minWidth; return /* @__PURE__ */ jsx( RcDrawer, { open: true, onClose, placement: "right", getContainer: ".main-view", className: styles.drawerContent, rootClassName: styles.drawer, classNames: { wrapper: wrapperStyles }, styles: { wrapper: { width: overrideWidth, minWidth } }, width: "", motion: { motionAppear: true, motionName: styles.drawerMotion }, maskClassName: styles.mask, maskClosable: closeOnMaskClick, maskMotion: { motionAppear: true, motionName: styles.maskMotion }, children: /* @__PURE__ */ jsx(FocusScope, { restoreFocus: true, contain: true, autoFocus: true, children: /* @__PURE__ */ jsxs( "div", { "aria-label": typeof title === "string" ? selectors.components.Drawer.General.title(title) : selectors.components.Drawer.General.title("no title"), className: styles.container, ...overlayProps, ...dialogProps, ref: overlayRef, children: [ /* @__PURE__ */ jsx( "div", { className: cx(dragStyles.dragHandleVertical, styles.resizer), onMouseDown, onTouchStart } ), /* @__PURE__ */ jsxs("div", { className: cx(styles.header, Boolean(tabs) && styles.headerWithTabs), children: [ /* @__PURE__ */ jsx("div", { className: styles.actions, children: /* @__PURE__ */ jsx( IconButton, { name: "times", variant: "secondary", onClick: onClose, "data-testid": selectors.components.Drawer.General.close, tooltip: t(`grafana-ui.drawer.close`, "Close") } ) }), typeof title === "string" ? /* @__PURE__ */ jsxs("div", { className: styles.titleWrapper, children: [ /* @__PURE__ */ jsx(Text, { element: "h3", ...titleProps, children: title }), subtitle && /* @__PURE__ */ jsx("div", { className: styles.subtitle, "data-testid": selectors.components.Drawer.General.subtitle, children: subtitle }) ] }) : title, tabs && /* @__PURE__ */ jsx("div", { className: styles.tabsWrapper, children: tabs }) ] }), !scrollableContent ? content : /* @__PURE__ */ jsx(ScrollContainer, { showScrollIndicators: true, children: content }) ] } ) }) } ); } function useResizebleDrawer() { const [drawerWidth, setDrawerWidth] = useState(void 0); const onMouseMove = useCallback((e) => { setDrawerWidth(getCustomDrawerWidth(e.clientX)); }, []); const onTouchMove = useCallback((e) => { const touch = e.touches[0]; setDrawerWidth(getCustomDrawerWidth(touch.clientX)); }, []); const onMouseUp = useCallback( (e) => { document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); }, [onMouseMove] ); const onTouchEnd = useCallback( (e) => { document.removeEventListener("touchmove", onTouchMove); document.removeEventListener("touchend", onTouchEnd); }, [onTouchMove] ); function onMouseDown(e) { e.stopPropagation(); e.preventDefault(); document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); } function onTouchStart(e) { e.stopPropagation(); e.preventDefault(); document.addEventListener("touchmove", onTouchMove); document.addEventListener("touchend", onTouchEnd); } return [drawerWidth, onMouseDown, onTouchStart]; } function getCustomDrawerWidth(clientX) { let offsetRight = document.body.offsetWidth - (clientX - document.body.offsetLeft); let widthPercent = Math.min(offsetRight / document.body.clientWidth * 100, 98).toFixed(2); return `${widthPercent}vw`; } function useBodyClassWhileOpen() { useEffect(() => { if (!document.body) { return; } document.body.classList.add("body-drawer-open"); return () => { document.body.classList.remove("body-drawer-open"); }; }, []); } const getStyles = (theme) => { var _a, _b; return { container: css({ display: "flex", flexDirection: "column", height: "100%", flex: "1 1 0", minHeight: "100%", position: "relative" }), drawer: css({ top: 0, ".rc-drawer-content-wrapper": { boxShadow: theme.shadows.z3 } }), drawerContent: css({ backgroundColor: `${theme.colors.background.primary} !important`, display: "flex", overflow: "unset !important", flexDirection: "column" }), drawerMotion: css({ "&-appear": { transform: "translateX(100%)", transition: "none !important", "&-active": { transition: `${theme.transitions.create("transform")} !important`, transform: "translateX(0)" } } }), // we want the mask itself to span the whole page including the top bar // this ensures trying to click something in the top bar will close the drawer correctly // but we don't want the backdrop styling to apply over the top bar as it looks weird // instead have a child pseudo element to apply the backdrop styling below the top bar mask: css({ // The !important here is to override the default .rc-drawer-mask styles backgroundColor: "transparent !important", // eslint-disable-next-line @typescript-eslint/consistent-type-assertions position: "fixed !important", "&:before": { backgroundColor: `${theme.components.overlay.background} !important`, bottom: 0, content: '""', left: 0, position: "fixed", right: 0, top: 0 } }), maskMotion: css({ "&-appear": { opacity: 0, "&-active": { opacity: 1, transition: theme.transitions.create("opacity") } } }), header: css({ label: "drawer-header", flexGrow: 0, padding: theme.spacing(2, 2, 3), borderBottom: `1px solid ${theme.colors.border.weak}` }), headerWithTabs: css({ borderBottom: "none" }), actions: css({ position: "absolute", right: theme.spacing(1), top: theme.spacing(1) }), titleWrapper: css({ label: "drawer-title", overflowWrap: "break-word" }), subtitle: css({ label: "drawer-subtitle", color: theme.colors.text.secondary, paddingTop: theme.spacing(1) }), content: css({ padding: theme.spacing((_b = (_a = theme.components.drawer) == null ? void 0 : _a.padding) != null ? _b : 2), height: "100%", flexGrow: 1, minHeight: 0 }), tabsWrapper: css({ label: "drawer-tabs", paddingLeft: theme.spacing(2), margin: theme.spacing(1, -1, -3, -3) }), resizer: css({ top: 0, left: theme.spacing(-1), bottom: 0, position: "absolute", zIndex: theme.zIndex.modal }) }; }; function getWrapperStyles(theme, size) { return css({ label: `drawer-content-wrapper-${size}`, overflow: "unset !important", [theme.breakpoints.down("md")]: { width: `calc(100% - ${theme.spacing(2)}) !important`, minWidth: "0 !important" } }); } export { Drawer }; //# sourceMappingURL=Drawer.mjs.map