import { OverlayContainer } from '@react-native-aria/overlays'; import React from 'react'; import { useControllableState, useKeyboardDismissable } from '../../../hooks'; import { Popper } from '../Popper'; import { composeEventHandlers, mergeRefs } from '../../../utils'; import { PresenceTransition } from '../Transitions'; import { Platform, StyleSheet } from 'react-native'; import { usePropsResolution } from '../../../hooks'; import Box, { IBoxProps } from '../../primitives/Box'; import { useId } from '@react-aria/utils'; interface ITooltipProps extends IBoxProps { /** * Text to be placed in the tooltip */ label: string; /** * Whether the tooltip is opened. Useful for conrolling the open state */ isOpen?: boolean; /** * Whether the tooltip is disabled */ isDisabled?: boolean; /** * If true, the popover will be opened by default */ defaultIsOpen?: boolean; /** * This function will be invoked when tooltip is closed. It'll also be called when user attempts to close the tooltip via Escape key */ onClose?: () => void; /** * This function will be invoked when tooltip is opened */ onOpen?: () => void; /** * Duration in ms to wait till displaying the tooltip * @default 0 */ openDelay?: number; /** * Duration in ms to wait till hiding the tooltip * @default 0 */ closeDelay?: number; /** * Tooltip placement * @default bottom */ placement?: | 'top' | 'bottom' | 'left' | 'right' | 'top left' | 'top right' | 'bottom left' | 'bottom right' | 'right top' | 'right bottom' | 'left top' | 'left bottom'; /** * Children passed will be used as Trigger element for the tooltip */ children: any; /** * Whether tooltip should be closed on Trigger click * @default true */ closeOnClick?: boolean; /** * Size of the arrow * @default 12 */ arrowSize?: number; /** * Whether tooltip should display arrow * @default false */ hasArrow?: boolean; /** * Distance between the trigger and the tooltip */ offset?: number; } export const Tooltip = ({ label, children, onClose, onOpen, defaultIsOpen, placement, openDelay = 0, closeDelay = 0, closeOnClick = true, offset, isDisabled, hasArrow, arrowSize = 12, isOpen: isOpenProp, ...rest }: ITooltipProps) => { if (hasArrow && offset === undefined) { offset = 0; } else { offset = 6; } const themeProps = usePropsResolution('Tooltip', rest); const [isOpen, setIsOpen] = useControllableState({ value: isOpenProp, defaultValue: defaultIsOpen, onChange: (value) => { value ? onOpen && onOpen() : onClose && onClose(); }, }); const arrowBg = rest.backgroundColor ?? rest.bgColor ?? rest.bg ?? themeProps.bg; const targetRef = React.useRef(null); const enterTimeout = React.useRef(); const exitTimeout = React.useRef(); const tooltipID = useId(); const openWithDelay = React.useCallback(() => { if (!isDisabled) { enterTimeout.current = setTimeout(() => setIsOpen(true), openDelay); } }, [isDisabled, setIsOpen, openDelay]); const closeWithDelay = React.useCallback(() => { if (enterTimeout.current) { clearTimeout(enterTimeout.current); } exitTimeout.current = setTimeout(() => setIsOpen(false), closeDelay); }, [closeDelay, setIsOpen]); React.useEffect( () => () => { clearTimeout(enterTimeout.current); clearTimeout(exitTimeout.current); }, [] ); let newChildren = children; if (typeof children === 'string') { newChildren = {children}; } newChildren = React.cloneElement(newChildren, { 'onPress': composeEventHandlers(newChildren.props.onPress, () => { if (closeOnClick) { closeWithDelay(); } }), 'onFocus': composeEventHandlers( newChildren.props.onFocus, openWithDelay ), 'onBlur': composeEventHandlers( newChildren.props.onBlur, closeWithDelay ), 'onMouseEnter': composeEventHandlers( newChildren.props.onMouseEnter, openWithDelay ), 'onMouseLeave': composeEventHandlers( newChildren.props.onMouseLeave, closeWithDelay ), 'ref': mergeRefs([newChildren.ref, targetRef]), 'aria-describedby': isOpen ? tooltipID : undefined, }); useKeyboardDismissable({ enabled: isOpen, callback: () => setIsOpen(false), }); return ( <> {newChildren} {isOpen && ( setIsOpen(false)} placement={placement} offset={offset} > {hasArrow && ( )} {label} )} ); };