<span class="has-tooltip has-tooltip-button has-tooltip-bottom">
    <a href="#" class="rtds-tooltip-button" aria-describedby="tooltip-id">

        Tooltip link

    </a>
    <span class="rtds-tooltip is-centered" id="tooltip-id">Contenuto descrittivo del tooltip link</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": "Tooltip link",
  "content": "Contenuto descrittivo del tooltip link",
  "id": "tooltip-id",
  "wrapperTag": "span",
  "classes": "",
  "buttonClasses": "",
  "href": "#",
  "buttonType": "button"
}
  • Content:
    @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;
        }
        
    }
  • URL: /components/raw/tooltip/tooltip.css
  • Filesystem Path: components/03-molecules/tooltip/tooltip.css
  • Size: 4.6 KB
  • Content:
    // 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();
                }
            }
        });
    }
    
  • URL: /components/raw/tooltip/tooltip.js
  • Filesystem Path: components/03-molecules/tooltip/tooltip.js
  • Size: 11.6 KB

Tooltip

Componente tooltip accessibile che simula l’attributo title con miglioramenti significativi per l’accessibilità e l’usabilità.

Uso

Il tooltip può essere usato come

  • etichetta testuale primaria per button o link con icone; nome accessibile ed etichetta testuale visuale - usa aria-labelledby=”{idTooltip}”
  • etichetta testuale secondaria per button o link ,informazione descrittiva in più in aggiunta al testo già presente; funzionalità assimilabile all’attributo title (advisory information) - usa aria-describedby=”{idTooltip}”

Verificare il markup delle varianti adatto al caso d’uso.

Comportamento

Il componente implementa un tooltip accessibile che:

  • Supporta trigger sia button che link
  • Gestisce il posizionamento automatico (sopra/sotto)
  • Si allinea automaticamente per evitare l’overflow dello schermo
  • Supporta interazioni mouse, focus e tastiera
  • Include gestione completa dell’accessibilità
  • Persiste al click fino alla chiusura manuale

Varianti

  • default: Tooltip con button con icona come trigger - il tooltip ha funzione di etichetta testuale primaria (nome accessibile ed etichetta testuale visuale) - usa aria-labelledby
  • title: Tooltip con funzione di title - etichetta testuale secondaria, informazione descrittiva in più in aggiunta al testo già presente (funzionalità dell’attributo title) usa aria-describedby
  • link: Tooltip con link come trigger

Parametri di Configurazione

Parametri Base

  • label (stringa, obbligatorio): Testo del trigger (button/link)
  • content (stringa, obbligatorio): Contenuto del tooltip
  • id (stringa, obbligatorio): ID univoco per l’attributo aria-describedby o aria-labelledby

Parametri di Struttura

  • wrapperTag (stringa, opzionale): Tag del wrapper (default: “span”)
  • classes (stringa, opzionale): Classi CSS aggiuntive per il wrapper
  • buttonClasses (stringa, opzionale): Classi CSS aggiuntive per il trigger
  • href (stringa, opzionale): URL per il trigger link
  • buttonType (stringa, opzionale): Tipo del button se non è un link (default: “button”)

Stili CSS

Classi Base

  • .has-tooltip:
    • Wrapper del componente
    • Layout: relative
  • .rtds-tooltip-button:
    • Trigger del tooltip
    • Layout: flex, w-max, h-min
    • Background: transparent
  • .rtds-tooltip:
    • Contenitore del tooltip
    • Background: background-07 (scuro)
    • Testo: content-inverse (chiaro)
    • Ombra: shadow-md

Classi di Stato

  • .is-visible: Rende visibile il tooltip

Accessibilità

Attributi ARIA

  • aria-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’icona
  • aria-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 aggiunto
  • Il tooltip utilizza un ID univoco per la relazione
  • Tab: Naviga verso il trigger
  • Enter: Attiva il tooltip quando il trigger ha il focus
  • Esc: Chiude il tooltip attivo e mantiene il focus sul trigger

Gestione del Focus

  • Il tooltip si mostra/nasconde automaticamente con focus/blur
  • La perdita di focus chiude il tooltip solo se il focus esce completamente dal componente
  • Il focus rimane gestibile per gli screen reader

Funzionalità JavaScript

Posizionamento Intelligente

  • Calcolo automatico della posizione per evitare overflow
  • Ricalcolo al resize della finestra
  • Applicazione automatica delle classi di allineamento

Gestione degli Eventi

  • Mouse: hover in/out per mostrare/nascondere
  • Click: attivazione persistente fino al click esterno
  • Focus: gestione completa del focus/blur
  • Tastiera: supporto per Enter ed Esc

Performance

  • Uso di requestAnimationFrame per il posizionamento
  • Event delegation efficiente
  • Controlli condizionali per evitare elaborazioni non necessarie

Note di Implementazione

Limitazioni Attuali

  • Non sono ancora disponibili varianti di colore “light” (sfondo chiaro, testo scuro)
  • Il posizionamento del tooltip non è ancora implementato
  • La larghezza massima è fissa (25ch per default, 20ch per small)

Considerazioni Tecniche

  • Il componente richiede JavaScript per il funzionamento completo
  • Utilizza CSS custom properties per la configurazione dinamica
  • Compatible con navigazione da tastiera e screen reader
  • Gestisce automaticamente l’RTL tramite variabili CSS