feat: add generic tooltip component
This commit is contained in:
1 parent
71734d6c1d
commit
5b86185cae
5 files changed
+277
No files matched your search
@@ -0,0 +1,164 @@
|
||||
(function initTooltipOverlayModule() {
|
||||
const getViewportWidth = () => {
|
||||
if (window.visualViewport && typeof window.visualViewport.width === 'number') {
|
||||
return window.visualViewport.width;
|
||||
}
|
||||
return window.innerWidth;
|
||||
};
|
||||
|
||||
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 setTooltipState = (wrap, isOpen) => {
|
||||
const trigger = wrap.querySelector('[data-component-part="tooltip-trigger"]');
|
||||
const panel = wrap.querySelector('[data-component-part="tooltip-panel"]');
|
||||
if (!trigger || !panel) {
|
||||
return;
|
||||
}
|
||||
|
||||
wrap.dataset.tooltipOpen = String(isOpen);
|
||||
panel.hidden = !isOpen;
|
||||
trigger.setAttribute('aria-expanded', String(isOpen));
|
||||
};
|
||||
|
||||
const positionTooltip = (wrap) => {
|
||||
const trigger = wrap.querySelector('[data-component-part="tooltip-trigger"]');
|
||||
const panel = wrap.querySelector('[data-component-part="tooltip-panel"]');
|
||||
if (!trigger || !panel || panel.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewportWidth = getViewportWidth();
|
||||
const safeInset = getPixelValue('--spacing-large');
|
||||
const triggerRect = trigger.getBoundingClientRect();
|
||||
const panelRect = panel.getBoundingClientRect();
|
||||
const gap = getPixelValue('--spacing-small');
|
||||
|
||||
let placement = 'bottom';
|
||||
if (triggerRect.right + (panelRect.width / 2) > viewportWidth - safeInset) {
|
||||
placement = 'left';
|
||||
} else if (triggerRect.left - (panelRect.width / 2) < safeInset) {
|
||||
placement = 'right';
|
||||
}
|
||||
|
||||
wrap.dataset.tooltipPlacement = placement;
|
||||
|
||||
if (placement === 'bottom') {
|
||||
const nextPanelRect = panel.getBoundingClientRect();
|
||||
if (nextPanelRect.bottom > window.innerHeight - safeInset
|
||||
&& triggerRect.top - panelRect.height - gap >= safeInset) {
|
||||
wrap.dataset.tooltipPlacement = 'top';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const closeTooltip = (wrap) => {
|
||||
wrap.dataset.tooltipPinned = 'false';
|
||||
wrap.dataset.tooltipHover = 'false';
|
||||
wrap.dataset.tooltipFocus = 'false';
|
||||
wrap.dataset.tooltipDismissed = 'false';
|
||||
setTooltipState(wrap, false);
|
||||
};
|
||||
|
||||
const openTooltip = (wrap) => {
|
||||
setTooltipState(wrap, true);
|
||||
positionTooltip(wrap);
|
||||
};
|
||||
|
||||
const initTooltip = (wrap) => {
|
||||
if (wrap.dataset.tooltipInit === 'true') {
|
||||
return;
|
||||
}
|
||||
wrap.dataset.tooltipInit = 'true';
|
||||
wrap.dataset.tooltipPlacement = 'bottom';
|
||||
wrap.dataset.tooltipPinned = 'false';
|
||||
wrap.dataset.tooltipHover = 'false';
|
||||
wrap.dataset.tooltipFocus = 'false';
|
||||
wrap.dataset.tooltipDismissed = 'false';
|
||||
setTooltipState(wrap, false);
|
||||
|
||||
const trigger = wrap.querySelector('[data-component-part="tooltip-trigger"]');
|
||||
if (!trigger) {
|
||||
return;
|
||||
}
|
||||
|
||||
wrap.addEventListener('pointerenter', () => {
|
||||
if (wrap.dataset.tooltipDismissed === 'true') {
|
||||
return;
|
||||
}
|
||||
wrap.dataset.tooltipHover = 'true';
|
||||
openTooltip(wrap);
|
||||
});
|
||||
|
||||
wrap.addEventListener('pointerleave', () => {
|
||||
wrap.dataset.tooltipHover = 'false';
|
||||
wrap.dataset.tooltipDismissed = 'false';
|
||||
if (wrap.dataset.tooltipPinned !== 'true' && wrap.dataset.tooltipFocus !== 'true') {
|
||||
setTooltipState(wrap, false);
|
||||
}
|
||||
});
|
||||
|
||||
wrap.addEventListener('focusin', () => {
|
||||
if (wrap.dataset.tooltipDismissed === 'true') {
|
||||
return;
|
||||
}
|
||||
wrap.dataset.tooltipFocus = 'true';
|
||||
openTooltip(wrap);
|
||||
});
|
||||
|
||||
wrap.addEventListener('focusout', (event) => {
|
||||
if (wrap.contains(event.relatedTarget)) {
|
||||
return;
|
||||
}
|
||||
wrap.dataset.tooltipFocus = 'false';
|
||||
wrap.dataset.tooltipDismissed = 'false';
|
||||
if (wrap.dataset.tooltipPinned !== 'true' && wrap.dataset.tooltipHover !== 'true') {
|
||||
setTooltipState(wrap, false);
|
||||
}
|
||||
});
|
||||
|
||||
trigger.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
const isPinned = wrap.dataset.tooltipPinned === 'true';
|
||||
wrap.dataset.tooltipPinned = String(!isPinned);
|
||||
|
||||
if (isPinned) {
|
||||
wrap.dataset.tooltipDismissed = 'true';
|
||||
setTooltipState(wrap, false);
|
||||
return;
|
||||
}
|
||||
|
||||
wrap.dataset.tooltipDismissed = 'false';
|
||||
openTooltip(wrap);
|
||||
});
|
||||
};
|
||||
|
||||
document.querySelectorAll('.sg-tooltip-wrap').forEach(initTooltip);
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
document.querySelectorAll('.sg-tooltip-wrap').forEach((wrap) => {
|
||||
if (!wrap.contains(event.target)) {
|
||||
closeTooltip(wrap);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const repositionOpenTooltips = () => {
|
||||
document.querySelectorAll('.sg-tooltip-wrap[data-tooltip-open="true"]').forEach(positionTooltip);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', repositionOpenTooltips);
|
||||
window.addEventListener('scroll', repositionOpenTooltips, true);
|
||||
})();
|
||||
Reference in new issue
Block a user