UNPKG

@zendeskgarden/container-tooltip

Version:

Containers relating to tooltip in the Garden Design System

149 lines (144 loc) 4.65 kB
/** * Copyright Zendesk, Inc. * * Use of this source code is governed under the Apache License, Version 2.0 * found at http://www.apache.org/licenses/LICENSE-2.0. */ import React, { useState, useRef, useCallback, useEffect, useMemo } from 'react'; import { useId, composeEventHandlers, KEYS } from '@zendeskgarden/container-utilities'; import PropTypes from 'prop-types'; const useTooltip = ({ delayMilliseconds = 500, id, isLabel, isVisible, triggerRef }) => { const _id = useId(id); const [visibility, setVisibility] = useState(isVisible); const isMounted = useRef(false); const openTooltipTimeoutId = useRef(); const closeTooltipTimeoutId = useRef(); const isTriggerPopupExpanded = useRef(false); const openTooltip = useCallback((delayMs = delayMilliseconds) => { if (isTriggerPopupExpanded.current) return; clearTimeout(closeTooltipTimeoutId.current); const timerId = setTimeout(() => { if (isMounted.current && !isTriggerPopupExpanded.current ) { setVisibility(true); } }, delayMs); openTooltipTimeoutId.current = Number(timerId); }, [delayMilliseconds]); const closeTooltip = useCallback((delayMs = delayMilliseconds) => { clearTimeout(openTooltipTimeoutId.current); const timerId = setTimeout(() => { if (isMounted.current) { setVisibility(false); } }, delayMs); closeTooltipTimeoutId.current = Number(timerId); }, [delayMilliseconds]); useEffect(() => { isMounted.current = true; return () => { isMounted.current = false; }; }, []); useEffect(() => { return () => { clearTimeout(openTooltipTimeoutId.current); clearTimeout(closeTooltipTimeoutId.current); }; }, [closeTooltipTimeoutId, openTooltipTimeoutId]); useEffect(() => { const triggerElement = triggerRef?.current?.getAttribute('aria-haspopup') === 'true' ? triggerRef.current : null; const handleTriggerPopupChange = () => { const isExpanded = triggerElement?.getAttribute('aria-expanded') === 'true'; if (triggerElement && isExpanded) { setVisibility(false); } isTriggerPopupExpanded.current = isExpanded; }; const mutationObserver = new MutationObserver(handleTriggerPopupChange); if (triggerElement) { mutationObserver.observe(triggerElement, { attributes: true, attributeFilter: ['aria-expanded'] }); } handleTriggerPopupChange(); return () => mutationObserver.disconnect(); }, [triggerRef]); const getTriggerProps = useCallback(({ tabIndex = 0, onMouseEnter, onMouseLeave, onFocus, onBlur, onKeyDown, ...other } = {}) => ({ tabIndex, onMouseEnter: composeEventHandlers(onMouseEnter, () => openTooltip()), onMouseLeave: composeEventHandlers(onMouseLeave, () => closeTooltip()), onFocus: composeEventHandlers(onFocus, event => { if (event.currentTarget.matches(':focus-visible')) { openTooltip(); } }), onBlur: composeEventHandlers(onBlur, () => closeTooltip(0)), onKeyDown: composeEventHandlers(onKeyDown, event => { if (event.key === KEYS.ESCAPE && visibility) { closeTooltip(0); } }), [isLabel ? 'aria-labelledby' : 'aria-describedby']: _id, 'data-garden-container-id': 'containers.tooltip', 'data-garden-container-version': '2.2.2', ref: triggerRef, ...other }), [_id, isLabel, closeTooltip, openTooltip, triggerRef, visibility]); const getTooltipProps = useCallback(({ role = 'tooltip', onMouseEnter, onMouseLeave, ...other } = {}) => ({ role, onMouseEnter: composeEventHandlers(onMouseEnter, () => openTooltip()), onMouseLeave: composeEventHandlers(onMouseLeave, () => closeTooltip()), 'aria-hidden': !visibility, id: _id, ...other }), [_id, closeTooltip, openTooltip, visibility]); return useMemo(() => ({ isVisible: visibility, getTooltipProps, getTriggerProps, openTooltip, closeTooltip }), [closeTooltip, getTooltipProps, getTriggerProps, openTooltip, visibility]); }; const TooltipContainer = props => { const { children, render = children, ...options } = props; return React.createElement(React.Fragment, null, render(useTooltip(options))); }; TooltipContainer.defaultProps = { delayMilliseconds: 500 }; TooltipContainer.propTypes = { children: PropTypes.func, render: PropTypes.func, delayMilliseconds: PropTypes.number, id: PropTypes.string, isLabel: PropTypes.bool, isVisible: PropTypes.bool, triggerRef: PropTypes.any.isRequired }; export { TooltipContainer, useTooltip };