From 7919ba3a57f58c78267899940dca5d92ff75f149 Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 17 Apr 2026 00:24:53 +0200 Subject: [PATCH] =?UTF-8?q?fix(#248):=20address=20PR=20review=20concerns?= =?UTF-8?q?=20=E2=80=94=20i18n,=20aria-label,=20stable=20keys,=20test=20se?= =?UTF-8?q?lectors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add filter_operator_and/or/and_label/or_label i18n keys to de/en/es locale files - Add aria-label and aria-pressed to AND/OR toggle buttons in SearchFilterBar - Add data-testid="operator-and/or" for unambiguous test targeting (fixes substring match on German "Schlagwort") - Use stable keys (tag.id ?? tag.name) for TagInput chip and suggestion lists - Remove aria-level from role="option" items in TagInput (invalid attribute for that role) - Add aria-live="polite" role="status" to TagMergeZone step indicator Co-Authored-By: Claude Sonnet 4.6 --- frontend/messages/de.json | 6 +++++- frontend/messages/en.json | 6 +++++- frontend/messages/es.json | 6 +++++- frontend/src/lib/components/TagInput.svelte | 4 ++-- frontend/src/routes/SearchFilterBar.svelte | 12 ++++++++++-- frontend/src/routes/SearchFilterBar.svelte.spec.ts | 10 +++++----- .../src/routes/admin/tags/[id]/TagMergeZone.svelte | 4 ++-- 7 files changed, 34 insertions(+), 14 deletions(-) diff --git a/frontend/messages/de.json b/frontend/messages/de.json index 9f4dc6ba..427baf85 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -604,5 +604,9 @@ "admin_tag_delete_only_this_sub_root": "Untergeordnete werden zu Root-Schlagwörtern", "admin_tag_delete_subtree": "Gesamten Teilbaum löschen", "admin_tag_delete_subtree_warn": "Löscht auch {count} untergeordnete Schlagwörter", - "admin_tag_delete_confirm_heading": "Gib «{name}» zur Bestätigung ein:" + "admin_tag_delete_confirm_heading": "Gib «{name}» zur Bestätigung ein:", + "filter_operator_and": "UND", + "filter_operator_or": "ODER", + "filter_operator_and_label": "Alle gewählten Schlagworte müssen zutreffen (UND)", + "filter_operator_or_label": "Mindestens ein Schlagwort muss zutreffen (ODER)" } diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 633835f6..fdc835e2 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -604,5 +604,9 @@ "admin_tag_delete_only_this_sub_root": "Children will become root tags", "admin_tag_delete_subtree": "Delete entire subtree", "admin_tag_delete_subtree_warn": "Also deletes {count} child tags", - "admin_tag_delete_confirm_heading": "Type «{name}» to confirm:" + "admin_tag_delete_confirm_heading": "Type «{name}» to confirm:", + "filter_operator_and": "AND", + "filter_operator_or": "OR", + "filter_operator_and_label": "All selected tags must match (AND)", + "filter_operator_or_label": "At least one tag must match (OR)" } diff --git a/frontend/messages/es.json b/frontend/messages/es.json index fbd52839..04180d98 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -604,5 +604,9 @@ "admin_tag_delete_only_this_sub_root": "Las subordinadas se convertirán en etiquetas raíz", "admin_tag_delete_subtree": "Eliminar todo el subárbol", "admin_tag_delete_subtree_warn": "También elimina {count} etiquetas subordinadas", - "admin_tag_delete_confirm_heading": "Escribe «{name}» para confirmar:" + "admin_tag_delete_confirm_heading": "Escribe «{name}» para confirmar:", + "filter_operator_and": "Y", + "filter_operator_or": "O", + "filter_operator_and_label": "Todas las etiquetas seleccionadas deben coincidir (Y)", + "filter_operator_or_label": "Al menos una etiqueta debe coincidir (O)" } diff --git a/frontend/src/lib/components/TagInput.svelte b/frontend/src/lib/components/TagInput.svelte index 7a2decc8..2dc13e6c 100644 --- a/frontend/src/lib/components/TagInput.svelte +++ b/frontend/src/lib/components/TagInput.svelte @@ -119,7 +119,7 @@ function handleKeydown(e: KeyboardEvent) { class="flex min-h-[42px] flex-wrap gap-2 rounded border border-line bg-surface p-2 focus-within:border-ink focus-within:ring-1 focus-within:ring-ink" > - {#each tags as tag, i (i)} + {#each tags as tag, i (tag.id ?? tag.name)} {#if tag.color} - {#each orderedSuggestions as suggestion, i (i)} + {#each orderedSuggestions as suggestion, i (suggestion.id ?? suggestion.name)}
  • {
    {m.filter_operator_and()} {m.filter_operator_or()}
    {/if} diff --git a/frontend/src/routes/SearchFilterBar.svelte.spec.ts b/frontend/src/routes/SearchFilterBar.svelte.spec.ts index 065f1721..26d1d333 100644 --- a/frontend/src/routes/SearchFilterBar.svelte.spec.ts +++ b/frontend/src/routes/SearchFilterBar.svelte.spec.ts @@ -61,7 +61,7 @@ describe('SearchFilterBar – AND/OR tag operator toggle', () => { tagNames: [{ name: 'Tag1' }] }); await openAdvanced(); - await expect.element(page.getByRole('button', { name: 'AND' })).not.toBeInTheDocument(); + await expect.element(page.getByTestId('operator-and')).not.toBeInTheDocument(); vi.unstubAllGlobals(); }); @@ -79,8 +79,8 @@ describe('SearchFilterBar – AND/OR tag operator toggle', () => { await openAdvanced(); const toggle = page.getByTestId('and-or-toggle'); await expect.element(toggle).toBeInTheDocument(); - await expect.element(toggle.getByRole('button', { name: 'AND' })).toBeInTheDocument(); - await expect.element(toggle.getByRole('button', { name: 'OR' })).toBeInTheDocument(); + await expect.element(toggle.getByTestId('operator-and')).toBeInTheDocument(); + await expect.element(toggle.getByTestId('operator-or')).toBeInTheDocument(); vi.unstubAllGlobals(); }); @@ -99,7 +99,7 @@ describe('SearchFilterBar – AND/OR tag operator toggle', () => { }); await openAdvanced(); const toggle = page.getByTestId('and-or-toggle'); - await toggle.getByRole('button', { name: 'OR' }).click(); + await toggle.getByTestId('operator-or').click(); await expect.poll(() => onSearch.mock.calls.length).toBeGreaterThan(0); vi.unstubAllGlobals(); }); @@ -121,7 +121,7 @@ describe('SearchFilterBar – AND/OR tag operator toggle', () => { }); await openAdvanced(); const toggle = page.getByTestId('and-or-toggle'); - await toggle.getByRole('button', { name: 'OR' }).click(); + await toggle.getByTestId('operator-or').click(); await expect.poll(() => onSearchImmediate.mock.calls.length).toBeGreaterThan(0); expect(onSearch).not.toHaveBeenCalled(); vi.unstubAllGlobals(); diff --git a/frontend/src/routes/admin/tags/[id]/TagMergeZone.svelte b/frontend/src/routes/admin/tags/[id]/TagMergeZone.svelte index 29959878..7ebebe34 100644 --- a/frontend/src/routes/admin/tags/[id]/TagMergeZone.svelte +++ b/frontend/src/routes/admin/tags/[id]/TagMergeZone.svelte @@ -50,8 +50,8 @@ const targetTag = $derived(allTags.find((t) => t.id === targetId));

    {m.admin_tag_merge_description()}

    - -

    + +

    {step === 1 ? m.admin_tag_merge_step1() : m.admin_tag_merge_step2()}