Splitters: Support pixel mode for useSplitter and useSnappingSplitter (#103590)
* splitter pixels * Progress * Splitters working * fixes * Update edit pane width to 330 * update
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { clamp, throttle } from 'lodash';
|
||||
import { useCallback, useId, useLayoutEffect, useRef } from 'react';
|
||||
import { clamp } from 'lodash';
|
||||
import { useCallback, useId, useRef } from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
@@ -12,16 +12,18 @@ import { DragHandlePosition, getDragStyles } from '../DragHandle/DragHandle';
|
||||
export interface UseSplitterOptions {
|
||||
/**
|
||||
* The initial size of the primary pane between 0-1, defaults to 0.5
|
||||
* If `usePixels` is true, this is the initial size in pixels of the second pane.
|
||||
*/
|
||||
initialSize?: number;
|
||||
direction: 'row' | 'column';
|
||||
dragPosition?: DragHandlePosition;
|
||||
usePixels?: boolean;
|
||||
/**
|
||||
* Called when ever the size of the primary pane changes
|
||||
* @param flexSize (float from 0-1)
|
||||
*/
|
||||
onSizeChanged?: (flexSize: number, pixelSize: number) => void;
|
||||
onResizing?: (flexSize: number, pixelSize: number) => void;
|
||||
onSizeChanged?: (flexSize: number, firstPanePixels: number, secondPanePixels: number) => void;
|
||||
onResizing?: (flexSize: number, firstPanePixels: number, secondPanePixels: number) => void;
|
||||
|
||||
// Size of the region left of the handle indicator that is responsive to dragging. At the same time acts as a margin
|
||||
// pushing the left pane content left.
|
||||
@@ -48,7 +50,14 @@ const propsForDirection = {
|
||||
} as const;
|
||||
|
||||
export function useSplitter(options: UseSplitterOptions) {
|
||||
const { direction, initialSize = 0.5, dragPosition = 'middle', onResizing, onSizeChanged } = options;
|
||||
const {
|
||||
direction,
|
||||
initialSize = options.usePixels ? 300 : 0.5,
|
||||
dragPosition = 'middle',
|
||||
onResizing,
|
||||
onSizeChanged,
|
||||
usePixels,
|
||||
} = options;
|
||||
|
||||
const handleSize = getPixelSize(options.handleSize);
|
||||
const splitterRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -56,43 +65,19 @@ export function useSplitter(options: UseSplitterOptions) {
|
||||
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 primarySizeRef = useRef<number | null>(null);
|
||||
const referencePaneSize = 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 = ariaValue(curSize, newDims[minDimProp], newDims[maxDimProp]);
|
||||
}
|
||||
},
|
||||
500,
|
||||
[maxDimProp, minDimProp, direction, measurementProp]
|
||||
);
|
||||
|
||||
const dragStart = useRef<number | null>(null);
|
||||
|
||||
const onPointerDown = useCallback(
|
||||
(e: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (!firstPaneRef.current) {
|
||||
if (!firstPaneRef.current || !secondPaneRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -100,32 +85,54 @@ export function useSplitter(options: UseSplitterOptions) {
|
||||
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);
|
||||
|
||||
if (usePixels) {
|
||||
referencePaneSize.current = measureElement(secondPaneRef.current, usePixels);
|
||||
} else {
|
||||
referencePaneSize.current = measureElement(firstPaneRef.current);
|
||||
}
|
||||
|
||||
savedPos.current = undefined;
|
||||
},
|
||||
[measurementProp, clientAxis]
|
||||
[measurementProp, clientAxis, usePixels]
|
||||
);
|
||||
|
||||
const onUpdateSize = useCallback(
|
||||
(diff: number) => {
|
||||
if (!containerSize.current || !primarySizeRef.current || !secondPaneRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstPanePixels = primarySizeRef.current;
|
||||
const secondPanePixels = containerSize.current - firstPanePixels - handleSize;
|
||||
const dims = referencePaneSize.current!;
|
||||
|
||||
if (usePixels) {
|
||||
const newSize = clamp(secondPanePixels - diff, dims[minDimProp], dims[maxDimProp]);
|
||||
secondPaneRef.current!.style.flexBasis = `${newSize}px`;
|
||||
splitterRef.current!.ariaValueNow = `${newSize}`;
|
||||
onResizing?.(newSize, firstPanePixels + diff, newSize);
|
||||
} else {
|
||||
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}`;
|
||||
splitterRef.current!.ariaValueNow = ariaValue(newSize, dims[minDimProp], dims[maxDimProp]);
|
||||
onResizing?.(newFlex, newSize, secondPanePixels - diff);
|
||||
}
|
||||
},
|
||||
[onResizing, handleSize, usePixels, minDimProp, maxDimProp]
|
||||
);
|
||||
|
||||
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}`;
|
||||
splitterRef.current!.ariaValueNow = ariaValue(newSize, dims[minDimProp], dims[maxDimProp]);
|
||||
|
||||
onResizing?.(newFlex, newSize);
|
||||
if (dragStart.current !== null) {
|
||||
onUpdateSize(e[clientAxis] - dragStart.current);
|
||||
}
|
||||
},
|
||||
[handleSize, clientAxis, minDimProp, maxDimProp, onResizing]
|
||||
[onUpdateSize, clientAxis]
|
||||
);
|
||||
|
||||
const onPointerUp = useCallback(
|
||||
@@ -133,14 +140,16 @@ export function useSplitter(options: UseSplitterOptions) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
splitterRef.current!.releasePointerCapture(e.pointerId);
|
||||
dragStart.current = null;
|
||||
|
||||
if (typeof primarySizeRef.current === 'number') {
|
||||
onSizeChanged?.(parseFloat(firstPaneRef.current!.style.flexGrow), primarySizeRef.current);
|
||||
}
|
||||
splitterRef.current!.releasePointerCapture(e.pointerId);
|
||||
|
||||
const firstPaneSize = firstPaneRef.current!.getBoundingClientRect()[measurementProp];
|
||||
const secondPanePixels = containerSize.current! - firstPaneSize - handleSize;
|
||||
|
||||
onSizeChanged?.(parseFloat(firstPaneRef.current!.style.flexGrow), firstPaneSize, secondPanePixels);
|
||||
},
|
||||
[onSizeChanged]
|
||||
[onSizeChanged, handleSize, measurementProp]
|
||||
);
|
||||
|
||||
const pressedKeys = useRef(new Set<string>());
|
||||
@@ -151,7 +160,7 @@ export function useSplitter(options: UseSplitterOptions) {
|
||||
if (nothingPressed) {
|
||||
keysLastHandledAt.current = null;
|
||||
return;
|
||||
} else if (primarySizeRef.current === '1fr') {
|
||||
} else if (primarySizeRef.current === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -175,21 +184,17 @@ export function useSplitter(options: UseSplitterOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
// measure primary and container
|
||||
primarySizeRef.current = firstPaneRef.current!.getBoundingClientRect()[measurementProp];
|
||||
containerSize.current = containerRef.current!.getBoundingClientRect()[measurementProp];
|
||||
|
||||
firstPaneRef.current!.style.flexGrow = `${newFlex}`;
|
||||
secondPaneRef.current!.style.flexGrow = `${1 - newFlex}`;
|
||||
splitterRef.current!.ariaValueNow = ariaValue(newSize, firstPaneDims[minDimProp], firstPaneDims[maxDimProp]);
|
||||
|
||||
onResizing?.(newFlex, newSize);
|
||||
onUpdateSize(sizeChange);
|
||||
|
||||
keysLastHandledAt.current = time;
|
||||
|
||||
window.requestAnimationFrame(handlePressedKeys);
|
||||
},
|
||||
[direction, handleSize, minDimProp, maxDimProp, measurementProp, onResizing]
|
||||
[direction, measurementProp, onUpdateSize]
|
||||
);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
@@ -198,35 +203,6 @@ export function useSplitter(options: UseSplitterOptions) {
|
||||
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)) ||
|
||||
@@ -240,9 +216,16 @@ export function useSplitter(options: UseSplitterOptions) {
|
||||
savedPos.current = undefined;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
primarySizeRef.current = firstPaneRef.current.getBoundingClientRect()[measurementProp];
|
||||
containerSize.current = containerRef.current!.getBoundingClientRect()[measurementProp];
|
||||
firstPaneMeasurements.current = measureElement(firstPaneRef.current);
|
||||
|
||||
if (usePixels) {
|
||||
referencePaneSize.current = measureElement(secondPaneRef.current!);
|
||||
} else {
|
||||
referencePaneSize.current = measureElement(firstPaneRef.current!);
|
||||
}
|
||||
|
||||
const newKey = !pressedKeys.current.has(e.key);
|
||||
|
||||
if (newKey) {
|
||||
@@ -254,7 +237,7 @@ export function useSplitter(options: UseSplitterOptions) {
|
||||
}
|
||||
}
|
||||
},
|
||||
[direction, handlePressedKeys, handleSize, maxDimProp, measurementProp, minDimProp]
|
||||
[direction, handlePressedKeys, , measurementProp, usePixels]
|
||||
);
|
||||
|
||||
const onKeyUp = useCallback(
|
||||
@@ -268,11 +251,12 @@ export function useSplitter(options: UseSplitterOptions) {
|
||||
|
||||
pressedKeys.current.delete(e.key);
|
||||
|
||||
if (typeof primarySizeRef.current === 'number') {
|
||||
onSizeChanged?.(parseFloat(firstPaneRef.current!.style.flexGrow), primarySizeRef.current);
|
||||
if (primarySizeRef.current !== null) {
|
||||
const secondPanePixels = containerSize.current! - primarySizeRef.current - handleSize;
|
||||
onSizeChanged?.(parseFloat(firstPaneRef.current!.style.flexGrow), primarySizeRef.current, secondPanePixels);
|
||||
}
|
||||
},
|
||||
[direction, onSizeChanged]
|
||||
[direction, onSizeChanged, handleSize]
|
||||
);
|
||||
|
||||
const onDoubleClick = useCallback(() => {
|
||||
@@ -280,13 +264,15 @@ export function useSplitter(options: UseSplitterOptions) {
|
||||
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 = `${ariaValue(primarySizeRef.current, dim[minDimProp], dim[maxDimProp])}`;
|
||||
}, [maxDimProp, measurementProp, minDimProp]);
|
||||
if (usePixels) {
|
||||
secondPaneRef.current.style.flexBasis = `${initialSize}px`;
|
||||
} else {
|
||||
firstPaneRef.current.style.flexGrow = '0.5';
|
||||
secondPaneRef.current.style.flexGrow = '0.5';
|
||||
primarySizeRef.current = firstPaneRef.current!.getBoundingClientRect()[measurementProp];
|
||||
splitterRef.current!.ariaValueNow = `50`;
|
||||
}
|
||||
}, [measurementProp, usePixels, initialSize]);
|
||||
|
||||
const onBlur = useCallback(() => {
|
||||
// If focus is lost while keys are held, stop changing panel sizes
|
||||
@@ -295,10 +281,11 @@ export function useSplitter(options: UseSplitterOptions) {
|
||||
dragStart.current = null;
|
||||
|
||||
if (typeof primarySizeRef.current === 'number') {
|
||||
onSizeChanged?.(parseFloat(firstPaneRef.current!.style.flexGrow), primarySizeRef.current);
|
||||
const secondPanePixels = containerSize.current! - primarySizeRef.current - handleSize;
|
||||
onSizeChanged?.(parseFloat(firstPaneRef.current!.style.flexGrow), primarySizeRef.current, secondPanePixels);
|
||||
}
|
||||
}
|
||||
}, [onSizeChanged]);
|
||||
}, [onSizeChanged, handleSize]);
|
||||
|
||||
const styles = useStyles2(getStyles, direction);
|
||||
const dragStyles = useStyles2(getDragStyles, dragPosition);
|
||||
@@ -306,7 +293,7 @@ export function useSplitter(options: UseSplitterOptions) {
|
||||
const id = useId();
|
||||
|
||||
const primaryStyles: React.CSSProperties = {
|
||||
flexGrow: clamp(initialSize ?? 0.5, 0, 1),
|
||||
flexGrow: clamp(initialSize, 0, 1),
|
||||
[minDimProp]: 'min-content',
|
||||
};
|
||||
|
||||
@@ -315,6 +302,12 @@ export function useSplitter(options: UseSplitterOptions) {
|
||||
[minDimProp]: 'min-content',
|
||||
};
|
||||
|
||||
if (usePixels) {
|
||||
primaryStyles.flexGrow = 1;
|
||||
secondaryStyles.flexGrow = 'unset';
|
||||
secondaryStyles.flexBasis = `${initialSize}px`;
|
||||
}
|
||||
|
||||
return {
|
||||
containerProps: {
|
||||
ref: containerRef,
|
||||
@@ -363,49 +356,34 @@ interface MeasureResult {
|
||||
maxHeight: number;
|
||||
}
|
||||
|
||||
function measureElement<T extends HTMLElement>(ref: T): MeasureResult {
|
||||
function measureElement<T extends HTMLElement>(ref: T, usePixels?: boolean): MeasureResult {
|
||||
const savedBodyOverflow = document.body.style.overflow;
|
||||
const savedWidth = ref.style.width;
|
||||
const savedHeight = ref.style.height;
|
||||
const savedFlex = ref.style.flexGrow;
|
||||
const savedFlexBasis = ref.style.flexBasis;
|
||||
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
ref.style.flexGrow = '0';
|
||||
ref.style.flexBasis = '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;
|
||||
ref.style.flexBasis = savedFlexBasis;
|
||||
|
||||
return { minWidth, maxWidth, minHeight, maxHeight };
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2, direction: UseSplitterOptions['direction']) {
|
||||
return {
|
||||
container: css({
|
||||
|
||||
@@ -43,14 +43,11 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls
|
||||
useSnappingSplitter({
|
||||
direction: 'row',
|
||||
dragPosition: 'end',
|
||||
initialSize: 0.8,
|
||||
initialSize: 330,
|
||||
handleSize: 'sm',
|
||||
usePixels: true,
|
||||
collapseBelowPixels: 250,
|
||||
collapsed: isCollapsed,
|
||||
|
||||
paneOptions: {
|
||||
collapseBelowPixels: 150,
|
||||
snapOpenToPixels: 400,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -116,7 +113,7 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls
|
||||
<div {...secondaryProps} className={cx(secondaryProps.className, styles.editPane)}>
|
||||
<DashboardEditPaneRenderer
|
||||
editPane={editPane}
|
||||
isCollapsed={splitterState.collapsed}
|
||||
isCollapsed={isCollapsed}
|
||||
onToggleCollapse={onToggleCollapse}
|
||||
openOverlay={selectionContext.selected.length > 0}
|
||||
/>
|
||||
|
||||
@@ -26,12 +26,10 @@ export function PanelEditorRenderer({ model }: SceneComponentProps<PanelEditor>)
|
||||
useSnappingSplitter({
|
||||
direction: 'row',
|
||||
dragPosition: 'end',
|
||||
initialSize: 0.8,
|
||||
initialSize: 330,
|
||||
usePixels: true,
|
||||
collapsed: isCollapsed,
|
||||
paneOptions: {
|
||||
collapseBelowPixels: 250,
|
||||
snapOpenToPixels: 400,
|
||||
},
|
||||
collapseBelowPixels: 250,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -87,9 +85,7 @@ function VizAndDataPane({ model }: SceneComponentProps<PanelEditor>) {
|
||||
direction: 'column',
|
||||
dragPosition: 'start',
|
||||
initialSize: 0.5,
|
||||
paneOptions: {
|
||||
collapseBelowPixels: 150,
|
||||
},
|
||||
collapseBelowPixels: 150,
|
||||
});
|
||||
|
||||
containerProps.className = cx(containerProps.className, styles.container);
|
||||
|
||||
@@ -5,20 +5,17 @@ import { ComponentSize, DragHandlePosition, useSplitter } from '@grafana/ui';
|
||||
export interface UseSnappingSplitterOptions {
|
||||
/**
|
||||
* The initial size of the primary pane between 0-1, defaults to 0.5
|
||||
* If `usePixels` is true, this is the initial size in pixels of the second pane.
|
||||
*/
|
||||
initialSize?: number;
|
||||
direction: 'row' | 'column';
|
||||
dragPosition?: DragHandlePosition;
|
||||
paneOptions: PaneOptions;
|
||||
collapsed?: boolean;
|
||||
// Size of the region left of the handle indicator that is responsive to dragging. At the same time acts as a margin
|
||||
// pushing the left pane content left.
|
||||
handleSize?: ComponentSize;
|
||||
}
|
||||
|
||||
interface PaneOptions {
|
||||
usePixels?: boolean;
|
||||
collapseBelowPixels: number;
|
||||
snapOpenToPixels?: number;
|
||||
}
|
||||
|
||||
interface PaneState {
|
||||
@@ -26,56 +23,58 @@ interface PaneState {
|
||||
snapSize?: number;
|
||||
}
|
||||
|
||||
export function useSnappingSplitter(options: UseSnappingSplitterOptions) {
|
||||
const { paneOptions } = options;
|
||||
|
||||
export function useSnappingSplitter({
|
||||
direction,
|
||||
initialSize,
|
||||
dragPosition,
|
||||
collapseBelowPixels,
|
||||
collapsed,
|
||||
handleSize,
|
||||
usePixels,
|
||||
}: UseSnappingSplitterOptions) {
|
||||
const [state, setState] = useState<PaneState>({
|
||||
collapsed: options.collapsed ?? false,
|
||||
snapSize: options.collapsed ? 0 : undefined,
|
||||
collapsed: collapsed ?? false,
|
||||
snapSize: collapsed ? 0 : undefined,
|
||||
});
|
||||
|
||||
const onResizing = useCallback(
|
||||
(flexSize: number, pixelSize: number) => {
|
||||
if (flexSize <= 0 && pixelSize <= 0) {
|
||||
(flexSize: number, firstPanePixels: number, secondPanePixels: number) => {
|
||||
if (flexSize <= 0 && firstPanePixels <= 0 && secondPanePixels <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const optionsPixelSize = (pixelSize / flexSize) * (1 - flexSize);
|
||||
|
||||
if (state.collapsed && optionsPixelSize > paneOptions.collapseBelowPixels) {
|
||||
if (state.collapsed && secondPanePixels > collapseBelowPixels) {
|
||||
setState({ collapsed: false });
|
||||
}
|
||||
|
||||
if (!state.collapsed && optionsPixelSize < paneOptions.collapseBelowPixels) {
|
||||
if (!state.collapsed && secondPanePixels < collapseBelowPixels) {
|
||||
setState({ collapsed: true });
|
||||
}
|
||||
},
|
||||
[state, paneOptions.collapseBelowPixels]
|
||||
[state, collapseBelowPixels]
|
||||
);
|
||||
|
||||
const onSizeChanged = useCallback(
|
||||
(flexSize: number, pixelSize: number) => {
|
||||
if (flexSize <= 0 && pixelSize <= 0) {
|
||||
(flexSize: number, firstPanePixels: number, secondPanePixels: number) => {
|
||||
if (flexSize <= 0 && firstPanePixels <= 0 && secondPanePixels <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newSecondPaneSize = 1 - flexSize;
|
||||
const isSnappedClosed = state.snapSize === 0;
|
||||
const sizeOfBothPanes = pixelSize / flexSize;
|
||||
const snapOpenToPixels = paneOptions.snapOpenToPixels ?? sizeOfBothPanes / 2;
|
||||
const snapSize = snapOpenToPixels / sizeOfBothPanes;
|
||||
|
||||
if (state.collapsed) {
|
||||
if (isSnappedClosed) {
|
||||
setState({ snapSize: Math.max(newSecondPaneSize, snapSize), collapsed: false });
|
||||
if (state.collapsed && !isSnappedClosed) {
|
||||
setState({ snapSize: 0, collapsed: state.collapsed });
|
||||
} else if (state.collapsed && isSnappedClosed) {
|
||||
if (usePixels) {
|
||||
const snapSize = Math.max(secondPanePixels, initialSize ?? 200);
|
||||
setState({ snapSize, collapsed: !state.collapsed });
|
||||
} else {
|
||||
setState({ snapSize: 0, collapsed: true });
|
||||
const snapSize = Math.max(1 - (initialSize ?? 0.5), 1 - flexSize);
|
||||
setState({ snapSize, collapsed: !state.collapsed });
|
||||
}
|
||||
} else if (isSnappedClosed) {
|
||||
setState({ snapSize: newSecondPaneSize, collapsed: false });
|
||||
}
|
||||
},
|
||||
[state, paneOptions.snapOpenToPixels]
|
||||
[state, initialSize, usePixels]
|
||||
);
|
||||
|
||||
const onToggleCollapse = useCallback(() => {
|
||||
@@ -83,10 +82,11 @@ export function useSnappingSplitter(options: UseSnappingSplitterOptions) {
|
||||
}, [state.collapsed]);
|
||||
|
||||
const { containerProps, primaryProps, secondaryProps, splitterProps } = useSplitter({
|
||||
direction: options.direction,
|
||||
dragPosition: options.dragPosition,
|
||||
handleSize: options.handleSize,
|
||||
initialSize: options.initialSize,
|
||||
direction: direction,
|
||||
dragPosition: dragPosition,
|
||||
handleSize: handleSize,
|
||||
initialSize: initialSize,
|
||||
usePixels: usePixels,
|
||||
onResizing,
|
||||
onSizeChanged,
|
||||
});
|
||||
@@ -97,17 +97,26 @@ export function useSnappingSplitter(options: UseSnappingSplitterOptions) {
|
||||
secondaryProps.style.minHeight = 'unset';
|
||||
|
||||
if (state.snapSize) {
|
||||
primaryProps.style = {
|
||||
...primaryProps.style,
|
||||
flexGrow: 1 - state.snapSize,
|
||||
};
|
||||
secondaryProps.style.flexGrow = state.snapSize;
|
||||
if (usePixels) {
|
||||
secondaryProps.style.flexBasis = `${state.snapSize}px`;
|
||||
} else {
|
||||
primaryProps.style = {
|
||||
...primaryProps.style,
|
||||
flexGrow: 1 - state.snapSize,
|
||||
};
|
||||
secondaryProps.style.flexGrow = state.snapSize;
|
||||
}
|
||||
} else if (state.snapSize === 0) {
|
||||
primaryProps.style.flexGrow = 1;
|
||||
secondaryProps.style.flexGrow = 0;
|
||||
secondaryProps.style.minWidth = 'unset';
|
||||
secondaryProps.style.minHeight = 'unset';
|
||||
secondaryProps.style.minWidth = 'min-content';
|
||||
secondaryProps.style.minHeight = 'min-content';
|
||||
secondaryProps.style.overflow = 'unset';
|
||||
|
||||
if (usePixels) {
|
||||
secondaryProps.style.flexBasis = '0px';
|
||||
} else {
|
||||
primaryProps.style.flexGrow = 1;
|
||||
secondaryProps.style.flexGrow = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return { containerProps, primaryProps, secondaryProps, splitterProps, splitterState: state, onToggleCollapse };
|
||||
|
||||
Reference in New Issue
Block a user