Popover

<button type="button" class="rtds-popover__trigger" popovertarget="popover-default" aria-expanded="false">
    Maggiori informazioni
</button>

<div popover id="popover-default" class="rtds-popover__panel" data-preferred-placement="bottom" aria-labelledby="popover-default-title">
    <div class="rtds-popover__arrow" aria-hidden="true"></div>
    <div data-popover>
        <div id="popover-default-title" class="rtds-popover__header">
            <h3 class="rtds-popover__title">Titolo pannello</h3>
        </div>
        <div class="rtds-popover__body" tabindex="0">
            <p>Contenuto ricco con eventuale <a href="#">link</a>.</p>
        </div>
    </div>
</div>
{%- set _popoverId = popoverId | default('popover') -%}

<button type="button"
  class="rtds-popover__trigger"
  popovertarget="{{ _popoverId }}"
  aria-expanded="false">
  {%- if triggerIconOnly %}
  <span class="rtds-sr-only">{{ triggerLabel }}</span>
  {% render '@icon', { id: triggerIcon | default('mini--information-circle') }, true %}
  {%- else %}
  {{ triggerLabel }}
  {%- endif %}
</button>

<div popover
  id="{{ _popoverId }}"
  class="rtds-popover__panel"
  data-preferred-placement="{{ placement | default('bottom') }}"
  {%- if title %} aria-labelledby="{{ _popoverId }}-title"
  {%- elif ariaLabel %} aria-label="{{ ariaLabel }}"
  {%- endif %}>
  <div class="rtds-popover__arrow" aria-hidden="true"></div>
  <div data-popover>
    {%- if title %}
    <div id="{{ _popoverId }}-title" class="rtds-popover__header">
      <{{ headingTag | default('h3') }} class="rtds-popover__title">{{ title }}</{{ headingTag | default('h3') }}>
    </div>
    {%- endif %}
    <div class="rtds-popover__body" tabindex="0">
      {{ body | safe }}
    </div>
    {%- if dismissible %}
    <button type="button" class="rtds-btn rtds-btn--icon rtds-btn--outlined rtds-border-none rtds-content-01 hover:rtds-bg-black focus:rtds-content-03 rtds-w-8 rtds-h-8 rtds-popover__close">
      {% render '@icon', {id: 'mini--x-mark', size: 'rtds-w-4 rtds-h-4'}, true %}
      <span class="rtds-sr-only">{{ dismissLabel | default('Chiudi') }}</span>
    </button>
    {%- endif %}
  </div>
