(function initTooltipOverlayModule() { let tooltipId = 0; const states = new WeakMap(); const tooltipStates = new Set(); const getViewport = () => ({ width: window.visualViewport?.width || window.innerWidth, height: window.visualViewport?.height || window.innerHeight, }); const getPixelValue = (propertyName) => { const rootStyles = getComputedStyle(document.documentElement); const rawValue = rootStyles.getPropertyValue(propertyName).trim(); const rootFontSize = parseFloat(rootStyles.fontSize) || 16; const value = parseFloat(rawValue); if (Number.isNaN(value)) { return 0; } if (rawValue.endsWith('rem')) { return value * rootFontSize; } return value; }; const getElements = (wrap) => ({ trigger: wrap.querySelector('[data-component-part="tooltip-trigger"]'), panel: wrap.querySelector('[data-component-part="tooltip-panel"]'), }); const ensureAccessibility = (state) => { const { trigger, panel } = state; panel.setAttribute('role', 'tooltip'); if (!panel.id) { tooltipId += 1; panel.id = `sg-tooltip-panel-${tooltipId}`; } const describedBy = (trigger.getAttribute('aria-describedby') || '') .split(/\s+/) .filter(Boolean); if (!describedBy.includes(panel.id)) { trigger.setAttribute('aria-describedby', [...describedBy, panel.id].join(' ')); } const hasAccessibleName = Boolean( trigger.getAttribute('aria-label') || trigger.getAttribute('aria-labelledby') || trigger.getAttribute('title') || trigger.textContent.trim(), ); if (!hasAccessibleName && panel.textContent.trim()) { trigger.setAttribute('aria-label', panel.textContent.trim()); } if (!trigger.matches('a,button,input,select,textarea,[tabindex]')) { trigger.setAttribute('tabindex', '0'); } }; const setOpenState = (state, isOpen) => { const { wrap, panel, trigger } = state; wrap.dataset.tooltipOpen = String(isOpen); panel.hidden = !isOpen; if (trigger.matches('button,[role="button"],[aria-expanded]')) { trigger.setAttribute('aria-expanded', String(isOpen)); } }; const restorePanel = (state) => { const { panel, placeholder } = state; panel.hidden = true; panel.removeAttribute('data-tooltip-portal'); panel.removeAttribute('data-tooltip-placement'); panel.style.removeProperty('left'); panel.style.removeProperty('top'); panel.style.removeProperty('visibility'); panel.style.removeProperty('--tooltip-arrow-offset'); if (placeholder.parentNode) { placeholder.parentNode.insertBefore(panel, placeholder.nextSibling); } }; const movePanelToPortal = (state) => { const { panel } = state; if (panel.parentNode !== document.body) { document.body.appendChild(panel); } panel.setAttribute('data-tooltip-portal', 'true'); panel.hidden = false; panel.style.visibility = 'hidden'; }; const setArrowOffset = (state, placement, panelLeft, panelTop, panelWidth, panelHeight) => { const { trigger, panel } = state; const triggerRect = trigger.getBoundingClientRect(); const offset = placement === 'left' || placement === 'right' ? triggerRect.top + (triggerRect.height / 2) - panelTop : triggerRect.left + (triggerRect.width / 2) - panelLeft; const maxOffset = placement === 'left' || placement === 'right' ? panelHeight : panelWidth; panel.style.setProperty('--tooltip-arrow-offset', `${Math.max(0, Math.min(offset, maxOffset))}px`); }; const positionTooltip = (state) => { const { trigger, panel } = state; if (panel.hidden || panel.parentNode !== document.body) { return; } const viewport = getViewport(); const safeInset = getPixelValue('--spacing-large'); const gap = getPixelValue('--spacing-small'); const triggerRect = trigger.getBoundingClientRect(); const panelWidth = panel.offsetWidth; const panelHeight = panel.offsetHeight; const centeredLeft = triggerRect.left + (triggerRect.width / 2) - (panelWidth / 2); const centeredRight = centeredLeft + panelWidth; const hasBottomSpace = triggerRect.bottom + gap + panelHeight <= viewport.height - safeInset; const hasTopSpace = triggerRect.top - gap - panelHeight >= safeInset; const hasLeftSpace = triggerRect.left - gap - panelWidth >= safeInset; const hasRightSpace = triggerRect.right + gap + panelWidth <= viewport.width - safeInset; let placement = 'bottom'; if (centeredRight > viewport.width - safeInset && hasLeftSpace) { placement = 'left'; } else if (centeredLeft < safeInset && hasRightSpace) { placement = 'right'; } else if (!hasBottomSpace && hasTopSpace) { placement = 'top'; } let panelLeft = centeredLeft; let panelTop = triggerRect.bottom + gap; if (placement === 'top') { panelTop = triggerRect.top - gap - panelHeight; } else if (placement === 'left') { panelLeft = triggerRect.left - gap - panelWidth; panelTop = triggerRect.top + (triggerRect.height / 2) - (panelHeight / 2); } else if (placement === 'right') { panelLeft = triggerRect.right + gap; panelTop = triggerRect.top + (triggerRect.height / 2) - (panelHeight / 2); } panelLeft = Math.max(safeInset, Math.min(panelLeft, viewport.width - safeInset - panelWidth)); panelTop = Math.max(safeInset, Math.min(panelTop, viewport.height - safeInset - panelHeight)); panel.dataset.tooltipPlacement = placement; state.wrap.dataset.tooltipPlacement = placement; panel.style.left = `${Math.round(panelLeft)}px`; panel.style.top = `${Math.round(panelTop)}px`; setArrowOffset(state, placement, panelLeft, panelTop, panelWidth, panelHeight); panel.style.visibility = 'visible'; }; const openTooltip = (state) => { tooltipStates.forEach((otherState) => { if (otherState !== state) { closeTooltip(otherState); } }); movePanelToPortal(state); setOpenState(state, true); positionTooltip(state); }; const closeTooltip = (state) => { state.pinned = false; state.hoverTrigger = false; state.hoverPanel = false; state.focused = false; state.dismissed = false; setOpenState(state, false); restorePanel(state); }; const closeIfInactive = (state) => { if (!state.pinned && !state.hoverTrigger && !state.hoverPanel && !state.focused) { setOpenState(state, false); restorePanel(state); } }; const togglePinned = (state) => { if (state.pinned) { state.pinned = false; state.dismissed = true; setOpenState(state, false); restorePanel(state); return; } state.pinned = true; state.dismissed = false; openTooltip(state); }; const initTooltip = (wrap) => { if (states.has(wrap)) { return; } const elements = getElements(wrap); if (!elements.trigger || !elements.panel || !elements.panel.parentNode) { return; } const placeholder = document.createComment('sg-tooltip-panel-placeholder'); elements.panel.parentNode.insertBefore(placeholder, elements.panel); const state = { wrap, trigger: elements.trigger, panel: elements.panel, placeholder, pinned: false, hoverTrigger: false, hoverPanel: false, focused: false, dismissed: false, }; wrap.dataset.tooltipInit = 'true'; wrap.dataset.tooltipPlacement = 'bottom'; states.set(wrap, state); tooltipStates.add(state); ensureAccessibility(state); setOpenState(state, false); wrap.addEventListener('pointerenter', (event) => { if (event.pointerType !== 'mouse') { return; } if (state.dismissed) { return; } state.hoverTrigger = true; openTooltip(state); }); wrap.addEventListener('pointerleave', (event) => { if (event.pointerType !== 'mouse') { return; } state.hoverTrigger = false; state.dismissed = false; if (!state.panel.contains(event.relatedTarget)) { closeIfInactive(state); } }); state.panel.addEventListener('pointerenter', (event) => { if (event.pointerType !== 'mouse') { return; } if (!state.dismissed) { state.hoverPanel = true; openTooltip(state); } }); state.panel.addEventListener('pointerleave', (event) => { if (event.pointerType !== 'mouse') { return; } state.hoverPanel = false; state.dismissed = false; closeIfInactive(state); }); wrap.addEventListener('focusin', () => { if (!state.dismissed) { state.focused = true; openTooltip(state); } }); wrap.addEventListener('focusout', (event) => { if (wrap.contains(event.relatedTarget) || state.panel.contains(event.relatedTarget)) { return; } state.focused = false; state.dismissed = false; closeIfInactive(state); }); state.trigger.addEventListener('click', (event) => { togglePinned(state); }); state.trigger.addEventListener('keydown', (event) => { const isNativeInteractive = state.trigger.matches('a,button,input,select,textarea'); if (!isNativeInteractive && (event.key === 'Enter' || event.key === ' ')) { event.preventDefault(); togglePinned(state); } }); }; const initializeWithin = (root) => { if (root.nodeType === Node.DOCUMENT_NODE) { root.querySelectorAll('.sg-tooltip-wrap').forEach(initTooltip); return; } if (root.nodeType !== Node.ELEMENT_NODE) { return; } const owningWrap = root.closest('.sg-tooltip-wrap'); if (owningWrap) { initTooltip(owningWrap); } if (root.matches('.sg-tooltip-wrap')) { initTooltip(root); } root.querySelectorAll('.sg-tooltip-wrap').forEach(initTooltip); }; const bootstrap = () => { window.sgInitTooltips = (options = {}) => { const root = options.root || document.body; initializeWithin(root); }; window.sgInitTooltips(); const observer = new MutationObserver((records) => { records.forEach((record) => { record.addedNodes.forEach(initializeWithin); }); }); observer.observe(document.body, { childList: true, subtree: true }); const closeFromOutside = (event) => { tooltipStates.forEach((state) => { if (!state.wrap.contains(event.target) && !state.panel.contains(event.target)) { closeTooltip(state); } }); }; document.addEventListener('pointerdown', closeFromOutside, true); document.addEventListener('touchstart', closeFromOutside, true); document.addEventListener('click', closeFromOutside, true); const repositionOpenTooltips = () => { document.querySelectorAll('.sg-tooltip-wrap[data-tooltip-open="true"]').forEach((wrap) => { const state = states.get(wrap); if (state) { positionTooltip(state); } }); }; window.addEventListener('resize', repositionOpenTooltips); window.addEventListener('scroll', repositionOpenTooltips, true); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', bootstrap, { once: true }); } else { bootstrap(); } })();