<!-- Desktop Search Modal -->
<div class="rtds-search-modal-desktop__wrapper rtds-hidden lg:rtds-block">
    <button type="button" aria-haspopup="dialog" aria-controls="searchModal" aria-expanded="false" id="searchModalTrigger" class="rtds-btn rtds-btn--outline rtds-btn--icon rtds-w-6 rtds-h-6 md:rtds-w-10 md:rtds-h-10 rtds-border-gray-03 hover:rtds-button-primary-hover focus:rtds-button-primary-hover active:rtds-button-primary-hover hover:rtds-text-white focus:rtds-text-white active:rtds-text-white">
        <span class="rtds-sr-only">Cerca nel sito - apri la modale</span>
        <svg class="rtds-icon rtds-fill-current rtds-w-4 rtds-h-4 md:rtds-w-5 md:rtds-h-5" aria-hidden="true" focusable="false" role="img">
            <use href="../../icons.svg#mini--magnifying-glass" />
        </svg>

    </button>

    <!-- Search Modal -->
    <div id="searchModal" class="rtds-search-modal-desktop rtds-fixed rtds-items-center rtds-content-center rtds-inset-0 rtds-bg-black rtds-bg-opacity-50 rtds-overflow-y-auto rtds-h-full rtds-w-full" style="display:none;" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="modalTitle" data-trigger-id="searchModalTrigger" data-close-button-id="closeModal">

        <div class="rtds-relative rtds-h-full lg:rtds-h-full lg:rtds-w-full rtds-mx-auto rtds-p-6 lg:rtds-p-16 rtds-shadow-lg rtds-bg-white">
            <div class="rtds-modal-content rtds-container rtds-px-16 rtds-grid rtds-gap-14">
                <div class="rtds-flex rtds-justify-between rtds-items-center">
                    <h2 id="modalTitle" class="rtds-text-3xl md:rtds-text-5xl lg:rtds-text-6xl">Cerca nel sito</h2>
                    <button type="button" class="rtds-btn 
    rtds-btn--icon rtds-btn--icon-square rtds-btn--icon rtds-border-0 hover:rtds-background-primary hover:rtds-text-white" id="closeModal"><svg class="rtds-icon rtds-fill-current rtds-w-8 rtds-h-8" aria-hidden="true" focusable="false" role="img">
                            <use href="../../icons.svg#mini--x-mark" />
                        </svg>
                        <span class="rtds-sr-only">

                            Chiudi
                        </span>

                    </button>

                </div>

                <!-- Search Bar Component -->
                <div class="rtds-w-full">
                    <div class="rtds-search-bar" data-search-bar data-all-results-href="/search?q=" data-all-results-label="Tutti i risultati">
                        <form class="rtds-search-bar__form" role="search" action="/search" method="get">
                            <div class="rtds-search-bar__wrapper rtds-grid rtds-gap-2">
                                <label for="desktopSearchBar" class="rtds-search-bar__label rtds-text-base md:rtds-text-lg rtds-font-medium rtds-content-03 ">
                                    Cerca servizi, informazioni, aiuti...
                                </label>

                                <div class="rtds-search-bar__input-wrapper">
                                    <div class="rtds-search-bar__autocomplete" id="desktopSearchBarContainer" data-placeholder="Search"></div>

                                    <button type="button" class="rtds-btn rtds-btn--icon rtds-btn--l rtds-search-bar__clear rtds-border-0 rtds-hidden hover:rtds-background-secondary hover:rtds-text-white" data-search-clear>
                                        <span class="rtds-sr-only">Cancella ricerca</span>
                                        <svg class="rtds-icon rtds-fill-current rtds-w-6 rtds-h-6" aria-hidden="true" focusable="false" role="img">
                                            <use href="../../icons.svg#outline--x-circle" />
                                        </svg>

                                    </button>

                                    <button type="submit" class="rtds-btn 
    r rtds-btn--icon rtds-btn--icon-square rtds-btn--primary rtds-search-bar__submit rtds-btn--l"><svg class="rtds-icon rtds-fill-current rtds-w-6 rtds-h-6" aria-hidden="true" focusable="false" role="img">
                                            <use href="../../icons.svg#mini--magnifying-glass" />
                                        </svg>
                                        <span class="rtds-sr-only">

                                            Cerca
                                        </span>

                                    </button>

                                </div>
                            </div>
                        </form>
                    </div>

                </div>

            </div><!-- /END MODAL CONTENT -->
        </div>
    </div>
</div>
{% extends '@search-modal-desktop' %}