</div>
{
  "popoverId": "popover-default",
  "triggerLabel": "Maggiori informazioni",
  "title": "Titolo pannello",
  "headingTag": "h3",
  "body": "<p>Contenuto ricco con eventuale <a href=\"#\">link</a>.</p>",
  "dismissible": false,
  "dismissLabel": "Chiudi",
  "triggerIconOnly": false,
  "triggerIcon": "mini--information-circle",
  "placement": "bottom"
}
  • Content:
    .rtds-popover__panel {
        --_popover-bg:     var(--popover-bg,     var(--color-background-01, var(--color-neutral-50)));
        --_popover-border: var(--popover-border, var(--color-border-gray-01, var(--color-neutral-300)));
        --_popover-shadow: var(--popover-shadow, 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1));
        --_popover-radius: var(--popover-radius, 0.375rem);
        --_popover-width:  var(--popover-width,  calc(var(--spacing-base) * 80));
        --_popover-arrow-size: var(--popover-arrow-size, var(--spacing-2-5, 0.625rem));
    
        background-color: var(--_popover-bg);
        border: 1px solid var(--_popover-border);
        border-radius: var(--_popover-radius);
        box-shadow: var(--_popover-shadow);
        width: var(--_popover-width);
        max-width: calc(100vw - var(--spacing-8));
        padding: var(--spacing-4);
        position: absolute;
        z-index: 100;
        margin: 0;
        overflow: visible;
      }
    .rtds-popover__panel:popover-open {
        display: block;
      }
    .rtds-popover__panel[hidden] {
        display: none;
      }
    .rtds-popover__header {
        margin-block-end: var(--spacing-2);
      }
    .rtds-popover__title {
        font-size: var(--fontsize-text-base);
        font-weight: 600;
        color: var(--color-content-01);
        margin: 0;
      }
    .rtds-popover__body {
        color: var(--color-content-01);
        font-size: var(--fontsize-text-sm);
        outline: none;
      }
    .rtds-popover__body:focus-visible {
        outline: 2px solid var(--color-content-primary);
        outline-offset: 2px;
      }
    .rtds-popover__close {
        margin-block-start: var(--spacing-3);
        display: flex;
        margin-inline-start: auto;
      }
    /* ================================
         Arrow
         ================================ */
    .rtds-popover__arrow {
        position: absolute;
        width: var(--_popover-arrow-size);
        height: var(--_popover-arrow-size);
        background: var(--_popover-bg);
        border: 1px solid var(--_popover-border);
        pointer-events: none;
        z-index: -1;
      }
    /* bottom: panel sotto il trigger → arrow sul bordo superiore, punta verso l'alto */
    :where(.rtds-popover__panel[data-placement="bottom"]) .rtds-popover__arrow,
      :where(.rtds-popover__panel:not([data-placement])) .rtds-popover__arrow {
        top: calc(var(--_popover-arrow-size) / -2);
        left: 50%;
        transform: translateX(-50%) rotate(45deg);
        border-bottom-color: transparent;
        border-right-color: transparent;
      }
    /* top: panel sopra il trigger → arrow sul bordo inferiore, punta verso il basso */
    :where(.rtds-popover__panel[data-placement="top"]) .rtds-popover__arrow {
        bottom: calc(var(--_popover-arrow-size) / -2);
        left: 50%;
        transform: translateX(-50%) rotate(45deg);
        border-top-color: transparent;
        border-left-color: transparent;
      }
    /* right: panel a destra del trigger → arrow sul bordo sinistro, punta verso sinistra */
    :where(.rtds-popover__panel[data-placement="right"]) .rtds-popover__arrow {
        left: calc(var(--_popover-arrow-size) / -2);
        top: 50%;
        transform: translateY(-50%) rotate(45deg);
        border-top-color: transparent;
        border-right-color: transparent;
      }
    /* left: panel a sinistra del trigger → arrow sul bordo destro, punta verso destra */
    :where(.rtds-popover__panel[data-placement="left"]) .rtds-popover__arrow {
        right: calc(var(--_popover-arrow-size) / -2);
        top: 50%;
        transform: translateY(-50%) rotate(45deg);
        border-bottom-color: transparent;
        border-left-color: transparent;
      }
    
  • URL: /components/raw/popover/popover.built.css
  • Filesystem Path: components/08-servizi/popover/popover.built.css
  • Size: 3.4 KB
  • Content:
    @layer components {
      .rtds-popover__panel {
        --_popover-bg:     var(--popover-bg,     var(--color-background-01, var(--color-neutral-50)));
        --_popover-border: var(--popover-border, var(--color-border-gray-01, var(--color-neutral-300)));
        --_popover-shadow: var(--popover-shadow, 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1));
        --_popover-radius: var(--popover-radius, 0.375rem);
        --_popover-width:  var(--popover-width,  calc(var(--spacing-base) * 80));
        --_popover-arrow-size: var(--popover-arrow-size, var(--spacing-2-5, 0.625rem));
    
        background-color: var(--_popover-bg);
        border: 1px solid var(--_popover-border);
        border-radius: var(--_popover-radius);
        box-shadow: var(--_popover-shadow);
        width: var(--_popover-width);
        max-width: calc(100vw - var(--spacing-8));
        padding: var(--spacing-4);
        position: absolute;
        z-index: 100;
        margin: 0;
        overflow: visible;
      }
    
      .rtds-popover__panel:popover-open {
        display: block;
      }
    
      .rtds-popover__panel[hidden] {
        display: none;
      }
    
      .rtds-popover__header {
        margin-block-end: var(--spacing-2);
      }
    
      .rtds-popover__title {
        font-size: var(--fontsize-text-base);
        font-weight: 600;
        color: var(--color-content-01);
        margin: 0;
      }
    
      .rtds-popover__body {
        color: var(--color-content-01);
        font-size: var(--fontsize-text-sm);
        outline: none;
      }
    
      .rtds-popover__body:focus-visible {
        outline: 2px solid var(--color-content-primary);
        outline-offset: 2px;
      }
    
      .rtds-popover__close {
        margin-block-start: var(--spacing-3);
        display: flex;
        margin-inline-start: auto;
      }
    
      /* ================================
         Arrow
         ================================ */
    
      .rtds-popover__arrow {
        position: absolute;
        width: var(--_popover-arrow-size);
        height: var(--_popover-arrow-size);
        background: var(--_popover-bg);
        border: 1px solid var(--_popover-border);
        pointer-events: none;
        z-index: -1;
      }
    
      /* bottom: panel sotto il trigger → arrow sul bordo superiore, punta verso l'alto */
      :where(.rtds-popover__panel[data-placement="bottom"]) .rtds-popover__arrow,
      :where(.rtds-popover__panel:not([data-placement])) .rtds-popover__arrow {
        top: calc(var(--_popover-arrow-size) / -2);
        left: 50%;
        transform: translateX(-50%) rotate(45deg);
        border-bottom-color: transparent;
        border-right-color: transparent;
      }
    
      /* top: panel sopra il trigger → arrow sul bordo inferiore, punta verso il basso */
      :where(.rtds-popover__panel[data-placement="top"]) .rtds-popover__arrow {
        bottom: calc(var(--_popover-arrow-size) / -2);
        left: 50%;
        transform: translateX(-50%) rotate(45deg);
        border-top-color: transparent;
        border-left-color: transparent;
      }
    
      /* right: panel a destra del trigger → arrow sul bordo sinistro, punta verso sinistra */
      :where(.rtds-popover__panel[data-placement="right"]) .rtds-popover__arrow {
        left: calc(var(--_popover-arrow-size) / -2);
        top: 50%;
        transform: translateY(-50%) rotate(45deg);
        border-top-color: transparent;
        border-right-color: transparent;
      }
    
      /* left: panel a sinistra del trigger → arrow sul bordo destro, punta verso destra */
      :where(.rtds-popover__panel[data-placement="left"]) .rtds-popover__arrow {
        right: calc(var(--_popover-arrow-size) / -2);
        top: 50%;
        transform: translateY(-50%) rotate(45deg);
        border-bottom-color: transparent;
        border-left-color: transparent;
      }
    }
    
  • URL: /components/raw/popover/popover.css
  • Filesystem Path: components/08-servizi/popover/popover.css
  • Size: 3.5 KB
  • Content:
    window.addEventListener('load', function () {
      var supportsPopover = HTMLElement.prototype.hasOwnProperty('popover');
      var supportsAnchorPositioning = CSS.supports('anchor-name: --a');
      var unsupportedPopovers = [];
    
      var triggers = document.querySelectorAll('.rtds-popover__trigger[popovertarget]');
      if (triggers.length === 0) return;
    
      triggers.forEach(function (trigger) {
        var panelId = trigger.getAttribute('popovertarget');
        var panel = document.getElementById(panelId);
        if (!panel) return;
    
        if (supportsPopover) {
          initSupportedPopover(trigger, panel);
        } else {
          initUnsupportedPopover(trigger, panel);
        }
      });
    
      // Livello 1 + 2: Popover API nativa
      function initSupportedPopover(trigger, panel) {
        panel.addEventListener('toggle', function (e) {
          trigger.setAttribute('aria-expanded', e.newState === 'open' ? 'true' : 'false');
          if (e.newState === 'open') {
            positionPanel(trigger, panel, false); // top layer: coordinate viewport
          }
        });
    
        // Chiudi con ESC e restituisci focus al trigger
        panel.addEventListener('keydown', function (e) {
          if (e.key === 'Escape') {
            panel.hidePopover();
            trigger.focus();
          }
        });
      }
    
      // Posizionamento via getBoundingClientRect.
      // Level 1/2 (top layer): coordinate viewport, senza offset scroll.
      // Level 3 (flusso normale): coordinate documento, con offset scroll.
      function getActualPlacement(preferred, rect, panelW, panelH, gap, vw, vh) {
        var space = {
          bottom: vh - rect.bottom - gap,
          top: rect.top - gap,
          right: vw - rect.right - gap,
          left: rect.left - gap
        };
        var needed = { bottom: panelH, top: panelH, right: panelW, left: panelW };
    
        if (space[preferred] >= needed[preferred]) return preferred;
    
        var opposite = { bottom: 'top', top: 'bottom', right: 'left', left: 'right' };
        var opp = opposite[preferred];
        if (space[opp] >= needed[opp]) return opp;
    
        var best = preferred;
        var bestSpace = -Infinity;
        ['bottom', 'top', 'right', 'left'].forEach(function (dir) {
          if (space[dir] > bestSpace) { bestSpace = space[dir]; best = dir; }
        });
        return best;
      }
    
      function positionPanel(trigger, panel, useDocumentCoords) {
        var preferred = panel.getAttribute('data-preferred-placement') || 'bottom';
        var rect = trigger.getBoundingClientRect();
        var panelWidth = panel.offsetWidth || 320;
        var panelHeight = panel.offsetHeight || 200;
        var viewportWidth = window.innerWidth;
        var viewportHeight = window.innerHeight;
        var gap = 12;
        var scrollX = useDocumentCoords ? window.scrollX : 0;
        var scrollY = useDocumentCoords ? window.scrollY : 0;
    
        var placement = getActualPlacement(preferred, rect, panelWidth, panelHeight, gap, viewportWidth, viewportHeight);
        panel.setAttribute('data-placement', placement);
    
        var top, left;
        switch (placement) {
          case 'top':
            top = rect.top - panelHeight - gap + scrollY;
            left = Math.min(
              Math.max(rect.left + scrollX, scrollX + 10),
              viewportWidth - panelWidth - 10 + scrollX
            );
            break;
          case 'right':
            top = Math.min(
              Math.max(rect.top + scrollY, scrollY + 10),
              viewportHeight - panelHeight - 10 + scrollY
            );
            left = rect.right + gap + scrollX;
            break;
          case 'left':
            top = Math.min(
              Math.max(rect.top + scrollY, scrollY + 10),
              viewportHeight - panelHeight - 10 + scrollY
            );
            left = rect.left - panelWidth - gap + scrollX;
            break;
          default: // bottom
            top = rect.bottom + gap + scrollY;
            left = Math.min(
              Math.max(rect.left + scrollX, scrollX + 10),
              viewportWidth - panelWidth - 10 + scrollX
            );
        }
    
        // Safety clamp: evita che il panel esca fuori dallo schermo
        if (top < scrollY + 10) top = scrollY + 10;
        if (left < scrollX + 10) left = scrollX + 10;
        if (left + panelWidth > viewportWidth + scrollX - 10) left = viewportWidth - panelWidth - 10 + scrollX;
        if (top + panelHeight > viewportHeight + scrollY - 10) top = viewportHeight - panelHeight - 10 + scrollY;
    
        panel.style.top = top + 'px';
        panel.style.left = left + 'px';
      }
    
      // Livello 3: nessun supporto Popover API
      function initUnsupportedPopover(trigger, panel) {
        // Inizializza stato chiuso
        panel.setAttribute('hidden', '');
        trigger.setAttribute('aria-expanded', 'false');
        panel.setAttribute('role', 'dialog');
        unsupportedPopovers.push({ trigger: trigger, panel: panel });
    
        trigger.addEventListener('click', function (e) {
          e.stopPropagation();
          var isOpen = !panel.hasAttribute('hidden');
          if (isOpen) {
            closeUnsupportedPopover(trigger, panel);
          } else {
            closeAllUnsupportedPopovers(trigger);
            openUnsupportedPopover(trigger, panel);
          }
        });
    
        // ESC
        document.addEventListener('keydown', function (e) {
          if (e.key === 'Escape' && !panel.hasAttribute('hidden')) {
            closeUnsupportedPopover(trigger, panel);
            trigger.focus();
          }
        });
      }
    
      function openUnsupportedPopover(trigger, panel) {
        panel.removeAttribute('hidden');
        trigger.setAttribute('aria-expanded', 'true');
        positionPanel(trigger, panel, true); // flusso normale: coordinate documento
      }
    
      function closeUnsupportedPopover(trigger, panel) {
        panel.setAttribute('hidden', '');
        trigger.setAttribute('aria-expanded', 'false');
      }
    
      function closeAllUnsupportedPopovers(exceptTrigger) {
        unsupportedPopovers.forEach(function (item) {
          if (item.trigger !== exceptTrigger) {
            closeUnsupportedPopover(item.trigger, item.panel);
          }
        });
      }
    
      // Light dismiss per Livello 3
      document.addEventListener('click', function (e) {
        unsupportedPopovers.forEach(function (item) {
          if (!item.panel.hasAttribute('hidden') &&
              !item.panel.contains(e.target) &&
              !item.trigger.contains(e.target)) {
            closeUnsupportedPopover(item.trigger, item.panel);
          }
        });
      });
    
      // Pulsante close interno al pannello (tutti i livelli)
      document.querySelectorAll('.rtds-popover__close').forEach(function (closeBtn) {
        closeBtn.addEventListener('click', function () {
          var panel = closeBtn.closest('.rtds-popover__panel');
          if (!panel) return;
          var panelId = panel.id;
          var trigger = document.querySelector('[popovertarget="' + panelId + '"]');
          if (supportsPopover) {
            panel.hidePopover();
          } else {
            closeUnsupportedPopover(trigger, panel);
          }
          if (trigger) trigger.focus();
        });
      });
    });
    
  • URL: /components/raw/popover/popover.js
  • Filesystem Path: components/08-servizi/popover/popover.js
  • Size: 6.7 KB

