Improve accessibility with ARIA roles, focus styles and language switching

This commit is contained in:
Dualmind-Assistant
2026-04-21 13:06:22 +00:00
parent 4d2a3595ed
commit 87c8d55505
4 changed files with 85 additions and 26 deletions
+23 -9
View File
@@ -10,12 +10,14 @@ const options = [
function select(code: string) {
modelValue.value = code
}
const hintId = 'language-hint'
</script>
<template>
<div>
<h2>Sprache auswählen</h2>
<p class="hint">Interface-Sprache für Fragen und Optionen</p>
<section aria-labelledby="language-title" aria-describedby="language-hint" role="radiogroup">
<h2 id="language-title">Sprache auswählen</h2>
<p :id="hintId" class="hint">Interface-Sprache für Fragen und Optionen</p>
<div class="grid">
<button
v-for="opt in options"
@@ -23,12 +25,15 @@ function select(code: string) {
type="button"
class="lang-btn"
:class="{ active: opt.code === modelValue }"
role="radio"
:aria-checked="opt.code === modelValue"
:aria-label="opt.label"
@click="select(opt.code)"
>
{{ opt.label }}
<span class="label">{{ opt.label }}</span>
</button>
</div>
</div>
</section>
</template>
<style scoped>
@@ -39,7 +44,7 @@ h2 {
.hint {
font-size: 0.8rem;
color: #9ca3af;
color: #6b7280;
margin-bottom: 0.75rem;
}
@@ -52,7 +57,7 @@ h2 {
.lang-btn {
padding: 0.55rem 0.5rem;
border-radius: 999px;
border: 1px solid #e5e7eb;
border: 1px solid #d1d5db;
background: #f9fafb;
font-size: 0.85rem;
color: #111827;
@@ -67,8 +72,17 @@ h2 {
.lang-btn.active {
background: #e5f0ff;
border-color: #3b82f6;
border-color: #2563eb;
color: #0b1f4a;
box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.2);
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.35);
}
.lang-btn:focus-visible {
outline: 3px solid #2563eb;
outline-offset: 2px;
}
.label {
display: inline-block;
}
</style>
+16 -3
View File
@@ -7,6 +7,9 @@ const props = defineProps<{
max: number
}>()
const sliderId = 'pain-slider'
const labelId = 'pain-title'
function onInput(ev: Event) {
const target = ev.target as HTMLInputElement
modelValue.value = Number(target.value)
@@ -14,21 +17,26 @@ function onInput(ev: Event) {
</script>
<template>
<div>
<h2>{{ props.title }}</h2>
<section>
<h2 :id="labelId">{{ props.title }}</h2>
<div class="value">{{ modelValue }}</div>
<input
:id="sliderId"
type="range"
:min="props.min"
:max="props.max"
:value="modelValue"
:aria-labelledby="labelId"
:aria-valuemin="props.min"
:aria-valuemax="props.max"
:aria-valuenow="modelValue"
@input="onInput"
/>
<div class="scale">
<span>{{ props.min }}</span>
<span>{{ props.max }}</span>
</div>
</div>
</section>
</template>
<style scoped>
@@ -54,6 +62,11 @@ input[type='range'] {
outline: none;
}
input[type='range']:focus-visible {
outline: 3px solid #2563eb;
outline-offset: 4px;
}
input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
+38 -14
View File
@@ -18,8 +18,9 @@ function select(value: string) {
</script>
<template>
<div>
<h2>{{ props.title }}</h2>
<section class="symptom-section" aria-labelledby="symptom-title" role="radiogroup">
<h2 id="symptom-title">{{ props.title }}</h2>
<p class="sr-only">Bitte wählen Sie genau eine Option aus.</p>
<div class="grid">
<button
v-for="opt in props.options"
@@ -27,18 +28,24 @@ function select(value: string) {
type="button"
class="symptom-btn"
:class="{ active: opt.value === modelValue }"
role="radio"
:aria-checked="opt.value === modelValue"
@click="select(opt.value)"
>
<span class="icon-wrapper" v-if="opt.icon">
<span v-if="opt.icon" class="icon-wrapper">
<img :src="opt.icon" :alt="opt.label" class="icon" />
</span>
<span class="label">{{ opt.label }}</span>
</button>
</div>
</div>
</section>
</template>
<style scoped>
.symptom-section {
display: block;
}
h2 {
font-size: 1rem;
margin-bottom: 0.75rem;
@@ -53,7 +60,7 @@ h2 {
.symptom-btn {
padding: 0.75rem 0.75rem;
border-radius: 18px;
border: 1px solid #e5e7eb;
border: 1px solid #d1d5db;
background: #f9fafb;
text-align: left;
font-size: 0.95rem;
@@ -66,6 +73,22 @@ h2 {
transform 0.08s ease-out;
}
.symptom-btn:hover {
background: #f3f4f6;
}
.symptom-btn.active {
background: linear-gradient(145deg, #16a34a, #15803d);
border-color: #15803d;
color: #ffffff;
box-shadow: 0 10px 20px rgba(22, 163, 74, 0.4);
}
.symptom-btn:focus-visible {
outline: 3px solid #2563eb;
outline-offset: 2px;
}
.icon-wrapper {
flex-shrink: 0;
width: 44px;
@@ -84,14 +107,15 @@ h2 {
flex: 1;
}
.symptom-btn:hover {
background: #f3f4f6;
}
.symptom-btn.active {
background: linear-gradient(145deg, #22c55e, #16a34a);
border-color: #16a34a;
color: #ffffff;
box-shadow: 0 10px 20px rgba(34, 197, 94, 0.35);
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
</style>
+8
View File
@@ -7,6 +7,14 @@ import { useRouter } from 'vue-router'
const router = useRouter()
const { language, isLoadingConfig, loadError, loadQuestions } = useTriageSession()
watch(
() => language.value,
(val) => {
document.documentElement.lang = val
},
{ immediate: true },
)
watch(
() => language.value,
() => {