mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Drawer: Resizable via draggable edge (#80796)
* Drawer: POC of draggable resizable drawer side * Cleaner solution * refinements * refinements * Add touch support
This commit is contained in:
parent
e08700c1b5
commit
05eb4fcd7f
@ -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 = <div className={styles.content}>{children}</div>;
|
||||
|
||||
return (
|
||||
@ -86,11 +88,10 @@ export function Drawer({
|
||||
open={true}
|
||||
onClose={onClose}
|
||||
placement="right"
|
||||
// Important to set this to empty string so that the width can be controlled by the css
|
||||
width={width ?? ''}
|
||||
getContainer={'.main-view'}
|
||||
className={styles.drawerContent}
|
||||
rootClassName={rootClass}
|
||||
width={''}
|
||||
motion={{
|
||||
motionAppear: true,
|
||||
motionName: styles.drawerMotion,
|
||||
@ -114,6 +115,8 @@ export function Drawer({
|
||||
{...dialogProps}
|
||||
ref={overlayRef}
|
||||
>
|
||||
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
||||
<div className={styles.resizer} onMouseDown={onMouseDown} onTouchStart={onTouchStart} />
|
||||
{typeof title === 'string' && (
|
||||
<div className={cx(styles.header, Boolean(tabs) && styles.headerWithTabs)}>
|
||||
<div className={styles.actions}>
|
||||
@ -146,6 +149,63 @@ export function Drawer({
|
||||
);
|
||||
}
|
||||
|
||||
function useResizebleDrawer(): [
|
||||
string | undefined,
|
||||
React.EventHandler<React.MouseEvent>,
|
||||
React.EventHandler<React.TouchEvent>,
|
||||
] {
|
||||
const [drawerWidth, setDrawerWidth] = useState<string | undefined>(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<HTMLDivElement>) {
|
||||
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<HTMLDivElement>) {
|
||||
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,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user