<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="searchBarVisibleLabel" 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="searchBarVisibleLabelContainer" 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 class="rtds-search-bar{% if variant %} rtds-search-bar--{{ variant }}{% endif %}{% if classes %} {{ classes }}{% endif %}" data-search-bar{% if searchOptions %} data-options='{{ searchOptions | tojson }}'{% endif %}{% if searchUrl %} data-search-url="{{ searchUrl }}"{% endif %}{% if allResultsHref %} data-all-results-href="{{ allResultsHref }}"{% endif %}{% if allResultsLabel %} data-all-results-label="{{ allResultsLabel }}"{% endif %}>
<form class="rtds-search-bar__form" role="search"{% if formAction %} action="{{ formAction }}"{% endif %}{% if formMethod %} method="{{ formMethod }}"{% endif %}>
<div class="rtds-search-bar__wrapper{% if showLabel == true %} rtds-grid rtds-gap-2{% endif %}">
<label for="{{ searchId }}" class="rtds-search-bar__label{% if showLabel != true %} rtds-sr-only{% else %} rtds-text-base md:rtds-text-lg rtds-font-medium rtds-content-03 {% endif %}">
{{ label|default('Cerca') }}
</label>
<div class="rtds-search-bar__input-wrapper">
<div class="rtds-search-bar__autocomplete" id="{{ searchId }}Container"{% if placeholder %} data-placeholder="{{ placeholder }}"{% endif %}></div>
{% if showClearButton %}
<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>
{% render '@icon', {id: 'outline--x-circle', size: 'rtds-w-6 rtds-h-6'}, true %}
</button>
{% endif %}
{% render '@button--icon-square-l', {
buttonType: 'submit',
icon: 'mini--magnifying-glass',
iconSize: 'rtds-w-6 rtds-h-6',
label: submitLabel|default('Cerca'),
classes: 'rtds-btn--primary rtds-search-bar__submit'
}, true %}
</div>
</div>
</form>
</div>
{
"label": "Cerca servizi, informazioni, aiuti...",
"searchId": "searchBarVisibleLabel",
"submitLabel": "Cerca",
"showClearButton": true,
"showLabel": true,
"formAction": "/search",
"formMethod": "get",
"placeholder": "Search",
"allResultsHref": "/search?q=",
"allResultsLabel": "Tutti i risultati"
}
/**
* SEARCH BAR COMPONENT
* Componente di ricerca con autocomplete
*/
.rtds-search-bar {
--_autocomplete-max-height: var(--autocomplete-max-height, 25rem);
position: relative;
width: 100%;
}
.rtds-search-bar__form {
width: 100%;
}
.rtds-search-bar__wrapper {
display: flex;
}
.rtds-search-bar__input-wrapper {
position: relative;
height: var(--spacing-16);
flex: 1 1 0%;
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
display: flex;
align-items: center;
border-radius: 0.5rem;
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 {
height: 100%;
flex: 1 1 0%;
}
.rtds-search-bar .autocomplete__wrapper {
height: 100%;
}
/* Autocomplete input styling */
.rtds-search-bar .autocomplete__input {
height: 100%;
width: 100%;
background-color: transparent;
padding-top: var(--spacing-2);
padding-bottom: var(--spacing-2);
padding-left: var(--spacing-3);
padding-right: var(--spacing-28);
font-size: var(--fontsize-text-sm);
font-weight: 500;
color: var(--color-content-03);
border-color: var(--color-border-input);
border-radius: 0.5rem;
border-width: 1px;
transition: border-color 0.2s ease;
}
.rtds-search-bar .autocomplete__input:hover,
.rtds-search-bar .autocomplete__input:focus:not(:focus-visible) {
border-color: var(--color-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 {
outline-width: 0px;
outline: 2px solid transparent;
box-shadow: none;
}
.rtds-search-bar .autocomplete__input::-moz-placeholder {
font-size: var(--fontsize-text-base);
color: var(--color-content-03);
}
.rtds-search-bar .autocomplete__input::placeholder {
font-size: var(--fontsize-text-base);
color: var(--color-content-03);
}
/* Clear button */
.rtds-search-bar__clear {
position: absolute;
right: var(--spacing-16);
top: 50%;
z-index: 10;
--tw-translate-y: -50%;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
transition: background-color 0.2s ease;
}
/*.rtds-search-bar__clear:hover {
@apply rtds-background-secondary rtds-text-white;
}*/
/* Submit button */
.rtds-search-bar__submit {
position: absolute;
right: var(--spacing-2);
top: 50%;
z-index: 10;
flex-shrink: 0;
--tw-translate-y: -50%;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
/* 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 {
margin-top: var(--spacing-2);
background-color: var(--color-background-01);
border-color: var(--color-border-gray-01);
border-radius: 0.5rem;
--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
border-width: 1px;
max-height: var(--_autocomplete-max-height);
overflow-y: auto;
}
:where(.rtds-search-bar) .autocomplete__option {
border-width: 0px;
padding-left: var(--spacing-3);
padding-right: var(--spacing-3);
padding-top: var(--spacing-2);
padding-bottom: var(--spacing-2);
font-size: var(--fontsize-text-sm);
font-weight: 500;
color: var(--color-content-03);
background-color: var(--color-background-01);
}
:where(.rtds-search-bar) .autocomplete__option:hover,
:where(.rtds-search-bar) .autocomplete__option--focused {
background-color: var(--color-background-02);
}
:where(.rtds-search-bar) .autocomplete__option--focused {
outline-style: solid;
outline-width: 2px;
outline-offset: -2px;
outline-color: var(--color-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 {
display: none;
}
/* Autocomplete suggestion styling */
.rtds-search-bar__autocomplete-suggestion {
flex-direction: column;
display: flex;
gap: var(--spacing-1);
}
.rtds-search-bar__suggestion-title {
font-size: var(--fontsize-text-sm);
color: var(--color-content-01);
font-weight: 700;
}
.rtds-search-bar__suggestion-type {
font-size: var(--fontsize-text-xs);
font-weight: 400;
color: var(--color-content-03);
}
/* All results option content wrapper */
/* All results option styling */
.autocomplete__option.rtds-search-bar__all-results-option-wrapper {
justify-content: flex-end;
padding-left: var(--spacing-3);
padding-right: var(--spacing-3);
display: flex;
}
.autocomplete__option.rtds-search-bar__all-results-option-wrapper:hover,
.autocomplete__option.rtds-search-bar__all-results-option-wrapper:focus {
background-color: var(--color-background-01);
}
.rtds-search-bar__all-results-option {
width: 100%;
flex-direction: row;
justify-content: flex-end;
border-top-width: 1px;
padding-top: var(--spacing-2);
padding-bottom: var(--spacing-2);
border-color: var(--color-border-gray-03);
display: flex;
}
.rtds-search-bar__all-results-option:hover .rtds-search-bar__all-results-link {
text-decoration-line: underline;
}
.rtds-search-bar__all-results-link {
display: flex;
align-items: center;
gap: var(--spacing-2);
}
.rtds-search-bar__all-results-link:hover {
text-decoration-line: underline;
}
/* Mobile Search Modal */
.rtds-search-bar__mobile-wrapper {
display: inline-flex;
}
.rtds-search-bar__modal {
z-index: 50;
}
.rtds-search-bar__modal[aria-hidden="false"] {
display: flex;
}
.rtds-search-bar__modal > div {
max-width: 100%;
}
/**
* 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();
}
});
}
window.addEventListener('load', 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();
}
}
});
}
window.addEventListener('load', initMobileSearchModal);
export default initSearchBar;
Componente di ricerca con autocomplete basato su Accessible Autocomplete.
Il componente è renderizzato come <div data-search-bar> contenente un <form role="search"> con:
<label> — visibile o nascosta tramite rtds-sr-only; sempre associata al campo tramite for/id<input type="text"> — campo di ricerca con autocomplete<button type="button"> (opzionale) — pulsante per cancellare il testo, con <span class="rtds-sr-only"> come nome accessibile<button type="submit"> — pulsante di ricercaLa variante mobile-modal aggiunge un <button> trigger esterno e una modale con focus trap.
default (md): Variante standard con dimensioni mediecompact: Variante compatta con dimensioni ridotte per spazi limitatifull-width: Variante che occupa tutta la larghezza disponibilewith-visible-label: Label visibile invece di nascosta con rtds-sr-only — utile per modali desktopmobile-modal: Include trigger button e modale mobile per la ricerca, con focus trap e gestione aria-expandedContesto 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.
label (stringa, obbligatorio): Testo della label (default: “Cerca”)searchId (stringa, obbligatorio): ID univoco per il campo di ricercasubmitLabel (stringa, opzionale): Testo per il bottone di ricerca (default: “Cerca”)placeholder (stringa, opzionale): Placeholder per l’input (default: “Search”)showClearButton (booleano, opzionale): Mostra/nasconde il bottone per cancellare (default: true)showLabel (booleano, opzionale): Mostra/nasconde visivamente il label (default: false — nascosto con rtds-sr-only)showResults (booleano, opzionale): Mostra/nasconde il dropdown dei risultati (default: true)showAllResultsButton (booleano, opzionale): Mostra/nasconde il bottone “Tutti i risultati” (default: true)formAction (stringa, opzionale): URL per il submit del form (default: “/search”)formMethod (stringa, opzionale): Metodo HTTP per il form (default: “get”)variant (stringa, opzionale): Variante del componente (“compact” o “full-width”)searchResults (array, opzionale): Array di oggetti con title e type per i risultati di ricercasearchFilters (array, opzionale): Array di oggetti con title e type per i filtri di ricercamodalId (stringa, opzionale): ID della modale (default: “searchModalMobile”)modalTitleId (stringa, opzionale): ID del titolo della modale (default: “mobileSearchModalTitle”)modalTitle (stringa, opzionale): Titolo della modale (default: “Cerca nel sito”)triggerId (stringa, opzionale): ID del trigger button (default: “searchModalMobileTrigger”)triggerLabel (stringa, opzionale): Label per screen reader del trigger (default: “Cerca nel sito - apri la modale”)closeButtonId (stringa, opzionale): ID del bottone di chiusura (default: “closeMobileSearchModal”)closeButtonLabel (stringa, opzionale): Label per screen reader del bottone di chiusura (default: “Chiudi modale di ricerca”)Livello AA — criteri pertinenti:
for / id; il form usa role="search" per identificare la funzione di ricerca<label> visibile o nascosta disponibile per screen readerrole="search"; i button icon-only hanno nomi accessibili tramite testo rtds-sr-only; nella variante mobile-modal, aria-expanded indica lo stato della modale<div data-search-bar> contenente un <form role="search"> con label, input autocomplete e pulsanti<label> — associata al campo tramite for/id; rimane disponibile per gli screen reader anche quando nascosta tramite rtds-sr-only<button type="button"> (cancella) — ha <span class="rtds-sr-only"> come nome accessibile; l’icona ha aria-hidden="true"<button type="submit"> — il testo visibile costituisce il nome accessibile| Attributo | Valori | Condizione |
|---|---|---|
role |
"search" |
sempre presente sul <form> |
aria-expanded |
"true" / "false" |
presente sul trigger della variante mobile-modal |
aria-hidden |
"true" / "false" |
gestito dinamicamente sulla modale mobile |
| Tasto | Azione |
|---|---|
Tab / Shift+Tab |
Sposta il focus tra label, input, pulsante cancella e pulsante cerca |
Enter |
Invia il form di ricerca |
Escape |
Chiude la modale mobile (variante mobile-modal) |
:focus-visible è sempre visibile e non viene rimosso via CSS; la classe is-opened-by-keyboard / is-opened-by-pointer può personalizzare il focus ring in base al tipo di interazionertds-sr-onlyrole="search" viene annunciato come landmark di ricercaaria-expanded sul trigger indica lo stato aperto/chiuso della modale<!-- Esempio base -->
<div data-search-bar>
<form role="search" action="/search" method="get">
<label for="site-search" class="rtds-sr-only">Cerca</label>
<input type="text" id="site-search" name="q" placeholder="Search">
<button type="submit">Cerca</button>
</form>
</div>
<!-- Esempio con opzioni autocomplete -->
<div data-search-bar data-options='["Opzione 1", "Opzione 2", "Opzione 3"]'>
<form role="search" action="/search" method="get">
<label for="autocomplete-search" class="rtds-sr-only">Cerca</label>
<!-- Input inizializzato da Accessible Autocomplete JS -->
</form>
</div>mobile-modal rileva il tipo di interazione (puntatore/tastiera) e aggiunge classi CSS per personalizzare il focus ring