@spaced-out/ui-design-system
Version:
Sense UI components library
207 lines (187 loc) • 5.16 kB
Flow
// @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>
)}
</>
);
};