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