a11y(transcription): hide visible @mention empty-state from AT and fold empty-query check
Round-4 polish from Leonie (S-2), Felix (#3), Sara (#4): - Add aria-hidden="true" to the visible empty-state <p> 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 <p>" 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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) {
|
||||
-->
|
||||
<p class="sr-only" aria-live="polite">
|
||||
{#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}
|
||||
<!--
|
||||
Visible empty-state copy — visual-only. The persistent sr-only <p>
|
||||
above is the announcer. Leonie #3 on PR #629 round 3.
|
||||
above is the sole AT announcer; this one is hidden from screen readers
|
||||
via aria-hidden="true" so VoiceOver does not double-announce
|
||||
(NVDA de-dups, VoiceOver does not). Leonie S-2 on PR #629 round 4.
|
||||
Do NOT add an aria-live attribute here — that would re-introduce
|
||||
the duplicate announcement.
|
||||
-->
|
||||
<p class="px-3 py-2.5 font-sans text-sm text-ink-3">
|
||||
{searchQuery.trim() === ''
|
||||
? m.person_mention_search_prompt()
|
||||
: m.person_mention_popup_empty()}
|
||||
<p aria-hidden="true" class="px-3 py-2.5 font-sans text-sm text-ink-3">
|
||||
{isQueryEmpty ? m.person_mention_search_prompt() : m.person_mention_popup_empty()}
|
||||
</p>
|
||||
<!--
|
||||
Empty-state escape hatch — without it the transcriber has to close
|
||||
|
||||
Reference in New Issue
Block a user