@wix/design-system
Version:
@wix/design-system
110 lines • 5.19 kB
JavaScript
import React, { useRef, useCallback, useEffect, useState } from 'react';
import { InfoCircle, InfoCircleSmall } from '@wix/wix-ui-icons-common';
import { dataHooks } from './constants';
import Tooltip from '../Tooltip';
import { st, classes } from './InfoIcon.st.css.js';
import { useId } from '../utils/useId';
import { useIcons } from '../WixDesignSystemIconThemeProvider';
import { FOCUSABLE_SELECTOR } from './constants';
const InfoIcon = ({ dataHook, content, size = 'medium', tooltipProps, className, children, ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, }) => {
const icons = useIcons('InfoIcon', {
InfoCircle,
InfoCircleSmall,
});
const iconComponentBySizeMap = {
small: icons.InfoCircleSmall,
medium: icons.InfoCircle,
};
const Icon = iconComponentBySizeMap[size];
const tooltipRef = useRef(null);
const triggerRef = useRef(null);
const [isOpenedViaKeyboard, setIsOpenedViaKeyboard] = useState(false);
const autoId = useId();
const propAriaLabelledBy = tooltipProps?.['aria-labelledby'] ?? ariaLabelledBy;
const propAriaDescribedBy = tooltipProps?.['aria-describedby'] ?? ariaDescribedBy;
const tooltipId = propAriaLabelledBy || propAriaDescribedBy || `infoicon-tooltip-${autoId}`;
const mergedTooltipProps = {
...tooltipProps,
...(propAriaLabelledBy
? { 'aria-labelledby': propAriaLabelledBy }
: { 'aria-describedby': propAriaDescribedBy || tooltipId }),
};
const focusFirstInteractiveElement = useCallback(() => {
requestAnimationFrame(() => {
const tooltipContent = tooltipRef.current?.getContentElement();
const focusable = tooltipContent?.querySelector(FOCUSABLE_SELECTOR);
focusable?.focus({ preventScroll: true });
});
}, []);
const closeAndRefocus = useCallback(() => {
tooltipRef.current?.close();
setIsOpenedViaKeyboard(false);
triggerRef.current?.focus();
}, []);
// Handle when focus returns to trigger from tooltip content (e.g., via Tab)
const handleTriggerFocus = useCallback((event) => {
if (!isOpenedViaKeyboard)
return;
const relatedTarget = event.relatedTarget;
const tooltipContent = tooltipRef.current?.getContentElement();
// If focus came from inside the tooltip content, close the tooltip
if (tooltipContent?.contains(relatedTarget)) {
tooltipRef.current?.close();
setIsOpenedViaKeyboard(false);
}
}, [isOpenedViaKeyboard]);
const handleKeyDown = (event) => {
if (event.key === 'Escape') {
closeAndRefocus();
}
if (event.key === 'Enter') {
event.preventDefault();
tooltipRef.current?.open();
setIsOpenedViaKeyboard(true);
focusFirstInteractiveElement();
}
};
// Global escape key handler for when focus is inside the tooltip
useEffect(() => {
if (!isOpenedViaKeyboard)
return;
const handleGlobalKeyDown = (event) => {
if (event.key === 'Escape') {
event.preventDefault();
event.stopPropagation();
closeAndRefocus();
}
};
// Use capture phase to run before Tooltip's handler
document.addEventListener('keydown', handleGlobalKeyDown, true);
return () => {
document.removeEventListener('keydown', handleGlobalKeyDown, true);
};
}, [isOpenedViaKeyboard, closeAndRefocus]);
const handleBlur = useCallback((event) => {
// Only handle blur when tooltip was opened via keyboard
// Hover-opened tooltips are closed by Tooltip's own handleMouseLeave
if (!isOpenedViaKeyboard) {
return;
}
const relatedTarget = event.relatedTarget;
// Check if focus is moving to the trigger
if (triggerRef.current?.contains(relatedTarget)) {
return;
}
// Check if focus is moving to an element inside the tooltip content
const tooltipContent = tooltipRef.current?.getContentElement();
if (tooltipContent?.contains(relatedTarget)) {
return;
}
// Focus left both trigger and tooltip - close and refocus
requestAnimationFrame(() => {
closeAndRefocus();
});
}, [isOpenedViaKeyboard, closeAndRefocus]);
return (React.createElement("div", { className: st(classes.root, className), "data-size": size, "data-hook": dataHook, onBlur: handleBlur },
React.createElement(Tooltip, { ...mergedTooltipProps, content: content, dataHook: dataHooks.tooltip, ref: tooltipRef }, () => (React.createElement("div", { ref: triggerRef, className: classes.trigger, role: "button", tabIndex: 0, "aria-label": ariaLabel, "aria-haspopup": true, "aria-controls": tooltipId, onKeyDown: handleKeyDown, onFocus: handleTriggerFocus }, children ?? React.createElement(Icon, { className: classes.icon }))))));
};
InfoIcon.displayName = 'InfoIcon';
export default InfoIcon;
//# sourceMappingURL=InfoIcon.js.map