<!-- Mobile Search Trigger and Modal -->
<div class="rtds-search-bar__mobile-wrapper">
<!-- Mobile Search Trigger -->
<div class="rtds-main-heading__mobile-search rtds-flex lg:rtds-hidden">
<button type="button" aria-haspopup="dialog" aria-controls="searchModalMobile" aria-expanded="false" id="searchModalMobileTrigger" class="rtds-btn rtds-btn--outline rtds-btn--icon 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-6 rtds-h-6" aria-hidden="true" focusable="false" role="img">
<use href="../../icons.svg#mini--magnifying-glass" />
</svg>
</button>
</div>
<!-- Mobile Search Modal -->
<div id="searchModalMobile" class="rtds-search-bar__modal rtds-fixed rtds-items-start rtds-content-center rtds-inset-0 rtds-background-01 rtds-overflow-y-auto rtds-h-full rtds-w-full lg:rtds-hidden" style="display:none;" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="mobileSearchModalTitle" data-trigger-id="searchModalMobileTrigger" data-close-button-id="closeMobileSearchModal">
<div class="rtds-relative rtds-w-full rtds-min-h-full rtds-bg-background-01 rtds-p-4">
<div class="rtds-flex rtds-flex-col rtds-gap-3 rtds-max-w-full">
<!-- Modal Header -->
<div class="rtds-flex rtds-justify-between rtds-items-center rtds-px-0 rtds-py-0">
<h2 id="mobileSearchModalTitle" class="rtds-text-lg rtds-font-bold rtds-content-01">Cerca nel sito</h2>
<button type="button" class="rtds-btn
rtds-btn--icon rtds-btn--icon-square rtds-border-0 hover:rtds-background-primary hover:rtds-text-white" id="closeMobileSearchModal"><svg class="rtds-icon rtds-fill-current rtds-w-10 rtds-h-10" aria-hidden="true" focusable="false" role="img">
<use href="../../icons.svg#mini--x-mark" />
</svg>
<span class="rtds-sr-only">
Chiudi modale di ricerca
</span>
</button>
</div>
<!-- Search Bar Component -->
<div class="rtds-w-full">
<div class="rtds-search-bar rtds-search-bar--mobile-modal" 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">
<label for="mobileSearchBar" class="rtds-search-bar__label rtds-sr-only">
Cerca
</label>
<div class="rtds-search-bar__input-wrapper">
<div class="rtds-search-bar__autocomplete" id="mobileSearchBarContainer" 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>
</div>
</div>
</div>
<!-- Mobile Search Trigger and Modal -->
<div class="rtds-search-bar__mobile-wrapper">
<!-- Mobile Search Trigger -->
<div class="rtds-main-heading__mobile-search rtds-flex lg:rtds-hidden">
<button
type="button"
aria-haspopup="dialog"
aria-controls="{{ modalId|default('searchModalMobile') }}"
aria-expanded="false"
id="{{ triggerId|default('searchModalMobileTrigger') }}"
class="rtds-btn rtds-btn--outline rtds-btn--icon 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-6 rtds-h-6' }, true %}
</button>
</div>
<!-- Mobile Search Modal -->
<div
id="{{ modalId|default('searchModalMobile') }}"
class="rtds-search-bar__modal rtds-fixed rtds-items-start rtds-content-center rtds-inset-0 rtds-background-01 rtds-overflow-y-auto rtds-h-full rtds-w-full lg:rtds-hidden"
style="display:none;"
aria-hidden="true"
role="dialog"
aria-modal="true"
aria-labelledby="{{ modalTitleId|default('mobileSearchModalTitle') }}"
{% if triggerId %}data-trigger-id="{{ triggerId }}"{% endif %}
{% if closeButtonId %}data-close-button-id="{{ closeButtonId }}"{% endif %}>
<div class="rtds-relative rtds-w-full rtds-min-h-full rtds-bg-background-01 rtds-p-4">
<div class="rtds-flex rtds-flex-col rtds-gap-3 rtds-max-w-full">
<!-- Modal Header -->
<div class="rtds-flex rtds-justify-between rtds-items-center rtds-px-0 rtds-py-0">
<h2 id="{{ modalTitleId|default('mobileSearchModalTitle') }}" class="rtds-text-lg rtds-font-bold rtds-content-01">{{ modalTitle|default('Cerca nel sito') }}</h2>
{% render '@button--icon-square', {
buttonType: 'button',
id: closeButtonId|default('closeMobileSearchModal'),
icon: 'mini--x-mark',
iconSize: 'rtds-w-10 rtds-h-10',
labelHidden: true,
label: closeButtonLabel|default('Chiudi modale di ricerca'),
classes: 'rtds-border-0 hover:rtds-background-primary hover:rtds-text-white'
}, true %}
</div>
<!-- Search Bar Component -->
<div class="rtds-w-full">
{% render '@search-bar', {
searchId: searchId|default('mobileSearchBar'),
label: label|default('Cerca'),
submitLabel: submitLabel|default('Cerca'),
showClearButton: showClearButton|default(true),
placeholder: placeholder|default('Search'),
allResultsHref: allResultsHref|default('/search?q='),
allResultsLabel: allResultsLabel|default('Tutti i risultati'),
classes: 'rtds-search-bar--mobile-modal'
}, true %}
</div>
</div>
</div>
</div>
</div>
{
"label": "Cerca",
"searchId": "mobileSearchBar",
"submitLabel": "Cerca",
"showClearButton": true,
"showLabel": false,
"formAction": "/search",
"formMethod": "get",
"placeholder": "Search",
"allResultsHref": "/search?q=",
"allResultsLabel": "Tutti i risultati",
"modalId": "searchModalMobile",
"modalTitleId": "mobileSearchModalTitle",
"modalTitle": "Cerca nel sito",
"triggerId": "searchModalMobileTrigger",
"triggerLabel": "Cerca nel sito - apri la modale",
"closeButtonId": "closeMobileSearchModal",
"closeButtonLabel": "Chiudi modale di ricerca"
}
/**
* SEARCH BAR COMPONENT
* Componente di ricerca con autocomplete
*/
@layer components {
.rtds-search-bar {
--_autocomplete-max-height: var(--autocomplete-max-height, 25rem);
@apply rtds-relative rtds-w-full;
}
.rtds-search-bar__form {
@apply rtds-w-full;
}
.rtds-search-bar__wrapper {
@apply rtds-flex;
}
.rtds-search-bar__input-wrapper {
@apply rtds-relative rtds-flex-1 rtds-flex rtds-items-center rtds-rounded-lg rtds-bg-white rtds-h-16;
transition: border-color 0.2s ease;
}
/*.rtds-search-bar__input-wrapper:hover:not(:has(.autocomplete__input:focus)) {
@apply rtds-border-gray-03;
}*/
/*.rtds-search-bar--focus .rtds-search-bar__input-wrapper {
@apply rtds-border-primary;
}*/
.rtds-search-bar__autocomplete {
@apply rtds-h-full rtds-flex-1;
}
.rtds-search-bar .autocomplete__wrapper {
@apply rtds-h-full;
}
/* Autocomplete input styling */
.rtds-search-bar .autocomplete__input {
@apply rtds-w-full rtds-py-2 rtds-pl-3 rtds-pr-28 rtds-border rtds-bg-transparent rtds-text-sm rtds-font-medium rtds-content-03 rtds-rounded-lg rtds-h-full rtds-border-input;
transition: border-color 0.2s ease;
}
.rtds-search-bar .autocomplete__input:hover,
.rtds-search-bar .autocomplete__input:focus:not(:focus-visible) {
@apply rtds-border-input;
}
.rtds-search-bar .autocomplete__input:hover,
.rtds-search-bar .autocomplete__input:focus:where(:not(:focus-visible)),
.rtds-search-bar .autocomplete__input--focused:where(:not(:focus-visible)),
.is-opened-by-pointer .autocomplete__input--focused {
@apply rtds-outline-0;
outline: 2px solid transparent;
box-shadow: none;
}
.rtds-search-bar .autocomplete__input::placeholder {
@apply rtds-content-03 rtds-text-base;
}
/* Clear button */
.rtds-search-bar__clear {
@apply rtds-absolute rtds-right-16 rtds-top-1/2 rtds--translate-y-1/2 rtds-z-10;
transition: background-color 0.2s ease;
}
/*.rtds-search-bar__clear:hover {
@apply rtds-background-secondary rtds-text-white;
}*/
/* Submit button */
.rtds-search-bar__submit {
@apply rtds-absolute rtds-right-2 rtds-top-1/2 rtds--translate-y-1/2 rtds-shrink-0 rtds-z-10;
}
/* Variant: Compact */
/* .rtds-search-bar--compact .rtds-search-bar__input-wrapper {
@apply rtds-h-12;
}
.rtds-search-bar--compact .autocomplete__input {
@apply rtds-py-1 rtds-pl-2 rtds-pr-16 rtds-text-sm rtds-h-full;
}
.rtds-search-bar--compact .rtds-search-bar__submit {
@apply rtds-w-8 rtds-h-8;
}
.rtds-search-bar--compact .rtds-search-bar__clear {
@apply rtds-w-5 rtds-h-5 rtds-right-10;
}*/
/* Variant: Full width */
/* .rtds-search-bar--full-width {
@apply rtds-w-full;
}*/
/* Autocomplete menu styling */
:where(.rtds-search-bar) .autocomplete__menu {
@apply rtds-border rtds-border-gray-01 rtds-rounded-lg rtds-shadow-md rtds-background-01 rtds-mt-2;
max-height: var(--_autocomplete-max-height);
overflow-y: auto;
}
:where(.rtds-search-bar) .autocomplete__option {
@apply rtds-px-3 rtds-py-2 rtds-text-sm rtds-font-medium rtds-background-01 rtds-content-03 rtds-border-0;
}
:where(.rtds-search-bar) .autocomplete__option:hover,
:where(.rtds-search-bar) .autocomplete__option--focused {
@apply rtds-background-02;
}
:where(.rtds-search-bar) .autocomplete__option--focused {
outline-style: solid;
outline-width: 2px;
outline-offset: -2px;
@apply rtds-outline-focusring;
}
:where(.rtds-search-bar) .autocomplete__option--focused:where(:not(:focus-visible)) {
outline: 2px solid transparent;
box-shadow: none;
}
/* Hide default dropdown arrow */
.rtds-search-bar .autocomplete__dropdown-arrow-down-wrapper {
@apply rtds-hidden;
}
/* Autocomplete suggestion styling */
.rtds-search-bar__autocomplete-suggestion {
@apply rtds-flex rtds-flex-col rtds-gap-1;
}
.rtds-search-bar__suggestion-title {
@apply rtds-text-sm rtds-font-bold rtds-content-01;
}
.rtds-search-bar__suggestion-type {
@apply rtds-text-xs rtds-font-normal rtds-content-03;
}
/* All results option content wrapper */
/* All results option styling */
.autocomplete__option.rtds-search-bar__all-results-option-wrapper {
@apply rtds-flex rtds-justify-end rtds-px-3;
}
.autocomplete__option.rtds-search-bar__all-results-option-wrapper:hover,
.autocomplete__option.rtds-search-bar__all-results-option-wrapper:focus {
@apply rtds-background-01;
}
.rtds-search-bar__all-results-option {
@apply rtds-border-t rtds-border-gray-03 rtds-py-2 rtds-w-full rtds-flex rtds-justify-end rtds-flex-row;
}
.rtds-search-bar__all-results-option:hover .rtds-search-bar__all-results-link {
@apply rtds-underline;
}
.rtds-search-bar__all-results-link {
@apply rtds-flex rtds-items-center rtds-gap-2 hover:rtds-underline;
}
/* Mobile Search Modal */
.rtds-search-bar__mobile-wrapper {
@apply rtds-inline-flex;
}
.rtds-search-bar__modal {
@apply rtds-z-50;
}
.rtds-search-bar__modal[aria-hidden="false"] {
@apply rtds-flex;
}
.rtds-search-bar__modal > div {
@apply rtds-max-w-full;
}
}
import accessibleAutocomplete from 'accessible-autocomplete';
/**
* Initialize search bar component
*/
function initSearchBar() {
const searchBars = document.querySelectorAll('[data-search-bar]');
searchBars.forEach((searchBar) => {
const container = searchBar.querySelector('[id$="Container"]');
const searchId = container?.id.replace('Container', '');
const clearButton = searchBar.querySelector('[data-search-clear]');
if (!container || !searchId) return;
// Get options from data attribute or use defaults
// Options can be an array of strings or objects with title and type
let options = [];
if (searchBar.dataset.options) {
options = JSON.parse(searchBar.dataset.options);
} else {
// Default options
options = [
{ title: 'Risultato di ricerca', type: 'Content type' },
{ title: 'Filtri di ricerca', type: 'Content type' },
{ title: 'Servizio 1', type: 'Content type' },
{ title: 'Servizio 2', type: 'Content type' },
{ title: 'Informazione 1', type: 'Content type' },
{ title: 'Informazione 2', type: 'Content type' }
];
}
// Normalize options to always be objects
const normalizedOptions = options.map(option => {
if (typeof option === 'string') {
return { title: option, type: 'Content type' };
}
return option;
});
// Get placeholder from data attribute
const placeholder = container.dataset.placeholder || 'Search';
// Get all results link configuration
const allResultsHref = searchBar.dataset.allResultsHref || '#';
const allResultsLabel = searchBar.dataset.allResultsLabel || 'Tutti i risultati';
// Get icon path - try to find existing icon usage in the page
const existingIcon = document.querySelector('svg use[href*="icons.svg"]');
const iconPath = existingIcon ? existingIcon.getAttribute('href').split('#')[0] : '/icons.svg';
// Salva un riferimento a syncResults per poter chiudere il menu programmaticamente
let syncResultsRef = null;
// Initialize accessible autocomplete
accessibleAutocomplete({
element: container,
id: searchId,
placeholder: placeholder,
source: (query, syncResults) => {
// Salva il riferimento a syncResults
syncResultsRef = syncResults;
if (query.length === 0) {
syncResults([]);
return;
}
// Filter options based on query
const filtered = normalizedOptions.filter(option =>
option.title.toLowerCase().includes(query.toLowerCase())
);
// Add "All results" option at the end if there are results
if (filtered.length > 0) {
filtered.push({
title: allResultsLabel,
type: 'all-results',
href: allResultsHref,
isAllResults: true
});
}
syncResults(filtered);
},
showAllValues: false,
dropdownArrow: () => '', // Hide default arrow
onConfirm: (option) => {
if (option) {
// If it's the "All results" option, navigate to the link
if (option.isAllResults && option.href) {
window.location.href = option.href;
return;
}
// Handle selection - option is the full object
console.log('Selected:', option);
}
},
templates: {
inputValue: (result) => {
if (!result) return '';
// Don't set input value for "All results" option
if (result.isAllResults) return '';
return typeof result === 'string' ? result : result.title;
},
suggestion: (result) => {
if (!result) return '';
// Check if it's the "All results" option
const isAllResults = typeof result === 'object' && result.isAllResults;
if (isAllResults) {
// Render as footer link
return `
<div class="rtds-search-bar__autocomplete-suggestion rtds-search-bar__all-results-option">
<a href="${result.href}" class="rtds-action-link rtds-action-link--sm rtds-search-bar__all-results-link">
${result.title}
<svg class="rtds-icon rtds-fill-current rtds-w-4 rtds-h-4" aria-hidden="true" focusable="false" role="img">
<use href="${iconPath}#mini--arrow-small-right" />
</svg>
</a>
</div>
`;
}
// Regular suggestion
const title = typeof result === 'string' ? result : result.title;
const type = typeof result === 'string' ? 'Content type' : result.type;
return `
<div class="rtds-search-bar__autocomplete-suggestion">
<span class="rtds-search-bar__suggestion-title">${title}</span>
<span class="rtds-search-bar__suggestion-type">${type}</span>
</div>
`;
}
}
});
// Get the actual input element created by accessible-autocomplete
const autocompleteInput = searchBar.querySelector(`#${searchId}`);
// Handle clicks on "All results" link and add class to option
if (autocompleteInput) {
const handleMenuClick = (e) => {
const link = e.target.closest('.rtds-search-bar__all-results-link');
if (link) {
e.preventDefault();
e.stopPropagation();
const href = link.getAttribute('href');
if (href && href !== '#') {
window.location.href = href;
}
}
};
const addClassToAllResultsOption = () => {
const menu = searchBar.querySelector('.autocomplete__menu');
if (menu) {
const allResultsOption = menu.querySelector('.autocomplete__option:has(.rtds-search-bar__all-results-option)');
if (allResultsOption) {
allResultsOption.classList.add('rtds-search-bar__all-results-option-wrapper');
}
}
};
// Funzione helper per verificare se il menu è visibile
const isMenuVisible = () => {
const menu = searchBar.querySelector('.autocomplete__menu');
if (!menu) return false;
const style = window.getComputedStyle(menu);
return style.display !== 'none' && style.visibility !== 'hidden';
};
// Funzione per chiudere il menu usando syncResults
const closeMenu = () => {
// Usa syncResults per svuotare i risultati, questo chiuderà il menu correttamente
if (syncResultsRef) {
syncResultsRef([]);
}
// Assicurati che aria-expanded sia false
autocompleteInput.setAttribute('aria-expanded', 'false');
};
// Gestione ESC per chiudere il menu e riportare il focus all'input
const handleEscapeKey = (e) => {
if (e.key === 'Escape' || e.keyCode === 27) {
// Verifica se il menu è visibile
if (isMenuVisible()) {
// Chiudi il menu usando syncResults
closeMenu();
// Nascondi il pulsante di cancellazione se l'input è vuoto
if (clearButton && autocompleteInput.value.length === 0) {
clearButton.classList.add('rtds-hidden');
}
// Riporta il focus all'input
autocompleteInput.focus();
// Previeni il comportamento di default e ferma la propagazione
// per evitare che l'evento raggiunga il listener della modale
e.preventDefault();
e.stopPropagation();
}
}
};
// Aggiungi listener per ESC sull'input
autocompleteInput.addEventListener('keydown', handleEscapeKey);
// Aggiungi listener anche sul menu quando viene creato per gestire ESC durante la navigazione
const handleMenuKeydown = (e) => {
if (e.key === 'Escape' || e.keyCode === 27) {
// Chiudi il menu usando syncResults
closeMenu();
// Nascondi il pulsante di cancellazione se l'input è vuoto
if (clearButton && autocompleteInput.value.length === 0) {
clearButton.classList.add('rtds-hidden');
}
// Riporta sempre il focus all'input quando si preme ESC dal menu
autocompleteInput.focus();
// Previeni il comportamento di default e ferma la propagazione
// per evitare che l'evento raggiunga il listener della modale
e.preventDefault();
e.stopPropagation();
}
};
// Listen for menu changes
const observer = new MutationObserver(() => {
const menu = searchBar.querySelector('.autocomplete__menu');
if (menu) {
menu.addEventListener('click', handleMenuClick);
// Aggiungi listener per ESC sul menu
menu.addEventListener('keydown', handleMenuKeydown);
// Add class to all results option
setTimeout(addClassToAllResultsOption, 0);
}
});
observer.observe(searchBar, {
childList: true,
subtree: true
});
// Also check on input events
autocompleteInput.addEventListener('input', () => {
setTimeout(addClassToAllResultsOption, 100);
});
}
if (autocompleteInput && clearButton) {
// Show/hide clear button based on input value
const updateClearButton = () => {
if (autocompleteInput.value.length > 0) {
clearButton.classList.remove('rtds-hidden');
} else {
clearButton.classList.add('rtds-hidden');
}
};
// Listen to input events
autocompleteInput.addEventListener('input', () => {
updateClearButton();
});
// Clear button handler
clearButton.addEventListener('click', () => {
autocompleteInput.value = '';
autocompleteInput.focus();
updateClearButton();
// Trigger input event to update autocomplete
autocompleteInput.dispatchEvent(new Event('input', { bubbles: true }));
});
// Initial state
updateClearButton();
}
});
}
// Initialize on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initSearchBar);
} else {
initSearchBar();
}
/**
* Initialize mobile search modal
*/
function initMobileSearchModal() {
const modal = document.querySelector('.rtds-search-bar__modal');
if (!modal) return;
const modalId = modal.id;
const triggerId = modal.dataset.triggerId || 'searchModalMobileTrigger';
const closeButtonId = modal.dataset.closeButtonId || 'closeMobileSearchModal';
const trigger = document.getElementById(triggerId);
const closeButton = document.getElementById(closeButtonId);
if (!trigger) return;
// Helper functions
const getFocusableElements = (container) => {
const focusableSelector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"]), [contenteditable], summary';
const elements = container.querySelectorAll(focusableSelector);
return Array.from(elements).filter(el =>
!el.disabled &&
el.offsetParent !== null &&
getComputedStyle(el).visibility !== 'hidden' &&
!el.getAttribute('aria-hidden')
);
};
let focusTrapHandler = null;
const trapFocus = (e) => {
const modalContent = modal.querySelector('.rtds-flex.rtds-flex-col');
// Se l'elemento che sta ricevendo il focus non è dentro la modale, riporta il focus dentro
if (modalContent && !modalContent.contains(e.target)) {
e.preventDefault();
const focusableElements = getFocusableElements(modalContent);
if (focusableElements.length > 0) {
focusableElements[0].focus();
}
}
};
const openMobileSearchModal = (interactionType = 'pointer') => {
const modalContent = modal.querySelector('.rtds-flex.rtds-flex-col');
const header = document.querySelector('.rtds-header');
modal.style.display = 'flex';
modal.classList.add("rtds-z-20");
modal.setAttribute('aria-hidden', 'false');
if (header) {
header.classList.add('has-search-modal-open');
}
// Aggiorna aria-expanded sul trigger
if (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');
}
// Attiva il focus trap a livello di document
focusTrapHandler = trapFocus;
document.addEventListener('focusin', focusTrapHandler);
// Get all the focusable elements inside the modal content
const focusableElements = getFocusableElements(modalContent);
const searchInput = modalContent.querySelector('input[type="search"], input[id*="Search"], .autocomplete__input');
// Focus on search input if available, otherwise first focusable element
if (searchInput) {
setTimeout(() => {
searchInput.focus();
}, 100);
} else if (focusableElements.length > 0) {
focusableElements[0].focus();
}
if (!document.documentElement.classList.contains('!rtds-overflow-hidden')) {
document.documentElement.classList.add('!rtds-overflow-hidden');
}
};
const closeMobileSearchModal = () => {
const header = document.querySelector('.rtds-header');
// Rimuovi il focus trap
if (focusTrapHandler) {
document.removeEventListener('focusin', focusTrapHandler);
focusTrapHandler = null;
}
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");
modal.classList.remove('is-opened-by-pointer', 'is-opened-by-keyboard');
if (header) {
header.classList.remove('has-search-modal-open');
}
// Aggiorna aria-expanded sul trigger
if (trigger) {
trigger.setAttribute('aria-expanded', 'false');
trigger.focus();
}
};
// Gestione apertura tramite click/touch (pointer)
trigger.addEventListener('click', (e) => {
openMobileSearchModal('pointer');
});
// Gestione apertura tramite tastiera (Enter/Space)
trigger.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
openMobileSearchModal('keyboard');
}
});
// Gestione chiusura
if (closeButton) {
closeButton.addEventListener('click', closeMobileSearchModal);
}
// Funzione helper per verificare se il menu autocomplete è visibile
const isAutocompleteMenuVisible = () => {
const autocompleteMenu = modal.querySelector('.autocomplete__menu');
if (!autocompleteMenu) return false;
const style = window.getComputedStyle(autocompleteMenu);
return style.display !== 'none' && style.visibility !== 'hidden';
};
// Gestione keyboard nella modale
modal.addEventListener('keydown', function (event) {
const modalContent = modal.querySelector('.rtds-flex.rtds-flex-col');
// 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);
// Se non ci sono elementi focusabili, previeni la tabulazione
if (focusableElements.length === 0) {
event.preventDefault();
return;
}
const firstFocusableElement = focusableElements[0];
const lastFocusableElement = focusableElements[focusableElements.length - 1];
if (event.shiftKey) {
// Shift + Tab: navigazione all'indietro
if (document.activeElement === firstFocusableElement) {
lastFocusableElement.focus();
event.preventDefault();
}
} else {
// Tab: navigazione in avanti
if (document.activeElement === lastFocusableElement) {
firstFocusableElement.focus();
event.preventDefault();
}
}
}
if (event.key === 'Escape') {
// Verifica se il menu autocomplete è aperto
const menuIsOpen = isAutocompleteMenuVisible();
// Verifica se l'evento proviene dal menu autocomplete (non dall'input)
const autocompleteMenu = modal.querySelector('.autocomplete__menu');
const isEventFromAutocompleteMenu = autocompleteMenu && autocompleteMenu.contains(event.target);
// Se il menu autocomplete è aperto E l'evento proviene dal menu stesso,
// NON chiudere la modale - il listener dell'autocomplete gestirà l'ESC
if (menuIsOpen && isEventFromAutocompleteMenu) {
// Ferma la propagazione per evitare che altri listener gestiscano l'evento
event.stopPropagation();
return;
}
// Se il menu autocomplete NON è aperto, chiudi la modale
// (anche se l'evento proviene dall'input o da altri elementi nella modale)
if (!menuIsOpen) {
closeMobileSearchModal();
}
}
});
}
// Initialize mobile search modal on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initMobileSearchModal);
} else {
initMobileSearchModal();
}
export default initSearchBar;
Componente di ricerca con autocomplete basato su Accessible Autocomplete.
Il componente implementa una barra di ricerca con funzionalità di autocompletamento accessibile, utilizzando la libreria Accessible Autocomplete. Supporta diversi stati (default, hover, focus, open) e varianti (base/md, compact, full-width).
label: Testo della label (default: “Cerca”)searchId: ID univoco per il campo di ricerca (obbligatorio)submitLabel: Testo per il bottone di ricerca (default: “Cerca”)placeholder: Placeholder per l’input (default: “Search”)showClearButton: Mostra/nasconde il bottone per cancellare (default: true)showLabel: Mostra/nasconde visivamente il label (default: false)false: Label nascosto con classe rtds-sr-only (solo per screen reader)true: Label visibile con stili appropriati (utile per modali desktop)showResults: Mostra/nasconde il dropdown dei risultati (default: true)showAllResultsButton: Mostra/nasconde il bottone “Tutti i risultati” (default: true)formAction: URL per il submit del form (default: “/search”)formMethod: Metodo HTTP per il form (default: “get”)variant: Variante del componente (“compact” o “full-width”)searchResults: Array di oggetti con title e type per i risultati di ricercasearchFilters: Array di oggetti con title e type per i filtri di ricercaUtilizzare @search-bar con i parametri: searchId: 'mySearch', label: 'Cerca', placeholder: 'Inserisci la tua ricerca'.
Utilizzare @search-bar con i parametri: searchId: 'mySearch', searchResults (array di oggetti con title e type), searchFilters (array di oggetti con title e type).
Variante standard con dimensioni medie.
Variante compatta con dimensioni ridotte per spazi limitati.
Variante che occupa tutta la larghezza disponibile.
Variante che mostra il label visivamente invece di nasconderlo con rtds-sr-only. Utile per modali desktop dove il label deve essere visibile all’utente.
Esempio:
Utilizzare @search-bar--with-visible-label con i parametri: searchId: 'mySearch', label: 'Cerca servizi, informazioni, aiuti...', placeholder: 'Search'.
Variante che include sia il trigger button che la modale mobile per la ricerca. Questa variante è progettata per essere inclusa nel componente main-heading e gestisce automaticamente l’apertura/chiusura della modale con supporto per interazioni da puntatore e tastiera.
Parametri specifici per Mobile Modal:
modalId: ID della modale (default: “searchModalMobile”)modalTitleId: ID del titolo della modale (default: “mobileSearchModalTitle”)modalTitle: Titolo della modale (default: “Cerca nel sito”)triggerId: ID del trigger button (default: “searchModalMobileTrigger”)triggerLabel: Label per screen reader del trigger (default: “Cerca nel sito - apri la modale”)closeButtonId: ID del bottone di chiusura (default: “closeMobileSearchModal”)closeButtonLabel: Label per screen reader del bottone di chiusura (default: “Chiudi modale di ricerca”)Esempio:
Utilizzare @search-bar--mobile-modal con i parametri: modalId: 'searchModalMobile', triggerId: 'searchModalMobileTrigger', searchId: 'mobileSearchBar', label: 'Cerca', placeholder: 'Search'.
Il componente utilizza Accessible Autocomplete per gestire l’autocompletamento. Le opzioni possono essere passate tramite l’attributo data-options del componente:
<div data-search-bar data-options='["Opzione 1", "Opzione 2", "Opzione 3"]'>
<!-- ... -->
</div>La variante mobile-modal include funzionalità JavaScript avanzate per:
aria-hidden e aria-expandedaria-expanded che viene aggiornato automaticamente:aria-expanded="false" quando la modale è chiusa (valore iniziale)aria-expanded="true" quando la modale è apertaLa modale rileva automaticamente il tipo di interazione utilizzato per aprirla e aggiunge classi CSS (is-opened-by-pointer o is-opened-by-keyboard) che possono essere utilizzate per personalizzare lo stile del focus ring.
showLabel: false, default) o visibile (showLabel: true) a seconda del contesto d’usortds-sr-onlyaria-expanded, aria-haspopup, aria-controls, aria-hidden)aria-expanded sui trigger button per indicare lo stato della modale agli screen reader