fix: a11y — tab panels v-show, radio roving-tabindex, table header label
This commit is contained in:
parent
29fb31d76c
commit
88b27a1454
3 changed files with 31 additions and 5 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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`)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue