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 };