feat(side-panels): Added resize handle interactive feedback for side panels (#4734)

This commit is contained in:
Joe Boccanfuso 2025-01-28 11:33:53 -05:00 committed by GitHub
parent fe658c6392
commit 6abb095b8a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 90 additions and 107 deletions

View File

@ -5,7 +5,7 @@ import { panelGroupDefinition } from './constants/panels';
/** /**
* Set the minimum and maximum css style width attributes for the given element. * Set the minimum and maximum css style width attributes for the given element.
* The two style attributes are cleared whenever the width * The two style attributes are cleared whenever the width
* arguments is undefined. * argument is undefined.
* <p> * <p>
* This utility is used as part of a HACK throughout the ViewerLayout component as * This utility is used as part of a HACK throughout the ViewerLayout component as
* the means of restricting the side panel widths during the resizing of the * the means of restricting the side panel widths during the resizing of the
@ -34,7 +34,7 @@ const useResizablePanels = (
); );
const [leftResizablePanelMinimumSize, setLeftResizablePanelMinimumSize] = useState(0); const [leftResizablePanelMinimumSize, setLeftResizablePanelMinimumSize] = useState(0);
const [rightResizablePanelMinimumSize, setRightResizablePanelMinimumSize] = useState(0); const [rightResizablePanelMinimumSize, setRightResizablePanelMinimumSize] = useState(0);
const [leftResizeablePanelCollapsedSize, setLeftResizePanelCollapsedSize] = useState(0); const [leftResizablePanelCollapsedSize, setLeftResizePanelCollapsedSize] = useState(0);
const [rightResizePanelCollapsedSize, setRightResizePanelCollapsedSize] = useState(0); const [rightResizePanelCollapsedSize, setRightResizePanelCollapsedSize] = useState(0);
const resizablePanelGroupElemRef = useRef(null); const resizablePanelGroupElemRef = useRef(null);
@ -44,15 +44,16 @@ const useResizablePanels = (
const resizableRightPanelAPIRef = useRef(null); const resizableRightPanelAPIRef = useRef(null);
const isResizableHandleDraggingRef = useRef(false); const isResizableHandleDraggingRef = useRef(false);
// The total width of both handles.
const resizableHandlesWidth = useRef(null);
// This useLayoutEffect is used to... // This useLayoutEffect is used to...
// - Grab a reference to the various resizable panel elements needed for // - Grab a reference to the various resizable panel elements needed for
// converting between percentages and pixels in various callbacks. // converting between percentages and pixels in various callbacks.
// - Expand those panels that are initially expanded. // - Expand those panels that are initially expanded.
useLayoutEffect(() => { useLayoutEffect(() => {
const panelGroupElem = getPanelGroupElement(panelGroupDefinition.groupId); const panelGroupElem = getPanelGroupElement(panelGroupDefinition.groupId);
resizablePanelGroupElemRef.current = panelGroupElem; resizablePanelGroupElemRef.current = panelGroupElem;
const { width: panelGroupWidth } = panelGroupElem.getBoundingClientRect();
const leftPanelElem = getPanelElement(panelGroupDefinition.left.panelId); const leftPanelElem = getPanelElement(panelGroupDefinition.left.panelId);
resizableLeftPanelElemRef.current = leftPanelElem; resizableLeftPanelElemRef.current = leftPanelElem;
@ -60,20 +61,29 @@ const useResizablePanels = (
const rightPanelElem = getPanelElement(panelGroupDefinition.right.panelId); const rightPanelElem = getPanelElement(panelGroupDefinition.right.panelId);
resizableRightPanelElemRef.current = rightPanelElem; resizableRightPanelElemRef.current = rightPanelElem;
// Calculate and set the width of both handles combined.
const resizeHandles = document.querySelectorAll('[data-panel-resize-handle-id]');
resizableHandlesWidth.current = 0;
resizeHandles.forEach(resizeHandle => {
resizableHandlesWidth.current += resizeHandle.offsetWidth;
});
// Since both resizable panels are collapsed by default (i.e. their default size is zero), // Since both resizable panels are collapsed by default (i.e. their default size is zero),
// on the very first render check if either/both side panels should be expanded. // on the very first render check if either/both side panels should be expanded.
// we use the initialExpandedOffsetWidth on the first render incase the panel has min width but we want the initial state to be larger than that // we use the initialExpandedOffsetWidth on the first render incase the panel has min width but we want the initial state to be larger than that
if (!leftPanelClosed) { if (!leftPanelClosed) {
const leftResizablePanelExpandedSize = const leftResizablePanelExpandedSize = getPercentageSize(
(panelGroupDefinition.left.initialExpandedOffsetWidth / panelGroupWidth) * 100; panelGroupDefinition.left.initialExpandedOffsetWidth
);
resizableLeftPanelAPIRef?.current?.expand(leftResizablePanelExpandedSize); resizableLeftPanelAPIRef?.current?.expand(leftResizablePanelExpandedSize);
setMinMaxWidth(leftPanelElem, panelGroupDefinition.left.initialExpandedOffsetWidth); setMinMaxWidth(leftPanelElem, panelGroupDefinition.left.initialExpandedOffsetWidth);
} }
if (!rightPanelClosed) { if (!rightPanelClosed) {
const rightResizablePanelExpandedSize = const rightResizablePanelExpandedSize = getPercentageSize(
(panelGroupDefinition.right.initialExpandedOffsetWidth / panelGroupWidth) * 100; panelGroupDefinition.right.initialExpandedOffsetWidth
);
resizableRightPanelAPIRef?.current?.expand(rightResizablePanelExpandedSize); resizableRightPanelAPIRef?.current?.expand(rightResizablePanelExpandedSize);
setMinMaxWidth(rightPanelElem, panelGroupDefinition.right.initialExpandedOffsetWidth); setMinMaxWidth(rightPanelElem, panelGroupDefinition.right.initialExpandedOffsetWidth);
} }
@ -89,103 +99,47 @@ const useResizablePanels = (
// values whenever the resizable panel group is resized (e.g. whenever the // values whenever the resizable panel group is resized (e.g. whenever the
// browser window is resized). // browser window is resized).
useLayoutEffect(() => { useLayoutEffect(() => {
const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect();
// Ensure the side panels' percentage size is in synch with the pixel width of the // Ensure the side panels' percentage size is in synch with the pixel width of the
// expanded side panels. In general the two get out-of-sync during a browser // expanded side panels. In general the two get out-of-sync during a browser
// window resize. Note that this code is here and NOT in the ResizeObserver // window resize. Note that this code is here and NOT in the ResizeObserver
// because it has to be done AFTER the minimum percentage size for a panel is // because it has to be done AFTER the minimum percentage size for a panel is
// updated which occurs only AFTER the render following a resize. And by virtue // updated which occurs only AFTER the render following a browser window resize.
// of the dependency on the `resizablePanelDefaultSize` state, this code // And by virtue of the dependency on the minimum size state variables, this code
// is executed on the render following an update of the minimum percentage size // is executed on the render following an update of the minimum percentage sizes
// for a panel. // for a panel.
if (!resizableLeftPanelAPIRef.current.isCollapsed()) { if (!resizableLeftPanelAPIRef.current.isCollapsed()) {
const leftSize = const leftSize = getPercentageSize(
((leftPanelExpandedWidth + panelGroupDefinition.shared.expandedInsideBorderSize) / leftPanelExpandedWidth + panelGroupDefinition.shared.expandedInsideBorderSize
panelGroupWidth) * );
100;
resizableLeftPanelAPIRef.current.resize(leftSize); resizableLeftPanelAPIRef.current.resize(leftSize);
} }
if (!resizableRightPanelAPIRef.current.isCollapsed()) { if (!resizableRightPanelAPIRef.current.isCollapsed()) {
const rightSize = const rightSize = getPercentageSize(
((rightPanelExpandedWidth + panelGroupDefinition.shared.expandedInsideBorderSize) / rightPanelExpandedWidth + panelGroupDefinition.shared.expandedInsideBorderSize
panelGroupWidth) * );
100;
resizableRightPanelAPIRef.current.resize(rightSize); resizableRightPanelAPIRef.current.resize(rightSize);
} }
// This observer kicks in when the ViewportLayout resizable panel group // This observer kicks in when the ViewportLayout resizable panel group
// component is resized. This typically occurs when the browser window resizes. // component is resized. This typically occurs when the browser window resizes.
const observer = new ResizeObserver(() => { const observer = new ResizeObserver(() => {
const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect(); const minimumLeftSize = getPercentageSize(
const minimumLeftSize =
(panelGroupDefinition.left.minimumExpandedOffsetWidth / panelGroupWidth) * 100;
const minimumRightSize =
(panelGroupDefinition.right.minimumExpandedOffsetWidth / panelGroupWidth) * 100;
// Set the new default and collapsed resizable panel sizes.
setLeftResizablePanelMinimumSize(Math.min(50, minimumLeftSize));
setRightResizablePanelMinimumSize(Math.min(50, minimumRightSize));
setLeftResizePanelCollapsedSize(
(panelGroupDefinition.left.collapsedOffsetWidth / panelGroupWidth) * 100
);
setRightResizePanelCollapsedSize(
(panelGroupDefinition.right.collapsedOffsetWidth / panelGroupWidth) * 100
);
if (
resizableLeftPanelAPIRef.current.isCollapsed() &&
resizableRightPanelAPIRef.current.isCollapsed()
) {
return;
}
// The code that follows is to handle cases when the group panel is resized to be
// too small to display either side panel at its current width.
// Determine the current widths of the two side panels.
let leftPanelOffsetWidth = resizableLeftPanelAPIRef.current.isCollapsed()
? panelGroupDefinition.left.collapsedOffsetWidth
: leftPanelExpandedWidth + panelGroupDefinition.shared.expandedInsideBorderSize;
let rightPanelOffsetWidth = resizableRightPanelAPIRef.current.isCollapsed()
? panelGroupDefinition.right.collapsedOffsetWidth
: rightPanelExpandedWidth + panelGroupDefinition.shared.expandedInsideBorderSize;
if (
!resizableLeftPanelAPIRef.current.isCollapsed() &&
leftPanelOffsetWidth + rightPanelOffsetWidth > panelGroupWidth
) {
// There is not enough space to show both panels at their pre-resize widths.
// Note that at this point, the viewport grid component is zero width.
// Reduce the left panel width so that both panels might fit.
leftPanelOffsetWidth = Math.max(
panelGroupWidth - rightPanelOffsetWidth,
panelGroupDefinition.left.minimumExpandedOffsetWidth panelGroupDefinition.left.minimumExpandedOffsetWidth
); );
setLeftPanelExpandedWidth( const minimumRightSize = getPercentageSize(
leftPanelOffsetWidth - panelGroupDefinition.shared.expandedInsideBorderSize
);
setMinMaxWidth(resizableLeftPanelElemRef.current, leftPanelOffsetWidth);
}
if (
!resizableRightPanelAPIRef.current.isCollapsed() &&
rightPanelOffsetWidth + leftPanelOffsetWidth > panelGroupWidth
) {
// There is not enough space to show both panels at their pre-resize widths.
// Note that at this point, the viewport grid component is zero width.
// Reduce the right panel width so that both panels might fit.
rightPanelOffsetWidth = Math.max(
panelGroupWidth - leftPanelOffsetWidth,
panelGroupDefinition.right.minimumExpandedOffsetWidth panelGroupDefinition.right.minimumExpandedOffsetWidth
); );
setRightPanelExpandedWidth(
rightPanelOffsetWidth - panelGroupDefinition.shared.expandedInsideBorderSize // Set the new minimum and collapsed resizable panel sizes.
setLeftResizablePanelMinimumSize(minimumLeftSize);
setRightResizablePanelMinimumSize(minimumRightSize);
setLeftResizePanelCollapsedSize(
getPercentageSize(panelGroupDefinition.left.collapsedOffsetWidth)
);
setRightResizePanelCollapsedSize(
getPercentageSize(panelGroupDefinition.right.collapsedOffsetWidth)
); );
setMinMaxWidth(resizableRightPanelElemRef.current, rightPanelOffsetWidth);
}
}); });
observer.observe(resizablePanelGroupElemRef.current); observer.observe(resizablePanelGroupElemRef.current);
@ -238,23 +192,26 @@ const useResizablePanels = (
}, [setLeftPanelClosed]); }, [setLeftPanelClosed]);
const onLeftPanelOpen = useCallback(() => { const onLeftPanelOpen = useCallback(() => {
const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect();
resizableLeftPanelAPIRef?.current?.expand( resizableLeftPanelAPIRef?.current?.expand(
(panelGroupDefinition.left.initialExpandedOffsetWidth / panelGroupWidth) * 100 getPercentageSize(panelGroupDefinition.left.initialExpandedOffsetWidth)
); );
setLeftPanelClosed(false); setLeftPanelClosed(false);
}, [setLeftPanelClosed]); }, [setLeftPanelClosed]);
const onLeftPanelResize = useCallback(size => { const onLeftPanelResize = useCallback(size => {
if (resizableLeftPanelAPIRef.current.isCollapsed()) { if (!resizablePanelGroupElemRef?.current || resizableLeftPanelAPIRef.current.isCollapsed()) {
return; return;
} }
const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect(); const newExpandedWidth = getExpandedPixelWidth(size);
const newExpandedWidth =
(size / 100) * panelGroupWidth - panelGroupDefinition.shared.expandedInsideBorderSize;
setLeftPanelExpandedWidth(newExpandedWidth); setLeftPanelExpandedWidth(newExpandedWidth);
if (!isResizableHandleDraggingRef.current) {
// This typically gets executed when the left panel is expanded via one of the UI
// buttons. It is done here instead of in the onLeftPanelOpen method
// because here we know the size of the expanded panel.
setMinMaxWidth(resizableLeftPanelElemRef.current, newExpandedWidth);
}
}, []); }, []);
const onRightPanelClose = useCallback(() => { const onRightPanelClose = useCallback(() => {
@ -264,25 +221,49 @@ const useResizablePanels = (
}, [setRightPanelClosed]); }, [setRightPanelClosed]);
const onRightPanelOpen = useCallback(() => { const onRightPanelOpen = useCallback(() => {
const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect();
resizableRightPanelAPIRef?.current?.expand( resizableRightPanelAPIRef?.current?.expand(
(panelGroupDefinition.right.initialExpandedOffsetWidth / panelGroupWidth) * 100 getPercentageSize(panelGroupDefinition.right.initialExpandedOffsetWidth)
); );
setRightPanelClosed(false); setRightPanelClosed(false);
}, [setRightPanelClosed]); }, [setRightPanelClosed]);
const onRightPanelResize = useCallback(size => { const onRightPanelResize = useCallback(size => {
if (resizableRightPanelAPIRef?.current?.isCollapsed()) { if (!resizablePanelGroupElemRef?.current || resizableRightPanelAPIRef.current.isCollapsed()) {
return; return;
} }
const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect(); const newExpandedWidth = getExpandedPixelWidth(size);
const newExpandedWidth =
(size / 100) * panelGroupWidth - panelGroupDefinition.shared.expandedInsideBorderSize;
setRightPanelExpandedWidth(newExpandedWidth); setRightPanelExpandedWidth(newExpandedWidth);
if (!isResizableHandleDraggingRef.current) {
// This typically gets executed when the right panel is expanded via one of the UI
// buttons. It is done here instead of in the onRightPanelOpen method
// because here we know the size of the expanded panel.
setMinMaxWidth(resizableRightPanelElemRef.current, newExpandedWidth);
}
}, []); }, []);
/**
* Gets the percentage size corresponding to the given pixel size.
* Note that the width attributed to the handles must be taken into account.
*/
const getPercentageSize = pixelSize => {
const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect();
return (pixelSize / (panelGroupWidth - resizableHandlesWidth.current)) * 100;
};
/**
* Gets the width in pixels for an expanded panel given its percentage size/width.
* Note that the width attributed to the handles must be taken into account.
*/
const getExpandedPixelWidth = percentageSize => {
const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect();
const expandedWidth =
(percentageSize / 100) * (panelGroupWidth - resizableHandlesWidth.current) -
panelGroupDefinition.shared.expandedInsideBorderSize;
return expandedWidth;
};
return [ return [
{ {
expandedWidth: leftPanelExpandedWidth, expandedWidth: leftPanelExpandedWidth,
@ -308,7 +289,7 @@ const useResizablePanels = (
minSize: leftResizablePanelMinimumSize, minSize: leftResizablePanelMinimumSize,
onResize: onLeftPanelResize, onResize: onLeftPanelResize,
collapsible: true, collapsible: true,
collapsedSize: leftResizeablePanelCollapsedSize, collapsedSize: leftResizablePanelCollapsedSize,
onCollapse: () => setLeftPanelClosed(true), onCollapse: () => setLeftPanelClosed(true),
onExpand: () => setLeftPanelClosed(false), onExpand: () => setLeftPanelClosed(false),
ref: resizableLeftPanelAPIRef, ref: resizableLeftPanelAPIRef,

View File

@ -1,6 +1,6 @@
const expandedInsideBorderSize = 4; const expandedInsideBorderSize = 0;
const collapsedInsideBorderSize = 4; const collapsedInsideBorderSize = 4;
const collapsedOutsideBorderSize = 8; const collapsedOutsideBorderSize = 4;
const collapsedWidth = 25; const collapsedWidth = 25;
const rightPanelInitialExpandedWidth = 280; const rightPanelInitialExpandedWidth = 280;

View File

@ -9,6 +9,8 @@ import SidePanelWithServices from '../Components/SidePanelWithServices';
import { Onboarding, ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@ohif/ui-next'; import { Onboarding, ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@ohif/ui-next';
import useResizablePanels from './ResizablePanelsHook'; import useResizablePanels from './ResizablePanelsHook';
const resizableHandleClassName = 'mt-[1px] bg-black';
function ViewerLayout({ function ViewerLayout({
// From Extension Module Params // From Extension Module Params
extensionManager, extensionManager,
@ -157,7 +159,7 @@ function ViewerLayout({
<ResizableHandle <ResizableHandle
onDragging={onHandleDragging} onDragging={onHandleDragging}
disabled={!leftPanelResizable} disabled={!leftPanelResizable}
className="!w-0" className={resizableHandleClassName}
/> />
</> </>
) : null} ) : null}
@ -178,7 +180,7 @@ function ViewerLayout({
<ResizableHandle <ResizableHandle
onDragging={onHandleDragging} onDragging={onHandleDragging}
disabled={!rightPanelResizable} disabled={!rightPanelResizable}
className="!w-0" className={resizableHandleClassName}
/> />
<ResizablePanel {...resizableRightPanelProps}> <ResizablePanel {...resizableRightPanelProps}>
<SidePanelWithServices <SidePanelWithServices

View File

@ -27,7 +27,7 @@ const ResizableHandle = ({
}) => ( }) => (
<ResizablePrimitive.PanelResizeHandle <ResizablePrimitive.PanelResizeHandle
className={cn( className={cn(
'bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90', "[&[data-resize-handle-state='hover']]:bg-primary/40 [&[data-resize-handle-state='drag']]:bg-primary/40 focus-visible:ring-ring relative flex w-1 items-center justify-center rounded-full after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className className
)} )}
{...props} {...props}

View File

@ -136,7 +136,7 @@ const createStyleMap = (
collapsedInsideBorderSize: number, collapsedInsideBorderSize: number,
collapsedOutsideBorderSize: number collapsedOutsideBorderSize: number
): StyleMap => { ): StyleMap => {
const collapsedHideWidth = expandedWidth - collapsedWidth - collapsedInsideBorderSize; const collapsedHideWidth = expandedWidth - collapsedWidth - collapsedOutsideBorderSize;
return { return {
open: { open: {
@ -146,11 +146,11 @@ const createStyleMap = (
closed: { closed: {
left: { left: {
marginLeft: `-${collapsedHideWidth}px`, marginLeft: `-${collapsedHideWidth}px`,
marginRight: `${collapsedOutsideBorderSize}px`, marginRight: `${collapsedInsideBorderSize}px`,
alignItems: `flex-end`, alignItems: `flex-end`,
}, },
right: { right: {
marginLeft: `${collapsedOutsideBorderSize}px`, marginLeft: `${collapsedInsideBorderSize}px`,
marginRight: `-${collapsedHideWidth}px`, marginRight: `-${collapsedHideWidth}px`,
alignItems: `flex-start`, alignItems: `flex-start`,
}, },