feat(a11y): add accessible names to modal dialogs

Passes aria-label to the <dialog> element via attribute inheritance
so screen readers announce the dialog's purpose.

Fixes WCAG 4.1.2 (Name, Role, Value).
This commit is contained in:
kolaente 2026-04-12 14:16:08 +02:00 committed by kolaente
parent 40ff558540
commit 21b7ae3f9f
4 changed files with 17 additions and 0 deletions

View File

@ -53,6 +53,7 @@
:enabled="typeof currentModal !== 'undefined'"
variant="scrolling"
class="task-detail-view-modal"
:aria-label="$t('task.detail.title')"
@close="closeModal()"
>
<component

View File

@ -2,6 +2,7 @@
<Modal
:overflow="true"
:wide="wide"
:aria-label="title"
@close="$router.back()"
>
<Card

View File

@ -3,6 +3,14 @@ import {mount, flushPromises} from '@vue/test-utils'
import {nextTick} from 'vue'
import Modal from './Modal.vue'
const globalMocks = {
global: {
mocks: {
$t: (key: string) => key,
},
},
}
// jsdom does not implement HTMLDialogElement.showModal/close.
// Provide stubs so that the [open] attribute — which CSS and our tests
// check — is flipped the same way the real browser would.
@ -50,6 +58,7 @@ afterEach(() => {
describe('Modal.vue — open race condition (#2590)', () => {
it('opens the dialog when enabled flips false → true', async () => {
const wrapper = mount(Modal, {
...globalMocks,
attachTo: document.body,
props: {enabled: false},
slots: {default: '<p class="test-body">hi</p>'},
@ -84,6 +93,7 @@ describe('Modal.vue — open race condition (#2590)', () => {
// resolves after the first state change, the dialog must already have
// [open] set — no additional flushPromises or extra ticks required.
const wrapper = mount(Modal, {
...globalMocks,
attachTo: document.body,
props: {enabled: false},
slots: {default: '<p class="test-body">hi</p>'},
@ -111,6 +121,7 @@ describe('Modal.vue — open race condition (#2590)', () => {
// nextTick callback whose timing could fire before the dialog mounted,
// skipping the showModal() call entirely and leaving .open === false.
const wrapper = mount(Modal, {
...globalMocks,
attachTo: document.body,
props: {enabled: true},
slots: {default: '<p class="test-body">hi</p>'},
@ -132,6 +143,7 @@ describe('Modal.vue — open race condition (#2590)', () => {
it('closes the dialog when enabled flips true → false', async () => {
const wrapper = mount(Modal, {
...globalMocks,
attachTo: document.body,
props: {enabled: true},
slots: {default: '<p class="test-body">hi</p>'},
@ -159,6 +171,7 @@ describe('Modal.vue — open race condition (#2590)', () => {
// element mounts. If props.enabled has flipped back to false by the
// time the mount happens, the watcher must not call showModal().
const wrapper = mount(Modal, {
...globalMocks,
attachTo: document.body,
props: {enabled: false},
slots: {default: '<p class="test-body">hi</p>'},
@ -189,6 +202,7 @@ describe('Modal.vue — open race condition (#2590)', () => {
// sure openDialog() clears the leftover data-closing flag itself;
// otherwise the dialog stays stuck at opacity 0.
const wrapper = mount(Modal, {
...globalMocks,
attachTo: document.body,
props: {enabled: true},
slots: {default: '<p class="test-body">hi</p>'},

View File

@ -982,6 +982,7 @@
"due": "Due {at}",
"closePopup": "Close popup",
"closeTaskDetail": "Close task detail",
"title": "Task detail",
"markAsDone": "Mark '{task}' as done",
"scrollToBottom": "Scroll to bottom",
"organization": "Organization",