mirror of
https://github.com/we-promise/sure.git
synced 2026-05-29 23:39:03 +00:00
fix(design-system): DS::Button a11y audit
Closes #1738. Four concrete fixes surfaced by the savings-goals audit + #1737 universal checklist: 1. Focus ring (WCAG 2.4.7). `base.css` had `focus-visible:outline-gray-900` which is **1.07:1** against the primary button's gray-900 background — invisible. Widen to `outline-2 outline-offset-2`, place outline outside the button via offset, and add a dark-mode `outline-white` so the ring is always visible against the page chrome regardless of the button surface. 2. Touch target (WCAG 2.5.5). Icon-only buttons at the default `:md` size were `w-9 h-9` = 36×36, below the 44×44 enhanced target. Bump `md.icon_container_classes` to `w-11 h-11` and `lg.icon_container_classes` to `w-12 h-12` to keep the size scale intact. `sm` stays at 32×32 (already passes WCAG 2.5.8 AA's 24×24 minimum; intentional compact-density variant). 3. Default button type. `content_tag(:button, ...)` inherits the HTML default `type="submit"`, so a DS::Button rendered inside a form steals Enter-key submission from the first text input (reproducible in the form stepper). Default to `type="button"` in the non-`href` branch; existing form submitters pass `type: "submit"` explicitly and continue to work. The `button_to` (href) branch keeps the submit default because button_to wraps its own form. 4. Icon-only accessible name. Icon-only buttons render no text node, so AT users hear "button" with no name. Derive a humanized aria-label from the icon key (e.g. `icon: "more-horizontal"` → `aria-label="More horizontal"`); explicit `aria: { label: }` on the caller still wins. Soft fallback — callers should still pass meaningful labels for richer copy. Plus: replace the stale `fg-white` icon class on the destructive variant with `text-inverse` (the `fg-*` namespace was deprecated in #1626 so `fg-white` resolved to nothing; the icon was using its helper-default color rather than the white the design intended). Out of scope: - Menu avatar trigger (custom 36×36 button bypassing DS::Button) — belongs to #1743 DS::Menu audit. - DS::FilledIcon `lg` size container (decorative, not interactive) — belongs to #1742.
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
@layer base {
|
||||
button {
|
||||
@apply cursor-pointer focus-visible:outline-gray-900;
|
||||
@apply cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-900;
|
||||
|
||||
@variant theme-dark {
|
||||
@apply focus-visible:outline-white;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
|
||||
@@ -33,6 +33,27 @@ class DS::Button < DS::Buttonish
|
||||
data = data.merge(turbo_frame: frame)
|
||||
end
|
||||
|
||||
# `content_tag(:button, ...)` defaults to `type="submit"` per the HTML
|
||||
# spec — meaning a DS::Button rendered inside a form will steal Enter-key
|
||||
# submission from the first text input. Default to `type="button"` so
|
||||
# callers must opt into submit behavior explicitly. `button_to` (href
|
||||
# branch) wraps the button in its own form, so submit there is correct.
|
||||
if href.blank?
|
||||
merged_opts[:type] ||= "button"
|
||||
end
|
||||
|
||||
# Icon-only buttons have no visible text node, so screen readers fall
|
||||
# back to announcing "button" with no name. Derive a humanized fallback
|
||||
# from the icon key so AT users hear *something* meaningful; explicit
|
||||
# `aria: { label: }` on the caller still wins.
|
||||
if icon_only? && icon.present?
|
||||
aria = (merged_opts[:aria] || {}).symbolize_keys
|
||||
if aria[:label].blank? && merged_opts[:"aria-label"].blank?
|
||||
aria[:label] = icon.to_s.tr("-_", " ").capitalize
|
||||
merged_opts[:aria] = aria
|
||||
end
|
||||
end
|
||||
|
||||
merged_opts.merge(
|
||||
class: class_names(container_classes, extra_classes),
|
||||
data: data
|
||||
|
||||
@@ -10,7 +10,7 @@ class DS::Buttonish < DesignSystemComponent
|
||||
},
|
||||
destructive: {
|
||||
container_classes: "text-inverse bg-red-500 theme-dark:bg-red-400 hover:bg-red-600 theme-dark:hover:bg-red-500 disabled:bg-red-200 theme-dark:disabled:bg-red-600",
|
||||
icon_classes: "fg-white"
|
||||
icon_classes: "text-inverse"
|
||||
},
|
||||
outline: {
|
||||
container_classes: "text-primary border border-secondary bg-transparent hover:bg-surface-hover",
|
||||
@@ -43,13 +43,13 @@ class DS::Buttonish < DesignSystemComponent
|
||||
},
|
||||
md: {
|
||||
container_classes: "px-3 py-2",
|
||||
icon_container_classes: "inline-flex items-center justify-center w-9 h-9",
|
||||
icon_container_classes: "inline-flex items-center justify-center w-11 h-11",
|
||||
radius_classes: "rounded-lg",
|
||||
text_classes: "text-sm"
|
||||
},
|
||||
lg: {
|
||||
container_classes: "px-4 py-3",
|
||||
icon_container_classes: "inline-flex items-center justify-center w-10 h-10",
|
||||
icon_container_classes: "inline-flex items-center justify-center w-12 h-12",
|
||||
radius_classes: "rounded-xl",
|
||||
text_classes: "text-base"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user