From 8ca3769ba187d5aa54b7da45b3af9cb22c002731 Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Wed, 22 Jul 2020 15:25:51 -0300 Subject: [PATCH] OHIF-304 - Create TooltipClipboard component for copying from Study List (#1887) * Create TooltipClipboard component * basic doc page for TooltipClipboard * enable clipboard tooltip for specific rows * wrap component with tooltip clipboard * remove logic from ui component * set clipbboard message * minor refactor to add a delay to show the tooltip clipboard * delay to hide comment * Remove titles which were interfering with tooltips Co-authored-by: Erik Ziegler --- platform/ui/index.js | 1 + platform/ui/src/assets/icons/clipboard.svg | 1 + platform/ui/src/components/Icon/getIcon.jsx | 2 + .../StudyListTable/StudyListTableRow.js | 2 +- .../TooltipClipboard/TooltipClipboard.jsx | 155 ++++++++++++++++++ .../TooltipClipboard/TooltipClipboard.mdx | 30 ++++ .../src/components/TooltipClipboard/index.js | 1 + platform/ui/src/components/index.js | 2 + .../viewer/src/routes/WorkList/WorkList.jsx | 13 +- 9 files changed, 198 insertions(+), 9 deletions(-) create mode 100644 platform/ui/src/assets/icons/clipboard.svg create mode 100644 platform/ui/src/components/TooltipClipboard/TooltipClipboard.jsx create mode 100644 platform/ui/src/components/TooltipClipboard/TooltipClipboard.mdx create mode 100644 platform/ui/src/components/TooltipClipboard/index.js diff --git a/platform/ui/index.js b/platform/ui/index.js index 5aa3402d6..bfb01724c 100644 --- a/platform/ui/index.js +++ b/platform/ui/index.js @@ -74,6 +74,7 @@ export { ThumbnailList, ToolbarButton, Tooltip, + TooltipClipboard, Typography, Viewport, ViewportActionBar, diff --git a/platform/ui/src/assets/icons/clipboard.svg b/platform/ui/src/assets/icons/clipboard.svg new file mode 100644 index 000000000..850511a0b --- /dev/null +++ b/platform/ui/src/assets/icons/clipboard.svg @@ -0,0 +1 @@ + diff --git a/platform/ui/src/components/Icon/getIcon.jsx b/platform/ui/src/components/Icon/getIcon.jsx index 775eca0ca..0a05a372b 100644 --- a/platform/ui/src/components/Icon/getIcon.jsx +++ b/platform/ui/src/components/Icon/getIcon.jsx @@ -4,6 +4,7 @@ import React from 'react'; import arrowDown from './../../assets/icons/arrow-down.svg'; import calendar from './../../assets/icons/calendar.svg'; import cancel from './../../assets/icons/cancel.svg'; +import clipboard from './../../assets/icons/clipboard.svg'; import close from './../../assets/icons/close.svg'; import dottedCircle from './../../assets/icons/dotted-circle.svg'; import circledCheckmark from './../../assets/icons/circled-checkmark.svg'; @@ -64,6 +65,7 @@ const ICONS = { 'arrow-down': arrowDown, calendar: calendar, cancel: cancel, + clipboard: clipboard, close: close, 'dotted-circle': dottedCircle, 'circled-checkmark': circledCheckmark, diff --git a/platform/ui/src/components/StudyListTable/StudyListTableRow.js b/platform/ui/src/components/StudyListTable/StudyListTableRow.js index 01c6e7893..fe6987c39 100644 --- a/platform/ui/src/components/StudyListTable/StudyListTableRow.js +++ b/platform/ui/src/components/StudyListTable/StudyListTableRow.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import Icon from './../Icon'; +import { Icon } from '@ohif/ui'; const StudyListTableRow = props => { const { tableData } = props; diff --git a/platform/ui/src/components/TooltipClipboard/TooltipClipboard.jsx b/platform/ui/src/components/TooltipClipboard/TooltipClipboard.jsx new file mode 100644 index 000000000..43b248f7e --- /dev/null +++ b/platform/ui/src/components/TooltipClipboard/TooltipClipboard.jsx @@ -0,0 +1,155 @@ +import React, { useState, useRef, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { Icon } from '@ohif/ui'; + +const DELAY_TO_SHOW = 1000; +const DELAY_TO_HIDE = 10; // it needs at least a little delay to prevent tooltip to suddenly hide +const DELAY_TO_HIDE_AFTER_COPYING = 1000; + +const TooltipClipboard = ({ children, text }) => { + const [isActive, setIsActive] = useState(false); + const [message, setMessage] = useState(null); + const [isCopying, setIsCopying] = useState(false); + const timeoutShow = useRef(null); + const timeoutHide = useRef(null); + const tooltipBoxRef = useRef(null); + const tooltipContainerRef = useRef(null); + + const copyToClipboard = async text => { + setIsCopying(true); + try { + await navigator.clipboard.writeText(text); + setMessage('Copied!'); + } catch (err) { + console.error('Failed to copy: ', err); + setMessage('Failed to copy!'); + } finally { + refreshElementPosition(); + + setTimeout(() => { + resetState(); + }, DELAY_TO_HIDE_AFTER_COPYING); + } + }; + + const resetState = () => { + setIsActive(false); + setMessage(null); + setIsCopying(false); + }; + + const resetTimeout = timeOut => { + if (timeOut.current !== null) { + clearTimeout(timeOut.current); + } + }; + + const handleMouseOver = () => { + resetTimeout(timeoutHide); + + if (!isActive) { + timeoutShow.current = setTimeout(() => { + timeoutShow.current = null; + setIsActive(true); + }, DELAY_TO_SHOW); + } + }; + + const handleMouseOut = e => { + resetTimeout(timeoutShow); + + if (isActive && !isCopying) { + timeoutHide.current = setTimeout(() => { + timeoutHide.current = null; + resetState(); + }, DELAY_TO_HIDE); + } + }; + + /** + * Trick to set the tooltip position based on its parent position + * because the tooltip box is not relative-positioned to avoid the tooltip + * to be clipped if the parent container is overflow-hidden + */ + const refreshElementPosition = () => { + const tooltipContainer = tooltipContainerRef.current; + const tooltipBox = tooltipBoxRef.current; + + const { + left: containerX, + top: containerY, + height: containerHeight, + } = tooltipContainer.getBoundingClientRect(); + + const top = containerY + containerHeight + 'px'; + const left = containerX + 'px'; + + tooltipBox.style.top = top; + tooltipBox.style.left = left; + }; + + useEffect(() => { + if (isActive) { + refreshElementPosition(); + window.addEventListener('scroll', refreshElementPosition); + } else { + window.removeEventListener('scroll', refreshElementPosition); + } + + return () => window.removeEventListener('scroll', refreshElementPosition); + }, [isActive]); + + const onClickHandler = e => { + e.stopPropagation(); + copyToClipboard(text || children); + }; + + return ( +
+ {children} +
+
+ {message || ( + <> + {children} +
+ +
+ + )} +
+
+
+ ); +}; + +TooltipClipboard.defaultProps = { + text: '', +}; + +TooltipClipboard.propTypes = { + text: PropTypes.string, + children: PropTypes.node.isRequired, +}; + +export default TooltipClipboard; diff --git a/platform/ui/src/components/TooltipClipboard/TooltipClipboard.mdx b/platform/ui/src/components/TooltipClipboard/TooltipClipboard.mdx new file mode 100644 index 000000000..63b69f613 --- /dev/null +++ b/platform/ui/src/components/TooltipClipboard/TooltipClipboard.mdx @@ -0,0 +1,30 @@ +--- +name: TooltipClipboard +menu: Data Display +route: components/tooltipClipboard +--- + +import { Playground, Props } from 'docz'; +import { TooltipClipboard } from '@ohif/ui'; + +# Tooltip Clipboard + +Tooltips display informative content when users hover over, focus on, or tap an +element. + +## Import + +```javascript +import { TooltipClipboard } from '@ohif/ui'; +``` + + +
+ Text to be copied +
+ ); }} +
+ +## Properties + + diff --git a/platform/ui/src/components/TooltipClipboard/index.js b/platform/ui/src/components/TooltipClipboard/index.js new file mode 100644 index 000000000..90916a56f --- /dev/null +++ b/platform/ui/src/components/TooltipClipboard/index.js @@ -0,0 +1 @@ +export { default } from './TooltipClipboard'; diff --git a/platform/ui/src/components/index.js b/platform/ui/src/components/index.js index 7bc54fe96..1288505c8 100644 --- a/platform/ui/src/components/index.js +++ b/platform/ui/src/components/index.js @@ -44,6 +44,7 @@ import ToolbarButton from './ToolbarButton'; import ExpandableToolbarButton from './ExpandableToolbarButton'; import ListMenu from './ListMenu'; import Tooltip from './Tooltip'; +import TooltipClipboard from './TooltipClipboard'; import Typography from './Typography'; import Viewport from './Viewport'; import ViewportActionBar from './ViewportActionBar'; @@ -99,6 +100,7 @@ export { ThumbnailList, ToolbarButton, Tooltip, + TooltipClipboard, Typography, Viewport, ViewportActionBar, diff --git a/platform/viewer/src/routes/WorkList/WorkList.jsx b/platform/viewer/src/routes/WorkList/WorkList.jsx index e57fa2f18..9b33e6573 100644 --- a/platform/viewer/src/routes/WorkList/WorkList.jsx +++ b/platform/viewer/src/routes/WorkList/WorkList.jsx @@ -21,6 +21,7 @@ import { StudyListTable, StudyListPagination, StudyListFilter, + TooltipClipboard, } from '@ohif/ui'; const seriesInStudiesMap = new Map(); @@ -213,17 +214,15 @@ function WorkList({ history, data: studies, isLoadingData, dataSource }) { { key: 'patientName', content: patientName ? ( - patientName + {patientName} ) : ( (Empty) ), - title: patientName, gridCol: 4, }, { key: 'mrn', - content: mrn, - title: mrn, + content: {mrn}, gridCol: 3, }, { @@ -239,8 +238,7 @@ function WorkList({ history, data: studies, isLoadingData, dataSource }) { }, { key: 'description', - content: description, - title: description, + content: {description}, gridCol: 4, }, { @@ -251,8 +249,7 @@ function WorkList({ history, data: studies, isLoadingData, dataSource }) { }, { key: 'accession', - content: accession, - title: accession, + content: {accession}, gridCol: 3, }, {