<!-- 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"
}
/**
* 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;
}
}
/* 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();
}
});
});
Componente molecola che rappresenta la modale di ricerca desktop. Include il trigger button e la modale con form di ricerca.
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.
aria-expanded sul triggerIl componente include due varianti:
@input-field--search e @button--icon-right@search-bar con autocompletemodalId: 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”)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”)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)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)Il componente include:
ARIA Attributes:
aria-haspopup="dialog" sul trigger buttonaria-controls che punta all’ID della modalearia-expanded gestito dinamicamente (false quando chiusa, true quando aperta)aria-hidden sulla modale (true quando chiusa, false quando aperta)aria-modal="true" sulla modalearia-labelledby che punta al titolo della modaleFocus Management:
Navigazione da Tastiera:
Screen Reader:
Il componente include un file JavaScript (search-modal-desktop.js) che gestisce:
Il file JavaScript viene automaticamente incluso nel bundle componentsJs.js dal sistema di build.
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>lg:rtds-block)rtds-bg-black rtds-bg-opacity-50)@input-field--search e @button--icon-right