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

This commit is contained in:
Joe Boccanfuso 2023-09-22 15:36:44 -04:00 committed by GitHub
parent 48bbd6281a
commit 85c899b399
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 827 additions and 246 deletions

View File

@ -45,10 +45,10 @@ module.exports = (env, argv) => {
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
}),
// new MiniCssExtractPlugin({
// filename: `./dist/${outputName}.css`,
// chunkFilename: `./dist/${outputName}.css`,
// }),
new MiniCssExtractPlugin({
filename: `./dist/${outputName}.css`,
chunkFilename: `./dist/${outputName}.css`,
}),
],
});
};

View File

@ -0,0 +1,60 @@
import React, { useEffect, useState } from 'react';
import { SidePanel } from '@ohif/ui';
import { PanelService, ServicesManager } from '@ohif/core';
export type SidePanelWithServicesProps = {
servicesManager: ServicesManager;
side: 'left' | 'right';
className: string;
activeTabIndex: number;
tabs: any;
};
const SidePanelWithServices = ({
servicesManager,
side,
className,
activeTabIndex: activeTabIndexProp,
tabs,
}) => {
const panelService: PanelService = servicesManager?.services?.panelService;
// 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(false);
const [activeTabIndex, setActiveTabIndex] = useState(activeTabIndexProp);
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) {
setActiveTabIndex(tabIndex);
}
}
}
);
return () => {
activatePanelSubscription.unsubscribe();
};
}
}, [tabs, hasBeenOpened, panelService]);
return (
<SidePanel
side={side}
className={className}
activeTabIndex={activeTabIndex}
tabs={tabs}
onOpen={() => {
setHasBeenOpened(true);
}}
></SidePanel>
);
};
export default SidePanelWithServices;

View File