{% block searchModalContent %}
            <!-- Search Bar Component -->
            <div class="rtds-w-full">
                {% render '@search-bar', {
                    searchId: searchId|default('desktopSearchBar'),
                    label: label|default('Cerca servizi, informazioni, aiuti...'),
                    submitLabel: submitLabel|default('Cerca'),
                    showClearButton: showClearButton|default(true),
                    showLabel: showLabel|default(true),
                    placeholder: placeholder|default('Search'),
                    formAction: formAction|default('/search'),
                    formMethod: formMethod|default('get'),
                    allResultsHref: allResultsHref|default('/search?q='),
                    allResultsLabel: allResultsLabel|default('Tutti i risultati'),
                    searchOptions: searchOptions,
                    variant: variant,
                    classes: searchBarClasses
                }, true %}
            </div>
{% endblock searchModalContent %}
{
  "modalId": "searchModal",
  "modalTitleId": "modalTitle",
  "modalTitle": "Cerca nel sito",
  "triggerId": "searchModalTrigger",
  "triggerLabel": "Cerca nel sito - apri la modale",
  "closeButtonId": "closeModal",
  "closeButtonLabel": "Chiudi",
  "inputLabel": "Cerca servizi, informazioni, aiuti...",
  "inputId": "inputSearch",
  "inputPlaceholder": "Cerca",
  "submitLabel": "Cerca",
  "searchButtonBorderColor": "rtds-border-gray-03",
  "searchButtonTextColor": null,
  "triggerClasses": null,
  "searchId": "desktopSearchBar",
  "label": "Cerca servizi, informazioni, aiuti...",
  "showClearButton": true,
  "showLabel": true,
  "placeholder": "Search",
  "formAction": "/search",
  "formMethod": "get",
  "allResultsHref": "/search?q=",
  "allResultsLabel": "Tutti i risultati"
}
  • Content:
    /**
     * SEARCH MODAL DESKTOP COMPONENT
     * Componente di ricerca con modale desktop
     */
     @layer components {
        .is-opened-by-pointer .rtds-search-modal-desktop__input [type="search"],
        .is-opened-by-pointer .rtds-search-modal-desktop__input [type="text"] {
            @apply rtds-outline-0;
            outline: 2px solid transparent;
            box-shadow: none;
        }
     }
  • URL: /components/raw/search-modal-desktop/search-modal-desktop.css
  • Filesystem Path: components/03-molecules/search-modal-desktop/search-modal-desktop.css
  • Size: 365 Bytes
  • Content:
    /* SEARCH MODAL DESKTOP SCRIPT */
    document.addEventListener('DOMContentLoaded', function () {
        'use strict';
        
        const modal = document.getElementById('searchModal');
        if (!modal) return;
        
        const trigger = document.getElementById('searchModalTrigger');
        const closeButton = document.getElementById('closeModal');
        const modalContent = modal.querySelector('.rtds-modal-content');
        const header = document.querySelector('.rtds-header');
        
        if (!trigger || !closeButton || !modalContent) return;
        
        // Helper function to get focusable elements
        const getFocusableElements = (container) => {
            const focusableSelectors = [
                'a[href]',
                'button:not([disabled])',
                'textarea:not([disabled])',
                'input:not([disabled])',
                'select:not([disabled])',
                '[tabindex]:not([tabindex="-1"])'
            ].join(', ');
            
            return Array.from(container.querySelectorAll(focusableSelectors)).filter(
                el => el.offsetParent !== null &&
                el.getAttribute('aria-hidden') !== 'true' &&
                el.style.display !== 'none' &&
                el.style.visibility !== 'hidden'
            );
        };
        
        // Focus trap handler
        let focusTrapHandler = null;
        
        const trapFocus = (e) => {
            if (modal.style.display === 'none' || modal.getAttribute('aria-hidden') === 'true') {
                return;
            }
            
            // Se l'elemento che sta ricevendo il focus non è dentro la modale, riporta il focus dentro
            if (!modalContent.contains(e.target)) {
                e.preventDefault();
                const focusableElements = getFocusableElements(modalContent);
                if (focusableElements.length > 0) {
                    focusableElements[0].focus();
                }
            }
        };
        
        // Open modal function
        const openModal = (interactionType = 'pointer') => {
            modal.style.display = 'flex';
            modal.classList.add("rtds-z-20");
            modal.setAttribute('aria-hidden', 'false');
            
            if (header) {
                header.classList.add('has-search-modal-open');
            }
            
            // Update aria-expanded on trigger
            trigger.setAttribute('aria-expanded', 'true');
            
            // Aggiungi classe per indicare il tipo di interazione
            modal.classList.remove('is-opened-by-pointer', 'is-opened-by-keyboard');
            if (interactionType === 'keyboard') {
                modal.classList.add('is-opened-by-keyboard');
            } else {
                modal.classList.add('is-opened-by-pointer');
            }
            
            // Activate focus trap at document level
            focusTrapHandler = trapFocus;
            document.addEventListener('focusin', focusTrapHandler);
            
            // Get all focusable elements inside modal content
            const focusableElements = getFocusableElements(modalContent);
            const fallbackFocusable = modalContent.querySelector('.is-focusable-element');
            
            // Cerca l'input di ricerca (può essere un input normale o l'input dell'autocomplete)
            const searchInput = modalContent.querySelector('input[type="search"], input[id*="Search"], input[id*="search"], .autocomplete__input');
            
            // Focus on search input if available, otherwise first focusable element or fallback
            if (searchInput) {
                setTimeout(() => {
                    searchInput.focus();
                }, 100);
            } else if (focusableElements.length > 0) {
                focusableElements[0].focus();
            } else if (fallbackFocusable) {
                fallbackFocusable.setAttribute('tabindex', '-1');
                fallbackFocusable.focus();
            }
            
            // Prevent body scroll
            if (!document.documentElement.classList.contains('!rtds-overflow-hidden')) {
                document.documentElement.classList.add('!rtds-overflow-hidden');
            }
        };
        
        // Close modal function
        const closeModal = () => {
            // Remove focus trap
            if (focusTrapHandler) {
                document.removeEventListener('focusin', focusTrapHandler);
                focusTrapHandler = null;
            }
            
            // Remove tabindex from fallback focusable element
            const fallbackFocusable = modalContent.querySelector('.is-focusable-element');
            if (fallbackFocusable) {
                fallbackFocusable.removeAttribute('tabindex');
            }
            
            // Restore body scroll
            if (document.documentElement.classList.contains('!rtds-overflow-hidden')) {
                document.documentElement.classList.remove('!rtds-overflow-hidden');
            }
            
            modal.style.display = 'none';
            modal.setAttribute('aria-hidden', 'true');
            modal.classList.remove("rtds-z-20");
            
            if (header) {
                header.classList.remove('has-search-modal-open');
            }
            
            // Update aria-expanded on trigger and return focus
            trigger.setAttribute('aria-expanded', 'false');
            trigger.focus();
        };
        
        // Event listeners
        // Gestione apertura tramite click/touch (pointer)
        trigger.addEventListener('click', (e) => {
            openModal('pointer');
        });
        
        // Gestione apertura tramite tastiera (Enter/Space)
        trigger.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' || e.key === ' ') {
                e.preventDefault();
                openModal('keyboard');
            }
        });
        
        closeButton.addEventListener('click', closeModal);
        
        // Keyboard navigation
        modal.addEventListener('keydown', function (event) {
            // Se l'utente inizia a navigare con la tastiera, rimuovi la classe is-opened-by-pointer
            // per permettere al focus ring di apparire normalmente
            if (modal.classList.contains('is-opened-by-pointer')) {
                // Rimuovi is-opened-by-pointer quando l'utente usa Tab, frecce, o altri tasti di navigazione
                if (event.key === 'Tab' || event.key.startsWith('Arrow') || event.key === 'Home' || event.key === 'End') {
                    modal.classList.remove('is-opened-by-pointer');
                }
            }
            
            if (event.key === 'Tab') {
                const focusableElements = getFocusableElements(modalContent);
                
                // If no focusable elements, prevent tabbing
                if (focusableElements.length === 0) {
                    event.preventDefault();
                    return;
                }
                
                const firstFocusableElement = focusableElements[0];
                const lastFocusableElement = focusableElements[focusableElements.length - 1];
                
                if (event.shiftKey) {
                    // Shift + Tab: backward navigation
                    if (document.activeElement === firstFocusableElement) {
                        lastFocusableElement.focus();
                        event.preventDefault();
                    }
                } else {
                    // Tab: forward navigation
                    if (document.activeElement === lastFocusableElement) {
                        firstFocusableElement.focus();
                        event.preventDefault();
                    }
                }
            }
            
            if (event.key === 'Escape') {
                closeModal();
            }
        });
    });
    
    
  • URL: /components/raw/search-modal-desktop/search-modal-desktop.js
  • Filesystem Path: components/03-molecules/search-modal-desktop/search-modal-desktop.js
  • Size: 7.3 KB

