feat(a11y): associate form errors with input fields
Adds aria-invalid, aria-describedby, and role='alert' to error messages in FormField and Password components so screen readers announce validation errors. Fixes WCAG 3.3.1 (Error Identification).
This commit is contained in:
parent
a0d0379e95
commit
4618f3491b
|
|
@ -35,6 +35,7 @@ const slots = useSlots()
|
|||
const generatedId = useId()
|
||||
|
||||
const inputId = computed(() => props.id ?? generatedId)
|
||||
const errorId = computed(() => props.error ? `${inputId.value}-error` : undefined)
|
||||
const hasAddon = computed(() => !!slots.addon)
|
||||
|
||||
const fieldClasses = computed(() => [
|
||||
|
|
@ -82,13 +83,18 @@ defineExpose({
|
|||
class="two-col"
|
||||
>
|
||||
<span>{{ label }}</span>
|
||||
<slot :id="inputId">
|
||||
<slot
|
||||
:id="inputId"
|
||||
:error-id="errorId"
|
||||
>
|
||||
<input
|
||||
:id="inputId"
|
||||
ref="inputRef"
|
||||
v-bind="{ ...$attrs, ...inputBindings }"
|
||||
:class="inputClasses"
|
||||
:disabled="disabled || undefined"
|
||||
:aria-invalid="error ? true : undefined"
|
||||
:aria-describedby="errorId"
|
||||
@input="handleInput"
|
||||
>
|
||||
</slot>
|
||||
|
|
@ -109,13 +115,18 @@ defineExpose({
|
|||
{{ label }}
|
||||
</label>
|
||||
<div :class="controlClasses">
|
||||
<slot :id="inputId">
|
||||
<slot
|
||||
:id="inputId"
|
||||
:error-id="errorId"
|
||||
>
|
||||
<input
|
||||
:id="inputId"
|
||||
ref="inputRef"
|
||||
v-bind="{ ...$attrs, ...inputBindings }"
|
||||
:class="inputClasses"
|
||||
:disabled="disabled || undefined"
|
||||
:aria-invalid="error ? true : undefined"
|
||||
:aria-describedby="errorId"
|
||||
@input="handleInput"
|
||||
>
|
||||
</slot>
|
||||
|
|
@ -129,7 +140,9 @@ defineExpose({
|
|||
</template>
|
||||
<p
|
||||
v-if="error"
|
||||
:id="errorId"
|
||||
class="help is-danger"
|
||||
role="alert"
|
||||
>
|
||||
{{ error }}
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@
|
|||
:type="passwordFieldType"
|
||||
:autocomplete="autocomplete"
|
||||
:tabindex="tabindex"
|
||||
:aria-invalid="isValid !== true ? true : undefined"
|
||||
:aria-describedby="errorId"
|
||||
@keyup.enter="e => $emit('submit', e)"
|
||||
@focusout="() => {validate(); validateAfterFirst = true}"
|
||||
@keyup="() => {validateAfterFirst ? validate() : null}"
|
||||
|
|
@ -25,14 +27,16 @@
|
|||
</div>
|
||||
<p
|
||||
v-if="isValid !== true"
|
||||
:id="errorId"
|
||||
class="help is-danger"
|
||||
role="alert"
|
||||
>
|
||||
{{ isValid }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {ref, watchEffect} from 'vue'
|
||||
import {computed, ref, watchEffect} from 'vue'
|
||||
import {useDebounceFn} from '@vueuse/core'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
|
@ -61,6 +65,7 @@ const password = ref('')
|
|||
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
|
||||
const isValid = ref<true | string>(props.validateInitially === true ? true : '')
|
||||
const validateAfterFirst = ref(false)
|
||||
const errorId = computed(() => isValid.value !== true ? 'password-error' : undefined)
|
||||
|
||||
const validate = useDebounceFn(() => {
|
||||
const valid = validatePassword(password.value, props.validateMinLength)
|
||||
|
|
|
|||
Loading…
Reference in New Issue