import classnames from 'classnames'; import React, { useCallback, useEffect, useState } from 'react'; import { Icons } from '../Icons'; import { TooltipTrigger, TooltipContent, Tooltip } from '../Tooltip'; import { Separator } from '../Separator'; /** * SidePanel component properties. * Note that the component monitors changes to the various widths and border sizes and will resize dynamically * @property {boolean} isExpanded - boolean indicating if the side panel is expanded/open or collapsed * @property {number} expandedWidth - the width of this side panel when expanded not including any borders or margins * @property {number} collapsedWidth - the width of this side panel when collapsed not including any borders or margins * @property {number} expandedInsideBorderSize - the width of the space between the expanded side panel content and viewport grid * @property {number} collapsedInsideBorderSize - the width of the space between the collapsed side panel content and the viewport grid * @property {number} collapsedOutsideBorderSize - the width of the space between the collapsed side panel content and the edge of the browser window */ type SidePanelProps = { side: 'left' | 'right'; className: string; activeTabIndex: number; onOpen: () => void; onClose: () => void; onActiveTabIndexChange: () => void; isExpanded: boolean; expandedWidth: number; collapsedWidth: number; expandedInsideBorderSize: number; collapsedInsideBorderSize: number; collapsedOutsideBorderSize: number; tabs: any; }; type StyleMap = { open: { left: { marginLeft: string; // the space between the expanded/open left side panel and the browser window left edge marginRight: string; // the space between the expanded/open left side panel and the viewport grid }; right: { marginLeft: string; // the space between the expanded/open right side panel and the viewport grid marginRight: string; // the space between the expanded/open right side panel and the browser window right edge }; }; closed: { left: { marginLeft: string; // the space between the collapsed/closed left panel and the browser window left edge marginRight: string; // the space between the collapsed/closed left panel and the viewport grid alignItems: 'flex-end'; // the flexbox layout align-items property }; right: { marginLeft: string; // the space between the collapsed/closed right panel and the viewport grid marginRight: string; // the space between the collapsed/closed right panel and the browser window right edge alignItems: 'flex-start'; // the flexbox layout align-items property }; }; }; const closeIconWidth = 30; const gridHorizontalPadding = 10; const tabSpacerWidth = 2; const baseClasses = 'bg-black border-black justify-start box-content flex flex-col'; const openStateIconName = { left: 'SidePanelCloseLeft', right: 'SidePanelCloseRight', }; const getTabWidth = (numTabs: number) => { if (numTabs < 3) { return 68; } else { return 40; } }; const getGridWidth = (numTabs: number, gridAvailableWidth: number) => { const spacersWidth = (numTabs - 1) * tabSpacerWidth; const tabsWidth = getTabWidth(numTabs) * numTabs; if (gridAvailableWidth > tabsWidth + spacersWidth) { return tabsWidth + spacersWidth; } return gridAvailableWidth; }; const getNumGridColumns = (numTabs: number, gridWidth: number) => { if (numTabs === 1) { return 1; } // Start by calculating the number of tabs assuming each tab was accompanied by a spacer. const tabWidth = getTabWidth(numTabs); const numTabsWithOneSpacerEach = Math.floor(gridWidth / (tabWidth + tabSpacerWidth)); // But there is always one less spacer than tabs, so now check if an extra tab with one less spacer fits. if ( (numTabsWithOneSpacerEach + 1) * tabWidth + numTabsWithOneSpacerEach * tabSpacerWidth <= gridWidth ) { return numTabsWithOneSpacerEach + 1; } return numTabsWithOneSpacerEach; }; const getTabClassNames = ( numColumns: number, numTabs: number, tabIndex: number, isActiveTab: boolean, isTabDisabled: boolean ) => classnames('h-[28px] mb-[2px] cursor-pointer text-foreground bg-primary/10 hover:bg-primary/20', { 'hover:text-primary': !isActiveTab && !isTabDisabled, 'rounded-l': tabIndex % numColumns === 0, 'rounded-r': (tabIndex + 1) % numColumns === 0 || tabIndex === numTabs - 1, }); const getTabStyle = (numTabs: number) => { return { width: `${getTabWidth(numTabs)}px`, }; }; const getTabIconClassNames = (numTabs: number, isActiveTab: boolean) => { return classnames('h-full w-full flex items-center justify-center', { 'bg-primary/20': isActiveTab, rounded: isActiveTab, }); }; const createStyleMap = ( expandedWidth: number, expandedInsideBorderSize: number, collapsedWidth: number, collapsedInsideBorderSize: number, collapsedOutsideBorderSize: number ): StyleMap => { const collapsedHideWidth = expandedWidth - collapsedWidth - collapsedOutsideBorderSize; return { open: { left: { marginLeft: '0px', marginRight: `${expandedInsideBorderSize}px` }, right: { marginLeft: `${expandedInsideBorderSize}px`, marginRight: '0px' }, }, closed: { left: { marginLeft: `-${collapsedHideWidth}px`, marginRight: `${collapsedInsideBorderSize}px`, alignItems: `flex-end`, }, right: { marginLeft: `${collapsedInsideBorderSize}px`, marginRight: `-${collapsedHideWidth}px`, alignItems: `flex-start`, }, }, }; }; const getToolTipContent = (label: string, disabled: boolean) => { return ( <>
{label}
{disabled && (
{'Not available based on current context'}
)} ); }; const createBaseStyle = (expandedWidth: number) => { return { maxWidth: `${expandedWidth}px`, width: `${expandedWidth}px`, // To align the top of the side panel with the top of the viewport grid, use position relative and offset the // top by the same top offset as the viewport grid. Also adjust the height so that there is no overflow. position: 'relative', top: '0.2%', height: '99.8%', }; }; const SidePanel = ({ side, className, activeTabIndex: activeTabIndexProp, isExpanded, tabs, onOpen, onClose, onActiveTabIndexChange, expandedWidth = 280, collapsedWidth = 25, expandedInsideBorderSize = 4, collapsedInsideBorderSize = 8, collapsedOutsideBorderSize = 4, }: SidePanelProps) => { const [panelOpen, setPanelOpen] = useState(isExpanded); const [activeTabIndex, setActiveTabIndex] = useState(activeTabIndexProp ?? 0); const [styleMap, setStyleMap] = useState( createStyleMap( expandedWidth, expandedInsideBorderSize, collapsedWidth, collapsedInsideBorderSize, collapsedOutsideBorderSize ) ); const [baseStyle, setBaseStyle] = useState(createBaseStyle(expandedWidth)); const [gridAvailableWidth, setGridAvailableWidth] = useState( expandedWidth - closeIconWidth - gridHorizontalPadding ); const [gridWidth, setGridWidth] = useState(getGridWidth(tabs.length, gridAvailableWidth)); const openStatus = panelOpen ? 'open' : 'closed'; const style = Object.assign({}, styleMap[openStatus][side], baseStyle); const updatePanelOpen = useCallback( (isOpen: boolean) => { setPanelOpen(isOpen); if (isOpen !== panelOpen) { // only fire events for changes if (isOpen && onOpen) { onOpen(); } else if (onClose && !isOpen) { onClose(); } } }, [panelOpen, onOpen, onClose] ); const updateActiveTabIndex = useCallback( (activeTabIndex: number, forceOpen: boolean = false) => { if (forceOpen) { updatePanelOpen(true); } setActiveTabIndex(activeTabIndex); if (onActiveTabIndexChange) { onActiveTabIndexChange({ activeTabIndex }); } }, [onActiveTabIndexChange, updatePanelOpen] ); useEffect(() => { updatePanelOpen(isExpanded); }, [isExpanded, updatePanelOpen]); useEffect(() => { setStyleMap( createStyleMap( expandedWidth, expandedInsideBorderSize, collapsedWidth, collapsedInsideBorderSize, collapsedOutsideBorderSize ) ); setBaseStyle(createBaseStyle(expandedWidth)); const gridAvailableWidth = expandedWidth - closeIconWidth - gridHorizontalPadding; setGridAvailableWidth(gridAvailableWidth); setGridWidth(getGridWidth(tabs.length, gridAvailableWidth)); }, [ collapsedInsideBorderSize, collapsedWidth, expandedWidth, expandedInsideBorderSize, tabs.length, collapsedOutsideBorderSize, ]); useEffect(() => { updateActiveTabIndex(activeTabIndexProp ?? 0); }, [activeTabIndexProp, updateActiveTabIndex]); const getCloseStateComponent = () => { const _childComponents = Array.isArray(tabs) ? tabs : [tabs]; return ( <>
{ updatePanelOpen(!panelOpen); }} data-cy={`side-panel-header-${side}`} >
{_childComponents.map((childComponent, index) => (
{ return childComponent.disabled ? null : updateActiveTabIndex(index, true); }} > {React.createElement(Icons[childComponent.iconName] || Icons.MissingIcon, { className: classnames({ 'text-primary': true, 'ohif-disabled': childComponent.disabled, }), style: { width: '22px', height: '22px', }, })}
{getToolTipContent(childComponent.label, childComponent.disabled)}
))}
); }; const getCloseIcon = () => { return (
{ updatePanelOpen(!panelOpen); }} data-cy={`side-panel-header-${side}`} > {React.createElement(Icons[openStateIconName[side]] || Icons.MissingIcon, { className: 'text-primary', })}
); }; const getTabGridComponent = () => { const numCols = getNumGridColumns(tabs.length, gridWidth); return ( <> {getCloseIcon()}
{tabs.map((tab, tabIndex) => { const { disabled } = tab; return ( {tabIndex % numCols !== 0 && (
)}
{ return disabled ? null : updateActiveTabIndex(tabIndex); }} data-cy={`${tab.name}-btn`} >
{React.createElement(Icons[tab.iconName] || Icons.MissingIcon, { className: classnames({ 'text-primary': true, 'ohif-disabled': disabled, }), style: { width: '22px', height: '22px', }, })}
{getToolTipContent(tab.label, disabled)}
); })}
); }; const getOneTabComponent = () => { return (
updatePanelOpen(!panelOpen)} > {getCloseIcon()} {tabs[0].label}
); }; const getOpenStateComponent = () => { return ( <>
{tabs.length === 1 ? getOneTabComponent() : getTabGridComponent()}
); }; return (
{panelOpen ? ( <> {getOpenStateComponent()} {tabs.map((tab, tabIndex) => { if (tabIndex === activeTabIndex) { return ; } return null; })} ) : ( {getCloseStateComponent()} )}
); }; export { SidePanel };