fix(#248): address PR review concerns — i18n, aria-label, stable keys, test selectors
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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)"
|
||||
}
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<!-- Render Selected Tags -->
|
||||
{#each tags as tag, i (i)}
|
||||
{#each tags as tag, i (tag.id ?? tag.name)}
|
||||
<span class="flex items-center gap-1 rounded bg-muted px-2 py-1 text-sm font-medium text-ink">
|
||||
{#if tag.color}
|
||||
<span
|
||||
@@ -169,7 +169,7 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
<ul
|
||||
class="absolute top-full left-0 z-50 mt-1 max-h-48 w-full overflow-y-auto rounded border border-line bg-surface shadow-lg"
|
||||
>
|
||||
{#each orderedSuggestions as suggestion, i (i)}
|
||||
{#each orderedSuggestions as suggestion, i (suggestion.id ?? suggestion.name)}
|
||||
<li
|
||||
role="option"
|
||||
aria-selected={i === activeIndex}
|
||||
|
||||
@@ -161,19 +161,27 @@ $effect(() => {
|
||||
<div data-testid="and-or-toggle" class="mt-2 flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="operator-and"
|
||||
aria-label={m.filter_operator_and_label()}
|
||||
aria-pressed={tagOperator === 'AND'}
|
||||
class="rounded px-2 py-0.5 text-xs font-bold tracking-widest uppercase transition-colors {tagOperator === 'AND' ? 'bg-primary text-primary-fg' : 'bg-muted text-ink-2 hover:bg-line'}"
|
||||
onclick={() => {
|
||||
tagOperator = 'AND';
|
||||
(onSearchImmediate ?? onSearch)();
|
||||
}}>AND</button
|
||||
}}
|
||||
>{m.filter_operator_and()}</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="operator-or"
|
||||
aria-label={m.filter_operator_or_label()}
|
||||
aria-pressed={tagOperator === 'OR'}
|
||||
class="rounded px-2 py-0.5 text-xs font-bold tracking-widest uppercase transition-colors {tagOperator === 'OR' ? 'bg-primary text-primary-fg' : 'bg-muted text-ink-2 hover:bg-line'}"
|
||||
onclick={() => {
|
||||
tagOperator = 'OR';
|
||||
(onSearchImmediate ?? onSearch)();
|
||||
}}>OR</button
|
||||
}}
|
||||
>{m.filter_operator_or()}</button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -50,8 +50,8 @@ const targetTag = $derived(allTags.find((t) => t.id === targetId));
|
||||
</h3>
|
||||
<p class="mb-4 text-xs text-ink-3">{m.admin_tag_merge_description()}</p>
|
||||
|
||||
<!-- Step indicator -->
|
||||
<p class="mb-3 text-xs font-medium text-ink-3">
|
||||
<!-- Step indicator (aria-live announces step changes to screen reader users) -->
|
||||
<p class="mb-3 text-xs font-medium text-ink-3" aria-live="polite" role="status">
|
||||
{step === 1 ? m.admin_tag_merge_step1() : m.admin_tag_merge_step2()}
|
||||
</p>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user