/** * `materialSearch`: the behaviour of `` — a search bar that opens into a search view. * * Focus or a press on the bar opens the view; Escape, the back arrow, a press outside, focus * leaving the search, or choosing a result closes it. ArrowDown from the input moves into the * results and the arrow keys walk them; ArrowUp past the first result returns to the input. On a * compact window (below `sm`) the view is full screen and modal: focus stays in it and the page * behind does not scroll. The results themselves are the caller's, rendered by Livewire into the * view as the query changes. */ const COMPACT = '(max-width: 39.99rem)' const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])' const CHOOSES = 'a[href], button:not([disabled]), [data-list-open]' // A close hands focus back to the input: Escape does, and so does the full-screen view's focus trap // when it lets go, a moment later. Focus arriving this soon after a close is that, not someone // coming to search. const RETURN_GUARD_MS = 250 document.addEventListener('alpine:init', () => { window.Alpine.data('materialSearch', (docked = false) => ({ open: false, compact: false, closedAt: -Infinity, init() { const query = window.matchMedia(COMPACT) this.compact = query.matches query.addEventListener('change', (event) => (this.compact = event.matches)) }, get fullScreen() { return this.open && this.compact && !docked }, show() { this.open = true }, focused() { if (performance.now() - this.closedAt > RETURN_GUARD_MS) { this.show() } }, close(refocus = false) { this.open = false this.closedAt = performance.now() if (refocus) { this.$refs.input.focus() } }, clear() { const input = this.$refs.input input.value = '' input.dispatchEvent(new Event('input', { bubbles: true })) input.focus() }, results() { return [...this.$refs.view.querySelectorAll(FOCUSABLE)].filter((element) => element.getClientRects().length > 0) }, step(delta) { const results = this.results() const index = results.indexOf(document.activeElement) if (results.length === 0) { return } if (index === -1) { results[delta > 0 ? 0 : results.length - 1].focus() } else if (index + delta < 0) { this.$refs.input.focus() } else { results[Math.min(index + delta, results.length - 1)].focus() } }, choose(event) { if (event.target.closest(CHOOSES)) { this.close() } }, leave(event) { if (event.relatedTarget && !this.$root.contains(event.relatedTarget)) { this.close() } }, })) })