mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Grafana/UI: Add new Splitter component (#82357)
* Initial thing working * Update * Progress * Update * Update * Simplify a bit more * minor refacto * more review fixes * Update * review fix * minor fix * update
This commit is contained in:
parent
7694e7bca3
commit
fe795417f7
@ -956,6 +956,9 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "18"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "19"]
|
||||
],
|
||||
"packages/grafana-ui/src/components/Splitter/Splitter.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
|
@ -2,12 +2,30 @@ import { css, cx } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
export const getDragStyles = (theme: GrafanaTheme2) => {
|
||||
export type DragHandlePosition = 'middle' | 'start' | 'end';
|
||||
|
||||
export const getDragStyles = (theme: GrafanaTheme2, handlePosition?: DragHandlePosition) => {
|
||||
const position = handlePosition || 'middle';
|
||||
const baseColor = theme.colors.emphasize(theme.colors.background.secondary, 0.15);
|
||||
const hoverColor = theme.colors.primary.border;
|
||||
const clickTargetSize = theme.spacing(2);
|
||||
const handlebarThickness = 4;
|
||||
const handlebarWidth = 200;
|
||||
let verticalOffset = '50%';
|
||||
let horizontalOffset = '50%';
|
||||
|
||||
switch (position) {
|
||||
case 'start': {
|
||||
verticalOffset = '0%';
|
||||
horizontalOffset = '0%';
|
||||
break;
|
||||
}
|
||||
case 'end': {
|
||||
verticalOffset = '100%';
|
||||
horizontalOffset = '100%';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const dragHandleBase = css({
|
||||
position: 'relative',
|
||||
@ -16,17 +34,17 @@ export const getDragStyles = (theme: GrafanaTheme2) => {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
transition: theme.transitions.create('border-color'),
|
||||
zIndex: 1,
|
||||
},
|
||||
|
||||
'&:after': {
|
||||
background: baseColor,
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
top: '50%',
|
||||
transition: theme.transitions.create('background'),
|
||||
transform: 'translate(-50%, -50%)',
|
||||
borderRadius: theme.shape.radius.pill,
|
||||
zIndex: 1,
|
||||
},
|
||||
|
||||
'&:hover': {
|
||||
@ -50,11 +68,13 @@ export const getDragStyles = (theme: GrafanaTheme2) => {
|
||||
'&:before': {
|
||||
borderRight: '1px solid transparent',
|
||||
height: '100%',
|
||||
left: '50%',
|
||||
left: verticalOffset,
|
||||
transform: 'translateX(-50%)',
|
||||
},
|
||||
|
||||
'&:after': {
|
||||
left: verticalOffset,
|
||||
top: '50%',
|
||||
height: handlebarWidth,
|
||||
width: handlebarThickness,
|
||||
},
|
||||
@ -68,12 +88,14 @@ export const getDragStyles = (theme: GrafanaTheme2) => {
|
||||
|
||||
'&:before': {
|
||||
borderTop: '1px solid transparent',
|
||||
top: '50%',
|
||||
top: horizontalOffset,
|
||||
transform: 'translateY(-50%)',
|
||||
width: '100%',
|
||||
},
|
||||
|
||||
'&:after': {
|
||||
left: '50%',
|
||||
top: horizontalOffset,
|
||||
height: handlebarThickness,
|
||||
width: handlebarWidth,
|
||||
},
|
||||
|
23
packages/grafana-ui/src/components/Splitter/Splitter.mdx
Normal file
23
packages/grafana-ui/src/components/Splitter/Splitter.mdx
Normal file
@ -0,0 +1,23 @@
|
||||
import { Meta, Preview, ArgTypes } from '@storybook/blocks';
|
||||
import { Box, Splitter, Text } from '@grafana/ui';
|
||||
|
||||
<Meta title="MDX|Splitter" component={Splitter} />
|
||||
|
||||
# Splitter
|
||||
|
||||
The splitter creates two resizable panes, either horizontally or vertically.
|
||||
|
||||
<Preview>
|
||||
<div style={{ display: 'flex', width: '900px', height: '700px' }}>
|
||||
<Splitter direction="row" initialSize={0.7}>
|
||||
<Box display="flex" grow={1} backgroundColor="secondary" padding={2}>
|
||||
<Text color="primary">Primary</Text>
|
||||
</Box>
|
||||
<Box display="flex" grow={1} backgroundColor="secondary" padding={2}>
|
||||
<Text color="primary">Secondary</Text>
|
||||
</Box>
|
||||
</Splitter>
|
||||
</div>
|
||||
</Preview>
|
||||
|
||||
<ArgTypes of={Splitter} />
|
@ -0,0 +1,55 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { Meta, StoryFn } from '@storybook/react';
|
||||
import React from 'react';
|
||||
|
||||
import { Splitter, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { DashboardStoryCanvas } from '../../utils/storybook/DashboardStoryCanvas';
|
||||
|
||||
import mdx from './Splitter.mdx';
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'General/Layout/Splitter',
|
||||
component: Splitter,
|
||||
parameters: {
|
||||
docs: {
|
||||
page: mdx,
|
||||
},
|
||||
controls: {
|
||||
exclude: [],
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
initialSize: { control: { type: 'number', min: 0.1, max: 1 } },
|
||||
},
|
||||
};
|
||||
|
||||
export const Basic: StoryFn = (args) => {
|
||||
const theme = useTheme2();
|
||||
const paneStyles = css({
|
||||
display: 'flex',
|
||||
flexGrow: 1,
|
||||
background: theme.colors.background.primary,
|
||||
padding: theme.spacing(2),
|
||||
border: `1px solid ${theme.colors.border.weak}`,
|
||||
height: '100%',
|
||||
});
|
||||
|
||||
return (
|
||||
<DashboardStoryCanvas>
|
||||
<div style={{ display: 'flex', width: '700px', height: '500px' }}>
|
||||
<Splitter {...args}>
|
||||
<div className={paneStyles}>Primary</div>
|
||||
<div className={paneStyles}>Secondary</div>
|
||||
</Splitter>
|
||||
</div>
|
||||
</DashboardStoryCanvas>
|
||||
);
|
||||
};
|
||||
|
||||
Basic.args = {
|
||||
direction: 'row',
|
||||
dragPosition: 'middle',
|
||||
};
|
||||
|
||||
export default meta;
|
475
packages/grafana-ui/src/components/Splitter/Splitter.tsx
Normal file
475
packages/grafana-ui/src/components/Splitter/Splitter.tsx
Normal file
@ -0,0 +1,475 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { clamp, throttle } from 'lodash';
|
||||
import React, { useCallback, useId, useLayoutEffect, useRef } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { useStyles2 } from '../../themes';
|
||||
import { DragHandlePosition, getDragStyles } from '../DragHandle/DragHandle';
|
||||
|
||||
export interface Props {
|
||||
/**
|
||||
* The initial size of the primary pane between 0-1, defaults to 0.5
|
||||
*/
|
||||
initialSize?: number;
|
||||
direction?: 'row' | 'column';
|
||||
dragPosition?: DragHandlePosition;
|
||||
primaryPaneStyles?: React.CSSProperties;
|
||||
secondaryPaneStyles?: React.CSSProperties;
|
||||
/**
|
||||
* Called when ever the size of the primary pane changes
|
||||
* @param size (float from 0-1)
|
||||
*/
|
||||
onSizeChange?: (size: number) => void;
|
||||
children: [React.ReactNode, React.ReactNode];
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits two children into two resizable panes
|
||||
* @alpha
|
||||
*/
|
||||
export function Splitter(props: Props) {
|
||||
const {
|
||||
direction = 'row',
|
||||
initialSize = 0.5,
|
||||
primaryPaneStyles,
|
||||
secondaryPaneStyles,
|
||||
onSizeChange,
|
||||
dragPosition = 'middle',
|
||||
children,
|
||||
} = props;
|
||||
|
||||
const { containerRef, firstPaneRef, minDimProp, splitterProps, secondPaneRef } = useSplitter(
|
||||
direction,
|
||||
onSizeChange,
|
||||
children
|
||||
);
|
||||
|
||||
const kids = React.Children.toArray(children);
|
||||
const styles = useStyles2(getStyles, direction);
|
||||
const dragStyles = useStyles2(getDragStyles, dragPosition);
|
||||
const dragHandleStyle = direction === 'column' ? dragStyles.dragHandleHorizontal : dragStyles.dragHandleVertical;
|
||||
const id = useId();
|
||||
|
||||
const secondAvailable = kids.length === 2;
|
||||
const visibilitySecond = secondAvailable ? 'visible' : 'hidden';
|
||||
let firstChildSize = initialSize;
|
||||
|
||||
// If second child is missing let first child have all the space
|
||||
if (!children[1]) {
|
||||
firstChildSize = 1;
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={styles.container}>
|
||||
<div
|
||||
ref={firstPaneRef}
|
||||
className={styles.panel}
|
||||
style={{
|
||||
flexGrow: clamp(firstChildSize, 0, 1),
|
||||
[minDimProp]: 'min-content',
|
||||
...primaryPaneStyles,
|
||||
}}
|
||||
id={`start-panel-${id}`}
|
||||
>
|
||||
{kids[0]}
|
||||
</div>
|
||||
|
||||
{kids[1] && (
|
||||
<>
|
||||
<div
|
||||
className={dragHandleStyle}
|
||||
{...splitterProps}
|
||||
role="separator"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
aria-valuenow={initialSize * 100}
|
||||
aria-controls={`start-panel-${id}`}
|
||||
aria-label="Pane resize widget"
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
||||
tabIndex={0}
|
||||
></div>
|
||||
|
||||
<div
|
||||
ref={secondPaneRef}
|
||||
className={styles.panel}
|
||||
style={{
|
||||
flexGrow: clamp(1 - initialSize, 0, 1),
|
||||
[minDimProp]: 'min-content',
|
||||
visibility: `${visibilitySecond}`,
|
||||
...secondaryPaneStyles,
|
||||
}}
|
||||
id={`end-panel-${id}`}
|
||||
>
|
||||
{kids[1]}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2, direction: Props['direction']) {
|
||||
return {
|
||||
container: css({
|
||||
display: 'flex',
|
||||
flexDirection: direction === 'row' ? 'row' : 'column',
|
||||
width: '100%',
|
||||
flexGrow: 1,
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
panel: css({ display: 'flex', position: 'relative', flexBasis: 0 }),
|
||||
dragEdge: {
|
||||
second: css({
|
||||
top: 0,
|
||||
left: theme.spacing(-1),
|
||||
bottom: 0,
|
||||
position: 'absolute',
|
||||
zIndex: theme.zIndex.modal,
|
||||
}),
|
||||
first: css({
|
||||
top: 0,
|
||||
left: theme.spacing(-1),
|
||||
bottom: 0,
|
||||
position: 'absolute',
|
||||
zIndex: theme.zIndex.modal,
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const PIXELS_PER_MS = 0.3 as const;
|
||||
const VERTICAL_KEYS = new Set(['ArrowUp', 'ArrowDown']);
|
||||
const HORIZONTAL_KEYS = new Set(['ArrowLeft', 'ArrowRight']);
|
||||
|
||||
const propsForDirection = {
|
||||
row: {
|
||||
dim: 'width',
|
||||
axis: 'clientX',
|
||||
min: 'minWidth',
|
||||
max: 'maxWidth',
|
||||
},
|
||||
column: {
|
||||
dim: 'height',
|
||||
axis: 'clientY',
|
||||
min: 'minHeight',
|
||||
max: 'maxHeight',
|
||||
},
|
||||
} as const;
|
||||
|
||||
function useSplitter(direction: 'row' | 'column', onSizeChange: Props['onSizeChange'], children: Props['children']) {
|
||||
const handleSize = 16;
|
||||
const splitterRef = useRef<HTMLDivElement | null>(null);
|
||||
const firstPaneRef = useRef<HTMLDivElement | null>(null);
|
||||
const secondPaneRef = useRef<HTMLDivElement | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const containerSize = useRef<number | null>(null);
|
||||
const primarySizeRef = useRef<'1fr' | number>('1fr');
|
||||
|
||||
const firstPaneMeasurements = useRef<MeasureResult | undefined>(undefined);
|
||||
const savedPos = useRef<string | undefined>(undefined);
|
||||
|
||||
const measurementProp = propsForDirection[direction].dim;
|
||||
const clientAxis = propsForDirection[direction].axis;
|
||||
const minDimProp = propsForDirection[direction].min;
|
||||
const maxDimProp = propsForDirection[direction].max;
|
||||
|
||||
// Using a resize observer here, as with content or screen based width/height the ratio between panes might
|
||||
// change after a window resize, so ariaValueNow needs to be updated accordingly
|
||||
useResizeObserver(
|
||||
containerRef.current!,
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
if (!entry.target.isSameNode(containerRef.current)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!firstPaneRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const curSize = firstPaneRef.current!.getBoundingClientRect()[measurementProp];
|
||||
const newDims = measureElement(firstPaneRef.current);
|
||||
|
||||
splitterRef.current!.ariaValueNow = `${clamp(
|
||||
((curSize - newDims[minDimProp]) / (newDims[maxDimProp] - newDims[minDimProp])) * 100,
|
||||
0,
|
||||
100
|
||||
)}`;
|
||||
}
|
||||
},
|
||||
500,
|
||||
[maxDimProp, minDimProp, direction, measurementProp]
|
||||
);
|
||||
|
||||
const dragStart = useRef<number | null>(null);
|
||||
const onPointerDown = useCallback(
|
||||
(e: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (!firstPaneRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// measure left-side width
|
||||
primarySizeRef.current = firstPaneRef.current!.getBoundingClientRect()[measurementProp];
|
||||
containerSize.current = containerRef.current!.getBoundingClientRect()[measurementProp];
|
||||
|
||||
// set position at start of drag
|
||||
dragStart.current = e[clientAxis];
|
||||
splitterRef.current!.setPointerCapture(e.pointerId);
|
||||
firstPaneMeasurements.current = measureElement(firstPaneRef.current);
|
||||
|
||||
savedPos.current = undefined;
|
||||
},
|
||||
[measurementProp, clientAxis]
|
||||
);
|
||||
|
||||
const onPointerMove = useCallback(
|
||||
(e: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (dragStart.current !== null && primarySizeRef.current !== '1fr') {
|
||||
const diff = e[clientAxis] - dragStart.current;
|
||||
const dims = firstPaneMeasurements.current!;
|
||||
const newSize = clamp(primarySizeRef.current + diff, dims[minDimProp], dims[maxDimProp]);
|
||||
const newFlex = newSize / (containerSize.current! - handleSize);
|
||||
firstPaneRef.current!.style.flexGrow = `${newFlex}`;
|
||||
secondPaneRef.current!.style.flexGrow = `${1 - newFlex}`;
|
||||
const ariaValueNow = clamp(
|
||||
((newSize - dims[minDimProp]) / (dims[maxDimProp] - dims[minDimProp])) * 100,
|
||||
0,
|
||||
100
|
||||
);
|
||||
|
||||
splitterRef.current!.ariaValueNow = `${ariaValueNow}`;
|
||||
}
|
||||
},
|
||||
[handleSize, clientAxis, minDimProp, maxDimProp]
|
||||
);
|
||||
|
||||
const onPointerUp = useCallback(
|
||||
(e: React.PointerEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
splitterRef.current!.releasePointerCapture(e.pointerId);
|
||||
dragStart.current = null;
|
||||
onSizeChange?.(parseFloat(firstPaneRef.current!.style.flexGrow));
|
||||
},
|
||||
[onSizeChange]
|
||||
);
|
||||
|
||||
const pressedKeys = useRef(new Set<string>());
|
||||
const keysLastHandledAt = useRef<number | null>(null);
|
||||
const handlePressedKeys = useCallback(
|
||||
(time: number) => {
|
||||
const nothingPressed = pressedKeys.current.size === 0;
|
||||
if (nothingPressed) {
|
||||
keysLastHandledAt.current = null;
|
||||
return;
|
||||
} else if (primarySizeRef.current === '1fr') {
|
||||
return;
|
||||
}
|
||||
|
||||
const dt = time - (keysLastHandledAt.current ?? time);
|
||||
const dx = dt * PIXELS_PER_MS;
|
||||
let sizeChange = 0;
|
||||
|
||||
if (direction === 'row') {
|
||||
if (pressedKeys.current.has('ArrowLeft')) {
|
||||
sizeChange -= dx;
|
||||
}
|
||||
if (pressedKeys.current.has('ArrowRight')) {
|
||||
sizeChange += dx;
|
||||
}
|
||||
} else {
|
||||
if (pressedKeys.current.has('ArrowUp')) {
|
||||
sizeChange -= dx;
|
||||
}
|
||||
if (pressedKeys.current.has('ArrowDown')) {
|
||||
sizeChange += dx;
|
||||
}
|
||||
}
|
||||
|
||||
const firstPaneDims = firstPaneMeasurements.current!;
|
||||
const curSize = firstPaneRef.current!.getBoundingClientRect()[measurementProp];
|
||||
const newSize = clamp(curSize + sizeChange, firstPaneDims[minDimProp], firstPaneDims[maxDimProp]);
|
||||
|
||||
const newFlex = newSize / (containerSize.current! - handleSize);
|
||||
|
||||
firstPaneRef.current!.style.flexGrow = `${newFlex}`;
|
||||
secondPaneRef.current!.style.flexGrow = `${1 - newFlex}`;
|
||||
const ariaValueNow =
|
||||
((newSize - firstPaneDims[minDimProp]) / (firstPaneDims[maxDimProp] - firstPaneDims[minDimProp])) * 100;
|
||||
splitterRef.current!.ariaValueNow = `${clamp(ariaValueNow, 0, 100)}`;
|
||||
|
||||
keysLastHandledAt.current = time;
|
||||
window.requestAnimationFrame(handlePressedKeys);
|
||||
},
|
||||
[direction, handleSize, minDimProp, maxDimProp, measurementProp]
|
||||
);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (!firstPaneRef.current || !secondPaneRef.current || !splitterRef.current || !containerRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'Enter') {
|
||||
if (savedPos.current === undefined) {
|
||||
savedPos.current = firstPaneRef.current!.style.flexGrow;
|
||||
firstPaneRef.current!.style.flexGrow = '0';
|
||||
secondPaneRef.current!.style.flexGrow = '1';
|
||||
} else {
|
||||
firstPaneRef.current!.style.flexGrow = savedPos.current;
|
||||
secondPaneRef.current!.style.flexGrow = `${1 - parseFloat(savedPos.current)}`;
|
||||
savedPos.current = undefined;
|
||||
}
|
||||
return;
|
||||
} else if (e.key === 'Home') {
|
||||
firstPaneMeasurements.current = measureElement(firstPaneRef.current);
|
||||
containerSize.current = containerRef.current!.getBoundingClientRect()[measurementProp];
|
||||
const newFlex = firstPaneMeasurements.current[minDimProp] / (containerSize.current - handleSize);
|
||||
firstPaneRef.current.style.flexGrow = `${newFlex}`;
|
||||
secondPaneRef.current.style.flexGrow = `${1 - newFlex}`;
|
||||
splitterRef.current.ariaValueNow = '0';
|
||||
return;
|
||||
} else if (e.key === 'End') {
|
||||
firstPaneMeasurements.current = measureElement(firstPaneRef.current);
|
||||
containerSize.current = containerRef.current!.getBoundingClientRect()[measurementProp];
|
||||
const newFlex = firstPaneMeasurements.current[maxDimProp] / (containerSize.current - handleSize);
|
||||
firstPaneRef.current!.style.flexGrow = `${newFlex}`;
|
||||
secondPaneRef.current!.style.flexGrow = `${1 - newFlex}`;
|
||||
splitterRef.current!.ariaValueNow = '100';
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!(
|
||||
(direction === 'column' && VERTICAL_KEYS.has(e.key)) ||
|
||||
(direction === 'row' && HORIZONTAL_KEYS.has(e.key))
|
||||
) ||
|
||||
pressedKeys.current.has(e.key)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
savedPos.current = undefined;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
primarySizeRef.current = firstPaneRef.current.getBoundingClientRect()[measurementProp];
|
||||
containerSize.current = containerRef.current!.getBoundingClientRect()[measurementProp];
|
||||
firstPaneMeasurements.current = measureElement(firstPaneRef.current);
|
||||
const newKey = !pressedKeys.current.has(e.key);
|
||||
|
||||
if (newKey) {
|
||||
const initiateAnimationLoop = pressedKeys.current.size === 0;
|
||||
pressedKeys.current.add(e.key);
|
||||
|
||||
if (initiateAnimationLoop) {
|
||||
window.requestAnimationFrame(handlePressedKeys);
|
||||
}
|
||||
}
|
||||
},
|
||||
[direction, handlePressedKeys, handleSize, maxDimProp, measurementProp, minDimProp]
|
||||
);
|
||||
|
||||
const onKeyUp = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (
|
||||
(direction === 'row' && !HORIZONTAL_KEYS.has(e.key)) ||
|
||||
(direction === 'column' && !VERTICAL_KEYS.has(e.key))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
pressedKeys.current.delete(e.key);
|
||||
onSizeChange?.(parseFloat(firstPaneRef.current!.style.flexGrow));
|
||||
},
|
||||
[direction, onSizeChange]
|
||||
);
|
||||
|
||||
const onDoubleClick = useCallback(() => {
|
||||
if (!firstPaneRef.current || !secondPaneRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
firstPaneRef.current.style.flexGrow = '0.5';
|
||||
secondPaneRef.current.style.flexGrow = '0.5';
|
||||
const dim = measureElement(firstPaneRef.current);
|
||||
firstPaneMeasurements.current = dim;
|
||||
primarySizeRef.current = firstPaneRef.current!.getBoundingClientRect()[measurementProp];
|
||||
splitterRef.current!.ariaValueNow = `${((primarySizeRef.current - dim[minDimProp]) / (dim[maxDimProp] - dim[minDimProp])) * 100}`;
|
||||
}, [maxDimProp, measurementProp, minDimProp]);
|
||||
|
||||
const onBlur = useCallback(() => {
|
||||
// If focus is lost while keys are held, stop changing panel sizes
|
||||
if (pressedKeys.current.size > 0) {
|
||||
pressedKeys.current.clear();
|
||||
dragStart.current = null;
|
||||
onSizeChange?.(parseFloat(firstPaneRef.current!.style.flexGrow));
|
||||
}
|
||||
}, [onSizeChange]);
|
||||
|
||||
return {
|
||||
containerRef,
|
||||
firstPaneRef,
|
||||
minDimProp,
|
||||
splitterProps: {
|
||||
onPointerUp,
|
||||
onPointerDown,
|
||||
onPointerMove,
|
||||
onKeyDown,
|
||||
onKeyUp,
|
||||
onDoubleClick,
|
||||
onBlur,
|
||||
ref: splitterRef,
|
||||
style: { [measurementProp]: `${handleSize}px` },
|
||||
},
|
||||
secondPaneRef,
|
||||
};
|
||||
}
|
||||
|
||||
interface MeasureResult {
|
||||
minWidth: number;
|
||||
maxWidth: number;
|
||||
minHeight: number;
|
||||
maxHeight: number;
|
||||
}
|
||||
|
||||
function measureElement<T extends HTMLElement>(ref: T): MeasureResult {
|
||||
const savedBodyOverflow = document.body.style.overflow;
|
||||
const savedWidth = ref.style.width;
|
||||
const savedHeight = ref.style.height;
|
||||
const savedFlex = ref.style.flexGrow;
|
||||
document.body.style.overflow = 'hidden';
|
||||
ref.style.flexGrow = '0';
|
||||
const { width: minWidth, height: minHeight } = ref.getBoundingClientRect();
|
||||
|
||||
ref.style.flexGrow = '100';
|
||||
const { width: maxWidth, height: maxHeight } = ref.getBoundingClientRect();
|
||||
|
||||
document.body.style.overflow = savedBodyOverflow;
|
||||
ref.style.width = savedWidth;
|
||||
ref.style.height = savedHeight;
|
||||
ref.style.flexGrow = savedFlex;
|
||||
|
||||
return { minWidth, maxWidth, minHeight, maxHeight } as MeasureResult;
|
||||
}
|
||||
|
||||
function useResizeObserver(
|
||||
target: Element,
|
||||
cb: (entries: ResizeObserverEntry[]) => void,
|
||||
throttleWait = 0,
|
||||
deps?: React.DependencyList
|
||||
) {
|
||||
const throttledCallback = throttle(cb, throttleWait);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(throttledCallback);
|
||||
|
||||
resizeObserver.observe(target, { box: 'device-pixel-content-box' });
|
||||
return () => resizeObserver.disconnect();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, deps);
|
||||
}
|
@ -265,6 +265,7 @@ export { Avatar } from './UsersIndicator/Avatar';
|
||||
export { InlineFormLabel } from './FormLabel/FormLabel';
|
||||
export { Divider } from './Divider/Divider';
|
||||
export { getDragStyles } from './DragHandle/DragHandle';
|
||||
export { Splitter } from './Splitter/Splitter';
|
||||
|
||||
export { LayoutItemContext, type LayoutItemContextProps } from './Layout/LayoutItemContext';
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user