zammad/app/frontend/shared/composables/useFocusWhenTyping.ts
2026-01-02 15:41:09 +02:00

45 lines
1.6 KiB
TypeScript

// Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
import { onKeyStroke, unrefElement } from '@vueuse/core'
import stopEvent from '#shared/utils/events.ts'
import { getFocusableElements } from '#shared/utils/getFocusableElements.ts'
import type { MaybeRefOrGetter, Ref } from 'vue'
// https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role#keyboard_interactions
// - Type-ahead is recommended for all listboxes, especially those with more than seven options
export const useFocusWhenTyping = (container: MaybeRefOrGetter<HTMLElement | undefined | null>) => {
let filter = ''
let timeout = 0
onKeyStroke(
(e) => {
// only process alphanumeric keys
if (e.location !== 0 || (e.key.length !== 1 && e.key !== 'Backspace')) return
if (e.key === ' ') {
if (filter === '') return // don't start timeout, if not filtering
stopEvent(e) // don't select option, if in the process of filtering
}
window.clearTimeout(timeout)
timeout = window.setTimeout(() => {
const option = getFocusableElements(unrefElement(container)).find((el) => {
const content = el.textContent?.toLowerCase().trim() ?? ''
const filtered = filter.toLowerCase()
if (content.startsWith(filtered)) return true
const label = el.getAttribute('aria-label')?.toLowerCase().trim() ?? ''
return label.startsWith(filtered)
})
option?.focus()
filter = ''
}, 250)
if (e.key === 'Backspace') filter = filter.slice(0, filter.length - 1)
else filter += e.key
},
{ target: container as Ref<EventTarget> },
)
}