Files
sure/app/components/DS/toggle.rb
Guillem Arias Fauste e56ad3de42 fix(design-system): DS::Toggle focus ring, role=switch, and semantic tokens (#1843)
* fix(design-system): DS::Toggle a11y + token swaps

Closes #1746. Four fixes on the toggle primitive (visual switch
backed by a sr-only checkbox).

1. **Focus ring (WCAG 2.4.7)** — the `<input>` is `sr-only`, so the
   browser-default focus ring lands on an invisible 0px element.
   The label (the track) had no focus styling, meaning the
   component had **no visible focus indicator at all**. Add
   `peer-focus-visible:ring-2 ring-offset-2 ring-gray-900` with
   `theme-dark:peer-focus-visible:ring-white` so the ring appears
   on the visible track when the underlying checkbox receives
   keyboard focus.

2. **Role semantics** — visual is a switch, but the element was
   announced as "checkbox, checked" because the native input is
   a checkbox. Add `role="switch"` so AT users hear "switch, on"
   / "switch, off". `aria-checked` is inherited from the
   checkbox's checked state, no manual wiring needed.

3. **Token swaps** — replace raw palette references with semantic
   tokens:
   - Track `bg-gray-100 theme-dark:bg-gray-700` → `bg-surface-inset`
   - Checked `peer-checked:bg-green-600` → `peer-checked:bg-success`
   Picks up the contrast bump from #1735 automatically.

4. **Motion safety (WCAG 2.3.3)** — gate the bg color +
   thumb-translate transitions behind `motion-safe:`. Reduced-motion
   users see an instant state snap; everyone else gets the
   existing 300ms ease.

API unchanged. Existing 8 callsites (settings/preferences,
settings/appearances, account_sharings, budgets/edit,
recurring_transactions, styled_form_builder bridge) work without
changes.

* fix(review): use alpha tokens for Toggle focus ring

Swap raw palette (ring-gray-900 / theme-dark:ring-white) on the
DS::Toggle focus ring to ring-alpha-black-300 / ring-alpha-white-300
to match the focus-ring token pattern already used by
form-field, provider_card, and shared/_badge.

Closes AI review feedback on #1843.
2026-05-20 18:08:46 +02:00

34 lines
1.5 KiB
Ruby

class DS::Toggle < DesignSystemComponent
attr_reader :id, :name, :checked, :disabled, :checked_value, :unchecked_value, :opts
def initialize(id:, name: nil, checked: false, disabled: false, checked_value: "1", unchecked_value: "0", **opts)
@id = id
@name = name
@checked = checked
@disabled = disabled
@checked_value = checked_value
@unchecked_value = unchecked_value
@opts = opts
end
def label_classes
class_names(
"relative block w-9 h-5 cursor-pointer",
"rounded-full bg-surface-inset",
# `motion-safe:` gates the bg + thumb-translate transitions on
# `prefers-reduced-motion`; reduced-motion users get a snap.
"motion-safe:transition-colors motion-safe:duration-300",
"after:content-[''] after:block after:bg-white after:absolute after:rounded-full",
"after:top-0.5 after:left-0.5 after:w-4 after:h-4",
"motion-safe:after:transition-transform motion-safe:after:duration-300 motion-safe:after:ease-in-out",
"peer-checked:bg-success peer-checked:after:translate-x-4",
# Focus ring driven from the sr-only input via `peer-focus-visible:`.
# Offset places the ring outside the track so it lands on the
# surrounding chrome regardless of theme.
"peer-focus-visible:ring-2 peer-focus-visible:ring-offset-2",
"peer-focus-visible:ring-alpha-black-300 theme-dark:peer-focus-visible:ring-alpha-white-300",
"peer-disabled:opacity-70 peer-disabled:cursor-not-allowed"
)
end
end