<!-- 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>

                <form role="search" class="rtds-flex rtds-gap-2">
                    <div class="rtds-input-field rtds-gap-1 rtds-flex-1 rtds-search-modal-desktop__input ">
                        <label for="inputSearch" class="rtds-input-field__label">
                            Cerca servizi, informazioni, aiuti...

                        </label>

                        <div class="rtds-input has-icon">

                            <svg class="rtds-icon rtds-fill-current rtds-input__icon" aria-hidden="true" focusable="false" role="img">
                                <use href="../../icons.svg#mini--magnifying-glass" />
                            </svg>

                            <input id="inputSearch" class="rtds-text-sm rtds-input-placeholder" type="search" placeholder="Cerca">
                        </div>

                    </div>
                    <button type="button" class="rtds-btn 
    rtds-btn--icon-right rtds-group/button rtds-btn--primary rtds-self-end">

                        Cerca nel sito

                        <svg class="rtds-icon rtds-fill-current rtds-w-4 rtds-h-4 md:rtds-w-5 md:rtds-h-5 lg:rtds-w-6 lg:rtds-h-6 group-hover/button:rtds-translate-x-1" aria-hidden="true" focusable="false" role="img">
                            <use href="../../icons.svg#mini--chevron-right" />
                        </svg>

                    </button>

                </form>

            </div><!-- /END MODAL CONTENT -->
        </div>
    </div>
</div>
<!-- Desktop Search Modal -->
<div class="rtds-search-modal-desktop__wrapper rtds-hidden lg:rtds-block">
    <button 
        type="button" 
        aria-haspopup="dialog" 
        aria-controls="{{ modalId|default('searchModal') }}" 
        aria-expanded="false"
        id="{{ triggerId|default('searchModalTrigger') }}" 
        class="rtds-btn rtds-btn--outline rtds-btn--icon rtds-w-6 rtds-h-6 md:rtds-w-10 md:rtds-h-10{% if triggerClasses %} {{ triggerClasses }}{% endif %}{% if searchButtonBorderColor %} {{ searchButtonBorderColor }}{% endif %}{% if searchButtonTextColor %} {{ searchButtonTextColor }}{% endif %} 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">{{ triggerLabel|default('Cerca nel sito - apri la modale') }}</span>
        {% render '@icon--small', { id: 'mini--magnifying-glass', size: 'rtds-w-4 rtds-h-4 md:rtds-w-5 md:rtds-h-5' }, true %}
    </button>

    <!-- Search Modal -->
    <div
        id="{{ modalId|default('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="{{ modalTitleId|default('modalTitle') }}"
        {% if triggerId %}data-trigger-id="{{ triggerId }}"{% endif %}
        {% if closeButtonId %}data-close-button-id="{{ closeButtonId }}"{% endif %}>

        <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="{{ modalTitleId|default('modalTitle') }}" class="rtds-text-3xl md:rtds-text-5xl lg:rtds-text-6xl">{{ modalTitle|default('Cerca nel sito') }}</h2>
                    {% render '@button--icon-square', {
                        buttonType: 'button',
                        id: closeButtonId|default('closeModal'),
                        icon: 'mini--x-mark',
                        iconSize: 'rtds-w-8 rtds-h-8',
                        label: closeButtonLabel|default('Chiudi'),
                        labelHidden: true,
                        classes: 'rtds-btn--icon rtds-border-0 hover:rtds-background-primary hover:rtds-text-white'
                    }, true %}
                </div>

                {% block searchModalContent %}
                <form role="search" class="rtds-flex rtds-gap-2">
                    {% render '@input-field--search', { 
                        classes: 'rtds-flex-1 rtds-search-modal-desktop__input',
                        label: inputLabel|default('Cerca servizi, informazioni, aiuti...'), 
                        inputId: inputId|default('inputSearch'), 
                        inputPlaceholder: inputPlaceholder|default('Cerca'), 
                        inputType: 'search'
                    }, true %}
                    {% render '@button--icon-right', {
                        label: submitLabel|default('Cerca nel sito'), 
                        classes: 'rtds-btn--primary rtds-self-end', 
                        icon: 'mini--chevron-right'
                    }, true %}
                </form>
                {% endblock searchModalContent %}
            </div><!-- /END MODAL CONTENT -->
        </div>
    </div>
</div>
{
  "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 nel sito",
  "searchButtonBorderColor": "rtds-border-gray-03",
  "searchButtonTextColor": null,
  "triggerClasses": null
}
  • 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