feat(side-panels): Added resize handle interactive feedback for side panels (#4734)
This commit is contained in:
parent
fe658c6392
commit
6abb095b8a
@ -5,7 +5,7 @@ import { panelGroupDefinition } from './constants/panels';
|
||||
/**
|
||||
* Set the minimum and maximum css style width attributes for the given element.
|
||||
* The two style attributes are cleared whenever the width
|
||||
* arguments is undefined.
|
||||
* argument is undefined.
|
||||
* <p>
|
||||
* 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
|
||||
@ -34,7 +34,7 @@ const useResizablePanels = (
|
||||
);
|
||||
const [leftResizablePanelMinimumSize, setLeftResizablePanelMinimumSize] = useState(0);
|
||||
const [rightResizablePanelMinimumSize, setRightResizablePanelMinimumSize] = useState(0);
|
||||
const [leftResizeablePanelCollapsedSize, setLeftResizePanelCollapsedSize] = useState(0);
|
||||
const [leftResizablePanelCollapsedSize, setLeftResizePanelCollapsedSize] = useState(0);
|
||||
const [rightResizePanelCollapsedSize, setRightResizePanelCollapsedSize] = useState(0);
|
||||
|
||||
const resizablePanelGroupElemRef = useRef(null);
|
||||
@ -44,15 +44,16 @@ const useResizablePanels = (
|
||||
const resizableRightPanelAPIRef = useRef(null);
|
||||
const isResizableHandleDraggingRef = useRef(false);
|
||||
|
||||
// The total width of both handles.
|
||||
const resizableHandlesWidth = useRef(null);
|
||||
|
||||
// This useLayoutEffect is used to...
|
||||
// - Grab a reference to the various resizable panel elements needed for
|
||||
// converting between percentages and pixels in various callbacks.
|
||||
// - Expand those panels that are initially expanded.
|
||||
useLayoutEffect(() => {
|
||||
const panelGroupElem = getPanelGroupElement(panelGroupDefinition.groupId);
|
||||
|
||||
resizablePanelGroupElemRef.current = panelGroupElem;
|
||||
const { width: panelGroupWidth } = panelGroupElem.getBoundingClientRect();
|
||||
|
||||
const leftPanelElem = getPanelElement(panelGroupDefinition.left.panelId);
|
||||
resizableLeftPanelElemRef.current = leftPanelElem;
|
||||
@ -60,20 +61,29 @@ const useResizablePanels = (
|
||||
const rightPanelElem = getPanelElement(panelGroupDefinition.right.panelId);
|
||||
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),
|
||||
// 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
|
||||
|
||||
if (!leftPanelClosed) {
|
||||
const leftResizablePanelExpandedSize =
|
||||
(panelGroupDefinition.left.initialExpandedOffsetWidth / panelGroupWidth) * 100;
|
||||
const leftResizablePanelExpandedSize = getPercentageSize(
|
||||
panelGroupDefinition.left.initialExpandedOffsetWidth
|
||||
);
|
||||
resizableLeftPanelAPIRef?.current?.expand(leftResizablePanelExpandedSize);
|
||||
setMinMaxWidth(leftPanelElem, panelGroupDefinition.left.initialExpandedOffsetWidth);
|
||||
}
|
||||
|
||||
if (!rightPanelClosed) {
|
||||
const rightResizablePanelExpandedSize =
|
||||
(panelGroupDefinition.right.initialExpandedOffsetWidth / panelGroupWidth) * 100;
|
||||
const rightResizablePanelExpandedSize = getPercentageSize(
|
||||
panelGroupDefinition.right.initialExpandedOffsetWidth
|
||||
);
|
||||
resizableRightPanelAPIRef?.current?.expand(rightResizablePanelExpandedSize);
|
||||
setMinMaxWidth(rightPanelElem, panelGroupDefinition.right.initialExpandedOffsetWidth);
|
||||
}
|
||||
@ -89,103 +99,47 @@ const useResizablePanels = (
|
||||
// values whenever the resizable panel group is resized (e.g. whenever the
|
||||
// browser window is resized).
|
||||
useLayoutEffect(() => {
|
||||
const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect();
|
||||
|
||||
// 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
|
||||
// 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
|
||||
// updated which occurs only AFTER the render following a resize. And by virtue
|
||||
// of the dependency on the `resizablePanelDefaultSize` state, this code
|
||||
// is executed on the render following an update of the minimum percentage size
|
||||
// updated which occurs only AFTER the render following a browser window resize.
|
||||
// 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 sizes
|
||||
// for a panel.
|
||||
if (!resizableLeftPanelAPIRef.current.isCollapsed()) {
|
||||
const leftSize =
|
||||
((leftPanelExpandedWidth + panelGroupDefinition.shared.expandedInsideBorderSize) /
|
||||
panelGroupWidth) *
|
||||
100;
|
||||
const leftSize = getPercentageSize(
|
||||
leftPanelExpandedWidth + panelGroupDefinition.shared.expandedInsideBorderSize
|
||||
);
|
||||
resizableLeftPanelAPIRef.current.resize(leftSize);
|
||||
}
|
||||
|
||||
if (!resizableRightPanelAPIRef.current.isCollapsed()) {
|
||||
const rightSize =
|
||||
((rightPanelExpandedWidth + panelGroupDefinition.shared.expandedInsideBorderSize) /
|
||||
panelGroupWidth) *
|
||||
100;
|
||||
const rightSize = getPercentageSize(
|
||||
rightPanelExpandedWidth + panelGroupDefinition.shared.expandedInsideBorderSize
|
||||
);
|
||||
resizableRightPanelAPIRef.current.resize(rightSize);
|
||||
}
|
||||
|
||||
// This observer kicks in when the ViewportLayout resizable panel group
|
||||
// component is resized. This typically occurs when the browser window resizes.
|
||||
const observer = new ResizeObserver(() => {
|
||||
const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect();
|
||||
const minimumLeftSize =
|
||||
(panelGroupDefinition.left.minimumExpandedOffsetWidth / panelGroupWidth) * 100;
|
||||
const minimumRightSize =
|
||||
(panelGroupDefinition.right.minimumExpandedOffsetWidth / panelGroupWidth) * 100;
|
||||
const minimumLeftSize = getPercentageSize(
|
||||
panelGroupDefinition.left.minimumExpandedOffsetWidth
|
||||
);
|
||||
const minimumRightSize = getPercentageSize(
|
||||
panelGroupDefinition.right.minimumExpandedOffsetWidth
|
||||
);
|
||||
|
||||
// Set the new default and collapsed resizable panel sizes.
|
||||
setLeftResizablePanelMinimumSize(Math.min(50, minimumLeftSize));
|
||||
setRightResizablePanelMinimumSize(Math.min(50, minimumRightSize));
|
||||
// Set the new minimum and collapsed resizable panel sizes.
|
||||
setLeftResizablePanelMinimumSize(minimumLeftSize);
|
||||
setRightResizablePanelMinimumSize(minimumRightSize);
|
||||
setLeftResizePanelCollapsedSize(
|
||||
(panelGroupDefinition.left.collapsedOffsetWidth / panelGroupWidth) * 100
|
||||
getPercentageSize(panelGroupDefinition.left.collapsedOffsetWidth)
|
||||
);
|
||||
setRightResizePanelCollapsedSize(
|
||||
(panelGroupDefinition.right.collapsedOffsetWidth / panelGroupWidth) * 100
|
||||
getPercentageSize(panelGroupDefinition.right.collapsedOffsetWidth)
|
||||
);
|
||||
|
||||
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
|
||||
);
|
||||
setLeftPanelExpandedWidth(
|
||||
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
|
||||
);
|
||||
setRightPanelExpandedWidth(
|
||||
rightPanelOffsetWidth - panelGroupDefinition.shared.expandedInsideBorderSize
|
||||
);
|
||||
setMinMaxWidth(resizableRightPanelElemRef.current, rightPanelOffsetWidth);
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(resizablePanelGroupElemRef.current);
|
||||
@ -238,23 +192,26 @@ const useResizablePanels = (
|
||||
}, [setLeftPanelClosed]);
|
||||
|
||||
const onLeftPanelOpen = useCallback(() => {
|
||||
const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect();
|
||||
resizableLeftPanelAPIRef?.current?.expand(
|
||||
(panelGroupDefinition.left.initialExpandedOffsetWidth / panelGroupWidth) * 100
|
||||
getPercentageSize(panelGroupDefinition.left.initialExpandedOffsetWidth)
|
||||
);
|
||||
setLeftPanelClosed(false);
|
||||
}, [setLeftPanelClosed]);
|
||||
|
||||
const onLeftPanelResize = useCallback(size => {
|
||||
if (resizableLeftPanelAPIRef.current.isCollapsed()) {
|
||||
if (!resizablePanelGroupElemRef?.current || resizableLeftPanelAPIRef.current.isCollapsed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect();
|
||||
const newExpandedWidth =
|
||||
(size / 100) * panelGroupWidth - panelGroupDefinition.shared.expandedInsideBorderSize;
|
||||
|
||||
const newExpandedWidth = getExpandedPixelWidth(size);
|
||||
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(() => {
|
||||
@ -264,25 +221,49 @@ const useResizablePanels = (
|
||||
}, [setRightPanelClosed]);
|
||||
|
||||
const onRightPanelOpen = useCallback(() => {
|
||||
const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect();
|
||||
resizableRightPanelAPIRef?.current?.expand(
|
||||
(panelGroupDefinition.right.initialExpandedOffsetWidth / panelGroupWidth) * 100
|
||||
getPercentageSize(panelGroupDefinition.right.initialExpandedOffsetWidth)
|
||||
);
|
||||
setRightPanelClosed(false);
|
||||
}, [setRightPanelClosed]);
|
||||
|
||||
const onRightPanelResize = useCallback(size => {
|
||||
if (resizableRightPanelAPIRef?.current?.isCollapsed()) {
|
||||
if (!resizablePanelGroupElemRef?.current || resizableRightPanelAPIRef.current.isCollapsed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect();
|
||||
const newExpandedWidth =
|
||||
(size / 100) * panelGroupWidth - panelGroupDefinition.shared.expandedInsideBorderSize;
|
||||
|
||||
const newExpandedWidth = getExpandedPixelWidth(size);
|
||||
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 [
|
||||
{
|
||||
expandedWidth: leftPanelExpandedWidth,
|
||||
@ -308,7 +289,7 @@ const useResizablePanels = (
|
||||
minSize: leftResizablePanelMinimumSize,
|
||||
onResize: onLeftPanelResize,
|
||||
collapsible: true,
|
||||
collapsedSize: leftResizeablePanelCollapsedSize,
|
||||
collapsedSize: leftResizablePanelCollapsedSize,
|
||||
onCollapse: () => setLeftPanelClosed(true),
|
||||
onExpand: () => setLeftPanelClosed(false),
|
||||
ref: resizableLeftPanelAPIRef,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
const expandedInsideBorderSize = 4;
|
||||
const expandedInsideBorderSize = 0;
|
||||
const collapsedInsideBorderSize = 4;
|
||||
const collapsedOutsideBorderSize = 8;
|
||||
const collapsedOutsideBorderSize = 4;
|
||||
const collapsedWidth = 25;
|
||||
|
||||
const rightPanelInitialExpandedWidth = 280;
|
||||
|
||||
@ -9,6 +9,8 @@ import SidePanelWithServices from '../Components/SidePanelWithServices';
|
||||
import { Onboarding, ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@ohif/ui-next';
|
||||
import useResizablePanels from './ResizablePanelsHook';
|
||||
|
||||
const resizableHandleClassName = 'mt-[1px] bg-black';
|
||||
|
||||
function ViewerLayout({
|
||||
// From Extension Module Params
|
||||
extensionManager,
|
||||
@ -157,7 +159,7 @@ function ViewerLayout({
|
||||
<ResizableHandle
|
||||
onDragging={onHandleDragging}
|
||||
disabled={!leftPanelResizable}
|
||||
className="!w-0"
|
||||
className={resizableHandleClassName}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
@ -178,7 +180,7 @@ function ViewerLayout({
|
||||
<ResizableHandle
|
||||
onDragging={onHandleDragging}
|
||||
disabled={!rightPanelResizable}
|
||||
className="!w-0"
|
||||
className={resizableHandleClassName}
|
||||
/>
|
||||
<ResizablePanel {...resizableRightPanelProps}>
|
||||
<SidePanelWithServices
|
||||
|
||||
@ -27,7 +27,7 @@ const ResizableHandle = ({
|
||||
}) => (
|
||||
<ResizablePrimitive.PanelResizeHandle
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@ -136,7 +136,7 @@ const createStyleMap = (
|
||||
collapsedInsideBorderSize: number,
|
||||
collapsedOutsideBorderSize: number
|
||||
): StyleMap => {
|
||||
const collapsedHideWidth = expandedWidth - collapsedWidth - collapsedInsideBorderSize;
|
||||
const collapsedHideWidth = expandedWidth - collapsedWidth - collapsedOutsideBorderSize;
|
||||
|
||||
return {
|
||||
open: {
|
||||
@ -146,11 +146,11 @@ const createStyleMap = (
|
||||
closed: {
|
||||
left: {
|
||||
marginLeft: `-${collapsedHideWidth}px`,
|
||||
marginRight: `${collapsedOutsideBorderSize}px`,
|
||||
marginRight: `${collapsedInsideBorderSize}px`,
|
||||
alignItems: `flex-end`,
|
||||
},
|
||||
right: {
|
||||
marginLeft: `${collapsedOutsideBorderSize}px`,
|
||||
marginLeft: `${collapsedInsideBorderSize}px`,
|
||||
marginRight: `-${collapsedHideWidth}px`,
|
||||
alignItems: `flex-start`,
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user