UNPKG

nishant-design-system

Version:
176 lines (161 loc) 4.41 kB
// @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, )} </> ); };