Improve accessibility with ARIA roles, focus styles and language switching
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
() => {
|
||||
|
||||
Reference in New Issue
Block a user