feat: add generic tooltip component

This commit is contained in:
gitea_admin committed 2026-09-21 14:24:15 +02:00
1 parent 71734d6c1d
commit 5b86185cae
5 files changed
+277

No files matched your search

+16
View File
@@ -836,6 +836,21 @@
</span>
</section>
<!-- Component: Tooltip -->
<section id="component-tooltip">
<p class="sg-preview-label">Component: Tooltip</p>
<span class="sg-tooltip-wrap" data-component="tooltip">
<button class="sg-interaction-element sg-button sg-button--active" type="button" aria-describedby="component-tooltip-panel" aria-expanded="false" data-component-part="tooltip-trigger">
Tooltip anzeigen
</button>
<span class="sg-tooltip-panel sg-table-label" id="component-tooltip-panel" role="tooltip" hidden data-component-part="tooltip-panel">
Diese Sprechblase ist trigger-verankert und wechselt bei fehlendem Platz automatisch die Seite.
</span>
</span>
</section>
<!-- Component: Hyperlink -->
<section id="component-hyperlink">
@@ -847,6 +862,7 @@
</section>
<script src="../scripts/help-icon-overlays.js"></script>
<script src="../scripts/tooltip-overlays.js"></script>
<section id="component-file-input">
<p class="sg-preview-label">Component: File Input</p>
<div class="sg-form-preview-area">
+164
View File
@@ -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);
})();
+4
View File
@@ -224,6 +224,10 @@
<tr><td>text-help-icon</td><td>color-font-dark</td><td>Textfarbe im Help-Icon.</td></tr>
<tr><td>text-help-panel</td><td>color-font-dark</td><td>Textfarbe im Help-Panel.</td></tr>
<tr><td>layout-help-panel-width</td><td>calc(interaction-height * 8)</td><td>Standardbreite des Help-Panels.</td></tr>
<tr><td>surface-tooltip</td><td>color-light-grey</td><td>Fläche der generischen Tooltip-Sprechblase.</td></tr>
<tr><td>text-tooltip</td><td>color-font-dark</td><td>Textfarbe der generischen Tooltip-Sprechblase.</td></tr>
<tr><td>layout-tooltip-width</td><td>calc(interaction-height * 8)</td><td>Standardbreite der generischen Tooltip-Sprechblase.</td></tr>
<tr><td>radius-tooltip</td><td>radius-interaction</td><td>Radius der generischen Tooltip-Sprechblase.</td></tr>
<tr><td>icon-sandwich-line-portal</td><td>color-font-dark</td><td>Linienfarbe des Sandwich-Icons für große und kleine Variante.</td></tr>
</tbody></table>
+4
View File
@@ -62,6 +62,7 @@
--surface-menu-panel-portal: var(--color-light-grey);
--surface-help-icon: var(--color-medium-grey);
--surface-help-panel: var(--color-light-grey);
--surface-tooltip: var(--color-light-grey);
--surface-pulldown-panel: var(--color-light-grey);
--surface-activatable-remove: var(--surface-control-default);
--surface-slider-track: var(--color-medium-grey);
@@ -89,6 +90,8 @@
--layout-mode-toggle-local-width-factor: 3;
--layout-mode-toggle-width: calc(var(--interaction-height) * 3.5);
--layout-help-panel-width: calc(var(--interaction-height) * 8);
--layout-tooltip-width: calc(var(--interaction-height) * 8);
--radius-tooltip: var(--radius-interaction);
--text-control-default: var(--color-font-dark);
--text-control-disabled: var(--color-dark-grey);
--text-button-process: var(--color-font-light);
@@ -98,6 +101,7 @@
--text-menu-link-portal: var(--color-font-dark);
--text-help-icon: var(--color-font-dark);
--text-help-panel: var(--color-font-dark);
--text-tooltip: var(--color-font-dark);
--text-activatable-remove: var(--text-control-default);
--text-hyperlink: var(--color-font-hyperlink);
--icon-pulldown-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M2 2l4 4 4-4' fill='none' stroke='currentColor' stroke-width='1.5' stroke-linecap='square' stroke-linejoin='miter'/%3E%3C/svg%3E");
@@ -1239,6 +1239,95 @@
right: 0;
}
.sg-tooltip-wrap {
position: relative;
display: inline-flex;
vertical-align: middle;
}
.sg-tooltip-panel {
position: absolute;
z-index: var(--layer-pulldown-panel);
display: block;
width: var(--layout-tooltip-width);
max-width: calc(100vw - (2 * var(--spacing-large)));
box-sizing: border-box;
overflow-wrap: anywhere;
padding: var(--spacing-large);
border-radius: var(--radius-tooltip);
color: var(--text-tooltip);
background: var(--surface-tooltip);
box-shadow: var(--shadow-overlay);
}
.sg-tooltip-panel[hidden] {
display: none;
}
.sg-tooltip-wrap[data-tooltip-placement="bottom"] .sg-tooltip-panel {
top: calc(100% + var(--spacing-small));
left: 50%;
transform: translateX(-50%);
}
.sg-tooltip-wrap[data-tooltip-placement="top"] .sg-tooltip-panel {
bottom: calc(100% + var(--spacing-small));
left: 50%;
transform: translateX(-50%);
}
.sg-tooltip-wrap[data-tooltip-placement="left"] .sg-tooltip-panel {
top: 50%;
right: calc(100% + var(--spacing-small));
transform: translateY(-50%);
}
.sg-tooltip-wrap[data-tooltip-placement="right"] .sg-tooltip-panel {
top: 50%;
left: calc(100% + var(--spacing-small));
transform: translateY(-50%);
}
.sg-tooltip-panel::before {
position: absolute;
width: 0;
height: 0;
border-style: solid;
content: "";
}
.sg-tooltip-wrap[data-tooltip-placement="bottom"] .sg-tooltip-panel::before {
top: calc(var(--spacing-small) / -2);
left: 50%;
border-width: 0 calc(var(--spacing-small) / 2) calc(var(--spacing-small) / 2);
border-color: transparent transparent var(--surface-tooltip);
transform: translateX(-50%);
}
.sg-tooltip-wrap[data-tooltip-placement="top"] .sg-tooltip-panel::before {
bottom: calc(var(--spacing-small) / -2);
left: 50%;
border-width: calc(var(--spacing-small) / 2) calc(var(--spacing-small) / 2) 0;
border-color: var(--surface-tooltip) transparent transparent;
transform: translateX(-50%);
}
.sg-tooltip-wrap[data-tooltip-placement="left"] .sg-tooltip-panel::before {
top: 50%;
right: calc(var(--spacing-small) / -2);
border-width: calc(var(--spacing-small) / 2) 0 calc(var(--spacing-small) / 2) calc(var(--spacing-small) / 2);
border-color: transparent transparent transparent var(--surface-tooltip);
transform: translateY(-50%);
}
.sg-tooltip-wrap[data-tooltip-placement="right"] .sg-tooltip-panel::before {
top: 50%;
left: calc(var(--spacing-small) / -2);
border-width: calc(var(--spacing-small) / 2) calc(var(--spacing-small) / 2) calc(var(--spacing-small) / 2) 0;
border-color: transparent var(--surface-tooltip) transparent transparent;
transform: translateY(-50%);
}
.sg-pulldown-panel {
position: absolute;
top: calc(100% + var(--spacing-small));