mirror of
https://github.com/we-promise/sure.git
synced 2026-05-25 21:44:56 +00:00
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.
63 lines
2.0 KiB
Ruby
63 lines
2.0 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# An extension to `button_to` helper. All options are passed through to the `button_to` helper with some additional
|
|
# options available.
|
|
class DS::Button < DS::Buttonish
|
|
attr_reader :confirm
|
|
|
|
def initialize(confirm: nil, **opts)
|
|
super(**opts)
|
|
@confirm = confirm
|
|
end
|
|
|
|
def container(&block)
|
|
if href.present?
|
|
button_to(href, **merged_opts, &block)
|
|
else
|
|
content_tag(:button, **merged_opts, &block)
|
|
end
|
|
end
|
|
|
|
private
|
|
def merged_opts
|
|
merged_opts = opts.dup || {}
|
|
extra_classes = merged_opts.delete(:class)
|
|
href = merged_opts.delete(:href)
|
|
data = merged_opts.delete(:data) || {}
|
|
|
|
if confirm.present?
|
|
data = data.merge(turbo_confirm: confirm.to_data_attribute)
|
|
end
|
|
|
|
if frame.present?
|
|
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
|
|
)
|
|
end
|
|
end
|