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 ( +