Sync styleguide 2026.05.18.1

This commit is contained in:
gitea_admin committed 2026-09-21 16:06:04 +02:00
1 parent 17e07d525b
commit 58c5912c30
5 files changed
+399 -102

No files matched your search

+271 -91
View File
@@ -1,10 +1,11 @@
(function initTooltipOverlayModule() {
const getViewportWidth = () => {
if (window.visualViewport && typeof window.visualViewport.width === 'number') {
return window.visualViewport.width;
}
return window.innerWidth;
};
let tooltipId = 0;
const states = new WeakMap();
const getViewport = () => ({
width: window.visualViewport?.width || window.innerWidth,
height: window.visualViewport?.height || window.innerHeight,
});
const getPixelValue = (propertyName) => {
const rootStyles = getComputedStyle(document.documentElement);
@@ -21,144 +22,323 @@
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;
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;
trigger.setAttribute('aria-expanded', String(isOpen));
if (trigger.matches('button,[role="button"],[aria-expanded]')) {
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) {
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 viewportWidth = getViewportWidth();
const viewport = getViewport();
const safeInset = getPixelValue('--spacing-large');
const triggerRect = trigger.getBoundingClientRect();
const panelRect = panel.getBoundingClientRect();
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 (triggerRect.right + (panelRect.width / 2) > viewportWidth - safeInset) {
if (centeredRight > viewport.width - safeInset && hasLeftSpace) {
placement = 'left';
} else if (triggerRect.left - (panelRect.width / 2) < safeInset) {
} else if (centeredLeft < safeInset && hasRightSpace) {
placement = 'right';
} else if (!hasBottomSpace && hasTopSpace) {
placement = 'top';
}
wrap.dataset.tooltipPlacement = placement;
let panelLeft = centeredLeft;
let panelTop = triggerRect.bottom + gap;
if (placement === 'bottom') {
const nextPanelRect = panel.getBoundingClientRect();
if (nextPanelRect.bottom > window.innerHeight - safeInset
&& triggerRect.top - panelRect.height - gap >= safeInset) {
wrap.dataset.tooltipPlacement = 'top';
}
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) => {
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 closeTooltip = (wrap) => {
wrap.dataset.tooltipPinned = 'false';
wrap.dataset.tooltipHover = 'false';
wrap.dataset.tooltipFocus = 'false';
wrap.dataset.tooltipDismissed = 'false';
setTooltipState(wrap, false);
};
const togglePinned = (state) => {
if (state.pinned) {
state.pinned = false;
state.dismissed = true;
setOpenState(state, false);
restorePanel(state);
return;
}
const openTooltip = (wrap) => {
setTooltipState(wrap, true);
positionTooltip(wrap);
state.pinned = true;
state.dismissed = false;
openTooltip(state);
};
const initTooltip = (wrap) => {
if (wrap.dataset.tooltipInit === 'true') {
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';
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;
}
states.set(wrap, state);
ensureAccessibility(state);
setOpenState(state, false);
wrap.addEventListener('pointerenter', () => {
if (wrap.dataset.tooltipDismissed === 'true') {
if (state.dismissed) {
return;
}
wrap.dataset.tooltipHover = 'true';
openTooltip(wrap);
state.hoverTrigger = true;
openTooltip(state);
});
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('pointerleave', (event) => {
state.hoverTrigger = false;
state.dismissed = false;
if (!state.panel.contains(event.relatedTarget)) {
closeIfInactive(state);
}
});
state.panel.addEventListener('pointerenter', () => {
if (!state.dismissed) {
state.hoverPanel = true;
openTooltip(state);
}
});
state.panel.addEventListener('pointerleave', () => {
state.hoverPanel = false;
state.dismissed = false;
closeIfInactive(state);
});
wrap.addEventListener('focusin', () => {
if (wrap.dataset.tooltipDismissed === 'true') {
return;
if (!state.dismissed) {
state.focused = true;
openTooltip(state);
}
wrap.dataset.tooltipFocus = 'true';
openTooltip(wrap);
});
wrap.addEventListener('focusout', (event) => {
if (wrap.contains(event.relatedTarget)) {
if (wrap.contains(event.relatedTarget) || state.panel.contains(event.relatedTarget)) {
return;
}
wrap.dataset.tooltipFocus = 'false';
wrap.dataset.tooltipDismissed = 'false';
if (wrap.dataset.tooltipPinned !== 'true' && wrap.dataset.tooltipHover !== 'true') {
setTooltipState(wrap, false);
}
state.focused = false;
state.dismissed = false;
closeIfInactive(state);
});
trigger.addEventListener('click', (event) => {
state.trigger.addEventListener('click', (event) => {
event.stopPropagation();
const isPinned = wrap.dataset.tooltipPinned === 'true';
wrap.dataset.tooltipPinned = String(!isPinned);
togglePinned(state);
});
if (isPinned) {
wrap.dataset.tooltipDismissed = 'true';
setTooltipState(wrap, false);
return;
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);
}
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);
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);
};
window.addEventListener('resize', repositionOpenTooltips);
window.addEventListener('scroll', repositionOpenTooltips, true);
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 });
document.addEventListener('click', (event) => {
document.querySelectorAll('.sg-tooltip-wrap').forEach((wrap) => {
const state = states.get(wrap);
if (state && !wrap.contains(event.target) && !state.panel.contains(event.target)) {
closeTooltip(state);
}
});
});
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();
}
})();
@@ -1240,9 +1240,7 @@
}
.sg-tooltip-wrap {
position: relative;
display: inline-flex;
vertical-align: middle;
display: contents;
}
.sg-tooltip-panel {
@@ -1328,6 +1326,47 @@
transform: translateY(-50%);
}
.sg-tooltip-panel[data-tooltip-portal="true"] {
position: fixed;
top: 0;
left: 0;
z-index: var(--layer-pulldown-panel);
max-width: calc(100vw - (2 * var(--spacing-large)));
transform: none;
}
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="bottom"]::before {
top: calc(var(--spacing-small) * -1);
left: var(--tooltip-arrow-offset);
border-width: 0 var(--spacing-small) var(--spacing-small);
border-color: transparent transparent var(--surface-tooltip);
transform: translateX(-50%);
}
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="top"]::before {
bottom: calc(var(--spacing-small) * -1);
left: var(--tooltip-arrow-offset);
border-width: var(--spacing-small) var(--spacing-small) 0;
border-color: var(--surface-tooltip) transparent transparent;
transform: translateX(-50%);
}
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="left"]::before {
top: var(--tooltip-arrow-offset);
right: calc(var(--spacing-small) * -1);
border-width: var(--spacing-small) 0 var(--spacing-small) var(--spacing-small);
border-color: transparent transparent transparent var(--surface-tooltip);
transform: translateY(-50%);
}
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="right"]::before {
top: var(--tooltip-arrow-offset);
left: calc(var(--spacing-small) * -1);
border-width: var(--spacing-small) var(--spacing-small) var(--spacing-small) 0;
border-color: transparent var(--surface-tooltip) transparent transparent;
transform: translateY(-50%);
}
.sg-pulldown-panel {
position: absolute;
top: calc(100% + var(--spacing-small));