Skip to content

BUGFIX: template-require-valid-alt-text — reject empty-string aria-label/labelledby/alt on <input type=image>, <object>, <area>#2730

Merged
NullVoxPopuli merged 12 commits intoember-cli:masterfrom
johanrd:fix/alt-text-empty-aria-label
Apr 25, 2026
Merged

BUGFIX: template-require-valid-alt-text — reject empty-string aria-label/labelledby/alt on <input type=image>, <object>, <area>#2730
NullVoxPopuli merged 12 commits intoember-cli:masterfrom
johanrd:fix/alt-text-empty-aria-label

Conversation

@johanrd
Copy link
Copy Markdown
Contributor

@johanrd johanrd commented Apr 21, 2026

Note

This is part of a series where Claude has audited eslint-plugin-ember against jsx-a11y, vuejs-accessibility, angular-eslint, lit-a11y and html-validate, ember-template-lint, and the HTML and WCAG specs.

Summary

  • Premise 1: An empty-string aria-label="" contributes no accessible name per ACCNAME 1.2 §4.3.2 step 2D: "…has an aria-label attribute whose value is not undefined, not the empty string, nor, when trimmed of whitespace, is not the empty string." Empty aria-labelledby="" likewise contributes no name per ACCNAME 1.2 §4.3.2 step 2B, which requires the attribute to "contain at least one valid IDREF" — an empty value references none. Empty title="" provides no useful accessible name. ACCNAME 1.2 step 2I (Tooltip) literally says "if the current node has a Tooltip attribute, return its value" — there is no emptiness check at this step, so an empty title returns the empty string as a candidate name. That empty name does not satisfy the host-language conformance requirements that follow. For <input type="image">, <object>, and <area>, WCAG SC 4.1.2 states that "all user interface components have a name and role that can be programmatically determined" — an empty fallback value leaves no programmatically determinable name.
  • Premise 2: Today our rule checks only for the presence of any fallback attribute, not its value — so <input type="image" aria-label="" />, <object aria-labelledby="" />, and <area title="" /> are accepted.
  • Conclusion: Reject empty / whitespace-only aria-label, aria-labelledby, and title on <input type="image">, <object>, and <area>. Dynamic values (mustache, concat) stay accepted — we can't know at lint time whether they resolve to empty.

Fix: add hasNonEmptyTextAttr() that requires a static value to be non-whitespace.

<img>'s alt="" is unchanged — an empty alt on <img> is spec-defined as a marker for decorative images.

Nine new invalid tests (3 elements × 3 fallback attrs) cover the fix.

Prior art

Verified each peer in source:

Plugin Rule Behavior
jsx-a11y alt-text ariaLabelHasValue (lines 37-46) rejects undefined and length === 0. Called for object, area, and input[type="image"]. Does NOT trim whitespace (differs from our rule which does).
vuejs-accessibility alt-text hasAriaLabel(node) + getElementAttributeValue truthiness. Empty strings are falsy → flagged.
@angular-eslint/template alt-text Existence-onlyisValidObjectNode/isValidAreaNode/isValidInputNode all return true on mere attribute-name presence (same bug class this PR fixes).
lit-a11y alt-text Existence-only via !elementHasAttribute(...). Also: no <object> or <area> handler — rule only checks img, input[type=image], role="img".

johanrd added 3 commits April 21, 2026 07:50
…aria-labelledby/alt

Before: for <input type="image">, <object>, and <area>, the rule checked
only for the PRESENCE of an accessible-name fallback attribute
(aria-label / aria-labelledby / alt / title). An empty-string value
provides no accessible name but slipped past.

Fix: add hasNonEmptyTextAttr() that requires the attribute's static
value to be non-whitespace. Dynamic values (mustache, concat) remain
accepted — we can't tell at lint time whether they resolve to empty.

<img>'s alt handling is unchanged — alt="" is still valid there
(spec-defined marker for decorative images).

Nine new invalid tests cover the three elements × three fallback attrs.
Translates 41 cases from peer-plugin rules:
  - jsx-a11y alt-text
  - vuejs-accessibility alt-text
  - lit-a11y alt-text

Fixture documents parity after this fix:
  - Empty-string aria-label/aria-labelledby on <object>, <area>, and
    <input type=image> is now flagged (reusing existing objectMissing /
    areaMissing / inputImage messageIds).

Remaining divergences (<img alt role=presentation> accepting non-empty
alt in jsx-a11y, <img aria-label> without alt) are annotated inline.
johanrd added 2 commits April 22, 2026 14:21
…verage (Copilot review)

- JSDoc for hasNonEmptyTextAttr() rewritten: no longer overstates the
  guarantee for dynamic values, and notes that aria-labelledby IDREFs
  are not validated.
- Added invalid-case coverage for whitespace-only aria-label /
  aria-labelledby / title — ACCNAME 1.2 §4.3.2 step 2D.
- hasNonEmptyTextAttr() already trims static values, so the new
  whitespace-only cases flag without further rule changes.
@johanrd johanrd force-pushed the fix/alt-text-empty-aria-label branch from c4885d4 to 82022bb Compare April 22, 2026 17:09
johanrd added 4 commits April 23, 2026 21:21
…t-a11y-behavior.md (Copilot review)

That doc was never checked in. Remove the dangling reference and note
that divergences are captured inline (grep for 'DIVERGENCE —') plus in
PR descriptions — matches how the other peer-parity fixtures describe
their own divergence records.
…ared helper (Copilot review)

Extract a new `getStaticAttrValue` util that resolves literal-valued
mustaches (`{{"foo"}}`, `{{true}}`, `{{-1}}`) and single-part concat
statements (`"{{true}}"`) to their static string value. `hasNonEmptyTextAttr`
now delegates to the helper — `aria-label={{""}}` / `aria-label="{{""}}"`
normalise to the empty string and flag the same as the text-node equivalent;
genuinely dynamic values (PathExpressions, multi-part concat) still
short-circuit to "assume truthy". Closes the bypass where authors wrapped
an empty accessible name in mustaches.

Byte-identical carrier of lib/utils/static-attr-value.js across all PRs
that land it.
@johanrd johanrd closed this Apr 25, 2026
johanrd added 2 commits April 25, 2026 06:46
…op audit fixture

Upstream maintainers don't want the per-PR `tests/audit/peer-parity`
pattern. Port one case that pinned distinct behavior:
- `<img alt=" " />` as VALID — whitespace-only alt is currently
  treated as decorative; jsx-a11y agrees.

All other audit cases were already covered by the regular tests on
this branch (extensive existing coverage of object/area/input variants,
empty aria-label/labelledby, presentation-role conflicts, etc).
@johanrd johanrd reopened this Apr 25, 2026
@johanrd johanrd marked this pull request as ready for review April 25, 2026 15:33
// Whitespace-only alt — pin our current behavior. Peer plugins
// (jsx-a11y) accept this; we don't trim before considering "empty alt".
'<template><img alt=" " /></template>',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But do we want this to be valid? Is it valuable?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's fine, to two cases above are also empty

@NullVoxPopuli NullVoxPopuli merged commit 9d0ba71 into ember-cli:master Apr 25, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants