<!-- 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
*/
.is-opened-by-pointer .rtds-search-modal-desktop__input [type="search"],
.is-opened-by-pointer .rtds-search-modal-desktop__input [type="text"] {
outline-width: 0px;
outline: 2px solid transparent;
box-shadow: none;
}
/**
* 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 è renderizzato con due elementi principali:
<button> — trigger esterno (icona ricerca); ha aria-haspopup="dialog", aria-controls e aria-expanded; il nome accessibile è in un <span class="rtds-sr-only"> interno<div role="dialog" aria-modal="true" aria-labelledby="..."> — modale con <h2> come titolo referenziato; contiene il form di ricerca e un <button> di chiusuraAll’apertura, JS rimuove aria-hidden dalla modale e sposta il focus al primo elemento; alla chiusura, aria-hidden="true" la nasconde agli screen reader e il focus torna al trigger.
default: Utilizza un form con @input-field--search e @button--icon-rightwith-search-bar: Utilizza il componente @search-bar con autocompleteContesto di sviluppo Fractal/Nunjucks — Questa sezione riguarda esclusivamente i parametri del context
.config.ymlusati durante lo sviluppo nella libreria. Non descrive attributi HTML o proprietà da usare nei temi, CMS o ambienti di implementazione: per quello, fare riferimento alla sezione Esempi di utilizzo.
modalId (stringa, opzionale): ID univoco per la modale (default: “searchModal”)modalTitleId (stringa, opzionale): ID per il titolo della modale (default: “modalTitle”)triggerId (stringa, opzionale): ID univoco per il trigger button (default: “searchModalTrigger”)closeButtonId (stringa, opzionale): ID univoco per il bottone di chiusura (default: “closeModal”)modalTitle (stringa, opzionale): Titolo della modale (default: “Cerca nel sito”)triggerLabel (stringa, opzionale): Label nascosta per il trigger button (default: “Cerca nel sito - apri la modale”)closeButtonLabel (stringa, opzionale): Label per il bottone di chiusura (default: “Chiudi”)inputLabel (stringa, opzionale): Label per il campo di ricerca (default: “Cerca servizi, informazioni, aiuti…”)inputId (stringa, opzionale): ID per il campo di ricerca (default: “inputSearch”)inputPlaceholder (stringa, opzionale): Placeholder per l’input (default: “Cerca”)submitLabel (stringa, opzionale): Testo per il bottone submit (default: “Cerca nel sito”)searchId (stringa, opzionale): ID univoco per il campo di ricerca (default: “desktopSearchBar”)label (stringa, opzionale): Label per il campo di ricerca (default: “Cerca”)submitLabel (stringa, opzionale): Testo per il bottone submit (default: “Cerca”)showClearButton (booleano, opzionale): Mostra/nasconde il bottone clear (default: true)placeholder (stringa, opzionale): Placeholder per l’input (default: “Search”)formAction (stringa, opzionale): URL per il submit del form (default: “/search”)formMethod (stringa, opzionale): Metodo HTTP per il form (default: “get”)allResultsHref (stringa, opzionale): URL per il link “Tutti i risultati”allResultsLabel (stringa, opzionale): Testo per il link “Tutti i risultati” (default: “Tutti i risultati”)searchOptions (array, opzionale): Array di opzioni per l’autocompletevariant (stringa, opzionale): Variante del componente search-barsearchBarClasses (stringa, opzionale): Classi CSS aggiuntive per il componente search-barsearchButtonBorderColor (stringa, opzionale): Classe CSS per il colore del bordo del trigger (default: “rtds-border-gray-03”)searchButtonTextColor (stringa, opzionale): Classe CSS per il colore del testo del triggertriggerClasses (stringa, opzionale): Classi CSS aggiuntive per il trigger buttonLivello AA — criteri pertinenti:
role="dialog" e aria-labelledby per associarla al proprio titoloEscape con ritorno del focus al triggeraria-haspopup="dialog" e aria-expanded sul trigger comunicano il tipo e lo stato della modale; aria-hidden sulla modale ne nasconde il contenuto quando chiusa<button> (trigger) — ruolo implicito button; ha aria-haspopup="dialog", aria-controls e aria-expanded; il nome accessibile è fornito da <span class="rtds-sr-only"> interno<div role="dialog" aria-modal="true" aria-labelledby="..."> — il titolo <h2> è referenziato tramite aria-labelledby; il contenuto è nascosto con aria-hidden="true" quando la modale è chiusa<button> (chiusura) — il nome accessibile è fornito dal proprio testo visibile<form role="search"> — landmark di ricerca all’interno della modale| Attributo | Valori | Condizione |
|---|---|---|
aria-haspopup |
"dialog" |
sempre presente sul trigger button |
aria-controls |
ID della modale | sempre presente sul trigger button |
aria-expanded |
"true" / "false" |
gestito dinamicamente sul trigger button |
aria-hidden |
"true" / "false" |
gestito dinamicamente sulla modale |
role |
"dialog" |
sempre presente sulla modale |
aria-modal |
"true" |
sempre presente sulla modale |
aria-labelledby |
ID del titolo | sempre presente sulla modale |
| Tasto | Azione |
|---|---|
Tab / Shift+Tab |
Navigazione ciclica tra gli elementi focusabili all’interno della modale |
Escape |
Chiude la modale e riporta il focus al trigger button |
:focus-visible è sempre visibile e non viene rimosso via CSS<button> ha <span class="rtds-sr-only"> come nome accessibile (es. “Cerca nel sito - apri la modale”) e aria-haspopup="dialog" — lo screen reader annuncia il nome del pulsante e il tipo di popuparia-labelledby collega la modale al suo <h2> — lo screen reader annuncia il titolo della modale all’ingresso nel dialogoaria-hidden="true" impedisce agli screen reader di leggerne il contenuto<button> di chiusura ha il testo visibile come nome accessibile (es. “Chiudi”); l’icona eventuale ha aria-hidden="true"<!-- Struttura base -->
<div class="rtds-search-modal-desktop__wrapper rtds-hidden lg:rtds-block">
<button id="searchModalTrigger"
aria-haspopup="dialog"
aria-controls="searchModal"
aria-expanded="false">
<span class="rtds-sr-only">Cerca nel sito - apri la modale</span>
<svg class="rtds-icon" aria-hidden="true" focusable="false"><use href="#outline--magnifying-glass"></use></svg>
</button>
<div id="searchModal"
class="rtds-search-modal-desktop"
aria-hidden="true"
role="dialog"
aria-modal="true"
aria-labelledby="modalTitle">
<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)search-modal-desktop.js