mdc-react
Version:
Material Components for the web implemented in React
146 lines (121 loc) • 4.97 kB
JSX
import { Children, forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
import classnames from 'classnames';
import { Clone } from '../component';
import Layer from '../layer';
import { numbers, cssClasses } from './constants';
const RichTooltip = forwardRef(({
title,
content,
actions,
element: Element = 'div',
className,
children
}, ref) => {
const tooltipRef = useRef();
const targetRef = useRef();
const [active, setActive] = useState(false);
useImperativeHandle(ref, () => tooltipRef.current);
useEffect(() => {
function handleScroll() {
setActive(active => active && !active);
}
document.addEventListener('scroll', handleScroll);
return () => {
document.removeEventListener('scroll', handleScroll);
};
}, []);
const handleTargetMouseEnter = useCallback(() => {
setActive(true);
}, []);
const handleTargetMouseLeave = useCallback(() => {
setActive(false);
}, []);
const handleTooltipMouseEnter = useCallback(() => {
setActive(true);
}, []);
const handleTooltipMouseLeave = useCallback(() => {
setActive(false);
}, []);
const handleEntering = useCallback(() => {
const tooltipRect = tooltipRef.current.getBoundingClientRect();
const targetRect = targetRef.current.getBoundingClientRect();
const width = tooltipRef.current.firstChild.clientWidth;
const height = tooltipRef.current.firstChild.scrollHeight;
const top = targetRect.bottom + window.scrollY + numbers.UNBOUNDED_ANCHOR_GAP;
const left = targetRect.right + window.scrollX + numbers.UNBOUNDED_ANCHOR_GAP;
const right = left + width + numbers.MIN_VIEWPORT_TOOLTIP_THRESHOLD;
const bottom = top + height + numbers.MIN_VIEWPORT_TOOLTIP_THRESHOLD;
const overflowBottom = window.innerHeight - bottom;
const overflowRight = window.innerWidth - right;
if (overflowBottom < 0) {
tooltipRef.current.style.top = `${top + overflowBottom}px`;
} else {
tooltipRef.current.style.top = `${top}px`;
}
if (overflowRight < 0) {
tooltipRef.current.style.left = `${targetRect.right - tooltipRect.width}px`;
} else {
tooltipRef.current.style.left = `${left < 0 ? targetRect.left : left}px`;
}
tooltipRef.current.getBoundingClientRect();
}, []);
const classNames = classnames(cssClasses.ROOT, cssClasses.RICH, className);
return (<>
<Clone
component={children}
ref={targetRef}
onMouseEnter={handleTargetMouseEnter}
onMouseLeave={handleTargetMouseLeave}
/>
<Layer
modal
in={active}
timeout={{
enter: numbers.SHOW_DELAY_MS,
exit: numbers.HIDE_DELAY_MS
}}
classNames={{
enter: cssClasses.SHOWING_TRANSITION,
enterActive: `${cssClasses.SHOWING_TRANSITION} ${cssClasses.SHOWING}`,
enterDone: `${cssClasses.SHOWING_TRANSITION} ${cssClasses.SHOWN}`,
exit: cssClasses.SHOWN,
exitActive: `${cssClasses.HIDE_TRANSITION} ${cssClasses.SHOWN}`,
exitDone: `${cssClasses.HIDE_TRANSITION} ${cssClasses.HIDE}`
}}
mountOnEnter
unmountOnExit
onEntering={handleEntering}
>
<Element
ref={tooltipRef}
className={classNames}
role="tooltip"
aria-hidden={active ? undefined : 'true'}
aria-expanded={active ? 'true' : undefined}
onMouseEnter={handleTooltipMouseEnter}
onMouseLeave={handleTooltipMouseLeave}
>
<div className={`${cssClasses.SURFACE} ${cssClasses.SURFACE_ANIMATION}`}>
{title &&
<h2 className={cssClasses.TITLE}>{title}</h2>
}
{content &&
<p className={cssClasses.CONTENT}>{content}</p>
}
{actions &&
<div className={cssClasses.ACTIONS}>
{Children.map(actions, action =>
<Clone
component={action}
aria-label="action"
/>
)}
</div>
}
</div>
</Element>
</Layer>
</>);
});
RichTooltip.displayName = 'MDCRichTooltip';
export default RichTooltip;