Popover

Componente non-modal che mostra contenuto ricco (testo, link, markup HTML) in un pannello sovrapposto, attivato da un click esplicito dell’utente. Diverso dal tooltip, che si apre su hover e contiene solo testo descrittivo.


Comportamento

Il componente produce due elementi indipendenti nel markup — collegati tramite ID, non da un wrapper comune:

  • <button> — trigger che apre/chiude il pannello; usa l’attributo nativo popovertarget
  • <div popover> — pannello flottante che contiene il contenuto ricco

Il posizionamento del pannello è gestito tramite progressive enhancement a 3 livelli (vedi Note).


Varianti

  • popover (default): trigger testuale, pannello con titolo visibile
  • no-title: pannello senza titolo visibile; richiede ariaLabel obbligatorio
  • with-close: aggiunge un pulsante di chiusura esplicito in fondo al pannello
  • icon-trigger: trigger a icona con label sr-only

Parametri di configurazione

Contesto di sviluppo Fractal/Nunjucks — parametri del context .config.yml usati nella libreria.

Parametri Base

  • popoverId (stringa, opzionale, default: "popover"): ID condiviso tra trigger e pannello. Deve essere univoco per pagina se il componente appare più volte.
  • triggerLabel (stringa, obbligatorio): Testo del trigger. Diventa sr-only se triggerIconOnly: true.
  • title (stringa, opzionale): Titolo visibile nel pannello. Se presente, genera aria-labelledby sul pannello.
  • headingTag (stringa, opzionale, default: h3): Tag HTML del titolo visibile. Valori: h2h6.
  • ariaLabel (stringa, opzionale): Nome accessibile del pannello quando title è assente. Obbligatorio se title non viene fornito.
  • body (stringa, obbligatorio): Contenuto HTML del pannello — renderizzato con | safe.
  • dismissible (booleano, opzionale, default: false): Se true, mostra il pulsante di chiusura dopo il body.
  • dismissLabel (stringa, opzionale, default: "Chiudi"): Label sr-only del pulsante di chiusura.
  • placement (stringa, opzionale, default: "bottom"): Placement preferito del panel: bottom | top | left | right. Il JS usa questo come preferenza e fa flip automatico se non c’è spazio.

