Files
2024-02-15 17:01:50 +00:00

219 lines
5.1 KiB
TypeScript

import {createContext, useCallback, useMemo} from 'react';
import styled from '@emotion/styled';
import {IconGrabbable} from 'sentry/icons';
import {space} from 'sentry/styles/space';
import {useResizableDrawer} from 'sentry/utils/useResizableDrawer';
export type DividerProps = {
'data-is-held': boolean;
'data-slide-direction': 'leftright' | 'updown';
onDoubleClick: React.MouseEventHandler<HTMLElement>;
onMouseDown: React.MouseEventHandler<HTMLElement>;
icon?: React.ReactNode;
} & React.DOMAttributes<HTMLDivElement>;
export const BaseSplitDivider = styled(({icon, ...props}: DividerProps) => (
<div {...props}>{icon || <IconGrabbable size="sm" />}</div>
))<DividerProps>`
display: grid;
place-items: center;
height: 100%;
width: 100%;
user-select: inherit;
background: inherit;
&:hover,
&[data-is-held='true'] {
background: ${p => p.theme.hover};
}
&[data-is-held='true'] {
user-select: none;
}
&[data-slide-direction='leftright'] {
cursor: ew-resize;
height: 100%;
width: ${space(2)};
}
&[data-slide-direction='updown'] {
cursor: ns-resize;
width: 100%;
height: ${space(2)};
& > svg {
transform: rotate(90deg);
}
}
`;
export const SplitPanelContext = createContext({
isMaximized: false,
isMinimized: false,
maximiseSize: () => {},
minimiseSize: () => {},
resetSize: () => {},
});
type Side = {
content: React.ReactNode;
default: number;
max: number;
min: number;
};
type CommonProps = {
availableSize: number;
SplitDivider?: React.ComponentType<DividerProps>;
onMouseDown?: (sizePct: `${number}%`) => void;
onResize?: (newSize: number) => void;
sizeStorageKey?: string;
};
export type SplitPanelProps = CommonProps &
(
| {
availableSize: number;
/**
* Content on the left side of the split
*/
left: Side;
/**
* Content on the right side of the split
*/
right: React.ReactNode;
}
| {
availableSize: number;
/**
* Content below the split
*/
bottom: React.ReactNode;
/**
* Content above of the split
*/
top: Side;
}
);
function SplitPanel(props: SplitPanelProps) {
const {
availableSize,
SplitDivider = BaseSplitDivider,
onMouseDown,
onResize,
sizeStorageKey,
} = props;
const isLeftRight = 'left' in props;
const initialSize = isLeftRight ? props.left.default : props.top.default;
const min = isLeftRight ? props.left.min : props.top.min;
const max = isLeftRight ? props.left.max : props.top.max;
const {
isHeld,
onDoubleClick,
onMouseDown: onDragStart,
size: containerSize,
setSize,
} = useResizableDrawer({
direction: isLeftRight ? 'left' : 'down',
initialSize,
min,
onResize: onResize ?? (() => {}),
sizeStorageKey,
});
const sizePct = `${
(Math.min(containerSize, max) / availableSize) * 100
}%` as `${number}%`;
const handleMouseDown = useCallback(
event => {
onMouseDown?.(sizePct);
onDragStart(event);
},
[onDragStart, sizePct, onMouseDown]
);
const isMaximized = containerSize >= max;
const isMinimized = containerSize <= min;
const contextValue = useMemo(
() => ({
isMaximized,
isMinimized,
maximiseSize: () => setSize(max),
minimiseSize: () => setSize(min),
resetSize: () => setSize(initialSize),
}),
[isMaximized, isMinimized, setSize, max, min, initialSize]
);
if (isLeftRight) {
const {left: a, right: b} = props;
return (
<SplitPanelContext.Provider value={contextValue}>
<SplitPanelContainer
className={isHeld ? 'disable-iframe-pointer' : undefined}
orientation="columns"
size={sizePct}
>
<Panel>{a.content}</Panel>
<SplitDivider
data-is-held={isHeld}
data-slide-direction="leftright"
onDoubleClick={onDoubleClick}
onMouseDown={handleMouseDown}
/>
<Panel>{b}</Panel>
</SplitPanelContainer>
</SplitPanelContext.Provider>
);
}
const {top: a, bottom: b} = props;
return (
<SplitPanelContext.Provider value={contextValue}>
<SplitPanelContainer
orientation="rows"
size={sizePct}
className={isHeld ? 'disable-iframe-pointer' : undefined}
>
<Panel>{a.content}</Panel>
<SplitDivider
data-is-held={isHeld}
data-slide-direction="updown"
onDoubleClick={onDoubleClick}
onMouseDown={handleMouseDown}
/>
<Panel>{b}</Panel>
</SplitPanelContainer>
</SplitPanelContext.Provider>
);
}
const SplitPanelContainer = styled('div')<{
orientation: 'rows' | 'columns';
size: `${number}px` | `${number}%`;
}>`
width: 100%;
height: 100%;
position: relative;
display: grid;
overflow: auto;
grid-template-${p => p.orientation}: ${p => p.size} auto 1fr;
&.disable-iframe-pointer iframe {
pointer-events: none !important;
}
`;
const Panel = styled('div')`
overflow: hidden;
`;
export default SplitPanel;