Skip to content

feat: add template-valid-label-for#2760

Draft
johanrd wants to merge 4 commits intoember-cli:masterfrom
johanrd:html-validate/template-valid-label-for
Draft

feat: add template-valid-label-for#2760
johanrd wants to merge 4 commits intoember-cli:masterfrom
johanrd:html-validate/template-valid-label-for

Conversation

@johanrd
Copy link
Copy Markdown
Contributor

@johanrd johanrd commented Apr 27, 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.

See html-validate input-missing-label for the related peer rule (checks from the input side; this rule checks from the label side).

Adds template-valid-label-for: validates that <label for="x"> points at a labelable form control in the same template, and flags for as redundant when the target is already nested inside the label.

What is checked

  1. Target must be labelable<label for="x"> where the element with id="x" is a <div> or other non-labelable element is flagged. Labelable per HTML §4.6.19: button, input (except type=hidden), meter, output, progress, select, textarea. Ember's built-in <Input> and <Textarea> components (which render to native form controls) are also accepted as labelable targets.
  2. Redundant for<label for="x"><input id="x" /></label> is flagged because the for attribute adds nothing when the target is the first labelable descendant already bound via containment. If for points at a non-first labelable descendant, the author is expressing an explicit choice and it is not flagged. HTML §4.6.19: "the first labelable element … that is a descendant of the label element."

Dynamic for values (mustache) are skipped. Targets not found in this template are also skipped (partial templates, yielded content).

Ember <Input> / <Textarea> resolution

  • Classic HBS: <Input> and <Textarea> resolve globally to the built-in components → always labelable.
  • Strict GJS/GTS: only a named import from @ember/component (directly or aliased) counts as the built-in. A local binding that shadows the name from a different module → not labelable (false-negative acceptable).

Flags

<label for="field">Label</label>
<div id="field">Not labelable</div>

<label for="email">
  <input id="email" type="email" />  {{! redundant — for points at the first (only) labelable descendant }}
</label>

Allows

<label for="email">Email</label>
<input id="email" type="email" />

<label for="second">
  <input id="first" type="text" />
  <input id="second" type="text" />  {{! not the first labelable descendant — explicit override, not redundant }}
</label>

<label for={{this.dynamicId}}>Label</label>   {{! dynamic for= skipped }}

Prior art

Plugin Equivalent
jsx-a11y label-has-associated-control — checks label has a text label and an associated control
vuejs-accessibility label-has-for — checks for present on label
html-validate input-missing-label — checks from the input side (does the input have a label?)

No peer checks labelable-target validity + redundant-for from the label side.

@johanrd johanrd marked this pull request as draft April 30, 2026 21:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant