Compare commits

...

6 Commits

7 changed files with 42 additions and 52 deletions

View File

@@ -7,7 +7,7 @@
<title>Notebrook</title> <title>Notebrook</title>
<meta name="description" content="Light note taking app in messenger style"> <meta name="description" content="Light note taking app in messenger style">
</head> </head>
<body> <body role="application">
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>

View File

@@ -5,9 +5,9 @@
{ '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="option"
@keydown="handleKeydown" @keydown="handleKeydown"
> >
<div class="message__content"> <div class="message__content">

View File

@@ -1,33 +1,17 @@
<template> <template>
<div <div class="messages-container" ref="containerRef" @keydown="handleKeydown" tabindex="0" role="listbox"
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" :aria-selected="index === focusedMessageIndex ? 'true' : 'false'"
:message="message" @focus="focusedMessageIndex = index" />
: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 +43,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 +58,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 +213,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;
} }

View File

@@ -6,6 +6,7 @@
<label class="setting-item"> <label class="setting-item">
<input <input
ref="soundInput"
type="checkbox" type="checkbox"
v-model="localSettings.soundEnabled" v-model="localSettings.soundEnabled"
class="checkbox" class="checkbox"
@@ -245,6 +246,7 @@ const isSaving = ref(false)
const isResetting = ref(false) const isResetting = ref(false)
const showResetConfirm = ref(false) const showResetConfirm = ref(false)
const selectedVoiceURI = ref('') const selectedVoiceURI = ref('')
const soundInput = ref()
// Computed property for current server URL // Computed property for current server URL
const currentServerUrl = computed(() => authStore.serverUrl) const currentServerUrl = computed(() => authStore.serverUrl)
@@ -338,6 +340,7 @@ onMounted(() => {
// Set up voice selection // Set up voice selection
selectedVoiceURI.value = appStore.settings.selectedVoiceURI || '' selectedVoiceURI.value = appStore.settings.selectedVoiceURI || ''
soundInput.value.focus();
}) })
</script> </script>

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="channel-list-container" ref="containerRef"> <div class="channel-list-container" ref="containerRef">
<ul class="channel-list" role="list" aria-label="Channels"> <ul class="channel-list" role="listbox" aria-label="Channels">
<ChannelListItem <ChannelListItem
v-for="(channel, index) in channels" v-for="(channel, index) in channels"
:key="channel.id" :key="channel.id"

View File

@@ -12,9 +12,11 @@
class="channel-button" class="channel-button"
@click="$emit('select', channel.id)" @click="$emit('select', channel.id)"
@focus="handleFocus" @focus="handleFocus"
role="option"
:aria-current="isActive"
aria-selected="true"
@keydown="handleKeydown" @keydown="handleKeydown"
:tabindex="tabindex" :tabindex="tabindex"
:aria-pressed="isActive"
:aria-label="channelAriaLabel" :aria-label="channelAriaLabel"
> >
<span class="channel-name">{{ channel.name }}</span> <span class="channel-name">{{ channel.name }}</span>
@@ -23,7 +25,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}`"
@@ -58,7 +60,7 @@ const props = defineProps<Props>()
// Better ARIA label that announces the channel name and unread count // Better ARIA label that announces the channel name and unread count
const channelAriaLabel = computed(() => { const channelAriaLabel = computed(() => {
let label = `${props.channel.name} channel` let label = `${props.channel.name}`
if (props.unreadCount) { if (props.unreadCount) {
label += `, ${props.unreadCount} unread message${props.unreadCount > 1 ? 's' : ''}` label += `, ${props.unreadCount} unread message${props.unreadCount > 1 ? 's' : ''}`
} }

View File

@@ -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>