From 3685797e18266b3a5f7895c1d5d32567382d0976 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Sat, 16 May 2026 21:53:04 +0200 Subject: [PATCH] fix(gui-v2): wire AppDialog accessible name + cover close/width in tests Co-Authored-By: Claude Sonnet 4.6 --- .../src/components-v2/shared/AppDialog.vue | 61 +++++++++++++------ .../shared/__tests__/AppDialog.spec.ts | 49 +++++++++++---- 2 files changed, 79 insertions(+), 31 deletions(-) diff --git a/apps/app/src/components-v2/shared/AppDialog.vue b/apps/app/src/components-v2/shared/AppDialog.vue index 2076b8df..62fddf72 100644 --- a/apps/app/src/components-v2/shared/AppDialog.vue +++ b/apps/app/src/components-v2/shared/AppDialog.vue @@ -27,6 +27,15 @@ * The header (title + optional sub + close button) is always rendered by this * component; PrimeVue's own header slot is suppressed via :pt. * + * ## Accessible name + * Because PrimeVue's built-in header is suppressed (`header: { class: 'hidden' }`), + * its default `aria-labelledby` would point to an empty hidden element, leaving + * the dialog without an accessible name. We give the custom

a stable + * useId() id and override the root `aria-labelledby` to it via :pt.root (which + * mergeProps-overrides PrimeVue's internal value). When no title is provided + * the override is omitted (a titleless dialog is an edge the caller is expected + * to name via its own content; documented limitation). + * * ## CSS translation (crewli-starter main.css → Tailwind + :pt) * .modal-host → PrimeVue Dialog handles centering + padding internally * .modal (border/bg/shadow/radius) → :pt root class overrides @@ -51,10 +60,11 @@ * the default header entirely (`header: { class: 'hidden' }`) and render our * own. The content wrapper gets `p-0 flex flex-col overflow-hidden` so the * body region controls its own padding and scroll independently. The root gets - * the border, radius, and shadow tokens from the design system. + * the border, radius, and shadow tokens from the design system, plus the + * aria-labelledby override described above. */ -import { computed } from 'vue' +import { computed, useId } from 'vue' import Dialog from 'primevue/dialog' import Icon from '@/components/Icon.vue' @@ -84,6 +94,35 @@ const visible = computed({ emit('close') }, }) + +/** + * Stable, document-unique id for the custom

, used to wire the dialog's + * accessible name (see "Accessible name" note above). + */ +const titleId = useId() + +/** + * Passthrough object. Extracted from the template so the root section can + * conditionally carry the aria-labelledby override (only meaningful when a + * title — and therefore the

— is rendered). + */ +const dialogPt = computed(() => ({ + root: { + class: [ + 'border border-[var(--p-content-border-color)]', + 'bg-[var(--p-content-background)]', + 'rounded-[var(--p-border-radius-lg)]', + 'shadow-[var(--p-overlay-modal-shadow)]', + 'max-h-[calc(100vh-48px)]', + 'flex flex-col', + 'overflow-hidden', + ].join(' '), + ...(props.title ? { 'aria-labelledby': titleId } : {}), + }, + header: { class: 'hidden' }, + content: { class: 'p-0 flex flex-col overflow-hidden flex-1' }, + footer: { class: 'hidden' }, +}))