<span class="has-tooltip has-tooltip-button has-tooltip-bottom">
<button type="button" class="rtds-tooltip-button" aria-describedby="tooltip-id">
Testo oggetto con tooltip
</button>
<span class="rtds-tooltip is-centered" id="tooltip-id">Testo etichetta descrittiva tooltip</span>
</span>
<{{ wrapperTag | default('span') }} class="has-tooltip has-tooltip-button has-tooltip-bottom{% block classes %}{% if classes %} {{ classes }}{% endif %}{% endblock classes %}">
<{% if href %}a href="{{ href }}"{% else %}button type="{% if buttonType %}{{ buttonType }}{% else %}button{% endif %}"{% endif %} class="rtds-tooltip-button{% block buttonClasses %}{% if buttonClasses %} {{ buttonClasses }}{% endif %}{% endblock buttonClasses %}"
{% if iconOnly %}aria-labelledby{% else %}aria-describedby{% endif %}="{{ id }}">
{% block buttonContent %}
{% if iconOnly %}
{% render "@icon", icon.context, true %}
{% else %}
{{ label | safe }}
{% endif %}
{% endblock buttonContent %}
</{% if href %}a{% else %}button{% endif %}>
<span class="rtds-tooltip is-centered" id="{{ id }}">{{ content | safe }}</span>
</{{ wrapperTag | default('span') }}>
{
"label": "Testo oggetto con tooltip",
"content": "Testo etichetta descrittiva tooltip",
"id": "tooltip-id",
"wrapperTag": "span",
"classes": "",
"buttonClasses": "",
"href": "",
"buttonType": "button"
}
@layer components {
.has-tooltip {
@apply rtds-relative;
}
.has-tooltip-button {
@apply rtds-flex rtds-w-max rtds-h-min rtds-bg-transparent rtds-p-0;
}
.rtds-tooltip {
--_tooltip-padding-inline: 1.5ch;
--_tooltip-padding-block: 0.75ch;
--_toolip-triangle-size: 0.5rem;
--tooltip-wrapper-width: var(--wrapper-width);
--_tooltip-bottom-tip: conic-gradient(
from -30deg at bottom,
rgba(0, 0, 0, 0),
#000 1deg 60deg,
rgba(0, 0, 0, 0) 61deg
)
bottom / 100% 50% no-repeat;
--_tooltip-top-tip: conic-gradient(from 150deg at top, rgba(0, 0, 0, 0), #000 1deg 60deg, rgba(0, 0, 0, 0) 61deg) top / 100% 50% no-repeat;
--_tooltip-z-index: var(--tooltip-z-index, 1);
--isRTL: -1;
opacity: 0;
visibility: hidden;
inline-size: max-content;
max-inline-size: 25ch;
transform: translateX(var(--_tooltip-x, 0)) translateY(var(--_tooltip-y, 0));
transition: opacity 0.2s ease, transform 0.2s ease;
pointer-events: none;
-webkit-user-select: none;
user-select: none;
position: absolute;
z-index: var(--_tooltip-z-index);
@apply rtds-font-medium rtds-background-07 rtds-rounded-md rtds-text-sm rtds-leading-none rtds-shadow-md rtds-content-inverse;
}
.rtds-tooltip::after {
content: "";
background: var(--_tooltip-bg);
position: absolute;
z-index: -1;
inset: 0;
mask: var(--_tip);
@apply rtds-background-07;
}
.rtds-tooltip-button:is(:hover, :active) + .rtds-tooltip,
.is-visible .rtds-tooltip-button + .rtds-tooltip {
padding: var(--_tooltip-padding-block) var(--_tooltip-padding-inline);
opacity: 1;
visibility: visible;
pointer-events: auto; /* Abilita l'interazione con il mouse sul tooltip visibile */
transition-delay: 200ms;
}
.has-tooltip-top .rtds-tooltip {
bottom: calc(100% + var(--_tooltip-padding-block) + var(--_toolip-triangle-size));
}
/* Posizionamento centrato esplicito */
.has-tooltip-top .rtds-tooltip.is-centered {
left: 50%;
--_tooltip-x: calc(50% * var(--isRTL));
}
.has-tooltip-top .rtds-tooltip::after {
--_tip: var(--_tooltip-bottom-tip);
bottom: calc(var(--_toolip-triangle-size) * -1);
border-block-end: var(--_toolip-triangle-size) solid transparent;
}
.has-tooltip-top .rtds-tooltip.is-left-aligned {
left: 0;
--_tooltip-x: 0;
text-align: left;
}
/*.has-tooltip-top .rtds-tooltip.is-left-aligned::after {
right: calc(100% - (var(--tooltip-wrapper-width) * 1px));
}*/
.has-tooltip-top .rtds-tooltip.is-right-aligned {
left: 100%;
--_tooltip-x: -100%;
text-align: left;
}
/*.has-tooltip-top .rtds-tooltip.is-right-aligned::after {
left: calc(100% - (var(--tooltip-wrapper-width) * 1px));
}*/
.has-tooltip-bottom .rtds-tooltip {
top: calc(100% + var(--_tooltip-padding-block) + var(--_toolip-triangle-size));
}
/* Posizionamento centrato esplicito */
.has-tooltip-bottom .rtds-tooltip.is-centered {
left: 50%;
--_tooltip-x: calc(50% * var(--isRTL));
}
.has-tooltip-bottom .rtds-tooltip::after {
--_tip: var(--_tooltip-top-tip);
top: calc(var(--_toolip-triangle-size) * -1);
border-block-start: var(--_toolip-triangle-size) solid transparent;
}
.has-tooltip-bottom .rtds-tooltip.is-left-aligned {
left: 0;
--_tooltip-x: 0;
text-align: left;
}
.has-tooltip-top .rtds-tooltip.is-left-aligned::after,
.has-tooltip-bottom .rtds-tooltip.is-left-aligned::after {
/*right: var(--tooltip-wrapper-width);*/
width: 0.5rem;
left: 1rem;
}
.has-tooltip-bottom .rtds-tooltip.is-right-aligned {
left: 100%;
--_tooltip-x: -100%;
text-align: left;
}
.has-tooltip-top .rtds-tooltip.is-right-aligned::after,
.has-tooltip-bottom .rtds-tooltip.is-right-aligned::after {
width: 0.5rem;
right: 1rem;
left: auto;
}
/* tooltip small */
.rtds-tooltip--sm {
--_tooltip-padding-block: 0.25rem;
--_tooltip-padding-inline: 0.5rem;
max-inline-size: 20ch;
@apply rtds-text-xs;
}
}
// Verifica se esistono elementi con classe .has-tooltip
const tooltipElements = document.querySelectorAll('.has-tooltip');
if (tooltipElements.length > 0) {
// TOOLTIP
// Funzione globale per chiudere tutti i tooltip
function closeAllTooltips(exceptElement = null) {
tooltipElements.forEach(function(wrapper) {
if (wrapper !== exceptElement && wrapper.classList.contains('is-visible')) {
const tooltip = wrapper.querySelector('.rtds-tooltip');
wrapper.classList.remove('is-visible');
// Reset dello stato click attivo
wrapper.setAttribute('data-click-active', 'false');
// Reset alle classi di default quando nascosto
tooltip.classList.remove('is-left-aligned', 'is-right-aligned');
if (!tooltip.classList.contains('is-centered')) {
tooltip.classList.add('is-centered');
}
wrapper.setAttribute('data-positioned', 'false');
}
});
}
tooltipElements.forEach(function (tooltipWrapper) {
const triggerButton = tooltipWrapper.querySelector('.rtds-tooltip-button');
const tooltip = tooltipWrapper.querySelector('.rtds-tooltip');
let isClickActive = false;
let isPositioned = false; // Flag per evitare ricalcoli inutili
function handleTooltipPosition() {
// Se già posizionato, non ricalcolare a meno che non sia resize
if (isPositioned && arguments[0] !== 'resize') {
return;
}
// Controlla anche il data attribute per casi come ESC
if (tooltipWrapper.getAttribute('data-positioned') === 'true' && arguments[0] !== 'resize') {
isPositioned = true;
return;
}
// Reset solo delle classi di allineamento speciali, mantieni is-centered per i calcoli
tooltip.classList.remove('is-left-aligned', 'is-right-aligned');
// Assicurati che abbia is-centered per i calcoli corretti
if (!tooltip.classList.contains('is-centered')) {
tooltip.classList.add('is-centered');
}
// Rendi il tooltip temporaneamente visibile per il calcolo ma con opacity 0
const wasVisible = tooltipWrapper.classList.contains('is-visible');
if (!wasVisible) {
tooltip.style.visibility = 'visible';
tooltip.style.opacity = '0';
tooltip.style.padding = 'var(--_tooltip-padding-block) var(--_tooltip-padding-inline)';
}
requestAnimationFrame(() => {
var ww = window.innerWidth;
var rect = tooltip.getBoundingClientRect();
var tl = rect.left;
var tw = tooltip.offsetWidth;
var tooltipWrapperWidth = tooltipWrapper.offsetWidth;
var tr = tl + tw;
if (tl < 10) {
tooltip.style.setProperty('--wrapper-width', tooltipWrapperWidth + 'px');
tooltip.classList.remove('is-centered');
tooltip.classList.add('is-left-aligned');
} else if (tr > (ww - 10)) {
tooltip.style.setProperty('--wrapper-width', tooltipWrapperWidth + 'px');
tooltip.classList.remove('is-centered');
tooltip.classList.add('is-right-aligned');
} else {
// Caso default: tooltip centrato - mantiene is-centered già presente
// Non fa nulla, is-centered rimane applicata
}
// Ripristina lo stato originale se il tooltip non era visibile
if (!wasVisible) {
tooltip.style.visibility = '';
tooltip.style.opacity = '';
tooltip.style.padding = '';
}
// Marca come posizionato
isPositioned = true;
tooltipWrapper.setAttribute('data-positioned', 'true');
});
}
// Il posizionamento viene ora calcolato dinamicamente quando serve
// Gestione resize della finestra - ricalcola sempre e resetta il flag
window.addEventListener('resize', function() {
isPositioned = false; // Resetta il flag per forzare il ricalcolo
tooltipWrapper.setAttribute('data-positioned', 'false');
if (tooltipWrapper.classList.contains('is-visible')) {
handleTooltipPosition('resize');
}
});
// Gestione click sul bottone
triggerButton.addEventListener('click', function (e) {
isClickActive = true;
tooltipWrapper.setAttribute('data-click-active', 'true');
// Chiudi tutti gli altri tooltip prima di mostrare questo
closeAllTooltips(tooltipWrapper);
// Pre-calcola la posizione PRIMA di rendere visibile il tooltip
handleTooltipPosition();
// Ora rendi visibile il tooltip con la posizione già corretta
tooltipWrapper.classList.add('is-visible');
});
// Gestione hover del mouse con timeout per permettere il passaggio tra trigger e tooltip
let hoverTimeout;
function showTooltip() {
// Sincronizza lo stato locale con il data attribute
const clickActiveAttr = tooltipWrapper.getAttribute('data-click-active');
if (clickActiveAttr === 'false') {
isClickActive = false;
}
if (!isClickActive) {
clearTimeout(hoverTimeout);
// Apertura immediata - rimozione del delay
// Chiudi tutti gli altri tooltip prima di mostrare questo
closeAllTooltips(tooltipWrapper);
// Pre-calcola la posizione PRIMA di rendere visibile il tooltip
handleTooltipPosition();
// Ora rendi visibile il tooltip con la posizione già corretta
tooltipWrapper.classList.add('is-visible');
}
}
function hideTooltip() {
// Sincronizza lo stato locale con il data attribute
const clickActiveAttr = tooltipWrapper.getAttribute('data-click-active');
if (clickActiveAttr === 'false') {
isClickActive = false;
}
if (!isClickActive) {
hoverTimeout = setTimeout(() => {
tooltipWrapper.classList.remove('is-visible');
// Reset alle classi di default quando nascosto per evitare stati inconsistenti
tooltip.classList.remove('is-left-aligned', 'is-right-aligned');
if (!tooltip.classList.contains('is-centered')) {
tooltip.classList.add('is-centered');
}
// Reset del flag per permettere il ricalcolo al prossimo hover
isPositioned = false;
tooltipWrapper.setAttribute('data-positioned', 'false');
}, 1000); // Delay di 1 secondo per la chiusura
}
}
// Eventi hover sul trigger
triggerButton.addEventListener('mouseenter', showTooltip);
triggerButton.addEventListener('mouseleave', hideTooltip);
// Eventi hover sul tooltip stesso
tooltip.addEventListener('mouseenter', showTooltip);
tooltip.addEventListener('mouseleave', hideTooltip);
// Gestione click fuori dal tooltip
document.addEventListener('click', function(e) {
if (isClickActive && !tooltipWrapper.contains(e.target)) {
isClickActive = false;
tooltipWrapper.setAttribute('data-click-active', 'false');
tooltipWrapper.classList.remove('is-visible');
// Reset alle classi di default quando nascosto
tooltip.classList.remove('is-left-aligned', 'is-right-aligned');
if (!tooltip.classList.contains('is-centered')) {
tooltip.classList.add('is-centered');
}
// Reset del flag per permettere il ricalcolo al prossimo hover
isPositioned = false;
tooltipWrapper.setAttribute('data-positioned', 'false');
}
});
// Gestione focus con tastiera (Enter)
triggerButton.addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
// Chiudi tutti gli altri tooltip prima di mostrare questo
closeAllTooltips(tooltipWrapper);
// Pre-calcola la posizione PRIMA di rendere visibile il tooltip
handleTooltipPosition();
// Ora rendi visibile il tooltip con la posizione già corretta
tooltipWrapper.classList.add('is-visible');
}
});
// Gestione focus generico
triggerButton.addEventListener('focusin', function (e) {
// Apertura immediata anche per focus
// Chiudi tutti gli altri tooltip prima di mostrare questo
closeAllTooltips(tooltipWrapper);
// Pre-calcola la posizione PRIMA di rendere visibile il tooltip
handleTooltipPosition();
// Ora rendi visibile il tooltip con la posizione già corretta
tooltipWrapper.classList.add('is-visible');
});
// Gestione perdita focus
triggerButton.addEventListener('focusout', function (e) {
// Verifichiamo che il nuovo elemento di focus non sia all'interno del tooltip wrapper
if (!tooltipWrapper.contains(e.relatedTarget)) {
tooltipWrapper.classList.remove('is-visible');
// Reset alle classi di default quando nascosto
tooltip.classList.remove('is-left-aligned', 'is-right-aligned');
if (!tooltip.classList.contains('is-centered')) {
tooltip.classList.add('is-centered');
}
// Reset del flag per permettere il ricalcolo al prossimo hover
isPositioned = false;
tooltipWrapper.setAttribute('data-positioned', 'false');
}
});
});
// Gestione tasto ESC
document.addEventListener('keydown', function (e) {
if (e.key === "Escape") {
// Troviamo il tooltip attualmente attivo
const activeTooltipWrapper = document.querySelector('.has-tooltip.is-visible');
if (activeTooltipWrapper) {
const activeTooltip = activeTooltipWrapper.querySelector('.rtds-tooltip');
activeTooltipWrapper.classList.remove('is-visible');
// Reset alle classi di default quando nascosto
activeTooltip.classList.remove('is-left-aligned', 'is-right-aligned');
if (!activeTooltip.classList.contains('is-centered')) {
activeTooltip.classList.add('is-centered');
}
// Reset del flag per permettere il ricalcolo al prossimo hover
activeTooltipWrapper.setAttribute('data-positioned', 'false');
// Manteniamo il focus sul bottone trigger
activeTooltipWrapper.querySelector('.rtds-tooltip-button').focus();
}
}
});
}
Componente tooltip accessibile che simula l’attributo title con miglioramenti significativi per l’accessibilità e l’usabilità.
Il tooltip può essere usato come
Verificare il markup delle varianti adatto al caso d’uso.
Il componente implementa un tooltip accessibile che:
default: Tooltip con button con icona come trigger - il tooltip ha funzione di etichetta testuale primaria (nome accessibile ed etichetta testuale visuale) - usa aria-labelledbytitle: Tooltip con funzione di title - etichetta testuale secondaria, informazione descrittiva in più in aggiunta al testo già presente (funzionalità dell’attributo title) usa aria-describedbylink: Tooltip con link come triggerlabel (stringa, obbligatorio): Testo del trigger (button/link)content (stringa, obbligatorio): Contenuto del tooltipid (stringa, obbligatorio): ID univoco per l’attributo aria-describedby o aria-labelledbywrapperTag (stringa, opzionale): Tag del wrapper (default: “span”)classes (stringa, opzionale): Classi CSS aggiuntive per il wrapperbuttonClasses (stringa, opzionale): Classi CSS aggiuntive per il triggerhref (stringa, opzionale): URL per il trigger linkbuttonType (stringa, opzionale): Tipo del button se non è un link (default: “button”).has-tooltip: .rtds-tooltip-button: .rtds-tooltip: .is-visible: Rende visibile il tooltiparia-labelledby: Collega il trigger al contenuto del tooltip nel caso il tooltip rappresenti l’etichetta testuale del componente a cui il tooltip viene aggiunto es: bottone con icona - il tooltip fornisce sia il nome accessibile che l’etichetta testuale visuale dell’iconaaria-describedby: Collega il trigger al contenuto del tooltip nel caso il tooltip rappresenti la funzionalità di title, ovvero di descrizione aggiuntiva rispetto all’etichetta testuale del componente a cui il tooltip viene aggiuntorequestAnimationFrame per il posizionamento