From 6b18cab40a27db9e52e5ecdfb0aa490cc0897ee7 Mon Sep 17 00:00:00 2001 From: Ridwan Sanusi Date: Fri, 12 Jun 2026 10:15:51 -0400 Subject: [PATCH] =?UTF-8?q?[a11y]=20WCAG=204.1.2=20=E2=80=94=20add=20label?= =?UTF-8?q?=20prop=20to=20Button=20primitive=20and=20enforce=20accessible?= =?UTF-8?q?=20names?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a `label` prop to the Button/anchor primitive that renders as `aria-label`, plus a dev-time `onMount` check that warns (prod) or throws (dev) when a button is rendered without any accessible name. Updates seven consumer call-sites and adds the `configure-columns` i18n key. A11y-Audit-Ref: 4.1.2-button-anchor-empty Co-Authored-By: Claude Sonnet 4.6 --- .../start-activity-button.svelte | 1 + .../workflow/start-workflow-button.svelte | 1 + ...orkflows-summary-configurable-table.svelte | 7 +++- src/lib/holocene/button.svelte | 32 +++++++++++++++++++ src/lib/i18n/locales/en/common.ts | 1 + src/lib/pages/schedule-view.svelte | 1 + src/lib/pages/schedules.svelte | 3 +- 7 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/lib/components/standalone-activities/start-activity-button.svelte b/src/lib/components/standalone-activities/start-activity-button.svelte index d4ca2fbe5f..d5ace9024a 100644 --- a/src/lib/components/standalone-activities/start-activity-button.svelte +++ b/src/lib/components/standalone-activities/start-activity-button.svelte @@ -45,6 +45,7 @@ variant="ghost" class="start-button" leadingIcon="lightning-bolt" + label={translate('standalone-activities.start-activity-like-this-one')} on:click={() => goto(href)} > diff --git a/src/lib/components/workflow/start-workflow-button.svelte b/src/lib/components/workflow/start-workflow-button.svelte index d37431ff1e..8fd8303d1f 100644 --- a/src/lib/components/workflow/start-workflow-button.svelte +++ b/src/lib/components/workflow/start-workflow-button.svelte @@ -31,6 +31,7 @@ variant="ghost" class="start-button" leadingIcon="lightning-bolt" + label={translate('workflows.start-workflow-like-this-one')} on:click={() => goto(href)} {...$$restProps} > diff --git a/src/lib/components/workflow/workflows-summary-configurable-table.svelte b/src/lib/components/workflow/workflows-summary-configurable-table.svelte index 5d55031fba..79cd0dc2c8 100644 --- a/src/lib/components/workflow/workflows-summary-configurable-table.svelte +++ b/src/lib/components/workflow/workflows-summary-configurable-table.svelte @@ -296,6 +296,9 @@ size="xs" variant="ghost" leadingIcon={dense ? 'table-dense' : 'table-comfy'} + label={dense + ? translate('common.dense') + : translate('common.comfortable')} > @@ -304,16 +307,18 @@ data-testid="export-history-button" size="xs" variant="ghost" + label={translate('common.download-json')} > - + diff --git a/src/lib/holocene/button.svelte b/src/lib/holocene/button.svelte index 60b05b7621..99071b9aae 100644 --- a/src/lib/holocene/button.svelte +++ b/src/lib/holocene/button.svelte @@ -60,6 +60,7 @@ 'data-testid'?: string; class?: string; disableTracking?: boolean; + label?: string; }; // Prevent Svelte 5 event handler props - use on:click instead @@ -92,6 +93,7 @@ } from 'svelte/elements'; import { cva, type VariantProps } from 'class-variance-authority'; + import { onMount } from 'svelte'; import { twMerge as merge } from 'tailwind-merge'; import { goto } from '$app/navigation'; @@ -115,9 +117,35 @@ export let target: string = null; export let disableTracking = false; + export let label: string | undefined = undefined; + let className = ''; export { className as class }; + let buttonElement: HTMLElement | undefined; + + onMount(() => { + if (!buttonElement) return; + const hasName = + Boolean(label) || + Boolean($$restProps['aria-label']) || + Boolean($$restProps['aria-labelledby']) || + Boolean($$restProps['title']) || + Boolean(buttonElement.textContent?.trim()) || + Boolean(leadingIcon) || + Boolean(trailingIcon) || + count > 0; + if (!hasName) { + const message = + 'Button primitive rendered without an accessible name. Provide `label`, slot text, `leadingIcon`, `trailingIcon`, or `aria-label`. See 4.1.2-button-anchor-empty.md.'; + if (import.meta.env.DEV) { + throw new Error(message); + } else { + console.error(message, buttonElement); + } + } + }); + const onLinkClick = (e: MouseEvent) => { // Skip if middle mouse click or new tab if (e.button === 1 || target || e.metaKey) return; @@ -137,10 +165,12 @@ {#if href && !disabled} {:else}