Parametri per Trigger Icona

  • triggerIconOnly (booleano, opzionale, default: false): Se true, la label del trigger è sr-only e l’icona è visibile.
  • triggerIcon (stringa, opzionale, default: "mini--information-circle"): ID dell’icona dallo sprite SVG. Attivo solo con triggerIconOnly: true.

Accessibilità

WCAG 2.1

Livello AA — criteri pertinenti:

  • 1.3.1 Informazioni e correlazioni — struttura semantica del pannello
  • 2.1.1 Tastiera — apertura, chiusura e navigazione senza mouse
  • 2.4.3 Ordine del focus — focus coerente all’apertura e alla chiusura
  • 4.1.2 Nome, Ruolo, Valore — trigger con aria-expanded, pannello con nome accessibile

Struttura e semantica

  • <button> — ruolo implicito button; attivabile con Enter e Space; trigger del pannello
  • <div popover> — contenitore del pannello; agisce come dialog nel Livello 3 (attributo role="dialog" aggiunto via JS)

Attributi ARIA

Attributo Valori Condizione
aria-expanded "true" / "false" Sul trigger; aggiornato via JS all’apertura/chiusura
aria-labelledby ID del titolo Sul pannello; quando title è presente
aria-label stringa Sul pannello; quando title è assente e ariaLabel è fornito

