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 { FocusScope } from '@react-aria/focus';
|
||||||
import { useOverlay } from '@react-aria/overlays';
|
import { useOverlay } from '@react-aria/overlays';
|
||||||
import RcDrawer from 'rc-drawer';
|
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 { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
@ -37,7 +37,6 @@ export interface Props {
|
|||||||
* sm = width 25vw & min-width 384px
|
* sm = width 25vw & min-width 384px
|
||||||
* md = width 50vw & min-width 568px
|
* md = width 50vw & min-width 568px
|
||||||
* lg = width 75vw & min-width 744px
|
* lg = width 75vw & min-width 744px
|
||||||
* xl = width 85vw & min-width 744px
|
|
||||||
**/
|
**/
|
||||||
size?: 'sm' | 'md' | 'lg';
|
size?: 'sm' | 'md' | 'lg';
|
||||||
/** Tabs */
|
/** Tabs */
|
||||||
@ -62,7 +61,11 @@ export function Drawer({
|
|||||||
size = 'md',
|
size = 'md',
|
||||||
tabs,
|
tabs,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const [drawerWidth, onMouseDown, onTouchStart] = useResizebleDrawer();
|
||||||
|
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
const sizeStyles = useStyles2(getSizeStyles, size, drawerWidth ?? width);
|
||||||
|
|
||||||
const overlayRef = React.useRef(null);
|
const overlayRef = React.useRef(null);
|
||||||
const { dialogProps, titleProps } = useDialog({}, overlayRef);
|
const { dialogProps, titleProps } = useDialog({}, overlayRef);
|
||||||
const { overlayProps } = useOverlay(
|
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
|
// Adds body class while open so the toolbar nav can hide some actions while drawer is open
|
||||||
useBodyClassWhileOpen();
|
useBodyClassWhileOpen();
|
||||||
|
|
||||||
// Apply size styles (unless deprecated width prop is used)
|
const rootClass = cx(styles.drawer, sizeStyles);
|
||||||
const rootClass = cx(styles.drawer, !width && styles.sizes[size]);
|
|
||||||
const content = <div className={styles.content}>{children}</div>;
|
const content = <div className={styles.content}>{children}</div>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -86,11 +88,10 @@ export function Drawer({
|
|||||||
open={true}
|
open={true}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
placement="right"
|
placement="right"
|
||||||
// Important to set this to empty string so that the width can be controlled by the css
|
|
||||||
width={width ?? ''}
|
|
||||||
getContainer={'.main-view'}
|
getContainer={'.main-view'}
|
||||||
className={styles.drawerContent}
|
className={styles.drawerContent}
|
||||||
rootClassName={rootClass}
|
rootClassName={rootClass}
|
||||||
|
width={''}
|
||||||
motion={{
|
motion={{
|
||||||
motionAppear: true,
|
motionAppear: true,
|
||||||
motionName: styles.drawerMotion,
|
motionName: styles.drawerMotion,
|
||||||
@ -114,6 +115,8 @@ export function Drawer({
|
|||||||
{...dialogProps}
|
{...dialogProps}
|
||||||
ref={overlayRef}
|
ref={overlayRef}
|
||||||
>
|
>
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
||||||
|
<div className={styles.resizer} onMouseDown={onMouseDown} onTouchStart={onTouchStart} />
|
||||||
{typeof title === 'string' && (
|
{typeof title === 'string' && (
|
||||||
<div className={cx(styles.header, Boolean(tabs) && styles.headerWithTabs)}>
|
<div className={cx(styles.header, Boolean(tabs) && styles.headerWithTabs)}>
|
||||||
<div className={styles.actions}>
|
<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() {
|
function useBodyClassWhileOpen() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!document.body) {
|
if (!document.body) {
|
||||||
@ -168,14 +228,15 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
height: '100%',
|
height: '100%',
|
||||||
flex: '1 1 0',
|
flex: '1 1 0',
|
||||||
minHeight: '100%',
|
minHeight: '100%',
|
||||||
|
position: 'relative',
|
||||||
}),
|
}),
|
||||||
drawer: css({
|
drawer: css({
|
||||||
'.main-view &': {
|
'.main-view &': {
|
||||||
top: 81,
|
top: 80,
|
||||||
},
|
},
|
||||||
|
|
||||||
'.main-view--search-bar-hidden &': {
|
'.main-view--search-bar-hidden &': {
|
||||||
top: 41,
|
top: 40,
|
||||||
},
|
},
|
||||||
|
|
||||||
'.main-view--chrome-hidden &': {
|
'.main-view--chrome-hidden &': {
|
||||||
@ -184,47 +245,13 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
|
|
||||||
'.rc-drawer-content-wrapper': {
|
'.rc-drawer-content-wrapper': {
|
||||||
boxShadow: theme.shadows.z3,
|
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({
|
drawerContent: css({
|
||||||
backgroundColor: `${theme.colors.background.primary} !important`,
|
backgroundColor: `${theme.colors.background.primary} !important`,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
overflow: 'unset',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
overflow: 'hidden',
|
|
||||||
zIndex: theme.zIndex.dropdown,
|
|
||||||
}),
|
}),
|
||||||
drawerMotion: css({
|
drawerMotion: css({
|
||||||
'&-appear': {
|
'&-appear': {
|
||||||
@ -255,11 +282,11 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
right: 0,
|
right: 0,
|
||||||
|
|
||||||
'.main-view &': {
|
'.main-view &': {
|
||||||
top: 81,
|
top: 80,
|
||||||
},
|
},
|
||||||
|
|
||||||
'.main-view--search-bar-hidden &': {
|
'.main-view--search-bar-hidden &': {
|
||||||
top: 41,
|
top: 40,
|
||||||
},
|
},
|
||||||
|
|
||||||
'.main-view--chrome-hidden &': {
|
'.main-view--chrome-hidden &': {
|
||||||
@ -310,5 +337,72 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
paddingLeft: theme.spacing(2),
|
paddingLeft: theme.spacing(2),
|
||||||
margin: theme.spacing(1, -1, -3, -3),
|
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