<div class="row no-gutters">
<div class="grid-auto u-m-b-2">
<label for="tooltip-example-address" class="u-m-r-1">Adress</label>
<div class="display-flex align-items-center">
<input class="a-input" id="tooltip-example-address" type="text" placeholder="Adress" autocomplete="street-address" />
<button type="button" class="a-tooltip a-button a-button--standalone-icon a-button--icon" aria-label="Visa mer information om fältet Adress" data-tooltip-placement="top">
<span class="a-tooltip__text" hidden>Ange en fullstandig adress. Verktyget flyttar automatiskt tooltipen om det inte finns plats ovanför, nedan, till vänster eller höger om triggern.</span>
<svg class="icon a-button__icon" aria-hidden="true">
<use xlink:href="#icon-info"></use>
</svg>
</button>
</div>
</div>
</div>
<div class="row no-gutters">
<div class="grid-auto u-m-b-2">
<label for="tooltip-example-address-2" class="u-m-r-1">
Postnummer
<button type="button" class="a-tooltip a-tooltip--small a-button a-button--standalone-icon a-button--icon" aria-label="Visa mer information om fältet Postnummer" data-tooltip-placement="top">
<span class="a-tooltip__text" hidden>Ange postnummer, 5 siffror utan mellanslag</span>
<svg class="icon a-button__icon" aria-hidden="true">
<use xlink:href="#icon-info"></use>
</svg>
</button>
</label>
<div class="display-flex align-items-center">
<input class="a-input" id="tooltip-example-address-2" type="text" placeholder="Postnummer" autocomplete="street-address" />
</div>
</div>
</div>
<div class="row no-gutters">
<div class="grid-auto u-m-b-2">
<label for="tooltip-example-address" class="u-m-r-1">Adress</label>
<div class="display-flex align-items-center">
<input
class="a-input"
id="tooltip-example-address"
type="text"
placeholder="Adress"
autocomplete="street-address"
/>
<button
type="button"
class="a-tooltip{{#if small}} a-tooltip--small{{/if}} a-button a-button--standalone-icon a-button--icon"
aria-label="{{label}}"
data-tooltip-placement="{{placement}}"
>
<span class="a-tooltip__text" hidden>{{{text}}}</span>
<svg class="icon a-button__icon" aria-hidden="true">
<use xlink:href="#icon-info"></use>
</svg>
</button>
</div>
</div>
</div>
<div class="row no-gutters">
<div class="grid-auto u-m-b-2">
<label for="tooltip-example-address-2" class="u-m-r-1">
Postnummer
<button
type="button"
class="a-tooltip a-tooltip--small a-button a-button--standalone-icon a-button--icon"
aria-label="{{label2}}"
data-tooltip-placement="{{placement}}"
>
<span class="a-tooltip__text" hidden>{{{postalCode}}}</span>
<svg class="icon a-button__icon" aria-hidden="true">
<use xlink:href="#icon-info"></use>
</svg>
</button>
</label>
<div class="display-flex align-items-center">
<input
class="a-input"
id="tooltip-example-address-2"
type="text"
placeholder="Postnummer"
autocomplete="street-address"
/>
</div>
</div>
</div>
{
"label": "Visa mer information om fältet Adress",
"label2": "Visa mer information om fältet Postnummer",
"placement": "top",
"text": "Ange en fullstandig adress. Verktyget flyttar automatiskt tooltipen om det inte finns plats ovanför, nedan, till vänster eller höger om triggern.",
"postalCode": "Ange postnummer, 5 siffror utan mellanslag"
}
@charset "UTF-8";
@use '../../configurations/mixins' as mixin;
@use '../../configurations/bem' as bem;
@use '../../configurations/variables' as var;
@use '../../configurations/functions' as func;
@use '../../configurations/colors/colors' as colors;
@include mixin.atom(tooltip) {
--tooltip-gap: #{func.rhythm(1)};
--tooltip-trigger-size: #{var.$icon-size};
--tooltip-trigger-icon-size: #{var.$icon-size};
--tooltip-trigger-margin: #{func.rhythm(1)};
--tooltip-trigger-padding: #{func.rhythm(1)};
--tooltip-focus-ring-size: #{func.to_rem(5px)};
position: relative;
width: var(--tooltip-trigger-size);
height: var(--tooltip-trigger-size);
margin-right: var(--tooltip-trigger-margin);
margin-left: var(--tooltip-trigger-margin);
padding: var(--tooltip-trigger-padding);
border-radius: 50%;
box-shadow: 0 0 0 var(--tooltip-focus-ring-size) transparent;
overflow: visible;
transition: box-shadow 0.2s ease;
&:hover,
&:focus,
&:focus-visible,
&[data-tooltip-visible='true'] {
box-shadow: 0 0 0 var(--tooltip-focus-ring-size) colors.$color-ocean-light;
}
@include bem.m(small) {
--tooltip-trigger-size: calc(#{var.$icon-size} / 1.5);
--tooltip-trigger-icon-size: calc(#{var.$icon-size} / 1.5);
--tooltip-trigger-margin: calc(#{func.rhythm(1)} / 1.5);
--tooltip-trigger-padding: calc(#{func.rhythm(1)} / 1.5);
--tooltip-focus-ring-size: calc(#{func.to_rem(5px)} / 1.5);
}
[class*='icon'] {
position: absolute;
top: 50%;
left: 50%;
width: var(--tooltip-trigger-icon-size);
height: var(--tooltip-trigger-icon-size);
transform: translateX(-50%) translateY(-50%);
color: var(--cyberspace-color);
fill: currentColor;
}
@include bem.e(text) {
--tooltip-arrow-size: 10px;
--tooltip-top: 0px;
--tooltip-left: 0px;
--tooltip-arrow-x: 50%;
--tooltip-arrow-y: 50%;
--tooltip-shift-x: 0px;
--tooltip-shift-y: 0px;
position: fixed;
z-index: func.z_index(foreground);
inset: auto;
margin: 0;
border: 0;
width: max-content;
max-width: min(func.to_rem(320px), calc(100vw - #{func.to_rem(24px)}));
padding: func.rhythm(1);
border-radius: var.$border-radius;
background-color: colors.$color-cyberspace;
box-shadow: 0 func.rhythm(0.5) func.rhythm(1.5) rgba(colors.$color-cyberspace, 0.25);
color: colors.$color-snow;
font-family: var.$font-family-base;
font-size: var.$size-medium;
line-height: 1.45;
text-align: left;
user-select: text;
cursor: text;
white-space: wrap;
&::selection {
color: colors.$color-cyberspace;
}
pointer-events: none;
overflow: visible;
&[hidden] {
display: none;
}
&:popover-open {
display: block;
pointer-events: auto;
}
&::after {
content: '';
position: absolute;
width: 0;
height: 0;
border-style: solid;
}
&[data-placement='top'] {
bottom: calc(anchor(top) + var(--tooltip-gap));
left: anchor(center);
transform: translateX(calc(-50% + var(--tooltip-shift-x)));
&::after {
top: calc(100% - 1px);
left: var(--tooltip-arrow-x);
transform: translateX(-50%);
border-width: var(--tooltip-arrow-size) var(--tooltip-arrow-size) 0;
border-color: colors.$color-cyberspace transparent transparent;
}
}
&[data-placement='bottom'] {
top: calc(anchor(bottom) + var(--tooltip-gap));
left: anchor(center);
transform: translateX(calc(-50% + var(--tooltip-shift-x)));
&::after {
bottom: calc(100% - 1px);
left: var(--tooltip-arrow-x);
transform: translateX(-50%);
border-width: 0 var(--tooltip-arrow-size) var(--tooltip-arrow-size);
border-color: transparent transparent colors.$color-cyberspace;
}
}
&[data-placement='left'] {
top: anchor(center);
right: calc(anchor(left) + var(--tooltip-gap));
transform: translateY(calc(-50% + var(--tooltip-shift-y)));
&::after {
top: var(--tooltip-arrow-y);
left: calc(100% - 1px);
transform: translateY(-50%);
border-width: var(--tooltip-arrow-size) 0 var(--tooltip-arrow-size) var(--tooltip-arrow-size);
border-color: transparent transparent transparent colors.$color-cyberspace;
}
}
&[data-placement='right'] {
top: anchor(center);
left: calc(anchor(right) + var(--tooltip-gap));
transform: translateY(calc(-50% + var(--tooltip-shift-y)));
&::after {
top: var(--tooltip-arrow-y);
right: calc(100% - 1px);
transform: translateY(-50%);
border-width: var(--tooltip-arrow-size) var(--tooltip-arrow-size) var(--tooltip-arrow-size) 0;
border-color: transparent colors.$color-cyberspace transparent transparent;
}
}
&[data-positioning='manual'] {
top: var(--tooltip-top);
left: var(--tooltip-left);
&[data-placement] {
right: auto;
bottom: auto;
transform: none;
}
}
}
}
import className from '../../assets/js/className';
const ROOT_SELECTOR = `.${className('a-tooltip')}`;
const TOOLTIP_SELECTOR = `.${className('a-tooltip__text')}`;
const VIEWPORT_PADDING = 12;
const TOOLTIP_GAP = 12;
const HIDE_DELAY = 40;
const ARROW_OFFSET = 18;
const VALID_PLACEMENTS = ['top', 'right', 'bottom', 'left'];
const PLACEMENT_FALLBACKS = {
top: ['top', 'bottom', 'right', 'left'],
right: ['right', 'left', 'bottom', 'top'],
bottom: ['bottom', 'top', 'left', 'right'],
left: ['left', 'right', 'top', 'bottom'],
};
const supportsAnchorPositioning = typeof CSS !== 'undefined'
&& CSS.supports('anchor-name: --tooltip-anchor')
&& CSS.supports('position-anchor: --tooltip-anchor')
&& CSS.supports('top: anchor(top)');
const openTooltips = new Set();
let tooltipCounter = 0;
let viewportFrame = 0;
function ensureUniqueId(element, fallbackId) {
const preferredId = element.id || fallbackId;
let nextId = preferredId;
let duplicate = document.getElementById(nextId);
let suffix = 0;
while (duplicate && duplicate !== element) {
suffix += 1;
nextId = `${preferredId}-${suffix}`;
duplicate = document.getElementById(nextId);
}
element.id = nextId;
return nextId;
}
function setDescribedByToken(element, token, shouldHaveToken) {
if (!element || !token) {
return;
}
const tokens = (element.getAttribute('aria-describedby') || '')
.split(/\s+/)
.filter(Boolean);
const nextTokens = shouldHaveToken
? Array.from(new Set([...tokens, token]))
: tokens.filter((existingToken) => existingToken !== token);
if (nextTokens.length) {
element.setAttribute('aria-describedby', nextTokens.join(' '));
return;
}
element.removeAttribute('aria-describedby');
}
function hasOwnVisibleText(element) {
const clone = element.cloneNode(true);
const clonedTooltip = clone.querySelector(TOOLTIP_SELECTOR);
if (clonedTooltip) {
clonedTooltip.remove();
}
return clone.textContent.trim().length > 0;
}
function getPreferredPlacement(element) {
const placement = (element.getAttribute('data-tooltip-placement') || 'top').toLowerCase();
return VALID_PLACEMENTS.includes(placement) ? placement : 'top';
}
function isVerticalPlacement(placement) {
return placement === 'top' || placement === 'bottom';
}
function isNativeButtonLike(element) {
return element.matches('button, a[href], input, select, textarea, summary');
}
function isMousePointerEvent(event) {
return event.pointerType === 'mouse';
}
function getViewportSize() {
return {
width: window.visualViewport ? window.visualViewport.width : document.documentElement.clientWidth,
height: window.visualViewport ? window.visualViewport.height : document.documentElement.clientHeight,
};
}
function clamp(value, min, max) {
if (max < min) {
return min;
}
return Math.min(Math.max(value, min), max);
}
function getPlacementScore(placement, triggerRect, tooltipRect, viewport) {
const centerX = triggerRect.left + (triggerRect.width / 2);
const centerY = triggerRect.top + (triggerRect.height / 2);
const inlineOverflow = placement === 'top' || placement === 'bottom'
? Math.max(0, (tooltipRect.width / 2) - (centerX - VIEWPORT_PADDING))
+ Math.max(0, (tooltipRect.width / 2) - (viewport.width - VIEWPORT_PADDING - centerX))
: Math.max(0, (tooltipRect.height / 2) - (centerY - VIEWPORT_PADDING))
+ Math.max(0, (tooltipRect.height / 2) - (viewport.height - VIEWPORT_PADDING - centerY));
const blockSpace = {
top: triggerRect.top - VIEWPORT_PADDING,
right: viewport.width - triggerRect.right - VIEWPORT_PADDING,
bottom: viewport.height - triggerRect.bottom - VIEWPORT_PADDING,
left: triggerRect.left - VIEWPORT_PADDING,
}[placement];
const requiredSpace = placement === 'top' || placement === 'bottom'
? tooltipRect.height + TOOLTIP_GAP
: tooltipRect.width + TOOLTIP_GAP;
const blockOverflow = Math.max(0, requiredSpace - blockSpace);
return (blockOverflow * 1000) + inlineOverflow;
}
function choosePlacement(preferredPlacement, triggerRect, tooltipRect) {
const viewport = getViewportSize();
return PLACEMENT_FALLBACKS[preferredPlacement]
.map((placement) => ({
placement,
score: getPlacementScore(placement, triggerRect, tooltipRect, viewport),
}))
.reduce((best, current) => (current.score < best.score ? current : best)).placement;
}
function updatePixelVar(element, name, value) {
if (value === null) {
element.style.removeProperty(name);
return;
}
element.style.setProperty(name, `${Math.round(value)}px`);
}
function queueTooltipPlacementUpdate() {
if (viewportFrame) {
return;
}
viewportFrame = window.requestAnimationFrame(() => {
viewportFrame = 0;
openTooltips.forEach((tooltip) => {
tooltip.updatePlacement();
});
});
}
class Tooltip {
constructor(element) {
this.root = element;
this.tooltip = element.querySelector(TOOLTIP_SELECTOR);
this.preferredPlacement = getPreferredPlacement(element);
this.hideTimer = null;
this.isHoveringTrigger = false;
this.isHoveringTooltip = false;
this.isPinned = false;
if (!this.tooltip || element.dataset.tooltipInitialized === 'true') {
return;
}
element.dataset.tooltipInitialized = 'true';
this.onTriggerPointerEnter = this.onTriggerPointerEnter.bind(this);
this.onTriggerPointerLeave = this.onTriggerPointerLeave.bind(this);
this.onTriggerFocusIn = this.onTriggerFocusIn.bind(this);
this.onTriggerFocusOut = this.onTriggerFocusOut.bind(this);
this.onTriggerClick = this.onTriggerClick.bind(this);
this.onTriggerKeydown = this.onTriggerKeydown.bind(this);
this.onTooltipPointerEnter = this.onTooltipPointerEnter.bind(this);
this.onTooltipPointerLeave = this.onTooltipPointerLeave.bind(this);
this.setupAccessibility();
this.bindEvents();
}
setupAccessibility() {
tooltipCounter += 1;
const anchorName = `--tooltip-anchor-${tooltipCounter}`;
if (this.root.id && !this.tooltip.id) {
this.tooltip.id = this.root.id;
this.root.removeAttribute('id');
}
if (this.root.getAttribute('role') === 'tooltip') {
this.root.removeAttribute('role');
}
if (
!this.root.hasAttribute('aria-label')
&& !this.root.hasAttribute('aria-labelledby')
&& !hasOwnVisibleText(this.root)
) {
this.root.setAttribute(
'aria-label',
this.root.getAttribute('data-tooltip-label') || 'Visa mer information',
);
}
if (!isNativeButtonLike(this.root)) {
if (!this.root.hasAttribute('tabindex')) {
this.root.setAttribute('tabindex', '0');
}
if (!this.root.hasAttribute('role')) {
this.root.setAttribute('role', 'button');
}
}
if (this.root.tagName === 'BUTTON' && !this.root.getAttribute('type')) {
this.root.setAttribute('type', 'button');
}
this.tooltip.setAttribute('popover', 'manual');
this.tooltip.setAttribute('role', 'tooltip');
this.tooltip.hidden = true;
this.tooltip.dataset.positioning = supportsAnchorPositioning ? 'anchor' : 'manual';
this.tooltip.dataset.placement = this.preferredPlacement;
if (supportsAnchorPositioning) {
this.root.style.setProperty('anchor-name', anchorName);
this.tooltip.style.setProperty('position-anchor', anchorName);
} else {
this.root.style.removeProperty('anchor-name');
this.tooltip.style.removeProperty('position-anchor');
}
ensureUniqueId(this.tooltip, `tooltip-content-${tooltipCounter}`);
}
bindEvents() {
this.root.addEventListener('pointerenter', this.onTriggerPointerEnter);
this.root.addEventListener('pointerleave', this.onTriggerPointerLeave);
this.root.addEventListener('focusin', this.onTriggerFocusIn);
this.root.addEventListener('focusout', this.onTriggerFocusOut);
this.root.addEventListener('click', this.onTriggerClick);
this.root.addEventListener('keydown', this.onTriggerKeydown);
this.tooltip.addEventListener('pointerenter', this.onTooltipPointerEnter);
this.tooltip.addEventListener('pointerleave', this.onTooltipPointerLeave);
}
isOpen() {
return this.tooltip.matches(':popover-open');
}
onTriggerPointerEnter(event) {
if (!isMousePointerEvent(event)) {
return;
}
this.isHoveringTrigger = true;
this.show();
}
onTriggerPointerLeave(event) {
if (!isMousePointerEvent(event)) {
return;
}
this.isHoveringTrigger = false;
this.scheduleHide();
}
onTriggerFocusIn() {
this.show();
}
onTriggerFocusOut(event) {
if (
this.isPinned
|| this.root.contains(event.relatedTarget)
|| this.tooltip.contains(event.relatedTarget)
) {
return;
}
this.scheduleHide();
}
onTriggerClick(event) {
if (this.tooltip.contains(event.target)) {
return;
}
this.clearHideTimer();
if (this.isPinned) {
this.isPinned = false;
this.hide();
return;
}
this.isPinned = true;
this.show();
}
onTriggerKeydown(event) {
if ((event.key !== 'Enter' && event.key !== ' ') || isNativeButtonLike(this.root)) {
return;
}
event.preventDefault();
this.root.click();
}
onTooltipPointerEnter(event) {
if (!isMousePointerEvent(event)) {
return;
}
this.isHoveringTooltip = true;
this.show();
}
onTooltipPointerLeave(event) {
if (!isMousePointerEvent(event)) {
return;
}
this.isHoveringTooltip = false;
this.scheduleHide();
}
handleDocumentPointerDown(event) {
if (!this.isPinned) {
return;
}
if (this.root.contains(event.target) || this.tooltip.contains(event.target)) {
return;
}
this.isPinned = false;
this.hide();
}
clearHideTimer() {
window.clearTimeout(this.hideTimer);
this.hideTimer = null;
}
scheduleHide() {
this.clearHideTimer();
this.hideTimer = window.setTimeout(() => {
if (
this.isPinned
|| this.isHoveringTrigger
|| this.isHoveringTooltip
|| this.root.matches(':focus-within')
) {
return;
}
this.hide();
}, HIDE_DELAY);
}
show() {
this.clearHideTimer();
this.tooltip.hidden = false;
if (!this.isOpen()) {
this.tooltip.showPopover();
}
this.root.dataset.tooltipVisible = 'true';
setDescribedByToken(this.root, this.tooltip.id, true);
openTooltips.add(this);
this.updatePlacement();
queueTooltipPlacementUpdate();
}
hide() {
this.clearHideTimer();
if (this.isOpen()) {
this.tooltip.hidePopover();
}
this.tooltip.hidden = true;
this.root.removeAttribute('data-tooltip-visible');
setDescribedByToken(this.root, this.tooltip.id, false);
openTooltips.delete(this);
}
updatePlacement() {
if (!this.isOpen()) {
return;
}
const triggerRect = this.root.getBoundingClientRect();
const tooltipRect = this.tooltip.getBoundingClientRect();
const viewport = getViewportSize();
const placement = choosePlacement(
this.preferredPlacement,
triggerRect,
tooltipRect,
);
const centerX = triggerRect.left + (triggerRect.width / 2);
const centerY = triggerRect.top + (triggerRect.height / 2);
const isVertical = isVerticalPlacement(placement);
this.tooltip.dataset.placement = placement;
if (!supportsAnchorPositioning) {
const idealCrossAxisOffset = isVertical
? centerX - (tooltipRect.width / 2)
: centerY - (tooltipRect.height / 2);
const maxCrossAxisOffset = isVertical
? viewport.width - tooltipRect.width - VIEWPORT_PADDING
: viewport.height - tooltipRect.height - VIEWPORT_PADDING;
const clampedCrossAxisOffset = clamp(idealCrossAxisOffset, VIEWPORT_PADDING, maxCrossAxisOffset);
const top = placement === 'top'
? triggerRect.top - tooltipRect.height - TOOLTIP_GAP
: placement === 'bottom'
? triggerRect.bottom + TOOLTIP_GAP
: clampedCrossAxisOffset;
const left = placement === 'left'
? triggerRect.left - tooltipRect.width - TOOLTIP_GAP
: placement === 'right'
? triggerRect.right + TOOLTIP_GAP
: clampedCrossAxisOffset;
const maxTop = viewport.height - tooltipRect.height - VIEWPORT_PADDING;
const maxLeft = viewport.width - tooltipRect.width - VIEWPORT_PADDING;
const clampedTop = clamp(top, VIEWPORT_PADDING, maxTop);
const clampedLeft = clamp(left, VIEWPORT_PADDING, maxLeft);
const arrowOffset = clamp(
(isVertical ? centerX - clampedLeft : centerY - clampedTop),
ARROW_OFFSET,
(isVertical ? tooltipRect.width : tooltipRect.height) - ARROW_OFFSET,
);
updatePixelVar(this.tooltip, '--tooltip-top', clampedTop);
updatePixelVar(this.tooltip, '--tooltip-left', clampedLeft);
updatePixelVar(this.tooltip, '--tooltip-shift-x', null);
updatePixelVar(this.tooltip, '--tooltip-shift-y', null);
updatePixelVar(this.tooltip, '--tooltip-arrow-x', isVertical ? arrowOffset : null);
updatePixelVar(this.tooltip, '--tooltip-arrow-y', isVertical ? null : arrowOffset);
return;
}
const idealOffset = isVertical
? centerX - (tooltipRect.width / 2)
: centerY - (tooltipRect.height / 2);
const maxOffset = isVertical
? viewport.width - tooltipRect.width - VIEWPORT_PADDING
: viewport.height - tooltipRect.height - VIEWPORT_PADDING;
const clampedOffset = clamp(idealOffset, VIEWPORT_PADDING, maxOffset);
const arrowOffset = clamp(
(isVertical ? centerX : centerY) - clampedOffset,
ARROW_OFFSET,
(isVertical ? tooltipRect.width : tooltipRect.height) - ARROW_OFFSET,
);
updatePixelVar(this.tooltip, '--tooltip-top', null);
updatePixelVar(this.tooltip, '--tooltip-left', null);
updatePixelVar(this.tooltip, '--tooltip-shift-x', isVertical ? clampedOffset - idealOffset : null);
updatePixelVar(this.tooltip, '--tooltip-shift-y', isVertical ? null : clampedOffset - idealOffset);
updatePixelVar(this.tooltip, '--tooltip-arrow-x', isVertical ? arrowOffset : null);
updatePixelVar(this.tooltip, '--tooltip-arrow-y', isVertical ? null : arrowOffset);
}
}
function initTooltips(root = document) {
root.querySelectorAll(ROOT_SELECTOR).forEach((element) => {
new Tooltip(element);
});
}
document.addEventListener('pointerdown', (event) => {
openTooltips.forEach((tooltip) => {
tooltip.handleDocumentPointerDown(event);
});
});
document.addEventListener('keydown', (event) => {
if (event.key !== 'Escape') {
return;
}
openTooltips.forEach((tooltip) => {
tooltip.isPinned = false;
tooltip.hide();
});
});
window.addEventListener('scroll', queueTooltipPlacementUpdate, true);
window.addEventListener('resize', queueTooltipPlacementUpdate);
initTooltips();
export { Tooltip, initTooltips };
export default Tooltip;
No notes defined.