fix: a11y — tab panels v-show, radio roving-tabindex, table header label

This commit is contained in:
pyr0ball 2026-05-13 16:53:41 -07:00
parent b41ca4910a
commit 2fbf623f02
3 changed files with 31 additions and 5 deletions

View file

@ -70,7 +70,9 @@
<caption class="sr-only">Source health last 24 hours</caption> <caption class="sr-only">Source health last 24 hours</caption>
<thead class="bg-surface-raised border-b border-surface-border"> <thead class="bg-surface-raised border-b border-surface-border">
<tr> <tr>
<th scope="col" class="text-left px-4 py-2.5 text-text-dim font-medium text-xs uppercase tracking-wider w-4"></th> <th scope="col" class="text-left px-4 py-2.5 text-text-dim font-medium text-xs uppercase tracking-wider w-4">
<span class="sr-only">Status</span>
</th>
<th scope="col" class="text-left px-4 py-2.5 text-text-dim font-medium text-xs uppercase tracking-wider">Source</th> <th scope="col" class="text-left px-4 py-2.5 text-text-dim font-medium text-xs uppercase tracking-wider">Source</th>
<th scope="col" class="text-right px-4 py-2.5 text-text-dim font-medium text-xs uppercase tracking-wider">Events</th> <th scope="col" class="text-right px-4 py-2.5 text-text-dim font-medium text-xs uppercase tracking-wider">Events</th>
<th scope="col" class="text-right px-4 py-2.5 text-text-dim font-medium text-xs uppercase tracking-wider">Errors</th> <th scope="col" class="text-right px-4 py-2.5 text-text-dim font-medium text-xs uppercase tracking-wider">Errors</th>

View file

@ -17,6 +17,7 @@
:aria-selected="activeTab === t.key" :aria-selected="activeTab === t.key"
:id="`tab-${t.key}`" :id="`tab-${t.key}`"
:aria-controls="`tabpanel-${t.key}`" :aria-controls="`tabpanel-${t.key}`"
:tabindex="activeTab === t.key ? 0 : -1"
@click="activeTab = t.key" @click="activeTab = t.key"
:class="[ :class="[
'px-4 py-2 text-sm transition-colors border-b-2 -mb-px', 'px-4 py-2 text-sm transition-colors border-b-2 -mb-px',
@ -29,20 +30,22 @@
<!-- Quick tab panel --> <!-- Quick tab panel -->
<div <div
v-if="activeTab === 'quick'" v-show="activeTab === 'quick'"
role="tabpanel" role="tabpanel"
id="tabpanel-quick" id="tabpanel-quick"
aria-labelledby="tab-quick" aria-labelledby="tab-quick"
tabindex="0"
> >
<QuickCapture /> <QuickCapture />
</div> </div>
<!-- Structured tab panel --> <!-- Structured tab panel -->
<div <div
v-else v-show="activeTab === 'structured'"
role="tabpanel" role="tabpanel"
id="tabpanel-structured" id="tabpanel-structured"
aria-labelledby="tab-structured" aria-labelledby="tab-structured"
tabindex="0"
> >
<IncidentForm @created="onCreated" /> <IncidentForm @created="onCreated" />
<div <div

View file

@ -16,11 +16,14 @@
</p> </p>
<div role="radiogroup" aria-labelledby="entry-point-label" class="flex gap-3"> <div role="radiogroup" aria-labelledby="entry-point-label" class="flex gap-3">
<button <button
v-for="opt in entryPointOptions" v-for="(opt, idx) in entryPointOptions"
:key="opt.value" :key="opt.value"
:ref="(el) => collectEntryRef(el, idx)"
role="radio" role="radio"
:aria-checked="prefs.entry_point_style === opt.value" :aria-checked="prefs.entry_point_style === opt.value"
:tabindex="prefs.entry_point_style === opt.value ? 0 : -1"
@click="setEntryPoint(opt.value as 'topbar' | 'fab')" @click="setEntryPoint(opt.value as 'topbar' | 'fab')"
@keydown="handleEntryPointKey($event, idx)"
:class="[ :class="[
'flex-1 px-4 py-3 rounded border text-sm transition-colors text-left', 'flex-1 px-4 py-3 rounded border text-sm transition-colors text-left',
prefs.entry_point_style === opt.value prefs.entry_point_style === opt.value
@ -186,7 +189,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted, nextTick } from 'vue'
import type { ComponentPublicInstance } from 'vue'
const BASE = import.meta.env.BASE_URL.replace(/\/$/, '') const BASE = import.meta.env.BASE_URL.replace(/\/$/, '')
@ -210,12 +214,29 @@ const saveStatus = ref<{ ok: boolean; msg: string } | null>(null)
const showAddOverride = ref(false) const showAddOverride = ref(false)
const showApiKey = ref(false) const showApiKey = ref(false)
const newRule = ref<SeverityOverride>({ name: '', pattern: '', override_severity: 'WARN', enabled: true }) const newRule = ref<SeverityOverride>({ name: '', pattern: '', override_severity: 'WARN', enabled: true })
const entryPointBtnRefs = ref<HTMLButtonElement[]>([])
const entryPointOptions = [ const entryPointOptions = [
{ value: 'topbar', label: 'Top bar', desc: 'Persistent input bar below the nav on every page' }, { value: 'topbar', label: 'Top bar', desc: 'Persistent input bar below the nav on every page' },
{ value: 'fab', label: 'FAB', desc: 'Floating action button in the bottom-right corner' }, { value: 'fab', label: 'FAB', desc: 'Floating action button in the bottom-right corner' },
] ]
function collectEntryRef(el: any, idx: number) {
if (el instanceof HTMLButtonElement) entryPointBtnRefs.value[idx] = el
}
function handleEntryPointKey(e: KeyboardEvent, idx: number) {
let next = idx
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') next = idx + 1
else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') next = idx - 1
else return
e.preventDefault()
const clamped = Math.max(0, Math.min(entryPointOptions.length - 1, next))
setEntryPoint(entryPointOptions[clamped]!.value as 'topbar' | 'fab')
const nextBtn = entryPointBtnRefs.value[clamped]
if (nextBtn) nextBtn.focus()
}
onMounted(async () => { onMounted(async () => {
try { try {
const res = await fetch(`${BASE}/api/settings`) const res = await fetch(`${BASE}/api/settings`)