Client - handle keyboard navigation on timezone selection in preferences

This commit is contained in:
Sam 2024-01-17 11:31:43 +01:00
parent 52964b4045
commit 07c8611304
3 changed files with 71 additions and 15 deletions

View File

@ -7,19 +7,35 @@
:value="timezone"
:disabled="disabled"
required
@keydown.esc="onUpdateTimezone(input)"
role="combobox"
aria-autocomplete="list"
aria-controls="tz-dropdown-list"
:aria-expanded="isOpen"
@keydown.esc="cancelUpdate()"
@keydown.enter="onEnter"
@input="openDropdown"
@blur="closeDropdown()"
@keydown.down="onKeyDown()"
@keydown.up="onKeyUp()"
/>
<ul class="tz-dropdown-list" v-if="isOpen" ref="tzList">
<ul
class="tz-dropdown-list"
id="tz-dropdown-list"
v-if="isOpen"
role="listbox"
tabindex="-1"
:aria-label="$t('user.PROFILE.TIMEZONE', 0)"
>
<li
v-for="(tz, index) in timeZones.filter((t) => matchTimezone(t))"
v-for="(tz, index) in filteredTimezones"
:key="tz"
:id="`tz-dropdown-item-${index}`"
class="tz-dropdown-item"
:class="{ focus: index === focusItemIndex }"
@click="onUpdateTimezone(tz)"
@click="onUpdateTimezone(index)"
@mouseover="onMouseOver(index)"
:autofocus="index === focusItemIndex"
role="option"
>
{{ tz }}
</li>
@ -28,8 +44,8 @@
</template>
<script setup lang="ts">
import { ref, toRefs, watch } from 'vue'
import type { Ref } from 'vue'
import { computed, ref, toRefs, watch } from 'vue'
import type { ComputedRef, Ref } from 'vue'
import { timeZones } from '@/utils/timezone'
@ -46,8 +62,10 @@
const { input, disabled } = toRefs(props)
const timezone: Ref<string> = ref(input.value)
const isOpen: Ref<boolean> = ref(false)
const tzList: Ref<HTMLInputElement | null> = ref(null)
const focusItemIndex: Ref<number> = ref(0)
const filteredTimezones: ComputedRef<string[]> = computed(() =>
input.value ? timeZones.filter((t) => matchTimezone(t)) : timeZones
)
function matchTimezone(t: string): RegExpMatchArray | null {
return t.toLowerCase().match(timezone.value.toLowerCase())
@ -55,15 +73,17 @@
function onMouseOver(index: number) {
focusItemIndex.value = index
}
function onUpdateTimezone(value: string) {
timezone.value = value
isOpen.value = false
emit('updateTimezone', value)
function onUpdateTimezone(index: number) {
if (filteredTimezones.value.length > index) {
timezone.value = filteredTimezones.value[index]
emit('updateTimezone', timezone.value)
isOpen.value = false
}
}
function onEnter(event: Event) {
event.preventDefault()
if (tzList.value?.firstElementChild?.innerHTML) {
onUpdateTimezone(tzList.value?.firstElementChild?.innerHTML)
if (filteredTimezones.value.length > 0) {
onUpdateTimezone(focusItemIndex.value)
}
}
function openDropdown(event: Event) {
@ -71,6 +91,42 @@
isOpen.value = true
timezone.value = (event.target as HTMLInputElement).value.trim()
}
function closeDropdown() {
onUpdateTimezone(focusItemIndex.value)
}
function scrollIntoOption(index: number) {
const option = document.getElementById(`tz-dropdown-item-${index}`)
if (option) {
option.focus()
option.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
}
}
function onKeyDown() {
isOpen.value = true
focusItemIndex.value =
focusItemIndex.value === null ? 0 : (focusItemIndex.value += 1)
if (focusItemIndex.value >= filteredTimezones.value.length) {
focusItemIndex.value = 0
}
scrollIntoOption(focusItemIndex.value)
}
function onKeyUp() {
isOpen.value = true
focusItemIndex.value =
focusItemIndex.value === null
? filteredTimezones.value.length - 1
: (focusItemIndex.value -= 1)
if (focusItemIndex.value <= -1) {
focusItemIndex.value = filteredTimezones.value.length - 1
}
scrollIntoOption(focusItemIndex.value)
}
function cancelUpdate() {
if (isOpen.value) {
isOpen.value = false
timezone.value = input.value
}
}
watch(
() => props.input,

View File

@ -123,7 +123,7 @@
"LIGHT": "Light"
}
},
"TIMEZONE": "Timezone",
"TIMEZONE": "Timezone | Timezones",
"UNITS": {
"IMPERIAL": "Imperial system (ft, mi, mph, °F)",
"LABEL": "Units for distance",

View File

@ -123,7 +123,7 @@
"LIGHT": "Clair"
}
},
"TIMEZONE": "Fuseau horaire",
"TIMEZONE": "Fuseau horaire | Fuseaux horaires",
"UNITS": {
"IMPERIAL": "Système impérial (ft, mi, mph, °F)",
"LABEL": "Unités pour les distances",