@grafana/ui
Version:
Grafana Components Library
293 lines (290 loc) • 9.39 kB
JavaScript
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