Merge pull request #1803 from OHIF/feat/ui-v2-Dropdown

Feat/ui v2 dropdown
This commit is contained in:
Igor Octaviano 2020-06-26 14:10:14 -03:00 committed by GitHub
commit 2c0f7bb9f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 359 additions and 78 deletions

View File

@ -28,6 +28,8 @@
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "^0.50.0", "@ohif/core": "^0.50.0",
"@ohif/i18n": "^0.52.8",
"react-i18next": "^10.11.0",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^16.13.1", "react": "^16.13.1",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",

View File

@ -1,22 +1,37 @@
import React from 'react'; import React, { useCallback } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
// TODO: This may fail if package is split from PWA build // TODO: This may fail if package is split from PWA build
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
// //
import { NavBar, Svg, Icon, IconButton } from '@ohif/ui'; import { NavBar, Svg, Icon, IconButton, Dropdown, useModal } from '@ohif/ui';
function Header({ children }) { function Header({ children }) {
const { t } = useTranslation();
const history = useHistory(); const history = useHistory();
// const dropdownContent = [ const { show } = useModal();
// {
// name: 'Soft tissue', // TODO: IT SHOULD BE REFACTORED WHEN THE MODAL CONTENT IS DEFINED
// value: '400/40', const showAboutModal = useCallback(() => {
// }, const modalComponent = () => (
// { name: 'Lung', value: '1500 / -600' }, <div>{t('AboutModal:OHIF Viewer - About')}</div>
// { name: 'Liver', value: '150 / 90' }, );
// { name: 'Bone', value: '2500 / 480' }, show({
// { name: 'Brain', value: '80 / 40' }, title: t('AboutModal:OHIF Viewer - About'),
// ] content: modalComponent,
});
}, [show, t]);
// TODO: IT SHOULD BE REFACTORED WHEN THE MODAL CONTENT IS DEFINED
const showPreferencesModal = useCallback(() => {
const modalComponent = () => (
<div>{t('UserPreferencesModal:User Preferences')}</div>
);
show({
title: t('UserPreferencesModal:User Preferences'),
content: modalComponent,
});
}, [show, t]);
return ( return (
<NavBar className="justify-between border-b-4 border-black"> <NavBar className="justify-between border-b-4 border-black">
@ -40,18 +55,40 @@ function Header({ children }) {
<div className="flex items-center">{children}</div> <div className="flex items-center">{children}</div>
<div className="flex items-center"> <div className="flex items-center">
<span className="mr-3 text-lg text-common-light"> <span className="mr-3 text-lg text-common-light">
FOR INVESTIGATIONAL USE ONLY {t('Header:INVESTIGATIONAL USE ONLY')}
</span> </span>
<IconButton <Dropdown
variant="text" showDropdownIcon={false}
color="inherit" list={[
className="text-primary-active" {
onClick={() => {}} title: t('Header:About'),
icon: 'info',
onClick: showAboutModal,
},
{
title: t('Header:Preferences'),
icon: 'settings',
onClick: showPreferencesModal,
},
]}
> >
<React.Fragment> <IconButton
<Icon name="settings" /> <Icon name="chevron-down" /> variant="text"
</React.Fragment> color="inherit"
</IconButton> size="initial"
className="text-primary-active"
>
<Icon name="settings" />
</IconButton>
<IconButton
variant="text"
color="inherit"
size="initial"
className="text-primary-active"
>
<Icon name="chevron-down" />
</IconButton>
</Dropdown>
</div> </div>
</div> </div>
</NavBar> </NavBar>

View File

@ -31,6 +31,7 @@ export {
ButtonGroup, ButtonGroup,
DateRange, DateRange,
Dialog, Dialog,
Dropdown,
EmptyStudies, EmptyStudies,
ExpandableToolbarButton, ExpandableToolbarButton,
ListMenu, ListMenu,

View File

@ -2,8 +2,6 @@
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 32 32" viewBox="0 0 32 32"
aria-labelledby="title" aria-labelledby="title"
width="1em"
height="1em"
fill="currentColor" fill="currentColor"
> >
<path d="m5.364 28.917c1.047 1.046 2.422 1.57 3.797 1.57 1.376 0 2.751-0.523 3.798-1.572l7.552-7.554c1.016-1.014 1.574-2.361 1.574-3.797 -0.001-1.432-0.558-2.781-1.574-3.796l-2.297-2.298 -2.009 2.009 2.297 2.298c0.478 0.478 0.741 1.113 0.741 1.788 0 0.674-0.264 1.309-0.741 1.786l-7.553 7.555c-0.987 0.985-2.59 0.985-3.576 0l-2.297-2.298c-0.987-0.985-0.987-2.589 0-3.576l3.775-3.776 -2.009-2.009 -3.776 3.776c-2.094 2.096-2.092 5.502 0 7.595l2.299 2.3zM26.695 2.992" /> <path d="m5.364 28.917c1.047 1.046 2.422 1.57 3.797 1.57 1.376 0 2.751-0.523 3.798-1.572l7.552-7.554c1.016-1.014 1.574-2.361 1.574-3.797 -0.001-1.432-0.558-2.781-1.574-3.796l-2.297-2.298 -2.009 2.009 2.297 2.298c0.478 0.478 0.741 1.113 0.741 1.788 0 0.674-0.264 1.309-0.741 1.786l-7.553 7.555c-0.987 0.985-2.59 0.985-3.576 0l-2.297-2.298c-0.987-0.985-0.987-2.589 0-3.576l3.775-3.776 -2.009-2.009 -3.776 3.776c-2.094 2.096-2.092 5.502 0 7.595l2.299 2.3zM26.695 2.992" />

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -2,8 +2,6 @@
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
aria-labelledby="unlink" aria-labelledby="unlink"
viewBox="0 0 512 512" viewBox="0 0 512 512"
width="1em"
height="1em"
fill="currentColor" fill="currentColor"
> >
<title id="title">Unlink</title> <title id="title">Unlink</title>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,119 @@
import React, { useEffect, useCallback, useState, useRef } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Icon, Typography } from '@ohif/ui';
const Dropdown = ({ children, showDropdownIcon, list }) => {
const [open, setOpen] = useState(false);
const element = useRef(null);
const DropdownItem = useCallback(({ title, icon, onClick }) => {
return (
<div
key={title}
className={classnames(
'flex px-4 py-2 cursor-pointer items-center transition duration-300 hover:bg-secondary-main border-b last:border-b-0 border-secondary-main'
)}
onClick={() => {
setOpen(false);
onClick();
}}
>
{!!icon && <Icon name={icon} className="text-white w-4 mr-2" />}
<Typography>{title}</Typography>
</div>
);
}, []);
DropdownItem.defaultProps = {
icon: '',
};
DropdownItem.propTypes = {
title: PropTypes.string.isRequired,
icon: PropTypes.string,
onClick: PropTypes.func.isRequired,
};
const renderTitleElement = () => {
return (
<div className="flex text-white items-center">
{children}
{showDropdownIcon && (
<Icon name="chevron-down" className="text-white ml-1" />
)}
</div>
);
};
const toggleList = () => {
setOpen(s => !s);
};
const handleClick = e => {
if (element.current && !element.current.contains(e.target)) {
setOpen(false);
}
};
const renderList = () => {
return (
<div
className={classnames(
'absolute top-100 right-0 mt-2 z-10 origin-top-right transition duration-300 transform bg-primary-dark border border-secondary-main rounded shadow',
{
'scale-0': !open,
'scale-100': open,
}
)}
>
{list.map((item, idx) => (
<DropdownItem
title={item.title}
icon={item.icon}
onClick={item.onClick}
key={idx}
/>
))}
</div>
);
};
useEffect(() => {
document.addEventListener('click', handleClick);
if (!open) {
document.removeEventListener('click', handleClick);
}
}, [open]);
return (
<div data-cy="dropdown" ref={element} className="relative">
<div className="cursor-pointer flex items-center" onClick={toggleList}>
{renderTitleElement()}
</div>
{renderList()}
</div>
);
};
Dropdown.defaultProps = {
showDropdownIcon: true,
};
Dropdown.propTypes = {
children: PropTypes.node.isRequired,
showDropdownIcon: PropTypes.bool,
/** Items to render in the select's drop down */
list: PropTypes.arrayOf(
PropTypes.shape({
title: PropTypes.string.isRequired,
icon: PropTypes.string,
onClick: PropTypes.func.isRequired,
})
).isRequired,
};
export default Dropdown;

View File

@ -0,0 +1,68 @@
---
name: Dropdown
menu: General
route: components/dropdown
---
import { Playground, Props } from 'docz';
import { Dropdown, IconButton, Icon } from '@ohif/ui';
# Dropdown
This component is used when there are more than a few options to choose from. By
hovering or clicking on the trigger, a dropdown menu will appear, which allows
you to choose an option and execute the relevant action.
## Import
```javascript
import { Dropdown } from '@ohif/ui';
```
<Playground>
{() => {
const handleClick = () => {
alert("Clicked");
}
return (
<div className="py-4">
<Dropdown
showDropdownIcon={false}
list={[
{
title: 'Option 1',
icon: 'info',
onClick: handleClick,
},
{
title: 'Option 2',
icon: 'settings',
onClick: handleClick,
},
]}
>
<IconButton
variant="text"
color="inherit"
size="initial"
className="text-primary-active"
>
<Icon name="settings" />
</IconButton>
<IconButton
variant="text"
color="inherit"
size="initial"
className="text-primary-active"
>
<Icon name="chevron-down" />
</IconButton>
</Dropdown>
</div>
);
}}
</Playground>
## Properties
<Props of={Dropdown} />

View File

@ -0,0 +1 @@
export { default } from './Dropdown';

View File

@ -8,7 +8,7 @@ const NavBar = ({ className, children, isSticky }) => {
return ( return (
<div <div
className={classnames( className={classnames(
'flex flex-row items-center bg-secondary-dark px-3 py-1 border-b-4 border-black', 'flex flex-row items-center bg-secondary-dark px-3 py-1 min-h-16 border-b-4 border-black',
isSticky && stickyClasses, isSticky && stickyClasses,
className className
)} )}

View File

@ -2,6 +2,7 @@ import Button from './Button';
import ButtonGroup from './ButtonGroup'; import ButtonGroup from './ButtonGroup';
import DateRange from './DateRange'; import DateRange from './DateRange';
import Dialog from './Dialog'; import Dialog from './Dialog';
import Dropdown from './Dropdown';
import EmptyStudies from './EmptyStudies'; import EmptyStudies from './EmptyStudies';
import Icon from './Icon'; import Icon from './Icon';
import IconButton from './IconButton'; import IconButton from './IconButton';
@ -54,6 +55,7 @@ export {
ButtonGroup, ButtonGroup,
DateRange, DateRange,
Dialog, Dialog,
Dropdown,
EmptyStudies, EmptyStudies,
ExpandableToolbarButton, ExpandableToolbarButton,
ListMenu, ListMenu,

View File

@ -1,12 +1,13 @@
// External // External
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import i18n from '@ohif/i18n';
import { I18nextProvider } from 'react-i18next';
import { BrowserRouter, HashRouter } from 'react-router-dom'; import { BrowserRouter, HashRouter } from 'react-router-dom';
import { import {
DialogProvider, DialogProvider,
Modal, Modal,
ModalProvider, ModalProvider,
Notification,
SnackbarProvider, SnackbarProvider,
ThemeWrapper, ThemeWrapper,
ViewportDialogProvider, ViewportDialogProvider,
@ -97,29 +98,31 @@ function App({ config, defaultExtensions }) {
return ( return (
<AppConfigProvider value={appConfigState}> <AppConfigProvider value={appConfigState}>
<Router basename={routerBasename}> <I18nextProvider i18n={i18n}>
<ThemeWrapper> <Router basename={routerBasename}>
<ViewportGridProvider <ThemeWrapper>
initialState={{ <ViewportGridProvider
numRows: 1, initialState={{
numCols: 1, numRows: 1,
viewports: [], numCols: 1,
activeViewportIndex: 0, viewports: [],
}} activeViewportIndex: 0,
reducer={viewportGridReducer} }}
> reducer={viewportGridReducer}
<ViewportDialogProvider service={UIViewportDialogService}> >
<SnackbarProvider service={UINotificationService}> <ViewportDialogProvider service={UIViewportDialogService}>
<DialogProvider service={UIDialogService}> <SnackbarProvider service={UINotificationService}>
<ModalProvider modal={Modal} service={UIModalService}> <DialogProvider service={UIDialogService}>
{appRoutes} <ModalProvider modal={Modal} service={UIModalService}>
</ModalProvider> {appRoutes}
</DialogProvider> </ModalProvider>
</SnackbarProvider> </DialogProvider>
</ViewportDialogProvider> </SnackbarProvider>
</ViewportGridProvider> </ViewportDialogProvider>
</ThemeWrapper> </ViewportGridProvider>
</Router> </ThemeWrapper>
</Router>
</I18nextProvider>
</AppConfigProvider> </AppConfigProvider>
); );
} }

View File

@ -0,0 +1,57 @@
import React from 'react';
import { Dropdown, IconButton, Icon, useModal } from '@ohif/ui';
const PreferencesDropdown = () => {
const { show } = useModal();
const showAboutModal = () => {
const modalComponent = () => <div>About modal</div>;
show({
content: modalComponent,
title: 'About',
});
};
const showPreferencesModal = () => {
const modalComponent = () => <div>Preferences modal</div>;
show({
content: modalComponent,
title: 'Preferences',
});
};
return (
<Dropdown
showDropdownIcon={false}
list={[
{ title: 'About', icon: 'info', onClick: showAboutModal },
{
title: 'Preferences',
icon: 'settings',
onClick: showPreferencesModal,
},
]}
>
<IconButton
variant="text"
color="inherit"
size="initial"
className="text-primary-active"
>
<Icon name="settings" />
</IconButton>
<IconButton
variant="text"
color="inherit"
size="initial"
className="text-primary-active"
onClick={() => {}}
>
<Icon name="chevron-down" />
</IconButton>
</Dropdown>
);
};
export default PreferencesDropdown;

View File

@ -0,0 +1 @@
export { default } from './PreferencesDropdown';

View File

@ -9,13 +9,14 @@ import filtersMeta from './filtersMeta.js';
import { useAppConfig } from '@state'; import { useAppConfig } from '@state';
import { useDebounce, useQuery } from '@hooks'; import { useDebounce, useQuery } from '@hooks';
import PreferencesDropdown from '../../components/PreferencesDropdown';
import { import {
Icon, Icon,
StudyListExpandedRow, StudyListExpandedRow,
Button, Button,
NavBar, NavBar,
Svg, Svg,
IconButton,
EmptyStudies, EmptyStudies,
StudyListTable, StudyListTable,
StudyListPagination, StudyListPagination,
@ -34,7 +35,10 @@ function WorkList({ history, data: studies, isLoadingData, dataSource }) {
// ~ Filters // ~ Filters
const query = useQuery(); const query = useQuery();
const queryFilterValues = _getQueryFilterValues(query); const queryFilterValues = _getQueryFilterValues(query);
const [filterValues, _setFilterValues] = useState({ ...defaultFilterValues, ...queryFilterValues }); const [filterValues, _setFilterValues] = useState({
...defaultFilterValues,
...queryFilterValues,
});
const debouncedFilterValues = useDebounce(filterValues, 200); const debouncedFilterValues = useDebounce(filterValues, 200);
const { resultsPerPage, pageNumber, sortBy, sortDirection } = filterValues; const { resultsPerPage, pageNumber, sortBy, sortDirection } = filterValues;
@ -211,8 +215,8 @@ function WorkList({ history, data: studies, isLoadingData, dataSource }) {
content: patientName ? ( content: patientName ? (
patientName patientName
) : ( ) : (
<span className="text-gray-700">(Empty)</span> <span className="text-gray-700">(Empty)</span>
), ),
title: patientName, title: patientName,
gridCol: 4, gridCol: 4,
}, },
@ -299,13 +303,13 @@ function WorkList({ history, data: studies, isLoadingData, dataSource }) {
seriesTableDataSource={ seriesTableDataSource={
seriesInStudiesMap.has(studyInstanceUid) seriesInStudiesMap.has(studyInstanceUid)
? seriesInStudiesMap.get(studyInstanceUid).map(s => { ? seriesInStudiesMap.get(studyInstanceUid).map(s => {
return { return {
description: s.description || '(empty)', description: s.description || '(empty)',
seriesNumber: s.seriesNumber || '', seriesNumber: s.seriesNumber || '',
modality: s.modality || '', modality: s.modality || '',
instances: s.numSeriesInstances || '', instances: s.numSeriesInstances || '',
}; };
}) })
: [] : []
} }
> >
@ -322,7 +326,7 @@ function WorkList({ history, data: studies, isLoadingData, dataSource }) {
<Link <Link
key={i} key={i}
to={`${mode.id}?StudyInstanceUIDs=${studyInstanceUid}`} to={`${mode.id}?StudyInstanceUIDs=${studyInstanceUid}`}
// to={`${mode.id}/dicomweb?StudyInstanceUIDs=${studyInstanceUid}`} // to={`${mode.id}/dicomweb?StudyInstanceUIDs=${studyInstanceUid}`}
> >
<Button <Button
rounded="full" rounded="full"
@ -364,17 +368,7 @@ function WorkList({ history, data: studies, isLoadingData, dataSource }) {
<span className="mr-3 text-lg text-common-light"> <span className="mr-3 text-lg text-common-light">
FOR INVESTIGATIONAL USE ONLY FOR INVESTIGATIONAL USE ONLY
</span> </span>
<IconButton <PreferencesDropdown />
variant="text"
color="inherit"
className="text-primary-active"
onClick={() => { }}
>
<React.Fragment>
<Icon name="settings" />
<Icon name="chevron-down" />
</React.Fragment>
</IconButton>
</div> </div>
</NavBar> </NavBar>
<StudyListFilter <StudyListFilter
@ -403,10 +397,10 @@ function WorkList({ history, data: studies, isLoadingData, dataSource }) {
/> />
</> </>
) : ( ) : (
<div className="flex flex-col items-center justify-center pt-48"> <div className="flex flex-col items-center justify-center pt-48">
<EmptyStudies isLoading={isLoadingData} /> <EmptyStudies isLoading={isLoadingData} />
</div> </div>
)} )}
</div> </div>
); );
} }