[OHIF-193 + OHIF-313] (#1955)
* create context menu * OHIF-193: Add context menu integration with measurement service * OHIF-193: Update comments * OHIF-CR Update: Add requested updates * CR Update: Update measurement service remove function * CR Update: Update casing * CR Updates * Update cornerstone measurments when the label is edited in the measurement service. * Implement click outside behavior * Address reviewer comments. Co-authored-by: Rodrigo Antinarelli <rodrigoantinarelli@gmail.com> Co-authored-by: igoroctaviano <igoroctaviano@gmail.com>
This commit is contained in:
20 files changed
+575
-196
No files matched your search
@@ -32,6 +32,7 @@ export {
|
||||
export {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
ContextMenu,
|
||||
DateRange,
|
||||
Dialog,
|
||||
Dropdown,
|
||||
@@ -76,6 +77,7 @@ export {
|
||||
ThumbnailTracked,
|
||||
ThumbnailList,
|
||||
ToolbarButton,
|
||||
ContextMenuMeasurements,
|
||||
Tooltip,
|
||||
TooltipClipboard,
|
||||
Typography,
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Typography } from '@ohif/ui';
|
||||
|
||||
const ContextMenu = ({ items }) => {
|
||||
return (
|
||||
<div
|
||||
className="relative bg-secondary-dark rounded z-50 block w-48"
|
||||
onContextMenu={e => e.preventDefault()}
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => item.action(item)}
|
||||
className="flex px-4 py-3 cursor-pointer items-center transition duration-300 hover:bg-primary-dark border-b border-primary-dark last:border-b-0"
|
||||
>
|
||||
<Typography>{item.label}</Typography>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ContextMenu.propTypes = {
|
||||
items: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
label: PropTypes.string.isRequired,
|
||||
actionType: PropTypes.string.isRequired,
|
||||
action: PropTypes.func.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
};
|
||||
|
||||
export default ContextMenu;
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: Context Menu
|
||||
menu: General
|
||||
route: components/contextMenu
|
||||
---
|
||||
|
||||
import { Playground, Props } from 'docz';
|
||||
import { ContextMenu } from '@ohif/ui';
|
||||
|
||||
# Context Menu
|
||||
|
||||
...
|
||||
|
||||
## Import
|
||||
|
||||
```javascript
|
||||
import { ContextMenu } from '@ohif/ui';
|
||||
```
|
||||
|
||||
<Playground>
|
||||
{() => {
|
||||
const items = [
|
||||
{
|
||||
label: 'Delete measurement',
|
||||
actionType: 'Delete',
|
||||
action: () => alert('Delete'),
|
||||
},
|
||||
{
|
||||
label: 'Relabel',
|
||||
actionType: 'setLabel',
|
||||
action: () => alert('Relabel'),
|
||||
},
|
||||
{
|
||||
label: 'Add Description',
|
||||
actionType: 'setDescription',
|
||||
action: () => alert('Add Description'),
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className="p-4">
|
||||
<ContextMenu items={items} />
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Playground>
|
||||
|
||||
## Properties
|
||||
|
||||
<Props of={ContextMenu} />
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './ContextMenu';
|
||||
@@ -0,0 +1,44 @@
|
||||
import { ContextMenu } from '@ohif/ui';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
const ContextMenuMeasurements = ({
|
||||
onGetMenuItems,
|
||||
onSetLabel,
|
||||
onClose,
|
||||
onDelete,
|
||||
}) => {
|
||||
const defaultMenuItems = [
|
||||
{
|
||||
label: 'Delete measurement',
|
||||
actionType: 'Delete',
|
||||
action: item => {
|
||||
onDelete(item);
|
||||
onClose();
|
||||
},
|
||||
value: {},
|
||||
},
|
||||
{
|
||||
label: 'Add Label',
|
||||
actionType: 'setLabel',
|
||||
action: item => {
|
||||
onSetLabel(item);
|
||||
onClose();
|
||||
},
|
||||
value: {},
|
||||
},
|
||||
];
|
||||
|
||||
const menuItems = onGetMenuItems(defaultMenuItems);
|
||||
|
||||
return <ContextMenu items={menuItems} />;
|
||||
};
|
||||
|
||||
ContextMenuMeasurements.propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onSetLabel: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onGetMenuItems: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ContextMenuMeasurements;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './ContextMenuMeasurements';
|
||||
@@ -1,5 +1,6 @@
|
||||
import Button from './Button';
|
||||
import ButtonGroup from './ButtonGroup';
|
||||
import ContextMenu from './ContextMenu';
|
||||
import DateRange from './DateRange';
|
||||
import Dialog from './Dialog';
|
||||
import Dropdown from './Dropdown';
|
||||
@@ -41,6 +42,7 @@ import ThumbnailNoImage from './ThumbnailNoImage';
|
||||
import ThumbnailTracked from './ThumbnailTracked';
|
||||
import ThumbnailList from './ThumbnailList';
|
||||
import ToolbarButton from './ToolbarButton';
|
||||
import ContextMenuMeasurements from './ContextMenuMeasurements';
|
||||
import ExpandableToolbarButton from './ExpandableToolbarButton';
|
||||
import ListMenu from './ListMenu';
|
||||
import Tooltip from './Tooltip';
|
||||
@@ -55,6 +57,7 @@ import ViewportPane from './ViewportPane';
|
||||
export {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
ContextMenu,
|
||||
DateRange,
|
||||
Dialog,
|
||||
Dropdown,
|
||||
@@ -99,6 +102,7 @@ export {
|
||||
ThumbnailTracked,
|
||||
ThumbnailList,
|
||||
ToolbarButton,
|
||||
ContextMenuMeasurements,
|
||||
Tooltip,
|
||||
TooltipClipboard,
|
||||
Typography,
|
||||
|
||||
@@ -4,7 +4,7 @@ import React, {
|
||||
useContext,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef
|
||||
useRef,
|
||||
} from 'react';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
@@ -154,6 +154,7 @@ const DialogProvider = ({ children, service }) => {
|
||||
onStart,
|
||||
onStop,
|
||||
onDrag,
|
||||
onClickOutside,
|
||||
showOverlay,
|
||||
} = dialog;
|
||||
|
||||
@@ -169,7 +170,7 @@ const DialogProvider = ({ children, service }) => {
|
||||
disabled={!isDraggable}
|
||||
position={position}
|
||||
defaultPosition={position}
|
||||
bounds='parent'
|
||||
bounds="parent"
|
||||
onStart={event => {
|
||||
const e = event || window.event;
|
||||
const target = e.target || e.srcElement;
|
||||
@@ -225,16 +226,27 @@ const DialogProvider = ({ children, service }) => {
|
||||
const background = 'bg-black bg-opacity-50';
|
||||
const overlay = 'fixed z-50 left-0 top-0 w-full h-full overflow-auto';
|
||||
return (
|
||||
<div
|
||||
className={classNames(overlay, background)}
|
||||
key={id}
|
||||
>
|
||||
<div className={classNames(overlay, background)} key={id}>
|
||||
{component}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return showOverlay ? withOverlay(dragableItem()) : dragableItem();
|
||||
let result = dragableItem();
|
||||
|
||||
if (showOverlay) {
|
||||
result = withOverlay(result);
|
||||
}
|
||||
|
||||
if (typeof onClickOutside === 'function') {
|
||||
result = (
|
||||
<OutsideAlerter onClickOutside={onClickOutside}>
|
||||
{result}
|
||||
</OutsideAlerter>
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -253,7 +265,7 @@ const DialogProvider = ({ children, service }) => {
|
||||
};
|
||||
|
||||
const onKeyDownHandler = event => {
|
||||
if (event.key === "Escape") {
|
||||
if (event.key === 'Escape') {
|
||||
dismissAll();
|
||||
}
|
||||
};
|
||||
@@ -262,11 +274,11 @@ const DialogProvider = ({ children, service }) => {
|
||||
|
||||
return (
|
||||
<DialogContext.Provider value={{ create, dismiss, dismissAll, isEmpty }}>
|
||||
{!isEmpty() &&
|
||||
<div className='w-full h-full absolute' onKeyDown={onKeyDownHandler}>
|
||||
{!isEmpty() && (
|
||||
<div className="w-full h-full absolute" onKeyDown={onKeyDownHandler}>
|
||||
{renderDialogs()}
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
{children}
|
||||
</DialogContext.Provider>
|
||||
);
|
||||
@@ -302,3 +314,29 @@ DialogProvider.propTypes = {
|
||||
};
|
||||
|
||||
export default DialogProvider;
|
||||
|
||||
function OutsideAlerter(props) {
|
||||
const wrapperRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Alert if clicked on outside of element
|
||||
*/
|
||||
function handleInteractionOutside(event) {
|
||||
if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
|
||||
props.onClickOutside();
|
||||
}
|
||||
}
|
||||
|
||||
// Bind the event listener
|
||||
document.addEventListener('mousedown', handleInteractionOutside);
|
||||
document.addEventListener('touchstart', handleInteractionOutside);
|
||||
return () => {
|
||||
// Unbind the event listener on clean up
|
||||
document.removeEventListener('mousedown', handleInteractionOutside);
|
||||
document.removeEventListener('touchstart', handleInteractionOutside);
|
||||
};
|
||||
}, [wrapperRef]);
|
||||
|
||||
return <div ref={wrapperRef}>{props.children}</div>;
|
||||
}
|
||||
Reference in new issue
Block a user