@ -5,6 +5,7 @@ import { SidePanel, ErrorBoundary, LoadingIndicatorProgress } from '@ohif/ui';
import { ServicesManager, HangingProtocolService, CommandsManager } from '@ohif/core';
import { useAppConfig } from '@state';
import ViewerHeader from './ViewerHeader';
import SidePanelWithServices from '../Components/SidePanelWithServices';
function ViewerLayout({
// From Extension Module Params
@ -119,7 +120,7 @@ function ViewerLayout({
{/* LEFT SIDEPANELS */}
{leftPanelComponents.length ? (
<ErrorBoundary context="Left Panel">
<SidePanel
<SidePanelWithServices
side="left"
activeTabIndex={leftPanelDefaultClosed ? null : 0}
tabs={leftPanelComponents}
@ -141,7 +142,7 @@ function ViewerLayout({
</div>
{rightPanelComponents.length ? (
<ErrorBoundary context="Right Panel">
<SidePanel
<SidePanelWithServices
side="right"
activeTabIndex={rightPanelDefaultClosed ? null : 0}
tabs={rightPanelComponents}

View File

@ -20,7 +20,7 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
return [
{
name: 'seriesList',
iconName: 'group-layers',
iconName: 'tab-studies',
iconLabel: 'Studies',
label: 'Studies',
component: WrappedPanelStudyBrowser.bind(null, {

View File

@ -9,7 +9,7 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager }):
return [
{
name: 'seriesList',
iconName: 'group-layers',
iconName: 'tab-studies',
iconLabel: 'Studies',
label: 'Studies',
component: PanelStudyBrowserTracking.bind(null, {

View File

@ -13,8 +13,12 @@ describe('OHIF Multi Study', () => {
it('Should display 2 comparison up', () => {
beforeSetup();
cy.get('[data-cy="viewport-pane"]').its('length').should('be.eq', 4);
cy.get('[data-cy="studyDate"]').should(studyDate => {
cy.get('[data-cy="viewport-pane"]').as('viewportPane');
cy.get('@viewportPane').its('length').should('be.eq', 4);
cy.get('[data-cy="studyDate"]').as('studyDate');
cy.get('@studyDate').should(studyDate => {
expect(studyDate.length).to.be.eq(4);
expect(studyDate.text()).to.contain('2014').contain('2001');
expect(studyDate.text().indexOf('2014')).to.be.lessThan(studyDate.text().indexOf('2001'));

View File

@ -23,26 +23,28 @@ describe('OHIF Measurement Panel', function () {
it('checks if measurement item can be Relabeled under Measurements panel', function () {
// Add length measurement
cy.addLengthMeasurement();
cy.get('[data-cy="viewport-notification"]').should('exist');
cy.get('[data-cy="viewport-notification"]').should('be.visible');
cy.get('[data-cy="prompt-begin-tracking-yes-btn"]').click();
cy.get('[data-cy="measurement-item"]').click();
cy.get('[data-cy="viewport-notification"]').as('viewportNotification').should('exist');
cy.get('[data-cy="viewport-notification"]').as('viewportNotification').should('be.visible');
cy.get('[data-cy="measurement-item"]').find('svg').click();
cy.get('[data-cy="prompt-begin-tracking-yes-btn"]').as('promptBeginTrackingYesBtn').click();
cy.get('[data-cy="measurement-item"]').as('measurementItem').click();
cy.get('[data-cy="measurement-item"]').find('svg').as('measurementItemSvg').click();
// enter Bone label
cy.get('[data-cy="input-annotation"]').should('exist');
cy.get('[data-cy="input-annotation"]').should('be.visible');
cy.get('[data-cy="input-annotation"]').type('Bone{enter}');
// Verify if 'Bone' label was added
cy.get('[data-cy="measurement-item"]').should('contain.text', 'Bone');
cy.get('[data-cy="measurement-item"]').as('measurementItem').should('contain.text', 'Bone');
});
it('checks if image would jump when clicked on a measurement item', function () {
// Add length measurement
cy.addLengthMeasurement();
cy.get('[data-cy="prompt-begin-tracking-yes-btn"]').click();
cy.get('[data-cy="prompt-begin-tracking-yes-btn"]').as('promptBeginTrackingYesBtn');
cy.get('@promptBeginTrackingYesBtn').click();
cy.scrollToIndex(13);

View File

@ -144,19 +144,22 @@ Cypress.Commands.add('drag', { prevSubject: 'element' }, (...args) =>
* @param {number[]} secondClick - Click position [x, y]
*/
Cypress.Commands.add('addLine', (viewport, firstClick, secondClick) => {
const performClick = (alias, x, y) => {
cy.get(alias).click(x, y, { force: true, multiple: true }).wait(250);
};
cy.get(viewport).as('viewportAlias');
const [x1, y1] = firstClick;
const [x2, y2] = secondClick;
// First click
cy.get('@viewportAlias').click(x1, y1, { force: true, multiple: true }).wait(250);
performClick('@viewportAlias', x1, y1);
// Move the mouse and then click again
cy.get('@viewportAlias')
.trigger('mousemove', { clientX: x2, clientY: y2 })
.get('@viewportAlias')
.click(x2, y2, { force: true, multiple: true })
.wait(250);
// Move the mouse
cy.get('@viewportAlias').trigger('mousemove', { clientX: x2, clientY: y2 }).wait(250);
// Second click
performClick('@viewportAlias', x2, y2);
});
/**

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

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

View File

@ -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" />

View File

@ -0,0 +1,2 @@
import LegacySidePanel from './LegacySidePanel';
export default LegacySidePanel;

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;

View File

@ -38,6 +38,7 @@ SidePanel is a component that renders the SidePanels.
args={{
side: 'left',
defaultComponentOpen: 'Example',
activeTabIndex: 0,
tabs: [
{
id: 'tab1',

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,

View File

@ -71,6 +71,7 @@ export {
LegacyButton,
LegacyButtonGroup,
LegacyCinePlayer,
LegacySidePanel,
LegacyViewportActionBar,
LoadingIndicatorProgress,
LoadingIndicatorTotalPercent,

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>

View File

@ -1523,6 +1523,16 @@
gl-matrix "^3.4.3"
lodash.clonedeep "4.5.0"
"@cornerstonejs/core@^1.16.6":
version "1.16.6"
resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-1.16.6.tgz#a216fc655f3c4b110196e63dcf129e02839d0890"
integrity sha512-ysd+t2N2KVk88TLYg0Nx174uYd47wQRBdFQZ4ApHhI/IHq5lY/qrP8prrtLR/DEtyLyUERIxDyGFRr+lEBZpxg==
dependencies:
"@kitware/vtk.js" "27.3.1"
detect-gpu "^5.0.22"
gl-matrix "^3.4.3"
lodash.clonedeep "4.5.0"
"@cornerstonejs/dicom-image-loader@^1.16.5":
version "1.16.5"
resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-1.16.5.tgz#23ca3e27e29b88cd05840c777578b37de60e5b87"
@ -1544,6 +1554,15 @@
dependencies:
"@cornerstonejs/core" "^1.16.5"
"@cornerstonejs/tools@^1.16.4":
version "1.16.6"
resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-1.16.6.tgz#0debc17fb9830288bff263bbf3a34cb940b726c2"
integrity sha512-qLjtEC0OGW+bpvvKXQPQs8TEE/ze+NIiBbg1qBSXo2vm7O3YO1DRWbWthJMVL7tIWYuVxZx5OsAI2WOkydQUOA==
dependencies:
"@cornerstonejs/core" "^1.16.6"
lodash.clonedeep "4.5.0"
lodash.get "^4.4.2"
"@cornerstonejs/tools@^1.16.5":
version "1.16.5"
resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-1.16.5.tgz#79948216521c591a02f395e2130d72e0b9347eca"