Skip to content

Commit e34533b

Browse files
authored
Merge branch 'master' into fix/invalid-role-aria-query
2 parents 41087e0 + f25c34e commit e34533b

39 files changed

Lines changed: 1869 additions & 336 deletions

.github/workflows/bench-compare.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
# (github.head_ref is a branch name that only exists on the fork remote).
2727
ref: ${{ github.event.pull_request.head.sha }}
2828

29-
- uses: wyvox/action-setup-pnpm@v3
29+
- uses: wyvox/action-setup-pnpm@v4
3030

3131
- name: Run benchmark comparison
3232
env:

.release-plan.json

Lines changed: 4 additions & 12 deletions
Large diffs are not rendered by default.

CHANGELOG.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,42 @@
11
# Changelog
22

3+
## Release (2026-04-25)
4+
5+
* eslint-plugin-ember 13.1.1 (patch)
6+
7+
#### :bug: Bug Fix
8+
* `eslint-plugin-ember`
9+
* [#2735](https://github.com/ember-cli/eslint-plugin-ember/pull/2735) Update ember-eslint-parser to 0.11 ([@NullVoxPopuli](https://github.com/NullVoxPopuli))
10+
* [#2736](https://github.com/ember-cli/eslint-plugin-ember/pull/2736) fix: ignore comment + whitespace nodes in template-no-yield-only ([@johanrd](https://github.com/johanrd))
11+
* [#2722](https://github.com/ember-cli/eslint-plugin-ember/pull/2722) refactor(template-require-presentational-children): source role list from aria-query ([@johanrd](https://github.com/johanrd))
12+
* [#2719](https://github.com/ember-cli/eslint-plugin-ember/pull/2719) BUGFIX: template-no-aria-unsupported-elements — source the reserved-element list from aria-query ([@johanrd](https://github.com/johanrd))
13+
* [#2718](https://github.com/ember-cli/eslint-plugin-ember/pull/2718) BUGFIX: template-require-media-caption — compare kind="captions" case-insensitively ([@johanrd](https://github.com/johanrd))
14+
* [#2714](https://github.com/ember-cli/eslint-plugin-ember/pull/2714) BUGFIX: accept tabindex="-1" in template-require-aria-activedescendant-tabindex ([@johanrd](https://github.com/johanrd))
15+
16+
#### :house: Internal
17+
* `eslint-plugin-ember`
18+
* [#2733](https://github.com/ember-cli/eslint-plugin-ember/pull/2733) test: cover {{! eslint-disable-* }} directives inside <template> in .gts ([@johanrd](https://github.com/johanrd))
19+
* [#2721](https://github.com/ember-cli/eslint-plugin-ember/pull/2721) refactor(template-no-abstract-roles): source abstract-role list from aria-query ([@johanrd](https://github.com/johanrd))
20+
21+
#### Committers: 2
22+
- [@NullVoxPopuli](https://github.com/NullVoxPopuli)
23+
- [@johanrd](https://github.com/johanrd)
24+
25+
## Release (2026-04-21)
26+
27+
* eslint-plugin-ember 13.1.0 (minor)
28+
29+
#### :rocket: Enhancement
30+
* `eslint-plugin-ember`
31+
* [#2715](https://github.com/ember-cli/eslint-plugin-ember/pull/2715) feat: re-export hbs parser and document HBS flat-config setup ([@johanrd](https://github.com/johanrd))
32+
33+
#### :bug: Bug Fix
34+
* `eslint-plugin-ember`
35+
* [#2713](https://github.com/ember-cli/eslint-plugin-ember/pull/2713) BUGFIX: false positive: interactive flow content inside <details> ([@johanrd](https://github.com/johanrd))
36+
37+
#### Committers: 1
38+
- [@johanrd](https://github.com/johanrd)
39+
340
## Release (2026-04-20)
441

542
* eslint-plugin-ember 13.0.0 (major)

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,36 @@ export default [
174174

175175
`template-lint-migration` mirrors the ember-template-lint `recommended` preset.
176176

177+
### Linting `.hbs` files
178+
179+
ESLint flat config only picks up `.hbs` files when a `files` glob names them. Add a dedicated block so they route to the Handlebars parser:
180+
181+
```js
182+
// eslint.config.mjs
183+
import { hbsParser, plugin as emberPlugin } from 'eslint-plugin-ember/recommended';
184+
185+
export default [
186+
// ...other blocks...
187+
{
188+
files: ['app/**/*.hbs'],
189+
languageOptions: { parser: hbsParser },
190+
plugins: { ember: emberPlugin },
191+
rules: {
192+
'ember/template-no-bare-strings': 'error',
193+
// ...other template rules...
194+
},
195+
},
196+
];
197+
```
198+
199+
Make sure no earlier `@typescript-eslint/parser` block's `files` glob reaches `.hbs` — narrow it to `['**/*.{js,ts,gjs,gts}']` (or similar). Flat config merges rules across every matching block, so even if our HBS block overrides the parser, type-info rules from a matching TS block still layer on and fail with errors like:
200+
201+
> Parsing error: `` was not found by the project service because the extension for the file (`.hbs`) is non-standard.
202+
203+
or
204+
205+
> Error while loading rule `@typescript-eslint/await-thenable`: You have used a rule which requires type information.
206+
177207
### Replacing `template-lint-disable` comments
178208

179209
Inline disable directives need to be rewritten to ESLint's syntax, prefixed with `ember/template-`. For now, only two scopes are supported: the next line, or the rest of the file. For example, replace:

docs/rules/template-no-autofocus-attribute.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,34 @@ Examples of **correct** code for this rule:
4040
</template>
4141
```
4242

43+
Explicit opt-out via a mustache boolean `false` is allowed — this is the
44+
only form that statically guarantees no rendered `autofocus` attribute
45+
(Glimmer VM normalizes `{{false}}` to attribute removal). The string
46+
`autofocus="false"` is still flagged per HTML boolean-attribute semantics
47+
(any attribute presence, including the string `"false"`, enables autofocus).
48+
49+
```gjs
50+
<template>
51+
<input autofocus={{false}} />
52+
{{!-- element syntax: the mustache-boolean form --}}
53+
54+
{{input autofocus=false}}
55+
{{!-- mustache syntax: the hash-pair form --}}
56+
</template>
57+
```
58+
59+
`<dialog>` and its descendants are exempt. A dialog is expected to focus its
60+
initial element on open, per
61+
[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog):
62+
63+
```gjs
64+
<template>
65+
<dialog>
66+
<button autofocus>Close</button>
67+
</dialog>
68+
</template>
69+
```
70+
4371
## When Not To Use It
4472

4573
If you need to autofocus for specific accessibility or UX requirements and have thoroughly tested with assistive technologies, you may disable this rule for those specific cases.

docs/rules/template-no-empty-headings.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ This rule **allows** the following:
5050

5151
If violations are found, remediation should be planned to ensure text content is present and visible and/or screen-reader accessible. Setting `aria-hidden="false"` or removing `hidden` attributes from the element(s) containing heading text may serve as a quickfix.
5252

53+
## Notes on `aria-hidden` semantics
54+
55+
This rule follows [WAI-ARIA 1.2 §`aria-hidden`](https://www.w3.org/TR/wai-aria-1.2/#aria-hidden) verbatim: only an explicit truthy value hides the element. Ambiguous shapes — valueless `aria-hidden`, empty string, and mustache literals that resolve to an empty / whitespace-only string — all resolve to the default `undefined` and do NOT exempt the heading from the empty-content check.
56+
57+
- `aria-hidden="true"` / `aria-hidden={{true}}` / `aria-hidden={{"true"}}` (any case, whitespace-trimmed) → hidden, exempts the heading.
58+
- `aria-hidden="false"` / `aria-hidden={{false}}` / `aria-hidden={{"false"}}` → not hidden, the empty-content check applies.
59+
- `<h1 aria-hidden>` / `aria-hidden=""` / `aria-hidden={{""}}` / `aria-hidden={{" "}}` → spec-default `undefined`, the empty-content check applies.
60+
5361
## References
5462

5563
- [WCAG SC 2.4.6 Headings and Labels](https://www.w3.org/TR/UNDERSTANDING-WCAG20/navigation-mechanisms-descriptive.html)

docs/rules/template-require-aria-activedescendant-tabindex.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66

77
<!-- end auto-generated rule header -->
88

9-
This rule requires all non-interactive HTML elements using the `aria-activedescendant` attribute to declare a `tabindex` of zero.
9+
This rule requires non-interactive HTML elements using the `aria-activedescendant` attribute to declare a `tabindex` of `0` or `-1`.
1010

1111
The `aria-activedescendant` attribute identifies the active descendant element of a composite widget, textbox, group, or application with document focus. This attribute is placed on the container element of the input control, and its value is set to the ID of the active child element. This allows screen readers to communicate information about the currently active element as if it has focus, while actual focus of the DOM remains on the container element.
1212

13-
Elements with `aria-activedescendant` must have a `tabindex` of zero in order to support keyboard navigation. Besides interactive elements, which are inherently keyboard-focusable, elements using the `aria-activedescendant` attribute must declare a `tabIndex` of zero with the `tabIndex` attribute.
13+
Elements with `aria-activedescendant` must be focusable to support keyboard navigation. `tabindex="0"` puts the element in the natural tab order; `tabindex="-1"` makes it focusable programmatically (e.g. via roving focus) but skips it in the tab order. Both are valid patterns for composite widgets — see the [W3C APG — Managing focus in composites using aria-activedescendant](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_focus_activedescendant).
1414

1515
## Examples
1616

@@ -19,8 +19,8 @@ This rule **forbids** the following:
1919
```gjs
2020
<template>
2121
<div aria-activedescendant='some-id'></div>
22-
<div aria-activedescendant='some-id' tabindex='-1'></div>
23-
<input aria-activedescendant={{some-id}} tabindex='-1' />
22+
<div aria-activedescendant='some-id' tabindex='-2'></div>
23+
<input aria-activedescendant={{some-id}} tabindex='-100' />
2424
</template>
2525
```
2626

@@ -32,9 +32,11 @@ This rule **allows** the following:
3232
<CustomComponent aria-activedescendant={{some-id}} />
3333
<CustomComponent aria-activedescendant={{some-id}} tabindex={{0}} />
3434
<div aria-activedescendant='some-id' tabindex='0'></div>
35+
<div aria-activedescendant='some-id' tabindex='-1'></div>
3536
<input />
3637
<input aria-activedescendant={{some-id}} />
3738
<input aria-activedescendant={{some-id}} tabindex={{0}} />
39+
<input aria-activedescendant={{some-id}} tabindex={{-1}} />
3840
</template>
3941
```
4042

docs/rules/template-require-iframe-title.md

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66

77
## `<iframe>`
88

9-
`<iframe>` elements must have a unique title property to indicate its content to the user.
10-
11-
This rule takes no arguments.
9+
`<iframe>` elements must have a unique title property so assistive
10+
technology can convey their content to the user. The normative
11+
requirement is [WCAG SC 4.1.2 (Name, Role, Value)](https://www.w3.org/TR/UNDERSTANDING-WCAG20/ensure-compat-rsv.html);
12+
the `title` attribute is _one sufficient technique_ for meeting it
13+
(sufficient technique [H64](https://www.w3.org/WAI/WCAG21/Techniques/html/H64)).
1214

1315
## Examples
1416

@@ -27,12 +29,21 @@ This rule **forbids** the following:
2729
<template>
2830
<iframe />
2931
<iframe title='' />
32+
<iframe title=' ' />
33+
<iframe title={{null}} />
34+
<iframe title={{undefined}} />
35+
<iframe title={{true}} />
36+
<iframe title={{false}} />
37+
<iframe title={{42}} />
3038
</template>
3139
```
3240

3341
## References
3442

35-
- [Deque University](https://dequeuniversity.com/rules/axe/1.1/frame-title)
36-
- [Technique H65: Using the title attribute of the frame and iframe elements](https://www.w3.org/TR/2014/NOTE-WCAG20-TECHS-20140408/H64)
37-
- [WCAG Success Criterion 2.4.1 - Bypass Blocks](https://www.w3.org/TR/UNDERSTANDING-WCAG20/navigation-mechanisms-skip.html)
38-
- [WCAG Success Criterion 4.1.2 - Name, Role, Value](https://www.w3.org/TR/UNDERSTANDING-WCAG20/ensure-compat-rsv.html)
43+
- [WCAG SC 4.1.2 — Name, Role, Value](https://www.w3.org/TR/UNDERSTANDING-WCAG20/ensure-compat-rsv.html)
44+
— the normative requirement.
45+
- [WCAG Technique H64 — Using the title attribute of the iframe element](https://www.w3.org/WAI/WCAG21/Techniques/html/H64)
46+
— a sufficient technique for SC 4.1.2, not itself normative.
47+
- [WCAG Success Criterion 2.4.1 — Bypass Blocks](https://www.w3.org/TR/UNDERSTANDING-WCAG20/navigation-mechanisms-skip.html)
48+
- [ACCNAME 1.2 — accessible-name computation](https://www.w3.org/TR/accname-1.2/)
49+
- [axe-core rule `frame-title`](https://dequeuniversity.com/rules/axe/4.10/frame-title)

docs/rules/template-require-mandatory-role-attributes.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,40 @@ This rule **allows** the following:
3131
<div role="option" aria-selected="false" />
3232
<CustomComponent role="checkbox" aria-required="true" aria-checked="false" />
3333
{{some-component role="heading" aria-level="2"}}
34+
35+
{{! Native inputs supply required ARIA state for matching roles. Lookup is
36+
based on axobject-query's elementAXObjects + AXObjectRoles (see below). }}
37+
<input type="checkbox" role="switch" />
38+
<input type="checkbox" role="checkbox" />
39+
<input type="radio" role="radio" />
40+
<input type="range" role="slider" />
3441
</template>
3542
```
3643

44+
## Semantic-role exemptions
45+
46+
When the role attribute explicitly declares a role that the native element already provides, the native element supplies the required ARIA state and the rule does not flag missing attributes. The exemption is looked up via [axobject-query](https://github.com/A11yance/axobject-query)'s `elementAXObjects` + `AXObjectRoles` maps, matching the approach used by `eslint-plugin-jsx-a11y` and `@angular-eslint/template`.
47+
48+
Exempt pairings include (non-exhaustive):
49+
50+
| Element | Role | Required ARIA state supplied by |
51+
| ------------------------- | -------------------- | ------------------------------------------------ |
52+
| `<input type="checkbox">` | `checkbox`, `switch` | native `checked` state |
53+
| `<input type="radio">` | `radio` | native `checked` state |
54+
| `<input type="range">` | `slider` | native `value` / `min` / `max` |
55+
| `<input type="number">` | `spinbutton` | native `value` (spinbutton has no required ARIA) |
56+
| `<input type="text">` | `textbox` | no required ARIA |
57+
| `<input type="search">` | `searchbox` | no required ARIA |
58+
59+
Undocumented pairings (e.g. `<input type="checkbox" role="menuitemcheckbox">` — axobject-query does not list this) remain flagged.
60+
3761
## References
3862

39-
- [WAI-ARIA Roles - Accessibility \_ MDN](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles)
63+
- [WAI-ARIA Roles](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles)
64+
- [WAI-ARIA APG — Switch pattern](https://www.w3.org/WAI/ARIA/apg/patterns/switch/)
65+
- [HTML-AAM — `<input type="checkbox">``checkbox` role mapping](https://www.w3.org/TR/html-aam-1.0/#el-input-checkbox)
66+
— primary-spec source: HTML-AAM maps the native element to the
67+
`checkbox` role and derives `aria-checked` from the element's
68+
checkedness (and `indeterminate` for `mixed`). axobject-query
69+
encodes that mapping for tooling.
70+
- [axobject-query](https://github.com/A11yance/axobject-query) — AX-tree data source for the exemption lookup (secondary, encodes HTML-AAM)

eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ module.exports = [
224224
'import/default': 'off',
225225
'import/no-named-as-default': 'off',
226226
'import/no-named-as-default-member': 'off',
227-
'import/no-unresalved': 'off',
227+
'import/no-unresolved': 'off',
228228
'import/no-missing-import': 'off',
229229

230230
// vite config format does not match regex

0 commit comments

Comments
 (0)