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

View File

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

View File

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