import classnames from 'classnames'; import PropTypes from 'prop-types'; import React, { useCallback, useEffect, useState } from 'react'; import { Icons } from '../Icons'; import { TooltipTrigger, TooltipContent, TooltipProvider, Tooltip } from '../Tooltip'; import { Separator } from '../Separator'; type StyleMap = { open: { left: { marginLeft: string }; right: { marginRight: string }; }; closed: { left: { marginLeft: string }; right: { marginRight: string }; }; }; const borderSize = 4; const collapsedWidth = 25; const closeIconWidth = 30; const gridHorizontalPadding = 10; const tabSpacerWidth = 2; const baseClasses = 'transition-all duration-300 ease-in-out bg-black border-black justify-start box-content flex flex-col'; const classesMap = { open: { left: `mr-1`, right: `ml-1`, }, closed: { left: `mr-2 items-end`, right: `ml-2 items-start`, }, }; 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-white bg-black', { 'hover:text-primary-active': !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-customblue-40': isActiveTab, rounded: isActiveTab, }); }; const createStyleMap = ( expandedWidth: number, borderSize: number, collapsedWidth: number ): StyleMap => { const collapsedHideWidth = expandedWidth - collapsedWidth - borderSize; return { open: { left: { marginLeft: '0px' }, right: { marginRight: '0px' }, }, closed: { left: { marginLeft: `-${collapsedHideWidth}px` }, right: { marginRight: `-${collapsedHideWidth}px` }, }, }; }; 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 = null, tabs, onOpen, expandedWidth = 280, onActiveTabIndexChange, }) => { const [panelOpen, setPanelOpen] = useState(activeTabIndexProp !== null); const [renderHeader, setRenderHeader] = useState(false); const [activeTabIndex, setActiveTabIndex] = useState(0); const styleMap = createStyleMap(expandedWidth, borderSize, collapsedWidth); const baseStyle = createBaseStyle(expandedWidth); const gridAvailableWidth = expandedWidth - closeIconWidth - gridHorizontalPadding; const gridWidth = getGridWidth(tabs.length, gridAvailableWidth); const openStatus = panelOpen ? 'open' : 'closed'; const style = Object.assign({}, styleMap[openStatus][side], baseStyle); const updatePanelOpen = useCallback( (panelOpen: boolean) => { setPanelOpen(panelOpen); if (panelOpen && onOpen) { onOpen(); } }, [onOpen] ); const updateActiveTabIndex = useCallback( (activeTabIndex: number) => { if (activeTabIndex === null) { updatePanelOpen(false); return; } setActiveTabIndex(activeTabIndex); updatePanelOpen(true); if (onActiveTabIndexChange) { onActiveTabIndexChange({ activeTabIndex }); } }, [onActiveTabIndexChange, updatePanelOpen] ); useEffect(() => { setRenderHeader(tabs.length === 1); }, [tabs]); useEffect(() => { updateActiveTabIndex(activeTabIndexProp); }, [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); }} > {React.createElement(Icons[childComponent.iconName] || Icons.MissingIcon, { className: classnames({ 'text-primary-active': 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-active', })}
); }; 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-active': true, 'ohif-disabled': disabled, }), style: { width: '22px', height: '22px', }, })}
{getToolTipContent(tab.label, disabled)}
); })}
); }; const getOpenStateComponent = () => { if (tabs.length === 1) { return null; } return ( <>
{getTabGridComponent()}
); }; return (
{panelOpen ? ( <> {getOpenStateComponent()} {tabs.map((tab, tabIndex) => { if (tabIndex === activeTabIndex) { return ( ); } return null; })} ) : ( {getCloseStateComponent()} )}
); }; SidePanel.propTypes = { side: PropTypes.oneOf(['left', 'right']).isRequired, className: PropTypes.string, activeTabIndex: PropTypes.number, tabs: PropTypes.oneOfType([ PropTypes.arrayOf( PropTypes.shape({ iconName: PropTypes.string.isRequired, iconLabel: PropTypes.string.isRequired, name: PropTypes.string.isRequired, label: PropTypes.string.isRequired, content: PropTypes.func, // TODO: Should be node, but it keeps complaining? }) ), ]), onOpen: PropTypes.func, onActiveTabIndexChange: PropTypes.func, expandedWidth: PropTypes.number, }; export { SidePanel };