126 lines
4.1 KiB
TypeScript
126 lines
4.1 KiB
TypeScript
import { useState, useCallback, useRef, useLayoutEffect } from 'react';
|
|
|
|
interface PaneResizeHandlers {
|
|
onPointerDown: (e: React.PointerEvent) => void;
|
|
onPointerMove: (e: React.PointerEvent) => void;
|
|
onPointerUp: () => void;
|
|
}
|
|
|
|
export function usePaneResize(
|
|
initialSize: number,
|
|
minSize: number,
|
|
maxSize: number,
|
|
side: 'left' | 'right' | 'top' | 'bottom'
|
|
): [number, PaneResizeHandlers, React.RefCallback<HTMLElement>] {
|
|
const [size, setSize] = useState(initialSize);
|
|
const draggingRef = useRef(false);
|
|
const liveSizeRef = useRef(initialSize);
|
|
const targetRef = useRef<HTMLElement | null>(null);
|
|
const containerOffsetRef = useRef(0);
|
|
const containerSizeRef = useRef(0);
|
|
const rafRef = useRef<number | null>(null);
|
|
|
|
const isVertical = side === 'top' || side === 'bottom';
|
|
const styleProp = isVertical ? 'height' : 'width';
|
|
|
|
const targetCallbackRef = useCallback(
|
|
(el: HTMLElement | null) => {
|
|
targetRef.current = el;
|
|
if (el) {
|
|
el.style[styleProp] = `${liveSizeRef.current}px`;
|
|
}
|
|
},
|
|
[styleProp]
|
|
);
|
|
|
|
// Keep DOM in sync when React state commits (e.g. on pointerUp).
|
|
// This ensures the ref-managed element always reflects the latest size
|
|
// without relying on React-controlled style props.
|
|
useLayoutEffect(() => {
|
|
if (targetRef.current) {
|
|
targetRef.current.style[styleProp] = `${size}px`;
|
|
}
|
|
}, [size, styleProp]);
|
|
|
|
const computeSize = useCallback(
|
|
(e: React.PointerEvent): number => {
|
|
if (isVertical) {
|
|
const total = containerSizeRef.current || window.innerHeight;
|
|
const resolvedMax = maxSize <= 1 ? total * maxSize : maxSize;
|
|
const pos = e.clientY - containerOffsetRef.current;
|
|
return side === 'top'
|
|
? Math.min(resolvedMax, Math.max(minSize, pos))
|
|
: Math.min(resolvedMax, Math.max(minSize, total - pos));
|
|
} else {
|
|
const resolvedMax = maxSize <= 1 ? window.innerWidth * maxSize : maxSize;
|
|
return side === 'left'
|
|
? Math.min(resolvedMax, Math.max(minSize, e.clientX))
|
|
: Math.min(resolvedMax, Math.max(minSize, window.innerWidth - e.clientX));
|
|
}
|
|
},
|
|
[side, isVertical, minSize, maxSize]
|
|
);
|
|
|
|
const handlePointerDown = useCallback(
|
|
(e: React.PointerEvent) => {
|
|
e.preventDefault();
|
|
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
|
draggingRef.current = true;
|
|
if (isVertical) {
|
|
const container = (e.currentTarget as HTMLElement).parentElement;
|
|
if (container) {
|
|
const rect = container.getBoundingClientRect();
|
|
containerOffsetRef.current = rect.top;
|
|
containerSizeRef.current = rect.height;
|
|
}
|
|
}
|
|
},
|
|
[isVertical]
|
|
);
|
|
|
|
const handlePointerMove = useCallback(
|
|
(e: React.PointerEvent) => {
|
|
if (!draggingRef.current) return;
|
|
liveSizeRef.current = computeSize(e);
|
|
if (targetRef.current) {
|
|
// Batch DOM updates to once per animation frame — on mobile, pointermove
|
|
// can fire multiple times per frame, and each direct style.height write
|
|
// forces a synchronous reflow that desynchronises MapLibre and deck.gl.
|
|
if (rafRef.current == null) {
|
|
rafRef.current = requestAnimationFrame(() => {
|
|
rafRef.current = null;
|
|
if (targetRef.current) {
|
|
targetRef.current.style[styleProp] = `${liveSizeRef.current}px`;
|
|
}
|
|
});
|
|
}
|
|
} else {
|
|
setSize(liveSizeRef.current);
|
|
}
|
|
},
|
|
[computeSize, styleProp]
|
|
);
|
|
|
|
const handlePointerUp = useCallback(() => {
|
|
draggingRef.current = false;
|
|
if (rafRef.current != null) {
|
|
cancelAnimationFrame(rafRef.current);
|
|
rafRef.current = null;
|
|
}
|
|
// Apply final size synchronously so the commit is immediate
|
|
if (targetRef.current) {
|
|
targetRef.current.style[styleProp] = `${liveSizeRef.current}px`;
|
|
}
|
|
setSize(liveSizeRef.current);
|
|
}, [styleProp]);
|
|
|
|
return [
|
|
size,
|
|
{
|
|
onPointerDown: handlePointerDown,
|
|
onPointerMove: handlePointerMove,
|
|
onPointerUp: handlePointerUp,
|
|
},
|
|
targetCallbackRef,
|
|
];
|
|
}
|