nishant-design-system
Version:
Sense UI components library
176 lines (161 loc) • 4.41 kB
Flow
// @flow strict
import * as React from 'react';
// $FlowFixMe[untyped-import]
import {createPortal} from 'react-dom';
import {
// $FlowFixMe[untyped-import]
autoUpdate,
// $FlowFixMe[untyped-import]
flip,
// $FlowFixMe[untyped-import]
offset,
// $FlowFixMe[untyped-import]
shift,
// $FlowFixMe[untyped-import]
useDismiss,
// $FlowFixMe[untyped-import]
useFloating,
// $FlowFixMe[untyped-import]
useFocus,
// $FlowFixMe[untyped-import]
useHover,
// $FlowFixMe[untyped-import]
useInteractions,
// $FlowFixMe[untyped-import]
useRole,
} from '@floating-ui/react-dom-interactions';
import * as ELEVATION from '../../styles/variables/_elevation';
import * as MOTION from '../../styles/variables/_motion';
import {spaceNone, spaceXXSmall} from '../../styles/variables/_space';
import {classify} from '../../utils/classify';
import {mergeRefs} from '../../utils/merge-refs';
import {capitalize} from '../../utils/string';
import {Truncate} from '../Truncate';
import css from './Tooltip.module.css';
/* eslint-disable flowtype/no-weak-types */
type ClassNames = $ReadOnly<{tooltip?: string, title?: string, body?: string}>;
export const DELAY_MOTION_DURATION_TYPES = Object.freeze({
none: 'none',
fast: 'fast',
normal: 'normal',
slow: 'slow',
slower: 'slower',
slowest: 'slowest',
});
export type DelayMotionDurationType = $Values<
typeof DELAY_MOTION_DURATION_TYPES,
>;
export type PlacementType =
| 'top'
| 'top-start'
| 'top-end'
| 'bottom'
| 'bottom-start'
| 'bottom-end'
| 'left'
| 'right';
export type TooltipProps = {
classNames?: ClassNames,
title?: string | React.Node,
body?: string | React.Node,
placement?: PlacementType,
bodyMaxLines?: number,
titleMaxLines?: number,
delayMotionDuration?: DelayMotionDurationType,
// TODO(Nishant): Decide on a type to use here
// $FlowFixMe
children: any,
elevation?: string,
};
export const Tooltip = ({
classNames,
children,
title,
body,
placement = 'top',
bodyMaxLines = 2,
titleMaxLines = 1,
delayMotionDuration = 'none',
elevation = ELEVATION['elevationTooltip'],
}: TooltipProps): React.Node => {
const bodyRef = React.useRef(document.querySelector('body'));
const [open, setOpen] = React.useState(false);
const {x, y, reference, floating, strategy, context} = useFloating({
placement,
open,
onOpenChange: setOpen,
middleware: [offset(parseInt(spaceXXSmall)), flip(), shift()],
whileElementsMounted: autoUpdate,
});
const motionDurationToken =
'motionDuration' + capitalize(delayMotionDuration);
const hoverDelay =
parseInt(MOTION[motionDurationToken]) === NaN
? 0
: parseInt(MOTION[motionDurationToken]);
const {getReferenceProps, getFloatingProps} = useInteractions([
useHover(context, {
delay: {
open: hoverDelay,
close: 0,
},
}),
useFocus(context),
useRole(context, {role: 'tooltip'}),
useDismiss(context),
]);
// Note(Nishant) Preserve the consumer's ref
const ref = React.useMemo(
() => mergeRefs([reference, children.ref]),
[reference, children],
);
return (
<>
{React.cloneElement(
children,
getReferenceProps({
ref,
...children.props,
}),
)}
{open &&
createPortal(
<div
ref={floating}
className={classify(css.tooltip, classNames?.tooltip)}
style={{
position: strategy,
top: y ?? spaceNone,
left: x ?? spaceNone,
'--tooltip-elevation': elevation,
}}
{...getFloatingProps()}
>
{!!title && (
<Truncate line={titleMaxLines}>
<div className={classify(css.title, classNames?.title)}>
{title}
</div>
</Truncate>
)}
{!!body && (
<Truncate line={bodyMaxLines}>
<div
className={classify(
css.body,
{
[css.hasTitle]: !!title,
},
classNames?.body,
)}
>
{body}
</div>
</Truncate>
)}
</div>,
bodyRef.current,
)}
</>
);
};