fix(shared-ui): TagChip nested button + Pill svelte:element ARIA role
Some checks are pending
CD Mac Mini / Detect Changes (push) Waiting to run
CD Mac Mini / Deploy (push) Blocked by required conditions
CI / Detect Changes (push) Waiting to run
CI / Validate (push) Waiting to run
CI / Build mana-search (push) Blocked by required conditions
CI / Build mana-sync (push) Blocked by required conditions
CI / Build mana-api-gateway (push) Blocked by required conditions
CI / Build mana-crawler (push) Blocked by required conditions
Docker Validate / Validate Dockerfiles (push) Waiting to run
Docker Validate / Build calendar-web (push) Blocked by required conditions
Docker Validate / Build quotes-web (push) Blocked by required conditions
Docker Validate / Build todo-backend (push) Blocked by required conditions
Docker Validate / Build todo-web (push) Blocked by required conditions
Docker Validate / Build mana-auth (push) Blocked by required conditions
Docker Validate / Build mana-sync (push) Blocked by required conditions
Docker Validate / Build mana-media (push) Blocked by required conditions
Mirror to Forgejo / Push to Forgejo (push) Waiting to run

Beide standen seit dem letzten shared-ui-Sync (ce923bbdc) als
svelte-check --fail-on-warnings Treffer im Pre-Push-Hook drin.
Aufgeräumt für die Cutover-PRs.

TagChip: outer war `<button>` mit innerem Remove-`<button>` —
verschachtelte interaktive Elemente sind invalid HTML und brechen
SSR-Hydration. Outer ist jetzt `<span role="button" tabindex="0">`
mit Enter/Space-Keyboard-Handler. CSS-Selektor `button.chip` →
`.chip-interactive` Klasse.

Pill: `<svelte:element this={tag}>` mit onclick/oncontextmenu
braucht explizite ARIA-Rolle (button bzw. link), weil der
statische Analyser den dynamischen Tag nicht aufdröselt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-05-21 17:24:56 +02:00
parent db1dc9a738
commit abafdfbeb3
2 changed files with 30 additions and 8 deletions

View file

@ -34,16 +34,31 @@
e.stopPropagation();
onRemove?.();
}
// Outer kann nicht `<button>` sein wenn innen ein Remove-`<button>`
// liegt — verschachtelte interaktive Elemente sind invalid HTML
// (svelte-check fail-on-warnings). Lösung: outer als `<span
// role="button">` mit Keyboard-Handler, damit der ganze Chip
// klickbar bleibt und der Remove-Button sein eigener interaktiver
// Bereich ist.
function handleChipKey(e: KeyboardEvent) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onclick?.(e as unknown as MouseEvent);
}
}
</script>
{#if onclick}
<button
type="button"
class="chip size-{size}"
<span
role="button"
tabindex="0"
class="chip chip-interactive size-{size}"
class:active
class:has-color={!!color}
style:--tag-color={color || null}
{onclick}
onkeydown={handleChipKey}
>
{#if color}
<span class="dot" aria-hidden="true"></span>
@ -54,7 +69,7 @@
<DynamicIcon name="x" size="xs" />
</button>
{/if}
</button>
</span>
{:else}
<span
class="chip size-{size}"
@ -91,19 +106,19 @@
cursor: default;
}
button.chip {
.chip-interactive {
cursor: pointer;
transition:
background-color 0.15s ease,
border-color 0.15s ease;
}
button.chip:hover {
.chip-interactive:hover {
background: hsl(var(--color-surface-hover));
border-color: hsl(var(--color-primary) / 0.4);
}
button.chip:focus-visible {
.chip-interactive:focus-visible {
outline: 2px solid hsl(var(--color-primary));
outline-offset: 2px;
}
@ -162,7 +177,7 @@
}
@media (prefers-reduced-motion: reduce) {
button.chip {
.chip-interactive {
transition: none;
}
}

View file

@ -55,11 +55,18 @@
}: Props = $props();
const tag = $derived(href ? 'a' : 'button');
// svelte-check `--fail-on-warnings` will an Dynamic-Elementen mit
// click/context-handler explizit eine ARIA-Rolle sehen, weil der
// statische Analyser nicht weiß, dass `<button>`/`<a>` schon
// inhärent interaktiv sind. Für button reicht `button`, für a
// (Link) ist `link` die korrekte Rolle.
const explicitRole = $derived(tag === 'a' ? 'link' : 'button');
</script>
<svelte:element
this={tag}
bind:this={element}
role={explicitRole}
class="pill size-{size} {className}"
class:active
class:disabled