feat(ui): bump react-resizable-panels & improve usePanel hook

This commit is contained in:
psychedelicious 2024-01-01 09:11:04 +11:00 committed by Kent Keirsey
parent ab3e689ee0
commit 793cf39964
3 changed files with 158 additions and 42 deletions

View File

@ -95,7 +95,7 @@
"react-icons": "^4.12.0",
"react-konva": "^18.2.10",
"react-redux": "^9.0.4",
"react-resizable-panels": "^1.0.6",
"react-resizable-panels": "^1.0.7",
"react-select": "5.8.0",
"react-textarea-autosize": "^8.5.3",
"react-use": "^17.4.2",

View File

@ -135,8 +135,8 @@ dependencies:
specifier: ^9.0.4
version: 9.0.4(@types/react@18.2.46)(react@18.2.0)(redux@5.0.1)
react-resizable-panels:
specifier: ^1.0.6
version: 1.0.6(react-dom@18.2.0)(react@18.2.0)
specifier: ^1.0.7
version: 1.0.7(react-dom@18.2.0)(react@18.2.0)
react-select:
specifier: 5.8.0
version: 5.8.0(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
@ -11569,8 +11569,8 @@ packages:
use-sidecar: 1.1.2(@types/react@18.2.46)(react@18.2.0)
dev: false
/react-resizable-panels@1.0.6(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-yZQiOP/uW2nTSdESDUBlBkQ1NQjUABpRKfBqonUQnNYSur7qRDy3W2wEEmrEyUY+W3opshpMiHf45zngIduJ0g==}
/react-resizable-panels@1.0.7(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-CluJkHQheeNqIJly2FYDfri3ME+2h2nCXpf0Y+hTO1K1eVtNxXFA5hVp5cUD6NS70iiufswOmnku9QZiLr1hYg==}
peerDependencies:
react: ^16.14.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0

View File

@ -11,33 +11,100 @@ import {
getResizeHandleElementsForGroup,
} from 'react-resizable-panels';
type Direction = 'horizontal' | 'vertical';
export type UsePanelOptions =
| { minSize: number; unit: 'percentages' }
| {
/**
* The minimum size of the panel as a percentage.
*/
minSize: number;
/**
* The unit of the minSize
*/
unit: 'percentages';
}
| {
/**
* The minimum size of the panel in pixels.
*/
minSize: number;
/**
* The unit of the minSize.
*/
unit: 'pixels';
fallbackMinSizePct: number;
/**
* The direction of the panel group.
* This is required to accurately calculate the available space for the panel, minus the space taken by the handles.
*/
panelGroupDirection: Direction;
/**
* A ref to the panel group.
*/
panelGroupRef: RefObject<ImperativePanelGroupHandle>;
panelGroupDirection: 'horizontal' | 'vertical';
};
export const usePanel = (arg: UsePanelOptions) => {
export type UsePanelReturn = {
/**
* The ref to the panel handle.
*/
ref: RefObject<ImperativePanelHandle>;
/**
* The dynamically calculated minimum size of the panel.
*/
minSize: number;
/**
* Whether the panel is collapsed.
*/
isCollapsed: boolean;
/**
* The onCollapse callback. This is required to update the isCollapsed state.
* This should be passed to the panel as the onCollapse prop. Wrap it if additional logic is required.
*/
onCollapse: PanelOnCollapse;
/**
* The onExpand callback. This is required to update the isCollapsed state.
* This should be passed to the panel as the onExpand prop. Wrap it if additional logic is required.
*/
onExpand: PanelOnExpand;
/**
* Reset the panel to the minSize. If the panel is already at the minSize, collapse it.
* This can be provided to the `onDoubleClick` prop of the panel's nearest resize handle.
*/
reset: () => void;
/**
* Toggle the panel between collapsed and expanded.
*/
toggle: () => void;
/**
* Expand the panel.
*/
expand: () => void;
/**
* Collapse the panel.
*/
collapse: () => void;
/**
* Resize the panel to the given size in the same units as the minSize.
*/
resize: (size: number) => void;
};
export const usePanel = (arg: UsePanelOptions): UsePanelReturn => {
const panelHandleRef = useRef<ImperativePanelHandle>(null);
const [_minSize, _setMinSize] = useState<number>(
arg.unit === 'percentages' ? arg.minSize : arg.fallbackMinSizePct
arg.unit === 'percentages' ? arg.minSize : 0
);
// If the units are pixels, we need to calculate the min size as a percentage of the available space
// If the units are pixels, we need to calculate the min size as a percentage of the available space,
// then resize the panel if it is too small.
useLayoutEffect(() => {
if (arg.unit === 'percentages' || !arg.panelGroupRef.current) {
return;
}
const panelGroupElement = getPanelGroupElement(
arg.panelGroupRef.current.getId()
);
const panelGroupHandleElements = getResizeHandleElementsForGroup(
arg.panelGroupRef.current.getId()
);
const id = arg.panelGroupRef.current.getId();
const panelGroupElement = getPanelGroupElement(id);
const panelGroupHandleElements = getResizeHandleElementsForGroup(id);
if (!panelGroupElement) {
return;
}
@ -45,33 +112,23 @@ export const usePanel = (arg: UsePanelOptions) => {
if (!panelHandleRef?.current) {
return;
}
// Calculate the available space for the panel, minus the space taken by the handles
let dim =
arg.panelGroupDirection === 'horizontal'
? panelGroupElement.offsetWidth
: panelGroupElement.offsetHeight;
panelGroupHandleElements.forEach(
(el) =>
(dim -=
arg.panelGroupDirection === 'horizontal'
? el.offsetWidth
: el.offsetHeight)
const minSizePct = getSizeAsPercentage(
arg.minSize,
arg.panelGroupRef,
arg.panelGroupDirection
);
// Calculate the min size as a percentage of the available space
const minSize = (arg.minSize * 100) / dim;
// Must store this to avoid race conditions
const currentSize = panelHandleRef.current.getSize();
// Resize if the current size is smaller than the new min size - happens when the window is resized smaller
if (currentSize < minSize) {
panelHandleRef.current.resize(minSize);
}
_setMinSize(minSizePct);
_setMinSize(minSize);
// Resize if the current size is smaller than the new min size - happens when the window is resized smaller
if (panelHandleRef.current.getSize() < minSizePct) {
panelHandleRef.current.resize(minSizePct);
}
});
resizeObserver.observe(panelGroupElement);
panelGroupHandleElements.forEach((el) => resizeObserver.observe(el));
return () => {
resizeObserver.disconnect();
@ -106,15 +163,34 @@ export const usePanel = (arg: UsePanelOptions) => {
panelHandleRef.current?.collapse();
}, []);
const resize = useCallback(
(size: number) => {
// If we are using percentages, we can just resize to the given size
if (arg.unit === 'percentages') {
panelHandleRef.current?.resize(size);
return;
}
// If we are using pixels, we need to calculate the size as a percentage of the available space
const sizeAsPct = getSizeAsPercentage(
size,
arg.panelGroupRef,
arg.panelGroupDirection
);
panelHandleRef.current?.resize(sizeAsPct);
},
[arg]
);
const reset = useCallback(() => {
// If the panel is really super close to the min size, collapse it
const shouldCollapse =
Math.abs((panelHandleRef.current?.getSize() ?? 0) - _minSize) < 0.01;
if (shouldCollapse) {
if (Math.abs((panelHandleRef.current?.getSize() ?? 0) - _minSize) < 0.01) {
collapse();
} else {
panelHandleRef.current?.resize(_minSize);
return;
}
// Otherwise, resize to the min size
panelHandleRef.current?.resize(_minSize);
}, [_minSize, collapse]);
return {
@ -127,5 +203,45 @@ export const usePanel = (arg: UsePanelOptions) => {
toggle,
expand,
collapse,
resize,
};
};
/**
* For a desired size in pixels, calculates the size of the panel as a percentage of the available space.
* @param sizeInPixels The desired size of the panel in pixels.
* @param panelGroupHandleRef The ref to the panel group handle.
* @param panelGroupDirection The direction of the panel group.
* @returns The size of the panel as a percentage.
*/
const getSizeAsPercentage = (
sizeInPixels: number,
panelGroupHandleRef: RefObject<ImperativePanelGroupHandle>,
panelGroupDirection: Direction
) => {
if (!panelGroupHandleRef.current) {
// No panel group handle ref, so we can't calculate the size
return 0;
}
const id = panelGroupHandleRef.current.getId();
const panelGroupElement = getPanelGroupElement(id);
if (!panelGroupElement) {
// No panel group element, size is 0
return 0;
}
// The available space is the width/height of the panel group...
let availableSpace =
panelGroupDirection === 'horizontal'
? panelGroupElement.offsetWidth
: panelGroupElement.offsetHeight;
// ...minus the width/height of the resize handles
getResizeHandleElementsForGroup(id).forEach((el) => {
availableSpace -=
panelGroupDirection === 'horizontal' ? el.offsetWidth : el.offsetHeight;
});
// The final value is a percentage of the available space
return (sizeInPixels / availableSpace) * 100;
};