Regola nome accessibile: almeno uno tra title e ariaLabel deve essere sempre fornito.

Tasto Azione
Tab Sposta il focus sul trigger
Enter / Space Apre/chiude il pannello
Escape Chiude il pannello; riporta il focus sul trigger
Tab (dentro il pannello) Naviga gli elementi interattivi nel body

Gestione del focus

  • Focus all’apertura: rimane sul trigger; il pannello non cattura focus automaticamente
  • Focus restoration: alla chiusura (ESC o pulsante close) il focus torna al trigger
  • Focus visibile: stile :focus-visible applicato su .rtds-popover__body per scroll da tastiera

Screen reader

  • aria-expanded — sul trigger; aggiornato via JS; comunica lo stato aperto/chiuso
  • aria-labelledby o aria-label — sul pannello; fornisce il nome accessibile annunciato all’apertura
  • <span class="rtds-sr-only"> — nel trigger icona-only; contiene triggerLabel; l’icona SVG ha aria-hidden="true" implicitamente via @icon
  • tabindex="0" sul body — consente lo scroll da tastiera su contenuto lungo

Stili CSS

Classi Elementi (BEM)

  • .rtds-popover__trigger: Pulsante trigger; nessuno stile base imposto, il consumer applica le classi rtds-btn--* desiderate
  • .rtds-popover__panel: Pannello flottante con sfondo, bordo, ombra e posizionamento assoluto
  • .rtds-popover__header: Intestazione del pannello; contiene il titolo
  • .rtds-popover__title: Titolo visibile nel pannello
  • .rtds-popover__body: Area del contenuto ricco; con tabindex="0" per scroll da tastiera
  • .rtds-popover__close: Pulsante di chiusura esplicito; posizionato in basso a destra del pannello — classi: rtds-btn rtds-btn--icon rtds-btn--outlined rtds-border-none rtds-content-01 hover:rtds-bg-black focus:rtds-content-03 rtds-w-8 rtds-h-8

