UNPKG

@spaced-out/ui-design-system

Version:
207 lines (187 loc) 5.16 kB
// @flow strict import * as React from 'react'; import { // $FlowFixMe[untyped-import] autoUpdate, // $FlowFixMe[untyped-import] flip, // $FlowFixMe[untyped-import] FloatingPortal, // $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] useMergeRefs, // $FlowFixMe[untyped-import] useRole, } from '@floating-ui/react'; import * as ELEVATION from '../../styles/variables/_elevation'; import * as MOTION from '../../styles/variables/_motion'; import {spaceXXSmall} from '../../styles/variables/_space'; import {classify} from '../../utils/classify'; 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 const ELEVATION_TYPES = Object.freeze({ none: 'none', card: 'card', tooltip: 'tooltip', menu: 'menu', backdrop: 'backdrop', modal: 'modal', toast: 'toast', }); export type ElevationType = $Values<typeof ELEVATION_TYPES>; export type PlacementType = | 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'right'; export type BaseTooltipProps = { classNames?: ClassNames, title?: string | React.Node, body?: string | React.Node, placement?: PlacementType, bodyMaxLines?: number, titleMaxLines?: number, delayMotionDuration?: DelayMotionDurationType, hidden?: boolean, elevation?: ElevationType, }; export type TooltipProps = { ...BaseTooltipProps, // TODO(Nishant): Decide on a type to use here // $FlowFixMe children: any, }; export const getElevationValue = (elevation: string): string => { const elevationKey = 'elevation' + capitalize(elevation); return ELEVATION[elevationKey] || elevation; }; export const Tooltip = ({ classNames, children, title, body, placement = 'top', bodyMaxLines = 2, titleMaxLines = 1, delayMotionDuration = 'none', elevation = 'tooltip', hidden, }: TooltipProps): React.Node => { const [isOpen, setIsOpen] = React.useState(false); const {refs, floatingStyles, context, strategy} = useFloating({ open: isOpen, onOpenChange: setIsOpen, placement, // Make sure the tooltip stays on the screen whileElementsMounted: autoUpdate, middleware: [ offset(parseInt(spaceXXSmall)), flip({ fallbackAxisSideDirection: 'start', }), shift(), ], }); 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 (React 19 safe) const mergedRef = useMergeRefs([refs.setReference, children.ref ?? null]); return ( <> {React.cloneElement( children, getReferenceProps({ ...children.props, ref: mergedRef, }), )} {isOpen && ( <FloatingPortal> <> {!hidden && ( <div ref={refs.setFloating} className={classify(css.tooltip, classNames?.tooltip)} style={{ position: strategy, ...floatingStyles, '--tooltip-elevation': getElevationValue(elevation), }} {...getFloatingProps()} > {!!title && ( <Truncate line={titleMaxLines} wordBreak="initial"> <div className={classify(css.title, classNames?.title)}> {title} </div> </Truncate> )} {!!body && ( <Truncate line={bodyMaxLines} wordBreak="initial"> <div className={classify( css.body, { [css.hasTitle]: !!title, }, classNames?.body, )} > {body} </div> </Truncate> )} </div> )} </> </FloatingPortal> )} </> ); };