diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx index b45cd3691..4a727ce48 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx @@ -22,7 +22,10 @@ function PanelStudyBrowserTracking({ // doesn't have to have such an intense shape. This works well enough for now. // Tabs --> Studies --> DisplaySets --> Thumbnails const [{ StudyInstanceUIDs }, dispatchImageViewer] = useImageViewer(); - const [{ activeViewportIndex, viewports }] = useViewportGrid(); + const [ + { activeViewportIndex, viewports }, + viewportGridService, + ] = useViewportGrid(); const [ trackedMeasurements, sendTrackedMeasurementsEvent, @@ -36,6 +39,16 @@ function PanelStudyBrowserTracking({ const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({}); const [jumpToDisplaySet, setJumpToDisplaySet] = useState(null); + const onDoubleClickThumbnailHandler = displaySetInstanceUID => { + viewportGridService.setDisplaysetForViewport({ + viewportIndex: activeViewportIndex, + displaySetInstanceUID, + }); + }; + + const activeDisplaySetInstanceUID = + viewports[activeViewportIndex]?.displaySetInstanceUID; + // TODO: Should this be somewhere else? Feels more like a mode "lifecycle" setup/destroy? useEffect(() => { const { unsubscribe } = MeasurementService.subscribe( @@ -287,6 +300,9 @@ function PanelStudyBrowserTracking({ SeriesInstanceUID: displaySet.SeriesInstanceUID, }); }} + onClickThumbnail={() => {}} + onDoubleClickThumbnail={onDoubleClickThumbnailHandler} + activeDisplaySetInstanceUID={activeDisplaySetInstanceUID} /> ); } diff --git a/platform/ui/src/components/StudyBrowser/StudyBrowser.jsx b/platform/ui/src/components/StudyBrowser/StudyBrowser.jsx index 048489e70..c5110f5c7 100644 --- a/platform/ui/src/components/StudyBrowser/StudyBrowser.jsx +++ b/platform/ui/src/components/StudyBrowser/StudyBrowser.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; @@ -25,10 +25,10 @@ const StudyBrowser = ({ onClickTab, onClickStudy, onClickThumbnail, + onDoubleClickThumbnail, onClickUntrack, + activeDisplaySetInstanceUID, }) => { - const [thumbnailActive, setThumbnailActive] = useState(null); - const getTabContent = () => { const tabData = tabs.find(tab => tab.name === activeTabName); @@ -58,18 +58,10 @@ const StudyBrowser = ({ {isExpanded && displaySets && ( { - setThumbnailActive( - displaySetInstanceUID === thumbnailActive - ? null - : displaySetInstanceUID - ); - onClickThumbnail(displaySetInstanceUID); - }} - onClickUntrack={displaySetInstanceUID => { - onClickUntrack(displaySetInstanceUID); - }} + activeDisplaySetInstanceUID={activeDisplaySetInstanceUID} + onThumbnailClick={onClickThumbnail} + onThumbnailDoubleClick={onDoubleClickThumbnail} + onClickUntrack={onClickUntrack} /> )} @@ -118,9 +110,11 @@ StudyBrowser.propTypes = { onClickTab: PropTypes.func.isRequired, onClickStudy: PropTypes.func, onClickThumbnail: PropTypes.func, + onDoubleClickThumbnail: PropTypes.func, onClickUntrack: PropTypes.func, activeTabName: PropTypes.string.isRequired, expandedStudyInstanceUIDs: PropTypes.arrayOf(PropTypes.string).isRequired, + activeDisplaySetInstanceUID: PropTypes.string, tabs: PropTypes.arrayOf( PropTypes.shape({ name: PropTypes.string.isRequired, @@ -173,6 +167,7 @@ StudyBrowser.defaultProps = { onClickTab: noop, onClickStudy: noop, onClickThumbnail: noop, + onDoubleClickThumbnail: noop, onClickUntrack: noop, }; diff --git a/platform/ui/src/components/Thumbnail/Thumbnail.jsx b/platform/ui/src/components/Thumbnail/Thumbnail.jsx index 7813928a0..ec378ae09 100644 --- a/platform/ui/src/components/Thumbnail/Thumbnail.jsx +++ b/platform/ui/src/components/Thumbnail/Thumbnail.jsx @@ -1,9 +1,9 @@ -import React from 'react'; +import React, { useRef } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import { useDrag } from 'react-dnd'; -// import { Icon } from '@ohif/ui'; +import blurHandlerListener from '../../utils/blurHandlerListener'; /** * @@ -19,6 +19,7 @@ const Thumbnail = ({ dragData, isActive, onClick, + onDoubleClick, }) => { // TODO: We should wrap our thumbnail to create a "DraggableThumbnail", as // this will still allow for "drag", even if there is no drop target for the @@ -30,52 +31,58 @@ const Thumbnail = ({ }, }); + const thumbnailElement = useRef(null); + return (
blurHandlerListener(thumbnailElement)} + ref={thumbnailElement} className={classnames( className, - 'flex flex-col flex-1 px-3 mb-8 cursor-pointer outline-none' + 'flex flex-col flex-1 px-3 mb-8 cursor-pointer outline-none group' )} id={`thumbnail-${displaySetInstanceUID}`} onClick={onClick} - onKeyDown={onClick} + onDoubleClick={onDoubleClick} role="button" tabIndex="0" > -
- {imageSrc ? ( - {imageAltText} - ) : ( -
{imageAltText}
- )} -
-
-
- {'S: '} - {seriesNumber} +
+
+ {imageSrc ? ( + {imageAltText} + ) : ( +
{imageAltText}
+ )}
-
- {numInstances} +
+
+ {'S: '} + {seriesNumber} +
+
+ {numInstances} +
+
{description}
-
{description}
); }; Thumbnail.propTypes = { + displaySetInstanceUID: PropTypes.string.isRequired, className: PropTypes.string, imageSrc: PropTypes.string, /** @@ -95,6 +102,7 @@ Thumbnail.propTypes = { numInstances: PropTypes.number.isRequired, isActive: PropTypes.bool.isRequired, onClick: PropTypes.func.isRequired, + onDoubleClick: PropTypes.func.isRequired, }; Thumbnail.defaultProps = { diff --git a/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx b/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx index 0b1febd82..80e87f667 100644 --- a/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx +++ b/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx @@ -5,8 +5,9 @@ import { Thumbnail, ThumbnailNoImage, ThumbnailTracked } from '@ohif/ui'; const ThumbnailList = ({ thumbnails, - thumbnailActive, + activeDisplaySetInstanceUID, onThumbnailClick, + onThumbnailDoubleClick, onClickUntrack, }) => { return ( @@ -26,7 +27,8 @@ const ThumbnailList = ({ imageSrc, imageAltText, }) => { - const isActive = thumbnailActive === displaySetInstanceUID; + const isActive = + activeDisplaySetInstanceUID === displaySetInstanceUID; switch (componentType) { case 'thumbnail': @@ -43,6 +45,9 @@ const ThumbnailList = ({ viewportIdentificator={viewportIdentificator} isActive={isActive} onClick={() => onThumbnailClick(displaySetInstanceUID)} + onDoubleClick={() => + onThumbnailDoubleClick(displaySetInstanceUID) + } /> ); case 'thumbnailTracked': @@ -60,12 +65,16 @@ const ThumbnailList = ({ isTracked={isTracked} isActive={isActive} onClick={() => onThumbnailClick(displaySetInstanceUID)} + onDoubleClick={() => + onThumbnailDoubleClick(displaySetInstanceUID) + } onClickUntrack={() => onClickUntrack(displaySetInstanceUID)} /> ); case 'thumbnailNoImage': return ( onThumbnailClick(displaySetInstanceUID)} + onDoubleClick={() => + onThumbnailDoubleClick(displaySetInstanceUID) + } /> ); default: @@ -114,8 +126,9 @@ ThumbnailList.propTypes = { }), }) ), - thumbnailActive: PropTypes.string, - onThumbnailClick: PropTypes.func, + activeDisplaySetInstanceUID: PropTypes.string, + onThumbnailClick: PropTypes.func.isRequired, + onThumbnailDoubleClick: PropTypes.func.isRequired, onClickUntrack: PropTypes.func.isRequired, }; diff --git a/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.jsx b/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.jsx index 205a7af37..c01e619f3 100644 --- a/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.jsx +++ b/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.jsx @@ -1,9 +1,9 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { useRef } from 'react'; import classnames from 'classnames'; +import PropTypes from 'prop-types'; import { useDrag } from 'react-dnd'; - import { Icon } from '@ohif/ui'; +import blurHandlerListener from '../../utils/blurHandlerListener'; const ThumbnailNoImage = ({ displaySetInstanceUID, @@ -11,7 +11,9 @@ const ThumbnailNoImage = ({ seriesDate, modality, onClick, + onDoubleClick, dragData, + isActive, }) => { const [collectedProps, drag, dragPreview] = useDrag({ item: { ...dragData }, @@ -20,26 +22,34 @@ const ThumbnailNoImage = ({ }, }); + const thumbnailElement = useRef(null); + return (
blurHandlerListener(thumbnailElement)} + className={classnames( + 'flex flex-row flex-1 px-4 py-3 cursor-pointer outline-none border-transparent hover:border-blue-300 focus:border-blue-300 rounded', + isActive ? 'border-2 border-primary-light' : 'border' + )} id={`thumbnail-${displaySetInstanceUID}`} onClick={onClick} - onKeyDown={onClick} + onDoubleClick={onDoubleClick} role="button" tabIndex="0" > -
-
- -
- {modality} +
+
+
+ +
+ {modality} +
+ {seriesDate} +
+
+ {description}
- {seriesDate} -
-
- {description}
@@ -47,10 +57,24 @@ const ThumbnailNoImage = ({ }; ThumbnailNoImage.propTypes = { + displaySetInstanceUID: PropTypes.string.isRequired, + /** + * Data the thumbnail should expose to a receiving drop target. Use a matching + * `dragData.type` to identify which targets can receive this draggable item. + * If this is not set, drag-n-drop will be disabled for this thumbnail. + * + * Ref: https://react-dnd.github.io/react-dnd/docs/api/use-drag#specification-object-members + */ + dragData: PropTypes.shape({ + /** Must match the "type" a dropTarget expects */ + type: PropTypes.string.isRequired, + }), description: PropTypes.string.isRequired, modality: PropTypes.string.isRequired, seriesDate: PropTypes.string.isRequired, onClick: PropTypes.func.isRequired, + onDoubleClick: PropTypes.func.isRequired, + isActive: PropTypes.bool.isRequired, }; export default ThumbnailNoImage; diff --git a/platform/ui/src/components/ThumbnailTracked/ThumbnailTracked.jsx b/platform/ui/src/components/ThumbnailTracked/ThumbnailTracked.jsx index 6715a77d3..2d8e09e05 100644 --- a/platform/ui/src/components/ThumbnailTracked/ThumbnailTracked.jsx +++ b/platform/ui/src/components/ThumbnailTracked/ThumbnailTracked.jsx @@ -14,6 +14,7 @@ const ThumbnailTracked = ({ numInstances, dragData, onClick, + onDoubleClick, onClickUntrack, viewportIdentificator, isTracked, @@ -72,6 +73,7 @@ const ThumbnailTracked = ({ )}
); @@ -97,6 +100,7 @@ ThumbnailTracked.propTypes = { /** Must match the "type" a dropTarget expects */ type: PropTypes.string.isRequired, }), + displaySetInstanceUID: PropTypes.string.isRequired, className: PropTypes.string, imageSrc: PropTypes.string, imageAltText: PropTypes.string, @@ -104,6 +108,7 @@ ThumbnailTracked.propTypes = { seriesNumber: PropTypes.number.isRequired, numInstances: PropTypes.number.isRequired, onClick: PropTypes.func.isRequired, + onDoubleClick: PropTypes.func.isRequired, onClickUntrack: PropTypes.func.isRequired, viewportIdentificator: PropTypes.string, isTracked: PropTypes.bool, diff --git a/platform/ui/src/contextProviders/ViewportGridProvider.jsx b/platform/ui/src/contextProviders/ViewportGridProvider.jsx index fe77ef0a0..a522ea165 100644 --- a/platform/ui/src/contextProviders/ViewportGridProvider.jsx +++ b/platform/ui/src/contextProviders/ViewportGridProvider.jsx @@ -10,7 +10,11 @@ import PropTypes from 'prop-types'; const DEFAULT_STATE = { numRows: 1, numCols: 1, - viewports: [], + viewports: [ + // { + // displaySetInstanceUID: string, + // } + ], activeViewportIndex: 0, }; @@ -19,8 +23,9 @@ export const ViewportGridContext = createContext(DEFAULT_STATE); export function ViewportGridProvider({ children, service }) { const viewportGridReducer = (state, action) => { switch (action.type) { - case 'SET_ACTIVE_VIEWPORT_INDEX': + case 'SET_ACTIVE_VIEWPORT_INDEX': { return { ...state, ...{ activeViewportIndex: action.payload } }; + } case 'SET_DISPLAYSET_FOR_VIEWPORT': { const { viewportIndex, displaySetInstanceUID } = action.payload; const viewports = state.viewports.slice(); @@ -55,7 +60,7 @@ export function ViewportGridProvider({ children, service }) { const [viewportGridState, dispatch] = useReducer( viewportGridReducer, - DEFAULT_STATE, + DEFAULT_STATE ); const getState = useCallback(() => viewportGridState, [viewportGridState]); diff --git a/platform/ui/src/utils/blurHandlerListener.js b/platform/ui/src/utils/blurHandlerListener.js new file mode 100644 index 000000000..4ffba68c6 --- /dev/null +++ b/platform/ui/src/utils/blurHandlerListener.js @@ -0,0 +1,10 @@ +export default element => { + const handleClickOutside = event => { + if (element.current && !element.current.contains(event.target)) { + element.current.blur(); + document.removeEventListener('mousedown', handleClickOutside); + } + }; + + document.addEventListener('mousedown', handleClickOutside); +}; diff --git a/platform/ui/tailwind.config.js b/platform/ui/tailwind.config.js index e70bbe86b..967575810 100644 --- a/platform/ui/tailwind.config.js +++ b/platform/ui/tailwind.config.js @@ -721,7 +721,7 @@ module.exports = { backgroundRepeat: ['responsive'], backgroundSize: ['responsive'], borderCollapse: ['responsive'], - borderColor: ['responsive', 'hover', 'focus', 'active'], + borderColor: ['responsive', 'hover', 'focus', 'active', 'group-focus'], borderRadius: ['responsive', 'focus', 'first', 'last'], borderStyle: ['responsive', 'focus'], borderWidth: ['responsive', 'focus', 'first', 'last'],