feat(SidePanel): new side panel tab look-and-feel (#3657)

This commit is contained in:
Joe Boccanfuso authored and GitHub committed 2023-09-22 15:36:44 -04:00
1 parent 48bbd6281a
commit 85c899b399
22 files changed
+827 -246

No files matched your search

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-panel-close-left</title>
<g id="Artboards-Updated" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Segmentation---assets" transform="translate(-370, -60)">
<g id="icon-panel-close-left" transform="translate(380, 70) scale(-1, 1) translate(-380, -70)translate(370, 60)">
<rect id="Rectangle" x="0" y="0" width="20" height="20"></rect>
<line x1="12.8336364" y1="10.4061473" x2="3" y2="10.4061473" id="Path" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" transform="translate(7.9168, 10.4061) scale(-1, 1) translate(-7.9168, -10.4061)"></line>
<polyline id="Path" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" transform="translate(10.7991, 10.4061) scale(-1, 1) translate(-10.7991, -10.4061)" points="12.8336364 6.33705636 8.76454545 10.4061473 12.8336364 14.4752382"></polyline>
<line x1="16.2418182" y1="14.4752382" x2="16.2418182" y2="6.33705636" id="Path" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" transform="translate(16.2418, 10.4061) scale(-1, 1) translate(-16.2418, -10.4061)"></line>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-panel-close-right</title>
<g id="Artboards-Updated" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Segmentation---assets" transform="translate(-418, -60)">
<g id="icon-panel-close-right" transform="translate(418, 60)">
<rect id="Rectangle" x="0" y="0" width="20" height="20"></rect>
<line x1="12.8336364" y1="10.4061473" x2="3" y2="10.4061473" id="Path" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" transform="translate(7.9168, 10.4061) scale(-1, 1) translate(-7.9168, -10.4061)"></line>
<polyline id="Path" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" transform="translate(10.7991, 10.4061) scale(-1, 1) translate(-10.7991, -10.4061)" points="12.8336364 6.33705636 8.76454545 10.4061473 12.8336364 14.4752382"></polyline>
<line x1="16.2418182" y1="14.4752382" x2="16.2418182" y2="6.33705636" id="Path" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" transform="translate(16.2418, 10.4061) scale(-1, 1) translate(-16.2418, -10.4061)"></line>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="22px" height="23px" viewBox="0 0 22 23" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>tab-studies</title>
<g id="Artboards-Updated" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Segmentation---assets" transform="translate(-61, -242)">
<g id="tab-studies" transform="translate(61, 242.5)">
<rect id="Rectangle" x="0" y="0" width="22" height="22"></rect>
<path d="M6.93478261,15.0652174 L3.23913043,15.0652174 C2.83091997,15.0652174 2.5,14.7342974 2.5,14.326087 L2.5,3.23913043 C2.5,2.83091997 2.83091997,2.5 3.23913043,2.5 L14.326087,2.5 C14.7342974,2.5 15.0652174,2.83091997 15.0652174,3.23913043 L15.0652174,6.93478261" id="Path" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"></path>
<rect id="Rectangle" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" x="6.93478261" y="6.93478261" width="12.5652174" height="12.5652174" rx="1"></rect>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -39,6 +39,8 @@ import profile from './../../assets/icons/profile.svg';
import pushLeft from './../../assets/icons/push-left.svg';
import pushRight from './../../assets/icons/push-right.svg';
import settings from './../../assets/icons/settings.svg';
import sidePanelCloseLeft from './../../assets/icons/side-panel-close-left.svg';
import sidePanelCloseRight from './../../assets/icons/side-panel-close-right.svg';
import sorting from './../../assets/icons/sorting.svg';
import sortingActiveDown from './../../assets/icons/sorting-active-down.svg';
import sortingActiveUp from './../../assets/icons/sorting-active-up.svg';
@@ -71,6 +73,7 @@ import tabLinear from './../../assets/icons/tab-linear.svg';
import tabPatientInfo from './../../assets/icons/tab-patient-info.svg';
import tabROIThreshold from './../../assets/icons/tab-roi-threshold.svg';
import tabSegmentation from './../../assets/icons/tab-segmentation.svg';
import tabStudies from './../../assets/icons/tab-studies.svg';
import uiArrowDown from './../../assets/icons/ui-arrow-down.svg';
import uiArrowUp from './../../assets/icons/ui-arrow-up.svg';
import uiArrowLeft from './../../assets/icons/ui-arrow-left.svg';
@@ -189,6 +192,8 @@ const ICONS = {
'push-left': pushLeft,
'push-right': pushRight,
settings: settings,
'side-panel-close-left': sidePanelCloseLeft,
'side-panel-close-right': sidePanelCloseRight,
'sorting-active-down': sortingActiveDown,
'sorting-active-up': sortingActiveUp,
'status-alert': statusAlert,
@@ -259,6 +264,7 @@ const ICONS = {
'tab-patient-info': tabPatientInfo,
'tab-roi-threshold': tabROIThreshold,
'tab-segmentation': tabSegmentation,
'tab-studies': tabStudies,
'ui-arrow-down': uiArrowDown,
'ui-arrow-up': uiArrowUp,
'ui-arrow-left': uiArrowLeft,
@@ -0,0 +1,376 @@
import classnames from 'classnames';
import PropTypes from 'prop-types';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import SwiperCore, { A11y, Controller, Navigation, Pagination, Scrollbar } from 'swiper';
import { Swiper, SwiperSlide } from 'swiper/react';
import { PanelService, ServicesManager, Types } from '@ohif/core';
import LegacyButton from '../LegacyButton';
import Icon from '../Icon';
import IconButton from '../IconButton';
import Tooltip from '../Tooltip';
import 'swiper/css';
import 'swiper/css/navigation';
import './style.css';
const borderSize = 4;
const expandedWidth = 248;
const collapsedWidth = 25;
const baseStyle = {
maxWidth: `${expandedWidth}px`,
width: `${expandedWidth}px`,
};
const collapsedHideWidth = expandedWidth - collapsedWidth - borderSize;
const styleMap = {
open: {
left: { marginLeft: '0px' },
right: { marginRight: '0px' },
},
closed: {
left: { marginLeft: `-${collapsedHideWidth}px` },
right: { marginRight: `-${collapsedHideWidth}px` },
},
};
const baseClasses =
'transition-all duration-300 ease-in-out h-100 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: 'push-left',
right: 'push-right',
};
const position = {
left: {
right: 5,
},
right: {
left: 5,
},
};
const LegacySidePanel = ({
servicesManager,
side,
className,
activeTabIndex: activeTabIndexProp,
tabs,
}) => {
const panelService: PanelService = servicesManager?.services?.panelService;
const { t } = useTranslation('LegacySidePanel');
// Tracks whether this LegacySidePanel has been opened at least once since this LegacySidePanel was inserted into the DOM.
// Thus going to the Study List page and back to the viewer resets this flag for a LegacySidePanel.
const [hasBeenOpened, setHasBeenOpened] = useState(activeTabIndexProp !== null);
const [panelOpen, setPanelOpen] = useState(activeTabIndexProp !== null);
const [activeTabIndex, setActiveTabIndex] = useState(activeTabIndexProp ?? 0);
const swiperRef = useRef() as any;
const [swiper, setSwiper] = useState<any>();
const prevRef = React.useRef();
const nextRef = React.useRef();
const openStatus = panelOpen ? 'open' : 'closed';
const style = Object.assign({}, styleMap[openStatus][side], baseStyle);
const ActiveComponent = tabs[activeTabIndex].content;
useEffect(() => {
if (panelOpen && swiper) {
swiper.slideTo(activeTabIndex, 500);
}
}, [panelOpen, swiper]);
useEffect(() => {
if (swiper) {
swiper.params.navigation.prevEl = prevRef.current;
swiper.params.navigation.nextEl = nextRef.current;
swiper.navigation.init();
swiper.navigation.update();
}
}, [swiper]);
const updatePanelOpen = useCallback((panelOpen: boolean) => {
setPanelOpen(panelOpen);
if (panelOpen) {
setHasBeenOpened(true);
}
}, []);
const updateActiveTabIndex = useCallback(
(activeTabIndex: number) => {
setActiveTabIndex(activeTabIndex);
updatePanelOpen(true);
},
[updatePanelOpen]
);
useEffect(() => {
if (panelService) {
const activatePanelSubscription = panelService.subscribe(
panelService.EVENTS.ACTIVATE_PANEL,
(activatePanelEvent: Types.ActivatePanelEvent) => {
if (!hasBeenOpened || activatePanelEvent.forceActive) {
const tabIndex = tabs.findIndex(tab => tab.id === activatePanelEvent.panelId);
if (tabIndex !== -1) {
updateActiveTabIndex(tabIndex);
}
}
}
);
return () => {
activatePanelSubscription.unsubscribe();
};
}
}, [tabs, hasBeenOpened, panelService, updateActiveTabIndex]);
const getCloseStateComponent = () => {
const _childComponents = Array.isArray(tabs) ? tabs : [tabs];
return (
<>
<div
className={classnames(
'bg-secondary-dark flex h-[28px] w-full cursor-pointer items-center rounded-md',
side === 'left' ? 'justify-end pr-2' : 'justify-start pl-2'
)}
onClick={() => {
updatePanelOpen(prev => !prev);
}}
data-cy={`side-panel-header-${side}`}
>
<Icon
name={'navigation-panel-right-reveal'}
className={classnames('text-primary-active', side === 'left' && 'rotate-180 transform')}
/>
</div>
<div className={classnames('mt-3 flex flex-col space-y-3')}>
{_childComponents.map((childComponent, index) => (
<Tooltip
position={side === 'left' ? 'right' : 'left'}
key={index}
content={`${childComponent.label}`}
className={classnames(
'flex items-center',
side === 'left' ? 'justify-end ' : 'justify-start '
)}
>
<IconButton
id={`${childComponent.name}-btn`}
variant="text"
color="inherit"
size="initial"
className="text-primary-active"
onClick={() => {
updateActiveTabIndex(index);
}}
>
<Icon
name={childComponent.iconName}
className="text-primary-active"
style={{
width: '22px',
height: '22px',
}}
/>
</IconButton>
</Tooltip>
))}
</div>
</>
);
};
return (
<div
className={classnames(className, baseClasses, classesMap[openStatus][side])}
style={style}
>
{panelOpen ? (
<React.Fragment>
{/** Panel Header with Arrow and Close Actions */}
<div
className={classnames(
'flex-static bg-primary-dark flex h-9 cursor-pointer px-[10px]',
tabs.length === 1 && 'mb-1'
)}
onClick={() => {
updatePanelOpen(prev => !prev);
// slideToActivePanel();
}}
data-cy={`side-panel-header-${side}`}
>
{/* TODO This should be redesigned to not be a button. */}
<LegacyButton
variant="text"
color="inherit"
border="none"
rounded="none"
className="flex-static relative flex w-full flex-row items-center px-3"
name={tabs.length === 1 ? `${tabs[activeTabIndex].name}` : ''}
>
<Icon
name={openStateIconName[side]}
className={classnames(
'text-primary-active absolute',
side === 'left' && 'order-last'
)}
style={{ ...position[side] }}
/>
{/* Todo: ass secondary label here */}
<span className="text-primary-active">
{tabs.length === 1 && (t(tabs[activeTabIndex].label) as string)}
</span>
</LegacyButton>
</div>
{tabs.length > 1 &&
_getMoreThanOneTabLayout(
swiperRef,
setSwiper,
prevRef,
nextRef,
tabs,
activeTabIndex,
updateActiveTabIndex
)}
{/** carousel navigation with the arrows */}
{/** only show carousel nav if tabs are more than 3 tabs */}
{tabs.length > 3 && (
<div className="text-primary-active bg-primary-dark flex w-full justify-end gap-2 py-1 px-2">
<button
ref={prevRef}
className="swiper-button-prev-custom"
>
<Icon
name={'icon-prev'}
className={classnames('text-primary-active')}
/>
</button>
<button
ref={nextRef}
className="swiper-button-next-custom"
>
<Icon
name={'icon-next'}
className={classnames('text-primary-active')}
/>
</button>
</div>
)}
<ActiveComponent />
</React.Fragment>
) : (
<React.Fragment>{getCloseStateComponent()}</React.Fragment>
)}
</div>
);
};
LegacySidePanel.defaultProps = {
defaultComponentOpen: null,
};
LegacySidePanel.propTypes = {
servicesManager: PropTypes.instanceOf(ServicesManager),
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?
})
),
]),
};
function _getMoreThanOneTabLayout(
swiperRef: any,
setSwiper: React.Dispatch<any>,
prevRef: React.MutableRefObject<undefined>,
nextRef: React.MutableRefObject<undefined>,
tabs: any,
activeTabIndex: any,
updateActiveTabIndex
) {
return (
<div
className="flex-static collapse-sidebar relative"
style={{
backgroundColor: '#06081f',
}}
>
<div className="w-full">
<Swiper
onInit={(core: SwiperCore) => {
swiperRef.current = core.el;
}}
simulateTouch={false}
modules={[Navigation, Pagination, Scrollbar, A11y, Controller]}
slidesPerView={3}
spaceBetween={5}
onSwiper={swiper => setSwiper(swiper)}
navigation={{
prevEl: prevRef?.current,
nextEl: nextRef?.current,
}}
>
{tabs.map((obj, index) => (
<SwiperSlide key={index}>
<div
className={classnames(
index === activeTabIndex ? 'bg-secondary-main text-white' : 'text-aqua-pale',
'flex cursor-pointer flex-col items-center justify-center rounded-[4px] px-4 py-1 text-center hover:text-white'
)}
key={index}
onClick={() => {
updateActiveTabIndex(index);
}}
data-cy={`${obj.name}-btn`}
>
<span>
<Icon
name={obj.iconName}
className={classnames(
index === activeTabIndex ? 'text-white' : 'text-primary-active'
)}
style={{
width: '22px',
height: '22px',
}}
/>
</span>
<span className="mt-[5px] select-none whitespace-nowrap text-[10px] font-medium">
{obj.label}
</span>
</div>
</SwiperSlide>
))}
</Swiper>
</div>
</div>
);
}
export default LegacySidePanel;
@@ -0,0 +1,74 @@
import LegacySidePanel from '../LegacySidePanel';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
import { createComponentTemplate } from '../../../storybook/functions/create-component-story';
export const argTypes = {
component: LegacySidePanel,
title: 'Components/LegacySidePanel',
};
<Meta
title="Components/LegacySidePanel"
component={LegacySidePanel}
/>
export const LegacySidePanelTemplate = args => (
<div className="w-96">
<LegacySidePanel {...args} />
</div>
);
<Heading
title="LegacySidePanel"
componentRelativePath="LegacySidePanel/LegacySidePanel.tsx"
/>
- [Overview](#overview)
- [Props](#props)
- [Contribute](#contribute)
## Overview
LegacySidePanel is a component that renders the LegacySidePanels.
<Canvas>
<Story
name="Overview"
args={{
side: 'left',
defaultComponentOpen: 'Example',
tabs: [
{
id: 'tab1',
name: 'tab1',
label: 'Tab 1',
iconName: 'group-layers',
iconLabel: 'group-layers',
content: () => <div class="text-primary-light p-3">Hello</div>,
},
],
childComponents: [
{
iconName: 'home',
iconLabel: 'launch-info',
name: 'Example',
label: 'home',
content: () => <div>Hello world</div>,
},
],
}}
>
{LegacySidePanelTemplate.bind({})}
</Story>
</Canvas>
## Props
<ArgsTable of={LegacySidePanel} />
## Usage
## Contribute
<Footer componentRelativePath="LegacySidePanel/__stories__/legacySidePanel.stories.mdx" />
@@ -0,0 +1,2 @@
import LegacySidePanel from './LegacySidePanel';
export default LegacySidePanel;
+205 -216
View File
@@ -1,28 +1,27 @@
import classnames from 'classnames';
import PropTypes from 'prop-types';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, { CSSProperties, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import SwiperCore, { A11y, Controller, Navigation, Pagination, Scrollbar } from 'swiper';
import { Swiper, SwiperSlide } from 'swiper/react';
import { PanelService, ServicesManager, Types } from '@ohif/core';
import LegacyButton from '../LegacyButton';
import Icon from '../Icon';
import IconButton from '../IconButton';
import Tooltip from '../Tooltip';
import 'swiper/css';
import 'swiper/css/navigation';
import './style.css';
const borderSize = 4;
const expandedWidth = 248;
const collapsedWidth = 25;
const closeIconWidth = 30;
const gridHorizontalPadding = 10;
const tabSpacerWidth = 2;
const gridAvailableWidth = expandedWidth - closeIconWidth - gridHorizontalPadding;
const baseStyle = {
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 collapsedHideWidth = expandedWidth - collapsedWidth - borderSize;
@@ -38,7 +37,7 @@ const styleMap = {
};
const baseClasses =
'transition-all duration-300 ease-in-out h-100 bg-black border-black justify-start box-content flex flex-col';
'transition-all duration-300 ease-in-out bg-black border-black justify-start box-content flex flex-col';
const classesMap = {
open: {
@@ -52,70 +51,110 @@ const classesMap = {
};
const openStateIconName = {
left: 'push-left',
right: 'push-right',
left: 'side-panel-close-left',
right: 'side-panel-close-right',
};
const position = {
left: {
right: 5,
},
right: {
left: 5,
},
const getTabWidth = (numTabs: number) => {
if (numTabs < 3) {
return 68;
} else {
return 40;
}
};
const SidePanel = ({
servicesManager,
side,
className,
activeTabIndex: activeTabIndexProp,
tabs,
}) => {
const panelService: PanelService = servicesManager?.services?.panelService;
const getGridWidth = (numTabs: number) => {
const spacersWidth = (numTabs - 1) * tabSpacerWidth;
const tabsWidth = getTabWidth(numTabs) * numTabs;
if (gridAvailableWidth > tabsWidth + spacersWidth) {
return tabsWidth + spacersWidth;
}
return gridAvailableWidth;
};
const getNumGridColumns = (numTabs: 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 gridWidth = getGridWidth(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 getGridStyle = (side: string, numTabs: number = 0): CSSProperties => {
const gridWidth = getGridWidth(numTabs);
const relativePosition = Math.max(0, Math.floor(expandedWidth - gridWidth) / 2 - closeIconWidth);
return {
position: 'relative',
...(side === 'left' ? { right: `${relativePosition}px` } : { left: `${relativePosition}px` }),
width: `${gridWidth}px`,
};
};
const getTabClassNames = (
numColumns: number,
numTabs: number,
tabIndex: number,
isActiveTab: boolean
) =>
classnames('h-[28px] mb-[2px] cursor-pointer text-white bg-black', {
'hover:text-primary-active': !isActiveTab,
'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 SidePanel = ({ side, className, activeTabIndex: activeTabIndexProp, tabs, onOpen }) => {
const { t } = useTranslation('SidePanel');
// Tracks whether this SidePanel has been opened at least once since this SidePanel was inserted into the DOM.
// Thus going to the Study List page and back to the viewer resets this flag for a SidePanel.
const [hasBeenOpened, setHasBeenOpened] = useState(activeTabIndexProp !== null);
const [panelOpen, setPanelOpen] = useState(activeTabIndexProp !== null);
const [activeTabIndex, setActiveTabIndex] = useState(activeTabIndexProp ?? 0);
const swiperRef = useRef() as any;
const [swiper, setSwiper] = useState<any>();
const prevRef = React.useRef();
const nextRef = React.useRef();
const [activeTabIndex, setActiveTabIndex] = useState(0);
const openStatus = panelOpen ? 'open' : 'closed';
const style = Object.assign({}, styleMap[openStatus][side], baseStyle);
const ActiveComponent = tabs[activeTabIndex].content;
useEffect(() => {
if (panelOpen && swiper) {
swiper.slideTo(activeTabIndex, 500);
}
}, [panelOpen, swiper]);
useEffect(() => {
if (swiper) {
swiper.params.navigation.prevEl = prevRef.current;
swiper.params.navigation.nextEl = nextRef.current;
swiper.navigation.init();
swiper.navigation.update();
}
}, [swiper]);
const ActiveComponent = tabs[activeTabIndex]?.content;
const updatePanelOpen = useCallback((panelOpen: boolean) => {
setPanelOpen(panelOpen);
if (panelOpen) {
setHasBeenOpened(true);
onOpen?.();
}
}, []);
const updateActiveTabIndex = useCallback(
(activeTabIndex: number) => {
if (activeTabIndex === null) {
updatePanelOpen(false);
return;
}
setActiveTabIndex(activeTabIndex);
updatePanelOpen(true);
},
@@ -123,24 +162,8 @@ const SidePanel = ({
);
useEffect(() => {
if (panelService) {
const activatePanelSubscription = panelService.subscribe(
panelService.EVENTS.ACTIVATE_PANEL,
(activatePanelEvent: Types.ActivatePanelEvent) => {
if (!hasBeenOpened || activatePanelEvent.forceActive) {
const tabIndex = tabs.findIndex(tab => tab.id === activatePanelEvent.panelId);
if (tabIndex !== -1) {
updateActiveTabIndex(tabIndex);
}
}
}
);
return () => {
activatePanelSubscription.unsubscribe();
};
}
}, [tabs, hasBeenOpened, panelService, updateActiveTabIndex]);
updateActiveTabIndex(activeTabIndexProp);
}, [activeTabIndexProp, updateActiveTabIndex]);
const getCloseStateComponent = () => {
const _childComponents = Array.isArray(tabs) ? tabs : [tabs];
@@ -172,12 +195,10 @@ const SidePanel = ({
side === 'left' ? 'justify-end ' : 'justify-start '
)}
>
<IconButton
<div
id={`${childComponent.name}-btn`}
variant="text"
color="inherit"
size="initial"
className="text-primary-active"
data-cy={`${childComponent.name}-btn`}
className="text-primary-active hover:cursor-pointer"
onClick={() => {
updateActiveTabIndex(index);
}}
@@ -190,7 +211,7 @@ const SidePanel = ({
height: '22px',
}}
/>
</IconButton>
</div>
</Tooltip>
))}
</div>
@@ -198,81 +219,116 @@ const SidePanel = ({
);
};
const getCloseIcon = () => {
return (
<div
className={classnames(
'flex h-[28px] cursor-pointer items-center justify-center',
side === 'left' ? 'order-last' : 'order-first'
)}
style={{ width: `${closeIconWidth}px` }}
onClick={() => {
updatePanelOpen(prev => !prev);
}}
data-cy={`side-panel-header-${side}`}
>
<Icon
name={openStateIconName[side]}
className="text-primary-active"
/>
</div>
);
};
const getTabGridComponent = () => {
const numCols = getNumGridColumns(tabs.length);
return (
<div className={classnames('flex grow ', side === 'right' ? 'justify-start' : 'justify-end')}>
<div
className={classnames('bg-primary-dark text-primary-active flex flex-wrap')}
style={getGridStyle(side, tabs.length)}
>
{tabs.map((tab, tabIndex) => {
return (
<React.Fragment key={tabIndex}>
{tabIndex % numCols !== 0 && (
<div
className={classnames(
'flex h-[28px] w-[2px] items-center bg-black',
tabSpacerWidth
)}
>
<div className="bg-primary-dark h-[20px] w-full"></div>
</div>
)}
<Tooltip
position={'bottom'}
key={tabIndex}
content={`${tab.label}`}
>
<div
className={getTabClassNames(
numCols,
tabs.length,
tabIndex,
tabIndex === activeTabIndex
)}
style={getTabStyle(tabs.length)}
onClick={() => updateActiveTabIndex(tabIndex)}
data-cy={`${tab.name}-btn`}
>
<div className={getTabIconClassNames(tabs.length, tabIndex === activeTabIndex)}>
<Icon name={tab.iconName}></Icon>
</div>
</div>
</Tooltip>
</React.Fragment>
);
})}
</div>
</div>
);
};
const getOneTabComponent = () => {
return (
<div
className={classnames(
'text-primary-active flex grow cursor-pointer justify-center self-center text-[13px]'
)}
style={{
...(side === 'left'
? { marginLeft: `${closeIconWidth}px` }
: { marginRight: `${closeIconWidth}px` }),
}}
data-cy={`${tabs[0].name}-btn`}
onClick={() => updatePanelOpen(prev => !prev)}
>
<span>{tabs[0].label}</span>
</div>
);
};
const getOpenStateComponent = () => {
return (
<div className="bg-primary-dark flex rounded-t pt-1.5 pb-[2px]">
{getCloseIcon()}
{tabs.length === 1 ? getOneTabComponent() : getTabGridComponent()}
</div>
);
};
return (
<div
className={classnames(className, baseClasses, classesMap[openStatus][side])}
style={style}
>
{panelOpen ? (
<React.Fragment>
{/** Panel Header with Arrow and Close Actions */}
<div
className={classnames('flex-static bg-primary-dark flex h-9 cursor-pointer px-[10px]')}
onClick={() => {
updatePanelOpen(prev => !prev);
// slideToActivePanel();
}}
data-cy={`side-panel-header-${side}`}
>
{/* TODO This should be redesigned to not be a button. */}
<LegacyButton
variant="text"
color="inherit"
border="none"
rounded="none"
className="flex-static relative flex w-full flex-row items-center px-3"
name={tabs.length === 1 ? `${tabs[activeTabIndex].name}` : ''}
>
<Icon
name={openStateIconName[side]}
className={classnames(
'text-primary-active absolute',
side === 'left' && 'order-last'
)}
style={{ ...position[side] }}
/>
{/* Todo: ass secondary label here */}
<span className="text-primary-active">
{tabs.length === 1 && (t(tabs[activeTabIndex].label) as string)}
</span>
</LegacyButton>
</div>
{tabs.length > 1 &&
_getMoreThanOneTabLayout(
swiperRef,
setSwiper,
prevRef,
nextRef,
tabs,
activeTabIndex,
updateActiveTabIndex
)}
{/** carousel navigation with the arrows */}
{/** only show carousel nav if tabs are more than 3 tabs */}
{tabs.length > 3 && (
<div className="text-primary-active bg-primary-dark flex w-full justify-end gap-2 py-1 px-2">
<button
ref={prevRef}
className="swiper-button-prev-custom"
>
<Icon
name={'icon-prev'}
className={classnames('text-primary-active')}
/>
</button>
<button
ref={nextRef}
className="swiper-button-next-custom"
>
<Icon
name={'icon-next'}
className={classnames('text-primary-active')}
/>
</button>
</div>
)}
<>
{getOpenStateComponent()}
<ActiveComponent />
</React.Fragment>
</>
) : (
<React.Fragment>{getCloseStateComponent()}</React.Fragment>
)}
@@ -282,10 +338,10 @@ const SidePanel = ({
SidePanel.defaultProps = {
defaultComponentOpen: null,
activeTabIndex: null, // the default is to close the side panel
};
SidePanel.propTypes = {
servicesManager: PropTypes.instanceOf(ServicesManager),
side: PropTypes.oneOf(['left', 'right']).isRequired,
className: PropTypes.string,
activeTabIndex: PropTypes.number,
@@ -300,74 +356,7 @@ SidePanel.propTypes = {
})
),
]),
onOpen: PropTypes.func,
};
function _getMoreThanOneTabLayout(
swiperRef: any,
setSwiper: React.Dispatch<any>,
prevRef: React.MutableRefObject<undefined>,
nextRef: React.MutableRefObject<undefined>,
tabs: any,
activeTabIndex: any,
updateActiveTabIndex
) {
return (
<div
className="flex-static collapse-sidebar relative"
style={{
backgroundColor: '#06081f',
}}
>
<div className="w-full">
<Swiper
onInit={(core: SwiperCore) => {
swiperRef.current = core.el;
}}
simulateTouch={false}
modules={[Navigation, Pagination, Scrollbar, A11y, Controller]}
slidesPerView={3}
spaceBetween={5}
onSwiper={swiper => setSwiper(swiper)}
navigation={{
prevEl: prevRef?.current,
nextEl: nextRef?.current,
}}
>
{tabs.map((obj, index) => (
<SwiperSlide key={index}>
<div
className={classnames(
index === activeTabIndex ? 'bg-secondary-main text-white' : 'text-aqua-pale',
'flex cursor-pointer flex-col items-center justify-center rounded-[4px] px-4 py-1 text-center hover:text-white'
)}
key={index}
onClick={() => {
updateActiveTabIndex(index);
}}
data-cy={`${obj.name}-btn`}
>
<span>
<Icon
name={obj.iconName}
className={classnames(
index === activeTabIndex ? 'text-white' : 'text-primary-active'
)}
style={{
width: '22px',
height: '22px',
}}
/>
</span>
<span className="mt-[5px] select-none whitespace-nowrap text-[10px] font-medium">
{obj.label}
</span>
</div>
</SwiperSlide>
))}
</Swiper>
</div>
</div>
);
}
export default SidePanel;
@@ -38,6 +38,7 @@ SidePanel is a component that renders the SidePanels.
args={{
side: 'left',
defaultComponentOpen: 'Example',
activeTabIndex: 0,
tabs: [
{
id: 'tab1',
+2
View File
@@ -74,6 +74,7 @@ import LoadingIndicatorProgress from './LoadingIndicatorProgress';
import LoadingIndicatorTotalPercent from './LoadingIndicatorTotalPercent';
import ViewportActionBar from './ViewportActionBar';
import ProgressLoadingBar from './ProgressLoadingBar';
import LegacySidePanel from './LegacySidePanel';
import PanelSection from './PanelSection';
import AdvancedToolbox from './AdvancedToolbox';
import InputDoubleRange from './InputDoubleRange';
@@ -117,6 +118,7 @@ export {
LegacyButton,
LegacyButtonGroup,
LegacyCinePlayer,
LegacySidePanel,
LegacyViewportActionBar,
LoadingIndicatorProgress,
LoadingIndicatorTotalPercent,
+1
View File
@@ -71,6 +71,7 @@ export {
LegacyButton,
LegacyButtonGroup,
LegacyCinePlayer,
LegacySidePanel,
LegacyViewportActionBar,
LoadingIndicatorProgress,
LoadingIndicatorTotalPercent,
+5 -5
View File
@@ -1,5 +1,5 @@
import React from 'react';
import { SidePanel, StudyBrowser } from '../../components';
import { LegacySidePanel, StudyBrowser } from '../../components';
import { DragAndDropProvider } from '../../contextProviders';
import Header from './components/Header';
@@ -13,7 +13,7 @@ const Viewer = () => {
className="flex w-full flex-1 flex-row flex-nowrap items-stretch overflow-hidden"
style={{ height: 'calc(100vh - 52px' }}
>
<SidePanel
<LegacySidePanel
side="left"
iconName="group-layers"
iconLabel="Studies"
@@ -21,12 +21,12 @@ const Viewer = () => {
defaultIsOpen={true}
>
<StudyBrowser />
</SidePanel>
</LegacySidePanel>
<div className="h-100 bg-primary-main flex flex-1 items-center justify-center overflow-hidden text-white">
{/* <ViewportToolbar /> */}
<div>CONTENT</div>
</div>
<SidePanel
<LegacySidePanel
side="right"
iconName="list-bullets"
iconLabel="Measure"
@@ -34,7 +34,7 @@ const Viewer = () => {
defaultIsOpen={false}
>
<div className="flex justify-center p-2 text-white">panel placeholder</div>
</SidePanel>
</LegacySidePanel>
</div>
</div>
</DragAndDropProvider>