Update frontend components for accessibility and functionality improvements
- Modified docker-compose.dev.yml to conditionally run npm install based on the presence of package-lock.json. - Upgraded Nuxt version in package-lock.json from 3.21.1 to 4.3.1 for enhanced features. - Enhanced ConfirmModal component with aria-modal attribute for better accessibility. - Updated MapErrorBoundary component's error message for clarity. - Added role and aria-label attributes to MapView and MapSearch components for improved screen reader support. - Refactored various components to manage focus behavior on modal close, enhancing user experience. - Improved ToastContainer styling for better responsiveness and visibility. - Updated layout components to include skip navigation links for improved accessibility.
This commit is contained in:
@@ -1,7 +1,14 @@
|
||||
<template>
|
||||
<dialog ref="modalRef" class="modal" @cancel="$emit('close')">
|
||||
<dialog
|
||||
ref="modalRef"
|
||||
class="modal"
|
||||
aria-modal="true"
|
||||
:aria-labelledby="titleId"
|
||||
@cancel="$emit('close')"
|
||||
@close="onDialogClose"
|
||||
>
|
||||
<div class="modal-box transition-all duration-200" @click.stop>
|
||||
<h3 class="font-bold text-lg">{{ title }}</h3>
|
||||
<h3 :id="titleId" class="font-bold text-lg">{{ title }}</h3>
|
||||
<div class="py-2">
|
||||
<label class="label py-0"><span>Name</span></label>
|
||||
<input
|
||||
@@ -39,14 +46,17 @@ const emit = defineEmits<{
|
||||
submit: [name: string]
|
||||
}>()
|
||||
|
||||
const titleId = computed(() => `bookmark-modal-title-${Math.random().toString(36).slice(2)}`)
|
||||
const modalRef = ref<HTMLDialogElement | null>(null)
|
||||
const inputRef = ref<HTMLInputElement | null>(null)
|
||||
const localName = ref(props.defaultName)
|
||||
let previousActiveElement: HTMLElement | null = null
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
(open) => {
|
||||
if (open) {
|
||||
previousActiveElement = (import.meta.client ? document.activeElement : null) as HTMLElement | null
|
||||
localName.value = props.defaultName || ''
|
||||
nextTick(() => {
|
||||
modalRef.value?.showModal()
|
||||
@@ -59,6 +69,12 @@ watch(
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function onDialogClose() {
|
||||
if (import.meta.client && previousActiveElement && typeof previousActiveElement.focus === 'function' && document.contains(previousActiveElement)) {
|
||||
nextTick(() => previousActiveElement?.focus())
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.defaultName,
|
||||
(name) => {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<div class="flex flex-col gap-1 max-h-40 overflow-y-auto">
|
||||
<template v-if="bookmarks.length === 0">
|
||||
<p class="text-xs text-base-content/60 py-1">No saved locations.</p>
|
||||
<p class="text-xs text-base-content/50 py-0">Add current location or a selected quest giver below.</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div
|
||||
|
||||
@@ -150,7 +150,10 @@
|
||||
@jump-to-marker="$emit('jumpToMarker', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="p-3 border-t border-base-300 shrink-0 safe-area-pb">
|
||||
<div class="p-3 border-t border-base-300 shrink-0 safe-area-pb flex flex-col gap-2">
|
||||
<p class="text-xs text-base-content/50 text-center">
|
||||
<kbd class="kbd kbd-xs">?</kbd> for shortcuts
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-block touch-manipulation min-h-12"
|
||||
|
||||
@@ -119,6 +119,9 @@
|
||||
:selected-marker-for-bookmark="selectedMarkerForBookmark ?? null"
|
||||
:touch-friendly="touchFriendly"
|
||||
/>
|
||||
<p class="text-xs text-base-content/50 mt-2">
|
||||
<kbd class="kbd kbd-xs">?</kbd> for shortcuts
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
<template>
|
||||
<dialog ref="modalRef" class="modal" @cancel="$emit('close')">
|
||||
<dialog
|
||||
ref="modalRef"
|
||||
class="modal"
|
||||
aria-modal="true"
|
||||
aria-labelledby="coord-set-modal-title"
|
||||
@cancel="$emit('close')"
|
||||
@close="onDialogClose"
|
||||
>
|
||||
<div class="modal-box transition-all duration-200" @click.stop>
|
||||
<h3 class="font-bold text-lg">Rewrite tile coords</h3>
|
||||
<h3 id="coord-set-modal-title" class="font-bold text-lg">Rewrite tile coords</h3>
|
||||
<p class="py-2">From ({{ coordSetFrom.x }}, {{ coordSetFrom.y }}) to:</p>
|
||||
<div class="flex gap-2">
|
||||
<input v-model.number="localTo.x" type="number" class="input flex-1" placeholder="X" />
|
||||
<input ref="firstInputRef" v-model.number="localTo.x" type="number" class="input flex-1" placeholder="X" />
|
||||
<input v-model.number="localTo.y" type="number" class="input flex-1" placeholder="Y" />
|
||||
</div>
|
||||
<div class="modal-action">
|
||||
@@ -31,14 +38,20 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const modalRef = ref<HTMLDialogElement | null>(null)
|
||||
const firstInputRef = ref<HTMLInputElement | null>(null)
|
||||
const localTo = ref({ x: props.coordSet.x, y: props.coordSet.y })
|
||||
let previousActiveElement: HTMLElement | null = null
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
(open) => {
|
||||
if (open) {
|
||||
previousActiveElement = (import.meta.client ? document.activeElement : null) as HTMLElement | null
|
||||
localTo.value = { x: props.coordSet.x, y: props.coordSet.y }
|
||||
nextTick(() => modalRef.value?.showModal())
|
||||
nextTick(() => {
|
||||
modalRef.value?.showModal()
|
||||
firstInputRef.value?.focus()
|
||||
})
|
||||
} else {
|
||||
modalRef.value?.close()
|
||||
}
|
||||
@@ -46,6 +59,12 @@ watch(
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function onDialogClose() {
|
||||
if (import.meta.client && previousActiveElement && typeof previousActiveElement.focus === 'function' && document.contains(previousActiveElement)) {
|
||||
nextTick(() => previousActiveElement?.focus())
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.coordSet,
|
||||
(c) => {
|
||||
|
||||
@@ -39,6 +39,7 @@ function copyShareUrl() {
|
||||
if (import.meta.client && navigator.clipboard?.writeText) {
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
copied.value = true
|
||||
useToast().success('Link copied')
|
||||
if (copyTimeout) clearTimeout(copyTimeout)
|
||||
copyTimeout = setTimeout(() => {
|
||||
copied.value = false
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
class="input input-sm w-full pr-8 focus:ring-2 focus:ring-primary"
|
||||
:class="touchFriendly ? 'min-h-11 text-base' : ''"
|
||||
placeholder="Coords (x, y) or marker name"
|
||||
aria-label="Search by coordinates or marker name"
|
||||
aria-expanded="showDropdown && (suggestions.length > 0 || recent.recent.length > 0)"
|
||||
aria-haspopup="listbox"
|
||||
aria-controls="map-search-listbox"
|
||||
autocomplete="off"
|
||||
@focus="showDropdown = true"
|
||||
@blur="scheduleCloseDropdown"
|
||||
@@ -30,15 +34,21 @@
|
||||
</button>
|
||||
<div
|
||||
v-if="showDropdown && (suggestions.length > 0 || recent.recent.length > 0)"
|
||||
id="map-search-listbox"
|
||||
role="listbox"
|
||||
class="absolute left-0 right-0 top-full z-[600] mt-1 max-h-56 overflow-y-auto rounded-lg border border-base-300 bg-base-100 shadow-xl py-1"
|
||||
:aria-activedescendant="suggestions.length > 0 ? `map-search-opt-${highlightIndex}` : recent.recent.length > 0 ? `map-search-recent-${highlightIndex}` : undefined"
|
||||
>
|
||||
<template v-if="suggestions.length > 0">
|
||||
<button
|
||||
v-for="(s, i) in suggestions"
|
||||
:id="`map-search-opt-${i}`"
|
||||
:key="s.key"
|
||||
type="button"
|
||||
role="option"
|
||||
class="w-full text-left px-3 py-2 text-sm hover:bg-base-200 flex items-center gap-2"
|
||||
:class="{ 'bg-primary/10': i === highlightIndex }"
|
||||
:aria-selected="i === highlightIndex"
|
||||
@mousedown.prevent="goToSuggestion(s)"
|
||||
>
|
||||
<icons-icon-map-pin class="size-4 shrink-0 opacity-70" />
|
||||
@@ -49,10 +59,13 @@
|
||||
<p class="px-3 py-1 text-xs text-base-content/60 uppercase tracking-wider">Recent</p>
|
||||
<button
|
||||
v-for="(r, i) in recent.recent"
|
||||
:id="`map-search-recent-${i}`"
|
||||
:key="r.at"
|
||||
type="button"
|
||||
role="option"
|
||||
class="w-full text-left px-3 py-2 text-sm hover:bg-base-200 flex items-center gap-2"
|
||||
:class="{ 'bg-primary/10': i === highlightIndex }"
|
||||
:aria-selected="i === highlightIndex"
|
||||
@mousedown.prevent="goToRecent(r)"
|
||||
>
|
||||
<icons-icon-map-pin class="size-4 shrink-0 opacity-70" />
|
||||
|
||||
Reference in New Issue
Block a user