a11y(savings_goals): ARIA semantics + unique IDs + h2 hierarchy

- Projection chart SVG: role=img + <title> + <desc> wired through new
  ariaLabelValue / ariaDescriptionValue Stimulus values. Show.html.erb
  passes a localized chart label and a strip_tags'd projection summary.
- Progress ring container: role=progressbar + aria-valuenow/min/max +
  aria-label so screen readers announce "Goal 27% complete. $13,250 of
  $50,000 saved." instead of four disjoint spans.
- Funding-account checkboxes (stepper step 1): explicit per-account id
  ("savings_goal_account_ids_<id>") so each row has a unique DOM id;
  duplicate-id HTML violation gone.
- show.html.erb: <h3> -> <h2> at six section headings (celebration,
  no-target-date, projection, contributions, funding accounts, notes)
  so the heading hierarchy is h1 -> h2, not h1 -> h3.
- goal_avatar + account_stack components: aria-hidden=true on the
  decorative wrappers; the textual goal/account name beside them is
  always read separately so the SR no longer prefixes every entry with
  the avatar initial.
- New locale keys: savings_goals.show.ring.aria_label and
  savings_goals.show.projection.aria_label.
This commit is contained in:
Guillem Arias
2026-05-11 19:31:29 +02:00
parent 6be5f813a4
commit c622dabd20
7 changed files with 26 additions and 9 deletions

View File

@@ -11,7 +11,7 @@ import * as d3 from "d3";
// Data shape passed via `data-savings-goal-projection-chart-data-value`
// matches SavingsGoal#projection_payload.
export default class extends Controller {
static values = { data: Object };
static values = { data: Object, ariaLabel: String, ariaDescription: String };
connect() {
this._draw();
@@ -96,6 +96,12 @@ export default class extends Controller {
.attr("viewBox", `0 0 ${width} ${height}`)
.attr("preserveAspectRatio", "none");
const titleId = `chart-title-${this._id()}`;
const descId = `chart-desc-${this._id()}`;
svg.attr("role", "img").attr("aria-labelledby", titleId).attr("aria-describedby", descId);
svg.append("title").attr("id", titleId).text(this.ariaLabelValue || "Savings goal projection");
svg.append("desc").attr("id", descId).text(this.ariaDescriptionValue || "");
const defs = svg.append("defs");
const gradient = defs
.append("linearGradient")