UNPKG

@itwin/itwinui-react

Version:

A react component library for iTwinUI

240 lines (239 loc) 5.91 kB
import * as React from 'react'; import cx from 'classnames'; import { useFloating, autoUpdate, offset, flip, shift, useHover, useFocus, useDismiss, useInteractions, safePolygon, size, autoPlacement, hide, inline, useDelayGroup, } from '@floating-ui/react'; import { Box, Portal, cloneElementWithRef, useControlledState, useId, useMergedRefs, } from '../../utils/index.js'; export const defaultTooltipDelay = { open: 200, close: 200, }; let useTooltip = (options = {}) => { let uniqueId = useId(); let { placement = 'top', visible, onVisibleChange, middleware = { flip: true, shift: true, }, autoUpdateOptions = {}, reference, ariaStrategy = 'description', id = uniqueId, ...props } = options; let [open, onOpenChange] = useControlledState( false, visible, onVisibleChange, ); let syncWithControlledState = React.useCallback( (element) => { queueMicrotask(() => { try { element?.togglePopover?.(open); } catch {} }); }, [open], ); let floating = useFloating({ placement, open, onOpenChange, strategy: 'fixed', whileElementsMounted: React.useMemo( () => open ? (...args) => autoUpdate(...args, autoUpdateOptions) : void 0, [autoUpdateOptions, open], ), middleware: React.useMemo( () => [ void 0 !== middleware.offset ? offset(middleware.offset) : offset(4), middleware.flip && flip({ padding: 4, }), middleware.shift && shift({ padding: 4, }), middleware.size && size({ padding: 4, }), middleware.autoPlacement && autoPlacement({ padding: 4, }), middleware.inline && inline(), middleware.hide && hide({ padding: 4, }), ].filter(Boolean), [middleware], ), ...(reference && { elements: { reference, }, }), }); let ariaProps = React.useMemo( () => 'description' === ariaStrategy ? { 'aria-describedby': id, } : 'label' === ariaStrategy ? { 'aria-labelledby': id, } : {}, [ariaStrategy, id], ); let { delay } = useDelayGroup(floating.context, { id: useId(), }); let interactions = useInteractions([ useHover(floating.context, { delay: 0 !== delay ? delay : defaultTooltipDelay, handleClose: safePolygon({ buffer: -1 / 0, }), move: false, }), useFocus(floating.context), useDismiss(floating.context, { referencePress: true, referencePressEvent: 'click', }), ]); React.useEffect(() => { if (!reference) return; let domEventName = (e) => e.toLowerCase().substring(2); let cleanupValues = {}; Object.entries({ ...ariaProps, ...interactions.getReferenceProps(), }).forEach(([key, value]) => { if ('function' == typeof value) { let patchedHandler = (event) => { value({ ...event, nativeEvent: event, }); }; reference.addEventListener(domEventName(key), patchedHandler); cleanupValues[key] = patchedHandler; } else if (value) { cleanupValues[key] = reference.getAttribute(key); reference.setAttribute(key, value); } }); return () => { Object.entries(cleanupValues).forEach(([key, value]) => { if ('function' == typeof value) reference.removeEventListener(domEventName(key), value); else if (value) reference.setAttribute(key, value); else reference.removeAttribute(key); }); }; }, [ariaProps, reference, interactions]); let getReferenceProps = React.useCallback( (userProps) => interactions.getReferenceProps({ ...userProps, ...ariaProps, }), [interactions, ariaProps], ); let floatingProps = React.useMemo( () => ({ ...interactions.getFloatingProps({ hidden: !open, 'aria-hidden': 'true', ...props, id, }), popover: 'manual', }), [interactions, props, id, open], ); return React.useMemo( () => ({ getReferenceProps, floatingProps, ...floating, refs: { ...floating.refs, setFloating: (element) => { floating.refs.setFloating(element); syncWithControlledState(element); }, }, floatingStyles: floating.context.open ? floating.floatingStyles : {}, }), [getReferenceProps, floatingProps, floating, syncWithControlledState], ); }; export const Tooltip = React.forwardRef((props, forwardedRef) => { let { content, children, portal = true, className, style, ...rest } = props; let tooltip = useTooltip(rest); let refs = useMergedRefs(tooltip.refs.setFloating, forwardedRef); return React.createElement( React.Fragment, null, cloneElementWithRef(children, (children) => ({ ...tooltip.getReferenceProps(children.props), ref: tooltip.refs.setReference, })), 'none' !== props.ariaStrategy || tooltip.context.open ? React.createElement( Portal, { portal: portal, }, React.createElement( Box, { className: cx('iui-tooltip', className), ref: refs, style: { ...tooltip.floatingStyles, ...style, }, ...tooltip.floatingProps, }, content, ), ) : null, ); }); if ('development' === process.env.NODE_ENV) Tooltip.displayName = 'Tooltip';