diff --git a/packages/grafana-ui/src/components/Drawer/Drawer.tsx b/packages/grafana-ui/src/components/Drawer/Drawer.tsx
index 4f455907e3e..ee8116d0ed8 100644
--- a/packages/grafana-ui/src/components/Drawer/Drawer.tsx
+++ b/packages/grafana-ui/src/components/Drawer/Drawer.tsx
@@ -3,7 +3,7 @@ import { useDialog } from '@react-aria/dialog';
import { FocusScope } from '@react-aria/focus';
import { useOverlay } from '@react-aria/overlays';
import RcDrawer from 'rc-drawer';
-import React, { ReactNode, useEffect } from 'react';
+import React, { ReactNode, useCallback, useEffect, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
@@ -37,7 +37,6 @@ export interface Props {
* sm = width 25vw & min-width 384px
* md = width 50vw & min-width 568px
* lg = width 75vw & min-width 744px
- * xl = width 85vw & min-width 744px
**/
size?: 'sm' | 'md' | 'lg';
/** Tabs */
@@ -62,7 +61,11 @@ export function Drawer({
size = 'md',
tabs,
}: Props) {
+ const [drawerWidth, onMouseDown, onTouchStart] = useResizebleDrawer();
+
const styles = useStyles2(getStyles);
+ const sizeStyles = useStyles2(getSizeStyles, size, drawerWidth ?? width);
+
const overlayRef = React.useRef(null);
const { dialogProps, titleProps } = useDialog({}, overlayRef);
const { overlayProps } = useOverlay(
@@ -77,8 +80,7 @@ export function Drawer({
// Adds body class while open so the toolbar nav can hide some actions while drawer is open
useBodyClassWhileOpen();
- // Apply size styles (unless deprecated width prop is used)
- const rootClass = cx(styles.drawer, !width && styles.sizes[size]);
+ const rootClass = cx(styles.drawer, sizeStyles);
const content =
@@ -146,6 +149,63 @@ export function Drawer({
);
}
+function useResizebleDrawer(): [
+ string | undefined,
+ React.EventHandler,
+ React.EventHandler,
+] {
+ const [drawerWidth, setDrawerWidth] = useState(undefined);
+
+ const onMouseMove = useCallback((e: MouseEvent) => {
+ setDrawerWidth(getCustomDrawerWidth(e.clientX));
+ }, []);
+
+ const onTouchMove = useCallback((e: TouchEvent) => {
+ const touch = e.touches[0];
+ setDrawerWidth(getCustomDrawerWidth(touch.clientX));
+ }, []);
+
+ const onMouseUp = useCallback(
+ (e: MouseEvent) => {
+ document.removeEventListener('mousemove', onMouseMove);
+ document.removeEventListener('mouseup', onMouseUp);
+ },
+ [onMouseMove]
+ );
+
+ const onTouchEnd = useCallback(
+ (e: TouchEvent) => {
+ document.removeEventListener('touchmove', onTouchMove);
+ document.removeEventListener('touchend', onTouchEnd);
+ },
+ [onTouchMove]
+ );
+
+ function onMouseDown(e: React.MouseEvent) {
+ e.stopPropagation();
+ e.preventDefault();
+ // we will only add listeners when needed, and remove them afterward
+ document.addEventListener('mousemove', onMouseMove);
+ document.addEventListener('mouseup', onMouseUp);
+ }
+
+ function onTouchStart(e: React.TouchEvent) {
+ e.stopPropagation();
+ e.preventDefault();
+ // we will only add listeners when needed, and remove them afterward
+ document.addEventListener('touchmove', onTouchMove);
+ document.addEventListener('touchend', onTouchEnd);
+ }
+
+ return [drawerWidth, onMouseDown, onTouchStart];
+}
+
+function getCustomDrawerWidth(clientX: number) {
+ 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) {
@@ -168,14 +228,15 @@ const getStyles = (theme: GrafanaTheme2) => {
height: '100%',
flex: '1 1 0',
minHeight: '100%',
+ position: 'relative',
}),
drawer: css({
'.main-view &': {
- top: 81,
+ top: 80,
},
'.main-view--search-bar-hidden &': {
- top: 41,
+ top: 40,
},
'.main-view--chrome-hidden &': {
@@ -184,47 +245,13 @@ const getStyles = (theme: GrafanaTheme2) => {
'.rc-drawer-content-wrapper': {
boxShadow: theme.shadows.z3,
-
- [theme.breakpoints.down('sm')]: {
- width: `calc(100% - ${theme.spacing(2)}) !important`,
- minWidth: '0 !important',
- },
},
}),
- sizes: {
- sm: css({
- '.rc-drawer-content-wrapper': {
- label: 'drawer-sm',
- width: '25vw',
- minWidth: theme.spacing(48),
- },
- }),
- md: css({
- '.rc-drawer-content-wrapper': {
- label: 'drawer-md',
- width: '50vw',
- minWidth: theme.spacing(60),
- },
- }),
- lg: css({
- '.rc-drawer-content-wrapper': {
- label: 'drawer-lg',
- width: '85vw',
- minWidth: theme.spacing(93),
-
- [theme.breakpoints.down('md')]: {
- width: `calc(100% - ${theme.spacing(2)}) !important`,
- minWidth: 0,
- },
- },
- }),
- },
drawerContent: css({
backgroundColor: `${theme.colors.background.primary} !important`,
display: 'flex',
+ overflow: 'unset',
flexDirection: 'column',
- overflow: 'hidden',
- zIndex: theme.zIndex.dropdown,
}),
drawerMotion: css({
'&-appear': {
@@ -255,11 +282,11 @@ const getStyles = (theme: GrafanaTheme2) => {
right: 0,
'.main-view &': {
- top: 81,
+ top: 80,
},
'.main-view--search-bar-hidden &': {
- top: 41,
+ top: 40,
},
'.main-view--chrome-hidden &': {
@@ -310,5 +337,72 @@ const getStyles = (theme: GrafanaTheme2) => {
paddingLeft: theme.spacing(2),
margin: theme.spacing(1, -1, -3, -3),
}),
+ resizer: css({
+ width: 8,
+ top: 0,
+ left: -4,
+ bottom: 0,
+ position: 'absolute',
+ cursor: 'col-resize',
+ zIndex: theme.zIndex.modal,
+
+ '&::after': {
+ background: theme.colors.emphasize(theme.colors.background.secondary, 0.15),
+ content: '""',
+ position: 'absolute',
+ left: '50%',
+ top: '50%',
+ transition: '0.2s background ease-in-out',
+ transform: 'translate(-50%, -50%)',
+ borderRadius: theme.shape.radius.default,
+ height: '200px',
+ width: '4px',
+ },
+
+ '&::before': {
+ content: '""',
+ position: 'absolute',
+ transition: '0.2s border-color ease-in-out',
+ borderRight: '2px solid transparent',
+ height: '100%',
+ left: '50%',
+ transform: 'translateX(-50%)',
+ },
+
+ '&:hover': {
+ '&::before': {
+ borderColor: theme.colors.primary.border,
+ },
+
+ '&::after': {
+ background: theme.colors.primary.border,
+ },
+ },
+ }),
};
};
+
+const drawerSizes = {
+ sm: { width: '25vw', minWidth: 384 },
+ md: { width: '50vw', minWidth: 568 },
+ lg: { width: '75vw', minWidth: 744 },
+};
+
+function getSizeStyles(theme: GrafanaTheme2, size: 'sm' | 'md' | 'lg', overrideWidth: number | string | undefined) {
+ let width = overrideWidth ?? drawerSizes[size].width;
+ let minWidth = drawerSizes[size].minWidth;
+
+ return css({
+ '.rc-drawer-content-wrapper': {
+ label: `drawer-content-wrapper-${size}`,
+ width: width,
+ minWidth: minWidth,
+ overflow: 'unset',
+
+ [theme.breakpoints.down('md')]: {
+ width: `calc(100% - ${theme.spacing(2)}) !important`,
+ minWidth: 0,
+ },
+ },
+ });
+}