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:
parent
8c30b21471
commit
8ca3769ba1
@ -74,6 +74,7 @@ export {
|
||||
ThumbnailList,
|
||||
ToolbarButton,
|
||||
Tooltip,
|
||||
TooltipClipboard,
|
||||
Typography,
|
||||
Viewport,
|
||||
ViewportActionBar,
|
||||
|
||||
1
platform/ui/src/assets/icons/clipboard.svg
Normal file
1
platform/ui/src/assets/icons/clipboard.svg
Normal 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 |
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
155
platform/ui/src/components/TooltipClipboard/TooltipClipboard.jsx
Normal file
155
platform/ui/src/components/TooltipClipboard/TooltipClipboard.jsx
Normal 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;
|
||||
@ -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} />
|
||||
1
platform/ui/src/components/TooltipClipboard/index.js
Normal file
1
platform/ui/src/components/TooltipClipboard/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './TooltipClipboard';
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
},
|
||||
{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user