UNPKG

@wix/design-system

Version:

@wix/design-system

110 lines 5.19 kB
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