Tematizzazione

Il componente espone variabili CSS per la personalizzazione dal CMS ospitante. Il CMS può sovrascrivere le variabili Tier 1; se non sovrascritte, usano i token semantici del DS.

Override pubblici (Tier 1)

Proprietà CSS Default DS Descrizione
--popover-bg var(--color-background-01) Colore di sfondo del pannello
--popover-border var(--color-border-gray-01) Colore del bordo del pannello
--popover-shadow ombra multi-layer Box shadow del pannello
--popover-radius 0.375rem Border radius del pannello
--popover-width calc(var(--spacing-base) * 80) Larghezza del pannello (default ~20rem)
--popover-arrow-size var(--spacing-2-5) Dimensione (width/height) dell’elemento arrow

Token semantici DS (Tier 2)

Token DS Fallback primitivo Descrizione
--color-background-01 var(--color-neutral-50) Sfondo principale pagina
--color-border-gray-01 var(--color-neutral-300) Bordo sottile grigio

Esempi di utilizzo

<!-- Popover con titolo (default) -->
<button type="button"
  class="rtds-popover__trigger rtds-btn rtds-btn--secondary"
  popovertarget="esempio-1"
  aria-expanded="false">
  Maggiori informazioni
</button>

<div popover id="esempio-1"
  class="rtds-popover__panel"
  data-preferred-placement="bottom"
  aria-labelledby="esempio-1-title">
  <div class="rtds-popover__arrow" aria-hidden="true"></div>
  <div data-popover>
    <div id="esempio-1-title" class="rtds-popover__header">
      <h3 class="rtds-popover__title">Titolo pannello</h3>
    </div>
    <div class="rtds-popover__body" tabindex="0">
      <p>Contenuto ricco con eventuale <a href="#">link</a>.</p>
    </div>
  </div>
