diff --git a/frontend/messages/de.json b/frontend/messages/de.json index f1b189ce..8ef11f81 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -423,6 +423,7 @@ "person_mention_open_link": "Zur Person", "person_mention_hover_hint": "Klick öffnet Seite", "person_mention_load_error": "Person konnte nicht geladen werden.", + "person_mention_loading": "Lade Person…", "person_mention_popup_empty": "Keine Personen gefunden", "person_mention_btn_label": "Person verlinken", "person_mention_create_new": "Neue Person anlegen", diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 02111802..c0909263 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -423,6 +423,7 @@ "person_mention_open_link": "Open person", "person_mention_hover_hint": "Click opens the page", "person_mention_load_error": "Could not load person.", + "person_mention_loading": "Loading person…", "person_mention_popup_empty": "No persons found", "person_mention_btn_label": "Link person", "person_mention_create_new": "Create new person", diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 5d416017..4b2fcdaf 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -423,6 +423,7 @@ "person_mention_open_link": "Ir a la persona", "person_mention_hover_hint": "Clic abre la página", "person_mention_load_error": "No se pudo cargar la persona.", + "person_mention_loading": "Cargando persona…", "person_mention_popup_empty": "No se encontraron personas", "person_mention_btn_label": "Vincular persona", "person_mention_create_new": "Crear nueva persona", diff --git a/frontend/src/lib/components/PersonHoverCard.svelte b/frontend/src/lib/components/PersonHoverCard.svelte index 3374de23..74c36260 100644 --- a/frontend/src/lib/components/PersonHoverCard.svelte +++ b/frontend/src/lib/components/PersonHoverCard.svelte @@ -41,6 +41,17 @@ const notesExcerpt = $derived.by(() => { if (notes.length <= NOTES_MAX) return notes; return notes.slice(0, NOTES_MAX) + '…'; }); + +// Accessible name for the region landmark — required by WCAG 1.3.1. +// Falls back to a localised loading label so axe-core never sees an unnamed +// region (Leonie FINDING-02 / Elicit NFR concern). +const ariaLabel = $derived( + state.status === 'loaded' ? state.person.displayName : m.person_mention_loading() +); + +// aria-busy="true" while loading so SR clients know the region's contents +// will change. Cleared on loaded/error so the new content is announced. +const ariaBusy = $derived(state.status === 'loading');
{ id={cardId} role="region" aria-live="polite" + aria-label={ariaLabel} + aria-busy={ariaBusy ? 'true' : undefined} style:position="absolute" style:top={`${position.top}px`} style:left={`${position.left}px`} > {#if state.status === 'loading'} -
+
diff --git a/frontend/src/lib/components/PersonHoverCard.svelte.spec.ts b/frontend/src/lib/components/PersonHoverCard.svelte.spec.ts index ac8bb400..0d9dd955 100644 --- a/frontend/src/lib/components/PersonHoverCard.svelte.spec.ts +++ b/frontend/src/lib/components/PersonHoverCard.svelte.spec.ts @@ -246,6 +246,54 @@ describe('PersonHoverCard — accessibility', () => { expect(root.getAttribute('aria-live')).toBe('polite'); }); + it('sets aria-busy="true" while loading so SR announces the state change on load', async () => { + render(PersonHoverCard, { + personId: 'p-aug', + cardId: 'card-1', + position: POSITION, + state: { status: 'loading' } + }); + const root = document.querySelector('[data-testid="person-hover-card"]')!; + expect(root.getAttribute('aria-busy')).toBe('true'); + }); + + it('does not set aria-busy when loaded (so the loaded content is announced)', async () => { + render(PersonHoverCard, { + personId: 'p-aug', + cardId: 'card-1', + position: POSITION, + state: { status: 'loaded', person: AUGUSTE, relationships: [] } + }); + const root = document.querySelector('[data-testid="person-hover-card"]')!; + // aria-busy is either absent or "false" + const busy = root.getAttribute('aria-busy'); + expect(busy === null || busy === 'false').toBe(true); + }); + + it('names the region with the person displayName when loaded (WCAG 1.3.1)', async () => { + render(PersonHoverCard, { + personId: 'p-aug', + cardId: 'card-1', + position: POSITION, + state: { status: 'loaded', person: AUGUSTE, relationships: [] } + }); + const root = document.querySelector('[data-testid="person-hover-card"]')!; + expect(root.getAttribute('aria-label')).toBe('Auguste Raddatz'); + }); + + it('names the region with a generic loading label while loading', async () => { + render(PersonHoverCard, { + personId: 'p-aug', + cardId: 'card-1', + position: POSITION, + state: { status: 'loading' } + }); + const root = document.querySelector('[data-testid="person-hover-card"]')!; + // Region must have an accessible name in every state — axe-core flags + // role="region" without aria-label / aria-labelledby. + expect(root.getAttribute('aria-label')).toBeTruthy(); + }); + it('exposes the cardId as the host element id (so anchor aria-describedby works)', async () => { render(PersonHoverCard, { personId: 'p-aug',