@zendeskgarden/container-tooltip
Version:
Containers relating to tooltip in the Garden Design System
149 lines (144 loc) • 4.65 kB
JavaScript
/**
* 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 };