feat: add generic tooltip component
This commit is contained in:
1 parent
71734d6c1d
commit
5b86185cae
5 files changed
+277
No files matched your search
@@ -836,6 +836,21 @@
|
|||||||
</span>
|
</span>
|
||||||
</section>
|
</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 -->
|
<!-- Component: Hyperlink -->
|
||||||
<section id="component-hyperlink">
|
<section id="component-hyperlink">
|
||||||
@@ -847,6 +862,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<script src="../scripts/help-icon-overlays.js"></script>
|
<script src="../scripts/help-icon-overlays.js"></script>
|
||||||
|
<script src="../scripts/tooltip-overlays.js"></script>
|
||||||
<section id="component-file-input">
|
<section id="component-file-input">
|
||||||
<p class="sg-preview-label">Component: File Input</p>
|
<p class="sg-preview-label">Component: File Input</p>
|
||||||
<div class="sg-form-preview-area">
|
<div class="sg-form-preview-area">
|
||||||
|
|||||||
@@ -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);
|
||||||
|
})();
|
||||||
@@ -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-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>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>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>
|
<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>
|
</tbody></table>
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@
|
|||||||
--surface-menu-panel-portal: var(--color-light-grey);
|
--surface-menu-panel-portal: var(--color-light-grey);
|
||||||
--surface-help-icon: var(--color-medium-grey);
|
--surface-help-icon: var(--color-medium-grey);
|
||||||
--surface-help-panel: var(--color-light-grey);
|
--surface-help-panel: var(--color-light-grey);
|
||||||
|
--surface-tooltip: var(--color-light-grey);
|
||||||
--surface-pulldown-panel: var(--color-light-grey);
|
--surface-pulldown-panel: var(--color-light-grey);
|
||||||
--surface-activatable-remove: var(--surface-control-default);
|
--surface-activatable-remove: var(--surface-control-default);
|
||||||
--surface-slider-track: var(--color-medium-grey);
|
--surface-slider-track: var(--color-medium-grey);
|
||||||
@@ -89,6 +90,8 @@
|
|||||||
--layout-mode-toggle-local-width-factor: 3;
|
--layout-mode-toggle-local-width-factor: 3;
|
||||||
--layout-mode-toggle-width: calc(var(--interaction-height) * 3.5);
|
--layout-mode-toggle-width: calc(var(--interaction-height) * 3.5);
|
||||||
--layout-help-panel-width: calc(var(--interaction-height) * 8);
|
--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-default: var(--color-font-dark);
|
||||||
--text-control-disabled: var(--color-dark-grey);
|
--text-control-disabled: var(--color-dark-grey);
|
||||||
--text-button-process: var(--color-font-light);
|
--text-button-process: var(--color-font-light);
|
||||||
@@ -98,6 +101,7 @@
|
|||||||
--text-menu-link-portal: var(--color-font-dark);
|
--text-menu-link-portal: var(--color-font-dark);
|
||||||
--text-help-icon: var(--color-font-dark);
|
--text-help-icon: var(--color-font-dark);
|
||||||
--text-help-panel: 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-activatable-remove: var(--text-control-default);
|
||||||
--text-hyperlink: var(--color-font-hyperlink);
|
--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");
|
--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;
|
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 {
|
.sg-pulldown-panel {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(100% + var(--spacing-small));
|
top: calc(100% + var(--spacing-small));
|
||||||
|
|||||||
Reference in new issue
Block a user