</div>

<!-- Senza titolo (ariaLabel obbligatorio) -->
<button type="button"
  class="rtds-popover__trigger rtds-btn rtds-btn--text"
  popovertarget="esempio-2"
  aria-expanded="false">
  Informazioni aggiuntive
</button>

<div popover id="esempio-2"
  class="rtds-popover__panel"
  data-preferred-placement="bottom"
  aria-label="Informazioni aggiuntive">
  <div class="rtds-popover__arrow" aria-hidden="true"></div>
  <div data-popover>
    <div class="rtds-popover__body" tabindex="0">
      <p>Contenuto senza heading visibile.</p>
    </div>
  </div>
</div>

<!-- Con pulsante di chiusura esplicito -->
<div popover id="esempio-3"
  class="rtds-popover__panel"
  data-preferred-placement="bottom"
  aria-labelledby="esempio-3-title">
  <div class="rtds-popover__arrow" aria-hidden="true"></div>
  <div data-popover>
    <div id="esempio-3-title" class="rtds-popover__header">
      <h3 class="rtds-popover__title">Titolo pannello</h3>
    </div>
    <div class="rtds-popover__body" tabindex="0">
      <p>Contenuto ricco.</p>
    </div>
    <button type="button"
      class="rtds-btn rtds-btn--icon rtds-btn--outlined rtds-border-none rtds-content-01 hover:rtds-bg-black focus:rtds-content-03 rtds-w-8 rtds-h-8 rtds-popover__close">
      <!-- icona mini--x-mark -->
      <span class="rtds-sr-only">Chiudi</span>
    </button>
  </div>
</div>

Note

  • Tooltip vs Popover: usa il tooltip (@tooltip) per testo descrittivo su hover; usa il popover per contenuto ricco (link, markup) aperto su click esplicito.
  • Nome accessibile obbligatorio: almeno uno tra i parametri title (rtds-popover__title, heading del popover) e ariaLabel (aria-label su elemento popover) deve essere sempre fornito — il pannello senza nome accessibile non è conforme WCAG 4.1.2.
  • ID univoci: se il componente appare più volte nella stessa pagina, ogni istanza deve avere un popoverId diverso.
  • Body con link: i link nel parametro body devono avere testo descrittivo (non “clicca qui”).
  • Progressive enhancement JS: il componente richiede popover.js. Browser con supporto nativo alla Popover API usano il Level 1/2; browser senza supporto usano il Level 3 con toggle manuale hidden.
  • Non usare role="alert": il popover è contenuto non urgente aperto su azione utente; non usare live region.
  • Dipende da: @icon (variante icon-trigger e pulsante with-close)