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": {
"@ohif/core": "^0.50.0",
"@ohif/i18n": "^0.52.8",
"react-i18next": "^10.11.0",
"prop-types": "^15.6.2",
"react": "^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 { useTranslation } from 'react-i18next';
// TODO: This may fail if package is split from PWA build
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 }) {
const { t } = useTranslation();
const history = useHistory();
// const dropdownContent = [
// {
// name: 'Soft tissue',
// value: '400/40',
// },
// { name: 'Lung', value: '1500 / -600' },
// { name: 'Liver', value: '150 / 90' },
// { name: 'Bone', value: '2500 / 480' },
// { name: 'Brain', value: '80 / 40' },
// ]
const { show } = useModal();
// TODO: IT SHOULD BE REFACTORED WHEN THE MODAL CONTENT IS DEFINED
const showAboutModal = useCallback(() => {
const modalComponent = () => (
<div>{t('AboutModal:OHIF Viewer - About')}</div>
);
show({
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 (
<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">
<span className="mr-3 text-lg text-common-light">
FOR INVESTIGATIONAL USE ONLY
{t('Header:INVESTIGATIONAL USE ONLY')}
</span>
<IconButton
variant="text"
color="inherit"
className="text-primary-active"
onClick={() => {}}
<Dropdown
showDropdownIcon={false}
list={[
{
title: t('Header:About'),
icon: 'info',
onClick: showAboutModal,
},
{
title: t('Header:Preferences'),
icon: 'settings',
onClick: showPreferencesModal,
},
]}
>
<React.Fragment>
<Icon name="settings" /> <Icon name="chevron-down" />
</React.Fragment>
</IconButton>
<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>
</div>
</NavBar>

View File

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

View File

@ -2,8 +2,6 @@
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 32 32"
aria-labelledby="title"
width="1em"
height="1em"
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" />

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"
aria-labelledby="unlink"
viewBox="0 0 512 512"
width="1em"
height="1em"
fill="currentColor"
>
<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 (
<div
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,
className
)}

View File

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

View File

@ -1,12 +1,13 @@
// External
import React from 'react';
import PropTypes from 'prop-types';
import i18n from '@ohif/i18n';
import { I18nextProvider } from 'react-i18next';
import { BrowserRouter, HashRouter } from 'react-router-dom';
import {
DialogProvider,
Modal,
ModalProvider,
Notification,
SnackbarProvider,
ThemeWrapper,
ViewportDialogProvider,
@ -97,29 +98,31 @@ function App({ config, defaultExtensions }) {
return (
<AppConfigProvider value={appConfigState}>
<Router basename={routerBasename}>
<ThemeWrapper>
<ViewportGridProvider
initialState={{
numRows: 1,
numCols: 1,
viewports: [],
activeViewportIndex: 0,
}}
reducer={viewportGridReducer}
>
<ViewportDialogProvider service={UIViewportDialogService}>
<SnackbarProvider service={UINotificationService}>
<DialogProvider service={UIDialogService}>
<ModalProvider modal={Modal} service={UIModalService}>
{appRoutes}
</ModalProvider>
</DialogProvider>
</SnackbarProvider>
</ViewportDialogProvider>
</ViewportGridProvider>
</ThemeWrapper>
</Router>
<I18nextProvider i18n={i18n}>
<Router basename={routerBasename}>
<ThemeWrapper>
<ViewportGridProvider
initialState={{
numRows: 1,
numCols: 1,
viewports: [],
activeViewportIndex: 0,
}}
reducer={viewportGridReducer}
>
<ViewportDialogProvider service={UIViewportDialogService}>
<SnackbarProvider service={UINotificationService}>
<DialogProvider service={UIDialogService}>
<ModalProvider modal={Modal} service={UIModalService}>
{appRoutes}
</ModalProvider>
</DialogProvider>
</SnackbarProvider>
</ViewportDialogProvider>
</ViewportGridProvider>
</ThemeWrapper>
</Router>
</I18nextProvider>
</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 { useDebounce, useQuery } from '@hooks';
import PreferencesDropdown from '../../components/PreferencesDropdown';
import {
Icon,
StudyListExpandedRow,
Button,
NavBar,
Svg,
IconButton,
EmptyStudies,
StudyListTable,
StudyListPagination,
@ -34,7 +35,10 @@ function WorkList({ history, data: studies, isLoadingData, dataSource }) {
// ~ Filters
const query = useQuery();
const queryFilterValues = _getQueryFilterValues(query);
const [filterValues, _setFilterValues] = useState({ ...defaultFilterValues, ...queryFilterValues });
const [filterValues, _setFilterValues] = useState({
...defaultFilterValues,
...queryFilterValues,
});
const debouncedFilterValues = useDebounce(filterValues, 200);
const { resultsPerPage, pageNumber, sortBy, sortDirection } = filterValues;
@ -211,8 +215,8 @@ function WorkList({ history, data: studies, isLoadingData, dataSource }) {
content: patientName ? (
patientName
) : (
<span className="text-gray-700">(Empty)</span>
),
<span className="text-gray-700">(Empty)</span>
),
title: patientName,
gridCol: 4,
},
@ -299,13 +303,13 @@ function WorkList({ history, data: studies, isLoadingData, dataSource }) {
seriesTableDataSource={
seriesInStudiesMap.has(studyInstanceUid)
? seriesInStudiesMap.get(studyInstanceUid).map(s => {
return {
description: s.description || '(empty)',
seriesNumber: s.seriesNumber || '',
modality: s.modality || '',
instances: s.numSeriesInstances || '',
};
})
return {
description: s.description || '(empty)',
seriesNumber: s.seriesNumber || '',
modality: s.modality || '',
instances: s.numSeriesInstances || '',
};
})
: []
}
>
@ -322,7 +326,7 @@ function WorkList({ history, data: studies, isLoadingData, dataSource }) {
<Link
key={i}
to={`${mode.id}?StudyInstanceUIDs=${studyInstanceUid}`}
// to={`${mode.id}/dicomweb?StudyInstanceUIDs=${studyInstanceUid}`}
// to={`${mode.id}/dicomweb?StudyInstanceUIDs=${studyInstanceUid}`}
>
<Button
rounded="full"
@ -364,17 +368,7 @@ function WorkList({ history, data: studies, isLoadingData, dataSource }) {
<span className="mr-3 text-lg text-common-light">
FOR INVESTIGATIONAL USE ONLY
</span>
<IconButton
variant="text"
color="inherit"
className="text-primary-active"
onClick={() => { }}
>
<React.Fragment>
<Icon name="settings" />
<Icon name="chevron-down" />
</React.Fragment>
</IconButton>
<PreferencesDropdown />
</div>
</NavBar>
<StudyListFilter
@ -403,10 +397,10 @@ function WorkList({ history, data: studies, isLoadingData, dataSource }) {
/>
</>
) : (
<div className="flex flex-col items-center justify-center pt-48">
<EmptyStudies isLoading={isLoadingData} />
</div>
)}
<div className="flex flex-col items-center justify-center pt-48">
<EmptyStudies isLoading={isLoadingData} />
</div>
)}
</div>
);
}