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 <erik.sweed@gmail.com>
This commit is contained in:
Rodrigo Antinarelli 2020-07-22 15:25:51 -03:00 committed by GitHub
parent 8c30b21471
commit 8ca3769ba1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 198 additions and 9 deletions

View File

@ -74,6 +74,7 @@ export {
ThumbnailList,
ToolbarButton,
Tooltip,
TooltipClipboard,
Typography,
Viewport,
ViewportActionBar,

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24"><path d="M6 22v-16h16v7.543c0 4.107-6 2.457-6 2.457s1.518 6-2.638 6h-7.362zm18-7.614v-10.386h-20v20h10.189c3.163 0 9.811-7.223 9.811-9.614zm-10 1.614h-5v-1h5v1zm5-4h-10v1h10v-1zm0-3h-10v1h10v-1zm2-7h-19v19h-2v-21h21v2z"/></svg>

After

Width:  |  Height:  |  Size: 308 B

View File

@ -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,

View File

@ -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;

View File

@ -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 (
<div
className={classnames('inline-flex max-w-full')}
onMouseOver={handleMouseOver}
onFocus={handleMouseOver}
onMouseOut={handleMouseOut}
onBlur={handleMouseOut}
role="tooltip"
ref={tooltipContainerRef}
>
<span className="truncate">{children}</span>
<div
className={classnames(`fixed pt-1`, {
block: isActive,
hidden: !isActive,
})}
ref={tooltipBoxRef}
onClick={onClickHandler}
>
<div
className={classnames(
'flex items-center relative bg-primary-dark border border-secondary-main text-white text-base rounded px-2 py-2'
)}
>
{message || (
<>
{children}
<div className="ml-2 pl-2 border-l border-secondary-light">
<Icon name="clipboard" className="w-4 text-white" />
</div>
</>
)}
</div>
</div>
</div>
);
};
TooltipClipboard.defaultProps = {
text: '',
};
TooltipClipboard.propTypes = {
text: PropTypes.string,
children: PropTypes.node.isRequired,
};
export default TooltipClipboard;

View File

@ -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';
```
<Playground>
<div className="p-4">
<TooltipClipboard>Text to be copied</TooltipClipboard>
</div>
); }}
</Playground>
## Properties
<Props of={TooltipClipboard} />

View File

@ -0,0 +1 @@
export { default } from './TooltipClipboard';

View File

@ -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,

View File

@ -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
<TooltipClipboard>{patientName}</TooltipClipboard>
) : (
<span className="text-gray-700">(Empty)</span>
),
title: patientName,
gridCol: 4,
},
{
key: 'mrn',
content: mrn,
title: mrn,
content: <TooltipClipboard>{mrn}</TooltipClipboard>,
gridCol: 3,
},
{
@ -239,8 +238,7 @@ function WorkList({ history, data: studies, isLoadingData, dataSource }) {
},
{
key: 'description',
content: description,
title: description,
content: <TooltipClipboard>{description}</TooltipClipboard>,
gridCol: 4,
},
{
@ -251,8 +249,7 @@ function WorkList({ history, data: studies, isLoadingData, dataSource }) {
},
{
key: 'accession',
content: accession,
title: accession,
content: <TooltipClipboard>{accession}</TooltipClipboard>,
gridCol: 3,
},
{