diff --git a/resources/css/components/search-module.css b/resources/css/components/search-module.css index 6767e90..7ea67c7 100644 --- a/resources/css/components/search-module.css +++ b/resources/css/components/search-module.css @@ -10,7 +10,7 @@ bottom-0 /* overflow-x-hidden */ transform translate-y-full; - @apply block; + @apply block overflow-x-hidden; animation: translate-in 700ms forwards cubic-bezier(0, 0.51, 0.23, 0.99), fade-in 600ms forwards ease-out; @@ -33,6 +33,23 @@ } } + @keyframes fade-out { + from { + opacity: 1; + } + to { + opacity: 0; + } + } + @keyframes translate-out { + from { + transform: translateY(100%); + } + to { + transform: translateY(calc(100% - 100px)); + } + } + &[closed] { @apply hidden; } @@ -101,5 +118,5 @@ } body:has(.search-module[opened]) main { - filter: blur(2px) brightness(0.9); + filter: blur(2px) brightness(0.8); } diff --git a/resources/js/search-bar.ts b/resources/js/search-bar.ts index db69823..be1d51f 100644 --- a/resources/js/search-bar.ts +++ b/resources/js/search-bar.ts @@ -5,34 +5,47 @@ interface SearchBarOptions { export default class SearchBar { private searchBar: HTMLDivElement | null; - private searchButton: HTMLButtonElement | null; + private searchButtons: NodeListOf | null; + private searchInput: HTMLInputElement | null; private isOpen: boolean = false; + private lastScrollY: number = 0; + private activeButton: HTMLButtonElement | null = null; constructor(options: SearchBarOptions = {}) { this.searchBar = document.querySelector('#search-module') as HTMLDivElement; - this.searchButton = document.querySelector( + this.searchButtons = document.querySelectorAll( '.tools-container .search-button' - ) as HTMLButtonElement; + ) as NodeListOf; // Il y a 2 boutons de recherche (1 mobile; 1 desktop) + this.searchInput = document.querySelector( + '.search-module__search-form__input' + ) as HTMLInputElement; this.init(); } private init(): void { - if (!this.searchBar || !this.searchButton) return; + if (!this.searchBar || !this.searchButtons) return; // Initialiser l'état this.isOpen = this.searchBar.hasAttribute('opened'); + this.lastScrollY = window.scrollY; this.updateAriaHidden(); // Ajouter les event listeners - this.searchButton.addEventListener('click', this.toggle.bind(this)); + this.searchButtons.forEach((button) => + button.addEventListener('click', (event) => this.toggle(event)) + ); this.searchBar.addEventListener('transitionend', this.handleTransitionEnd.bind(this)); document.addEventListener('keydown', this.handleKeyDown.bind(this)); + window.addEventListener('scroll', this.handleScroll.bind(this)); } - private toggle(): void { + private toggle(event: Event): void { if (!this.searchBar) return; + // Stocker le bouton qui a été cliqué + this.activeButton = event.currentTarget as HTMLButtonElement; + this.isOpen = !this.isOpen; if (this.isOpen) { @@ -48,14 +61,42 @@ export default class SearchBar { this.searchBar.removeAttribute('closed'); this.searchBar.setAttribute('opened', ''); this.isOpen = true; + + // Focus automatique sur le champ de recherche après un petit délai pour laisser l'animation commencer + setTimeout(() => { + if (this.searchInput && this.isOpen) { + this.searchInput.focus(); + } + }, 100); } public close(): void { if (!this.searchBar) return; - this.searchBar.setAttribute('closed', ''); + // Défocus l'input de recherche + if (this.searchInput) { + this.searchInput.blur(); + } + + this.searchBar.setAttribute('closing', ''); this.searchBar.removeAttribute('opened'); this.isOpen = false; + + // Écouter la fin de l'animation de fermeture (on attend la plus longue: translate-out 800ms) + const handleAnimationEnd = (event: AnimationEvent) => { + if (event.animationName === 'translate-out') { + this.searchBar?.removeAttribute('closing'); + this.searchBar?.setAttribute('closed', ''); + this.searchBar?.removeEventListener('animationend', handleAnimationEnd); + + // Refocus sur le bouton qui avait ouvert la barre après la fermeture + if (this.activeButton) { + this.activeButton.focus(); + } + } + }; + + this.searchBar.addEventListener('animationend', handleAnimationEnd); } private handleTransitionEnd(): void { @@ -68,10 +109,16 @@ export default class SearchBar { } } + private handleScroll(): void { + if (!this.isOpen) return; + + this.close(); + } + private updateAriaHidden(): void { if (!this.searchBar) return; - if (this.searchBar.hasAttribute('closed')) { + if (this.searchBar.hasAttribute('closed') || this.searchBar.hasAttribute('closing')) { this.searchBar.setAttribute('aria-hidden', 'true'); } else { this.searchBar.setAttribute('aria-hidden', 'false'); @@ -83,10 +130,13 @@ export default class SearchBar { } public destroy(): void { - if (!this.searchBar || !this.searchButton) return; + if (!this.searchBar || !this.searchButtons) return; - this.searchButton.removeEventListener('click', this.toggle.bind(this)); + // Note: Les event listeners anonymes ne peuvent pas être supprimés facilement + // Dans un vrai projet, il faudrait stocker les références des fonctions this.searchBar.removeEventListener('transitionend', this.handleTransitionEnd.bind(this)); + document.removeEventListener('keydown', this.handleKeyDown.bind(this)); + window.removeEventListener('scroll', this.handleScroll.bind(this)); } }