useFocusWhenTyping.ts 1.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
  1. // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. import { onKeyStroke, unrefElement } from '@vueuse/core'
  3. import stopEvent from '#shared/utils/events.ts'
  4. import { getFocusableElements } from '#shared/utils/getFocusableElements.ts'
  5. import type { MaybeRefOrGetter } from '@vueuse/core'
  6. import type { Ref } from 'vue'
  7. // https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role#keyboard_interactions
  8. // - Type-ahead is recommended for all listboxes, especially those with more than seven options
  9. export const useFocusWhenTyping = (
  10. container: MaybeRefOrGetter<HTMLElement | undefined | null>,
  11. ) => {
  12. let filter = ''
  13. let timeout = 0
  14. onKeyStroke(
  15. (e) => {
  16. // only process alphanumeric keys
  17. if (e.location !== 0 || (e.key.length !== 1 && e.key !== 'Backspace'))
  18. return
  19. if (e.key === ' ') {
  20. if (filter === '') return // don't start timeout, if not filtering
  21. stopEvent(e) // don't select option, if in the process of filtering
  22. }
  23. window.clearTimeout(timeout)
  24. timeout = window.setTimeout(() => {
  25. const option = getFocusableElements(unrefElement(container)).find(
  26. (el) => {
  27. const content = el.textContent?.toLowerCase().trim() ?? ''
  28. const filtered = filter.toLowerCase()
  29. if (content.startsWith(filtered)) return true
  30. const label =
  31. el.getAttribute('aria-label')?.toLowerCase().trim() ?? ''
  32. return label.startsWith(filtered)
  33. },
  34. )
  35. option?.focus()
  36. filter = ''
  37. }, 250)
  38. if (e.key === 'Backspace') filter = filter.slice(0, filter.length - 1)
  39. else filter += e.key
  40. },
  41. { target: container as Ref<EventTarget> },
  42. )
  43. }