Search Modal Desktop

Componente molecola che rappresenta la modale di ricerca desktop. Include il trigger button e la modale con form di ricerca.

Panoramica

Il componente search-modal-desktop è progettato per essere utilizzato nella top-bar dell’header. Fornisce un trigger button che apre una modale full-screen contenente un form di ricerca.

Caratteristiche

  • Trigger button con icona di ricerca
  • Modale full-screen con overlay scuro
  • Form di ricerca con input field e bottone submit
  • Gestione completa dell’accessibilità (ARIA, focus trap, navigazione da tastiera)
  • Supporto per chiusura con ESC
  • Gestione dinamica di aria-expanded sul trigger

Varianti

Il componente include due varianti:

  1. default: Utilizza un form con @input-field--search e @button--icon-right
  2. with-search-bar: Utilizza il componente @search-bar con autocomplete

Configurazione

Parametri Principali

Identificatori

  • modalId: ID univoco per la modale (default: “searchModal”)
  • modalTitleId: ID per il titolo della modale (default: “modalTitle”)
  • triggerId: ID univoco per il trigger button (default: “searchModalTrigger”)
  • closeButtonId: ID univoco per il bottone di chiusura (default: “closeModal”)

Testi

  • modalTitle: Titolo della modale (default: “Cerca nel sito”)
  • triggerLabel: Label nascosta per il trigger button (default: “Cerca nel sito - apri la modale”)
  • closeButtonLabel: Label per il bottone di chiusura (default: “Chiudi”)

