Client - handle keyboard navigation on timezone selection in preferences
This commit is contained in:
parent
52964b4045
commit
07c8611304
@ -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
|
||||
function onUpdateTimezone(index: number) {
|
||||
if (filteredTimezones.value.length > index) {
|
||||
timezone.value = filteredTimezones.value[index]
|
||||
emit('updateTimezone', timezone.value)
|
||||
isOpen.value = false
|
||||
emit('updateTimezone', value)
|
||||
}
|
||||
}
|
||||
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,
|
||||
|
@ -123,7 +123,7 @@
|
||||
"LIGHT": "Light"
|
||||
}
|
||||
},
|
||||
"TIMEZONE": "Timezone",
|
||||
"TIMEZONE": "Timezone | Timezones",
|
||||
"UNITS": {
|
||||
"IMPERIAL": "Imperial system (ft, mi, mph, °F)",
|
||||
"LABEL": "Units for distance",
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user