Skip to content

feat: add template-no-autoplay#2759

Draft
johanrd wants to merge 8 commits intoember-cli:masterfrom
johanrd:html-validate/template-no-autoplay
Draft

feat: add template-no-autoplay#2759
johanrd wants to merge 8 commits intoember-cli:masterfrom
johanrd:html-validate/template-no-autoplay

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 no-autoplay for the peer rule concept.

Adds template-no-autoplay: flags autoplay on <audio> / <video> when the attribute is statically present.

Premises

Premise 1 (HTML): The HTML spec's autoplay attribute is a boolean attribute: presence alone means "true", regardless of value. <audio autoplay="false"> reads .autoplay === true at the DOM level (verified in Chrome).

Premise 2 (WCAG): WCAG 2.1 SC 1.4.2 Audio Control (Level A) applies to audio that plays automatically for more than 3 seconds. The W3C ACT rule aaa1bf that operationalizes SC 1.4.2 is explicitly inapplicable when muted is true or the media has no audio track. <video autoplay muted> (GIF-style) is out of SC 1.4.2's scope. Silent-motion concerns fall under SC 2.2.2 Pause/Stop/Hide, which depends on runtime facts static analysis cannot determine.

Conclusion: Flag <audio> with a statically-present autoplay. Flag <video> with a statically-present autoplay unless a statically-truthy muted is also present. Skip mustache-unknown values — false positives are worse than false negatives. Recognize explicit {{false}} / {{"false"}} literals as opt-outs.

Flags

<audio autoplay></audio>
<video autoplay src='/intro.mp4'></video>
<audio autoplay='false'></audio>              {{! boolean attr — any presence is truthy }}
<video autoplay={{true}}></video>
<video autoplay muted={{false}}></video>
<audio autoplay muted></audio>                {{! muted exception is video-only }}

Allows

<audio src='/track.mp3' controls></audio>
<video autoplay muted></video>                {{! GIF-style: muted video is out of SC 1.4.2 scope }}
<video autoplay muted loop playsinline></video>
<audio autoplay={{this.shouldAutoplay}}></audio>   {{! unknown mustache — skipped }}
<video autoplay={{false}}></video>            {{! explicit opt-out literal }}
<audio autoplay={{"false"}}></audio>

Schema option

additionalElements: string[] extends the default {audio, video} set. The muted exception applies only to <video>; elements added via additionalElements are flagged on autoplay alone (their semantics are unknown).

Prior art

Plugin Equivalent Verified behavior
jsx-a11y No equivalent rule (media-has-caption covers captions, not autoplay).
vuejs-accessibility No equivalent rule.
lit-a11y No equivalent rule.
@angular-eslint/template No equivalent rule.
html-validate no-autoplay Reports autoplay on configured tags; include/exclude options; skips DynamicValue. No muted-video exception.

Notes

  • <video autoplay muted> is not flagged — aligned with W3C ACT aaa1bf's scoping of SC 1.4.2 to audio output. <audio autoplay muted> is still flagged — the combination is spec-nonsensical and strongly suggests author confusion.
  • SC 2.2.2 (motion pause/stop/hide) is not enforced statically — duration, parallel-content layout, and essentialness are runtime concerns.
  • Defaults to {audio, video} rather than html-validate's any-element default, because Ember codebases commonly pass autoplay as a non-media component arg (e.g. <Carousel autoplay={{...}} />).
  • Opt-in: not added to any preset config.

johanrd added 8 commits April 27, 2026 21:29
…ases

Cover mixed static+dynamic concat (unknown → skip), single {{false}}
part on muted (falsy → autoplay allowed), and dynamic muted concat
(unknown → skip).
Empirically verified Glimmer behavior:
- <video muted="false">     → kept    (muted=ON via HTML boolean attr)
- <video muted={{false}}>   → omitted (muted=OFF)
- <video muted="{{false}}"> → omitted (muted=OFF)

So `<video autoplay muted="{{false}}">` actually has no muted attribute at
render time and the muted exemption shouldn't apply — it's a real autoplay
violation. Move the case from valid to invalid.
…ed Glimmer behavior

The rule's classifyAttrValue was based on an intuitive model that turns out
to be wrong in two ways, both confirmed empirically (see new
docs/glimmer-attribute-behavior.md):

- Bare-mustache string "false" (`attr={{"false"}}`) is JS-truthy, so Glimmer
  renders it as `attr="false"` and sets the IDL property — not omitted as the
  literal "false" suggests.
- Concat-mustache (`attr="{{X}}"`) sets the IDL property to true regardless
  of the literal value inside, including `"{{false}}"`. Verified against
  <video muted="{{false}}"> → videoEl.muted === true.

Result: the only literal form that genuinely makes Glimmer omit the
attribute is bare `{{false}}`. Everything else with a literal is truthy at
runtime.

Simplified classifyAttrValue accordingly and corrected three test fixtures:

- <audio autoplay={{"false"}}> → moved valid → invalid (autoplay plays).
- <audio autoplay="{{false}}">  → moved valid → invalid (autoplay plays).
- <video autoplay muted={{"false"}}> → moved invalid → valid (muted IDL=true,
  exemption applies).

Plus added lock-in tests for forms that were silently ambiguous before:
muted={{"true"}}, muted="{{true}}", muted="{{false}}", autoplay="{{'false'}}".

Reverts the earlier "test: correct muted='{{false}}' expectation" commit on
this branch, which fixed the test in the wrong direction (muted IDL is
actually true under that form, exemption applies).
@johanrd johanrd marked this pull request as draft April 30, 2026 21:39
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