Variante Default (con form)

  • inputLabel: Label per il campo di ricerca (default: “Cerca servizi, informazioni, aiuti…”)
  • inputId: ID per il campo di ricerca (default: “inputSearch”)
  • inputPlaceholder: Placeholder per l’input (default: “Cerca”)
  • submitLabel: Testo per il bottone submit (default: “Cerca nel sito”)
  • searchId: ID univoco per il campo di ricerca (default: “desktopSearchBar”)
  • label: Label per il campo di ricerca (default: “Cerca”)
  • submitLabel: Testo per il bottone submit (default: “Cerca”)
  • showClearButton: Mostra/nasconde il bottone clear (default: true)
  • placeholder: Placeholder per l’input (default: “Search”)
  • formAction: URL per il submit del form (default: “/search”)
  • formMethod: Metodo HTTP per il form (default: “get”)
  • allResultsHref: URL per il link “Tutti i risultati” (default: “/search?q=”)
  • allResultsLabel: Testo per il link “Tutti i risultati” (default: “Tutti i risultati”)
  • searchOptions: Array di opzioni per l’autocomplete (opzionale)
  • variant: Variante del componente search-bar (opzionale)
  • searchBarClasses: Classi CSS aggiuntive per il componente search-bar (opzionale)

Stili

  • searchButtonBorderColor: Classe CSS per il colore del bordo del trigger (default: “rtds-border-gray-03”)
  • searchButtonTextColor: Classe CSS per il colore del testo del trigger (opzionale)
  • triggerClasses: Classi CSS aggiuntive per il trigger button (opzionale)

Accessibilità

Il componente include:

  • ARIA Attributes:

    • aria-haspopup="dialog" sul trigger button
    • aria-controls che punta all’ID della modale
    • aria-expanded gestito dinamicamente (false quando chiusa, true quando aperta)
    • aria-hidden sulla modale (true quando chiusa, false quando aperta)
    • aria-modal="true" sulla modale
    • aria-labelledby che punta al titolo della modale
  • Focus Management:

    • Focus trap all’interno della modale quando è aperta
    • Focus automatico sul primo elemento focusabile all’apertura
    • Ritorno del focus al trigger button alla chiusura
  • Navigazione da Tastiera:

    • Tab/Shift+Tab: Navigazione ciclica tra gli elementi focusabili
    • ESC: Chiude la modale e riporta il focus al trigger
  • Screen Reader:

    • Label nascoste per i bottoni icona
    • Testi descrittivi per le azioni

JavaScript

Il componente include un file JavaScript (search-modal-desktop.js) che gestisce:

  • Apertura/chiusura della modale
  • Focus trap
  • Gestione degli attributi ARIA
  • Navigazione da tastiera (Tab, Shift+Tab, ESC)
  • Prevenzione dello scroll del body quando la modale è aperta

Il file JavaScript viene automaticamente incluso nel bundle componentsJs.js dal sistema di build.

Struttura HTML

Il componente genera la seguente struttura:

<div class="rtds-search-modal-desktop__wrapper rtds-hidden lg:rtds-block">
  <button id="searchModalTrigger" aria-haspopup="dialog" aria-controls="searchModal" aria-expanded="false">
    <!-- Trigger button -->
  </button>
  
  <div id="searchModal" class="rtds-search-modal-desktop" aria-hidden="true" role="dialog" aria-modal="true">
    <div class="rtds-modal-content">
      <h2 id="modalTitle">Cerca nel sito</h2>
      <button id="closeModal">Chiudi</button>
      <form role="search">
        <!-- Form di ricerca -->
      </form>
    </div>
  </div>
</div>

Note

  • Il componente è visibile solo su schermi desktop (lg:rtds-block)
  • La modale utilizza un overlay scuro (rtds-bg-black rtds-bg-opacity-50)
  • Il form di ricerca utilizza @input-field--search e @button--icon-right
  • La gestione JavaScript è completamente autonoma e non dipende da altri componenti