From 3547a3d809f5b628050e69d0be2bf9a42bc0a024 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 20 May 2026 07:15:15 +0200 Subject: [PATCH] a11y(transcription): hide visible @mention empty-state from AT and fold empty-query check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-4 polish from Leonie (S-2), Felix (#3), Sara (#4): - Add aria-hidden="true" to the visible empty-state

so VoiceOver does not double-announce — the persistent sr-only live region is now the sole AT source of truth (NVDA already de-duped, VoiceOver did not). - Extract `searchQuery.trim() === ''` into an `isQueryEmpty` $derived; both the announcer branch and the visible empty-state branch now read from the single intent-named alias. - Cover the singular branch of the persistent live region (1 item -> "1 Person gefunden" / "1 person found" / "1 persona encontrada"). Plural was already covered; this closes the missing-branch gap. - Extend the existing "no aria-live on visible

" test to also assert aria-hidden="true" so a regression on the AT-source-of-truth contract goes red immediately. Co-Authored-By: Claude Opus 4.7 --- .../shared/discussion/MentionDropdown.svelte | 21 +++++++++------- .../discussion/MentionDropdown.svelte.test.ts | 24 ++++++++++++++++++- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/frontend/src/lib/shared/discussion/MentionDropdown.svelte b/frontend/src/lib/shared/discussion/MentionDropdown.svelte index b0d1202a..c647f434 100644 --- a/frontend/src/lib/shared/discussion/MentionDropdown.svelte +++ b/frontend/src/lib/shared/discussion/MentionDropdown.svelte @@ -43,6 +43,11 @@ let { let searchQuery = $state(untrack(() => editorQuery.slice(0, MAX_QUERY_LENGTH))); let userHasEdited = $state(false); +// Intent-revealing alias used by both the persistent aria-live announcer and +// the visible empty-state copy. Folding the duplicated rule into one $derived +// keeps the two branches in lockstep. Felix #3 on PR #629 round 4. +const isQueryEmpty = $derived(searchQuery.trim() === ''); + // Mirror the editor's typed text until the user takes ownership. // // Why `$state + $effect` (not `$derived`): `searchQuery` is also written by @@ -203,9 +208,7 @@ function selectItem(item: Person) { -->

{#if model.items.length === 0} - {searchQuery.trim() === '' - ? m.person_mention_search_prompt() - : m.person_mention_popup_empty()} + {isQueryEmpty ? m.person_mention_search_prompt() : m.person_mention_popup_empty()} {:else if model.items.length === 1} {m.person_mention_results_count_singular()} {:else} @@ -215,12 +218,14 @@ function selectItem(item: Person) { {#if model.items.length === 0} -

- {searchQuery.trim() === '' - ? m.person_mention_search_prompt() - : m.person_mention_popup_empty()} +