fix/a11y: fix focus server input, double focus on messsages view and show only active channel info button
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
{ 'message--unsent': isUnsent }
|
{ 'message--unsent': isUnsent }
|
||||||
]"
|
]"
|
||||||
:data-message-id="message.id"
|
:data-message-id="message.id"
|
||||||
:tabindex="tabindex || 0"
|
:tabindex="tabindex || -1"
|
||||||
:aria-label="messageAriaLabel"
|
:aria-label="messageAriaLabel"
|
||||||
role="listitem"
|
role="listitem"
|
||||||
@keydown="handleKeydown"
|
@keydown="handleKeydown"
|
||||||
|
@@ -1,33 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div class="messages-container" ref="containerRef" @keydown="handleKeydown" tabindex="0" role="list"
|
||||||
class="messages-container"
|
:aria-label="messagesAriaLabel">
|
||||||
ref="containerRef"
|
|
||||||
@keydown="handleKeydown"
|
|
||||||
tabindex="0"
|
|
||||||
role="list"
|
|
||||||
:aria-label="messagesAriaLabel"
|
|
||||||
>
|
|
||||||
<div class="messages" role="presentation">
|
<div class="messages" role="presentation">
|
||||||
<!-- Regular Messages -->
|
<!-- Regular Messages -->
|
||||||
<MessageItem
|
<MessageItem v-for="(message, index) in messages" :key="message.id" :message="message"
|
||||||
v-for="(message, index) in messages"
|
:tabindex="index === focusedMessageIndex ? 0 : -1" :data-message-index="index"
|
||||||
:key="message.id"
|
@focus="focusedMessageIndex = index" />
|
||||||
:message="message"
|
|
||||||
:tabindex="index === focusedMessageIndex ? 0 : -1"
|
|
||||||
:data-message-index="index"
|
|
||||||
@focus="focusedMessageIndex = index"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Unsent Messages -->
|
<!-- Unsent Messages -->
|
||||||
<MessageItem
|
<MessageItem v-for="(unsentMsg, index) in unsentMessages" :key="unsentMsg.id" :message="unsentMsg"
|
||||||
v-for="(unsentMsg, index) in unsentMessages"
|
:is-unsent="true" :tabindex="(messages.length + index) === focusedMessageIndex ? 0 : -1"
|
||||||
:key="unsentMsg.id"
|
:data-message-index="messages.length + index" @focus="focusedMessageIndex = messages.length + index" />
|
||||||
:message="unsentMsg"
|
|
||||||
:is-unsent="true"
|
|
||||||
:tabindex="(messages.length + index) === focusedMessageIndex ? 0 : -1"
|
|
||||||
:data-message-index="messages.length + index"
|
|
||||||
@focus="focusedMessageIndex = messages.length + index"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -59,13 +42,13 @@ const totalMessages = computed(() => allMessages.value.length)
|
|||||||
const messagesAriaLabel = computed(() => {
|
const messagesAriaLabel = computed(() => {
|
||||||
const total = totalMessages.value
|
const total = totalMessages.value
|
||||||
const current = focusedMessageIndex.value + 1
|
const current = focusedMessageIndex.value + 1
|
||||||
|
|
||||||
if (total === 0) {
|
if (total === 0) {
|
||||||
return 'Messages list, no messages'
|
return 'Messages list, no messages'
|
||||||
} else if (total === 1) {
|
} else if (total === 1) {
|
||||||
return 'Messages list, 1 message'
|
return 'Messages list, 1 message'
|
||||||
} else {
|
} else {
|
||||||
return `Messages list, ${total} messages, currently focused on message ${current} of ${total}`
|
return `Messages list, ${total} messages`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -74,50 +57,50 @@ const navigationHint = 'Use arrow keys to navigate, Page Up/Down to jump 10 mess
|
|||||||
// Keyboard navigation
|
// Keyboard navigation
|
||||||
const handleKeydown = (event: KeyboardEvent) => {
|
const handleKeydown = (event: KeyboardEvent) => {
|
||||||
if (totalMessages.value === 0) return
|
if (totalMessages.value === 0) return
|
||||||
|
|
||||||
let newIndex = focusedMessageIndex.value
|
let newIndex = focusedMessageIndex.value
|
||||||
|
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case 'ArrowUp':
|
case 'ArrowUp':
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
newIndex = Math.max(0, focusedMessageIndex.value - 1)
|
newIndex = Math.max(0, focusedMessageIndex.value - 1)
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
newIndex = Math.min(totalMessages.value - 1, focusedMessageIndex.value + 1)
|
newIndex = Math.min(totalMessages.value - 1, focusedMessageIndex.value + 1)
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'PageUp':
|
case 'PageUp':
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
newIndex = Math.max(0, focusedMessageIndex.value - 10)
|
newIndex = Math.max(0, focusedMessageIndex.value - 10)
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'PageDown':
|
case 'PageDown':
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
newIndex = Math.min(totalMessages.value - 1, focusedMessageIndex.value + 10)
|
newIndex = Math.min(totalMessages.value - 1, focusedMessageIndex.value + 10)
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'Home':
|
case 'Home':
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
newIndex = 0
|
newIndex = 0
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'End':
|
case 'End':
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
newIndex = totalMessages.value - 1
|
newIndex = totalMessages.value - 1
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'Enter':
|
case 'Enter':
|
||||||
case ' ':
|
case ' ':
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
selectCurrentMessage()
|
selectCurrentMessage()
|
||||||
return
|
return
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newIndex !== focusedMessageIndex.value) {
|
if (newIndex !== focusedMessageIndex.value) {
|
||||||
focusMessage(newIndex)
|
focusMessage(newIndex)
|
||||||
}
|
}
|
||||||
@@ -229,19 +212,19 @@ defineExpose({
|
|||||||
.messages-container {
|
.messages-container {
|
||||||
background: #111827;
|
background: #111827;
|
||||||
}
|
}
|
||||||
|
|
||||||
.messages-container:focus {
|
.messages-container:focus {
|
||||||
outline-color: #60a5fa;
|
outline-color: #60a5fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.messages-container::-webkit-scrollbar-track {
|
.messages-container::-webkit-scrollbar-track {
|
||||||
background: #1f2937;
|
background: #1f2937;
|
||||||
}
|
}
|
||||||
|
|
||||||
.messages-container::-webkit-scrollbar-thumb {
|
.messages-container::-webkit-scrollbar-thumb {
|
||||||
background: #4b5563;
|
background: #4b5563;
|
||||||
}
|
}
|
||||||
|
|
||||||
.messages-container::-webkit-scrollbar-thumb:hover {
|
.messages-container::-webkit-scrollbar-thumb:hover {
|
||||||
background: #6b7280;
|
background: #6b7280;
|
||||||
}
|
}
|
||||||
|
@@ -23,7 +23,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button v-if="isActive"
|
||||||
class="channel-info-button"
|
class="channel-info-button"
|
||||||
@click.stop="$emit('info', channel)"
|
@click.stop="$emit('info', channel)"
|
||||||
:aria-label="`Channel info for ${channel.name}`"
|
:aria-label="`Channel info for ${channel.name}`"
|
||||||
|
@@ -9,6 +9,7 @@
|
|||||||
<form @submit.prevent="handleAuth" class="auth-form">
|
<form @submit.prevent="handleAuth" class="auth-form">
|
||||||
<BaseInput
|
<BaseInput
|
||||||
v-model="serverUrl"
|
v-model="serverUrl"
|
||||||
|
ref="serverInput"
|
||||||
type="url"
|
type="url"
|
||||||
label="Server URL (optional)"
|
label="Server URL (optional)"
|
||||||
:placeholder="defaultServerUrl"
|
:placeholder="defaultServerUrl"
|
||||||
@@ -59,7 +60,7 @@ const serverUrl = ref('')
|
|||||||
const error = ref('')
|
const error = ref('')
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const tokenInput = ref()
|
const tokenInput = ref()
|
||||||
|
const serverInput = ref()
|
||||||
// Get default server URL for placeholder
|
// Get default server URL for placeholder
|
||||||
const defaultServerUrl = authStore.getDefaultServerUrl()
|
const defaultServerUrl = authStore.getDefaultServerUrl()
|
||||||
|
|
||||||
@@ -80,7 +81,7 @@ const handleAuth = async () => {
|
|||||||
router.push('/')
|
router.push('/')
|
||||||
} else {
|
} else {
|
||||||
error.value = 'Invalid authentication token or server URL'
|
error.value = 'Invalid authentication token or server URL'
|
||||||
tokenInput.value?.focus()
|
serverInput.value?.focus()
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = 'Authentication failed. Please check your token and server URL.'
|
error.value = 'Authentication failed. Please check your token and server URL.'
|
||||||
@@ -91,7 +92,7 @@ const handleAuth = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
tokenInput.value?.focus()
|
serverInput.value?.focus()
|
||||||
playSound('intro')
|
playSound('intro')
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
Reference in New Issue
Block a user