Skip to content

BUGFIX: template-require-iframe-title — flag title={{null|undefined|number}}#57

Closed
johanrd wants to merge 4 commits intomasterfrom
fix/iframe-title-value-checks
Closed

BUGFIX: template-require-iframe-title — flag title={{null|undefined|number}}#57
johanrd wants to merge 4 commits intomasterfrom
fix/iframe-title-value-checks

Conversation

@johanrd
Copy link
Copy Markdown
Owner

@johanrd johanrd commented Apr 22, 2026

[Mirror of ember-cli#2731 for Copilot review]

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: <iframe> elements require an accessible name so assistive tech can convey their content. The normative source is WCAG SC 4.1.2 Name, Role, Value; Technique H64 is a sufficient technique citing title as one way to provide it.

  • Premise 2: Mustache literal title values don't supply an author-intended name. At runtime Ember either drops the attribute when the bound value is nullish (no accessible name) or stringifies the literal to "null" / "undefined" / "42" (a coerced placeholder). Empty string literals like {{""}} resolve to "" — same effect as title="".

  • Premise 3: Today our rule only rejects title={{false}}. title={{null}}, title={{undefined}}, title={{42}}, title={{""}}, and their title="{{x}}" concat equivalents silently pass.

  • Conclusion: Treat GlimmerBooleanLiteral, GlimmerNullLiteral, GlimmerUndefinedLiteral, GlimmerNumberLiteral, and empty/whitespace GlimmerStringLiteral as invalid title values in both the title={{x}} and title="{{x}}" positions.

Fix: extract isInvalidTitleLiteralPath() for the four non-string literal types and a shared processStaticTitle() helper that handles text-node, mustache-string-literal, and concat-string-literal forms uniformly. Empty / whitespace string literals follow the same emptiness path as text-node title="".

Tests cover each literal type × each syntax form (bare-mustache and concat).

Whitespace-only title — now opt-out via schema option

The rule previously flagged title=" " (whitespace-only static) as a hard error. This is stricter than ACCNAMEACCNAME 1.2 §4.3.2 step 2I (Tooltip) just says "return its value" with no trim (unlike step D AriaLabel, which has an explicit nor, when trimmed of whitespace, is not the empty string check), so a 3-space accessible name is technically assigned. The check remains on by default as an authoring-hygiene guard (useless in practice), but teams that want spec-aligned behavior can opt out via the new allowWhitespaceOnlyTitle: true option. Empty-string title="", the non-string literal cases above, and empty-string-literal mustaches are not affected by this option — they are always flagged as correctness.

Prior art

Peers diverge materially on empty/non-string title handling. Don't assume parity:

Plugin Rule Verified behavior
jsx-a11y iframe-has-title Check is if (title && typeof title === 'string') → bail. Flags title="" (falsy), flags title={42} (not a string), flags title={null} (falsy). Does NOT flag title=" " (truthy string).
vuejs-accessibility iframe-has-title Check is title === null || !["string","object"].includes(typeof title) → flag. Does NOT flag title="" (typeof "string"). Flags :title='2' (number not a string).
lit-a11y iframe-title Existence-only at line 38: !element.attribs.title || element.attribs.title === undefined. Flags title="" (falsy), does NOT flag non-string values (parse5 yields strings only).
@angular-eslint/template No iframe-title equivalent in the rules directory.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 22, 2026

🏎️ Benchmark Comparison

Benchmark Control (p50) Experiment (p50) Δ
🟠 js small 13.91 ms 14.31 ms +2.9%
🟢 js medium 7.23 ms 6.99 ms -3.3%
js large 2.84 ms 2.82 ms -0.6%
gjs small 1.25 ms 1.25 ms -0.2%
gjs medium 615.95 µs 619.19 µs +0.5%
gjs large 243.82 µs 245.60 µs +0.7%
gts small 1.23 ms 1.23 ms -0.1%
gts medium 616.29 µs 618.03 µs +0.3%
gts large 246.12 µs 246.90 µs +0.3%

🟢 faster · 🔴 slower · 🟠 slightly slower · ⚪ within 2%

Full mitata output
clk: ~3.02 GHz
cpu: AMD EPYC 7763 64-Core Processor
runtime: node 24.14.1 (x64-linux)

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
js small (control)            16.99 ms/iter  19.05 ms ██                   
                      (12.51 ms … 29.15 ms)  27.53 ms ███                  
                    (  5.60 mb …  10.13 mb)   7.25 mb █████▁▄█▁▄▄▁▁▄▁▁▄▄▄▄█

js small (experiment)         14.88 ms/iter  15.59 ms    ██                
                      (12.94 ms … 20.60 ms)  19.16 ms  █▅██▅▅  ▅           
                    (  6.41 mb …   8.24 mb)   6.83 mb ████████▅█▅▁██▁▁▅▁▁▁▅

                             ┌                                            ┐
                             ╷ ┌──────────┬──────┐                        ╷
          js small (control) ├─┤          │      ├────────────────────────┤
                             ╵ └──────────┴──────┘                        ╵
                              ╷  ┌──┬─┐          ╷
       js small (experiment)  ├──┤  │ ├──────────┤
                              ╵  └──┴─┘          ╵
                             └                                            ┘
                             12.51 ms           20.02 ms           27.53 ms

summary
  js small (experiment)
   1.14x faster than js small (control)

------------------------------------------- -------------------------------
js medium (control)            7.84 ms/iter   8.24 ms  █                   
                       (6.72 ms … 13.93 ms)  12.86 ms ▇█▆                  
                    (  2.99 mb …   5.14 mb)   3.56 mb ███▆▄▇▄▃▂▂▂▂▁▂▂▁▂▁▂▂▂

js medium (experiment)         7.48 ms/iter   7.62 ms  █                   
                       (6.58 ms … 12.90 ms)  11.23 ms ▂█▅                  
                    (  2.81 mb …   4.55 mb)   3.51 mb ███▄▆▃▃▃▂▄▂▃▁▁▃▂▁▂▂▁▂

                             ┌                                            ┐
                              ╷ ┌─────┬──┐                                ╷
         js medium (control)  ├─┤     │  ├────────────────────────────────┤
                              ╵ └─────┴──┘                                ╵
                             ╷┌────┬┐                         ╷
      js medium (experiment) ├┤    │├─────────────────────────┤
                             ╵└────┴┘                         ╵
                             └                                            ┘
                             6.58 ms            9.72 ms            12.86 ms

summary
  js medium (experiment)
   1.05x faster than js medium (control)

------------------------------------------- -------------------------------
js large (control)             3.30 ms/iter   3.22 ms  █                   
                        (2.47 ms … 9.47 ms)   8.13 ms  █                   
                    (320.09 kb …   2.90 mb)   1.45 mb ▄█▅▄▂▂▂▂▁▁▁▂▂▂▁▁▁▁▁▁▁

js large (experiment)          3.10 ms/iter   2.98 ms  █                   
                        (2.61 ms … 7.47 ms)   5.96 ms  █                   
                    (189.23 kb …   2.52 mb)   1.43 mb ▅█▆▂▃▂▂▂▂▂▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷ ┌────┬                                     ╷
          js large (control) ├─┤    │─────────────────────────────────────┤
                             ╵ └────┴                                     ╵
                              ╷┌──┬                      ╷
       js large (experiment)  ├┤  │──────────────────────┤
                              ╵└──┴                      ╵
                             └                                            ┘
                             2.47 ms            5.30 ms             8.13 ms

summary
  js large (experiment)
   1.07x faster than js large (control)

------------------------------------------- -------------------------------
gjs small (control)            1.39 ms/iter   1.32 ms █                    
                        (1.19 ms … 6.12 ms)   5.47 ms █▃                   
                    (581.30 kb …   2.15 mb)   1.06 mb ██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs small (experiment)         1.37 ms/iter   1.30 ms █                    
                        (1.19 ms … 5.99 ms)   4.36 ms █▅                   
                    (216.29 kb …   1.76 mb)   1.06 mb ██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌─┬                                          ╷
         gjs small (control) │ │──────────────────────────────────────────┤
                             └─┴                                          ╵
                             ┌─┬                              ╷
      gjs small (experiment) │ │──────────────────────────────┤
                             └─┴                              ╵
                             └                                            ┘
                             1.19 ms            3.33 ms             5.47 ms

summary
  gjs small (experiment)
   1.01x faster than gjs small (control)

------------------------------------------- -------------------------------
gjs medium (control)         667.70 µs/iter 639.63 µs  █                   
                      (584.96 µs … 5.31 ms)   1.49 ms ▄█                   
                    (568.00  b …   1.20 mb) 542.15 kb ██▇▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs medium (experiment)      666.60 µs/iter 636.35 µs █▂                   
                      (589.25 µs … 6.04 ms)   1.86 ms ██                   
                    (165.52 kb …   1.10 mb) 541.11 kb ██▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷┌─┬                            ╷
        gjs medium (control) ├┤ │────────────────────────────┤
                             ╵└─┴                            ╵
                             ╷┌─┬                                         ╷
     gjs medium (experiment) ├┤ │─────────────────────────────────────────┤
                             ╵└─┴                                         ╵
                             └                                            ┘
                             584.96 µs           1.22 ms            1.86 ms

summary
  gjs medium (experiment)
   1x faster than gjs medium (control)

------------------------------------------- -------------------------------
gjs large (control)          266.68 µs/iter 260.62 µs  █                   
                      (233.08 µs … 5.43 ms) 381.42 µs ▂█▄                  
                    (176.24 kb … 777.60 kb) 217.18 kb ███▃█▆▃▁▂▁▁▁▁▁▁▁▁▁▁▁▁

gjs large (experiment)       266.65 µs/iter 261.35 µs  █                   
                      (232.95 µs … 4.77 ms) 366.96 µs  ██                  
                    ( 16.10 kb … 692.81 kb) 216.68 kb ▄██▅▆▇▅▂▂▂▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷ ┌───────┬                                  ╷
         gjs large (control) ├─┤       │──────────────────────────────────┤
                             ╵ └───────┴                                  ╵
                             ╷ ┌───────┬                              ╷
      gjs large (experiment) ├─┤       │──────────────────────────────┤
                             ╵ └───────┴                              ╵
                             └                                            ┘
                             232.95 µs         307.18 µs          381.42 µs

summary
  gjs large (experiment)
   1x faster than gjs large (control)

------------------------------------------- -------------------------------
gts small (control)            1.32 ms/iter   1.26 ms █                    
                        (1.19 ms … 5.94 ms)   4.55 ms █                    
                    (165.94 kb …   1.91 mb)   1.06 mb █▄▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts small (experiment)         1.32 ms/iter   1.25 ms █                    
                        (1.19 ms … 6.19 ms)   4.84 ms █                    
                    (536.82 kb …   1.63 mb)   1.05 mb █▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌─┬                                       ╷
         gts small (control) │ │───────────────────────────────────────┤
                             └─┴                                       ╵
                             ┌─┬                                          ╷
      gts small (experiment) │ │──────────────────────────────────────────┤
                             └─┴                                          ╵
                             └                                            ┘
                             1.19 ms            3.01 ms             4.84 ms

summary
  gts small (experiment)
   1x faster than gts small (control)

------------------------------------------- -------------------------------
gts medium (control)         658.63 µs/iter 631.46 µs  █                   
                      (586.17 µs … 5.10 ms)   1.31 ms  █                   
                    ( 89.16 kb … 999.98 kb) 541.58 kb ██▅▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts medium (experiment)      663.50 µs/iter 634.65 µs █▅                   
                      (588.87 µs … 5.39 ms)   1.78 ms ██                   
                    (245.84 kb …   1.20 mb) 540.97 kb ██▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷┌─┬                       ╷
        gts medium (control) ├┤ │───────────────────────┤
                             ╵└─┴                       ╵
                             ╷┌─┬                                         ╷
     gts medium (experiment) ├┤ │─────────────────────────────────────────┤
                             ╵└─┴                                         ╵
                             └                                            ┘
                             586.17 µs           1.18 ms            1.78 ms

summary
  gts medium (control)
   1.01x faster than gts medium (experiment)

------------------------------------------- -------------------------------
gts large (control)          272.31 µs/iter 262.59 µs ▆█                   
                      (235.11 µs … 5.04 ms) 458.28 µs ██ ▃                 
                    (192.56 kb … 777.27 kb) 216.87 kb ████▄▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts large (experiment)       274.14 µs/iter 263.31 µs ▄█                   
                      (235.50 µs … 5.71 ms) 470.70 µs ██                   
                    (215.70 kb … 769.77 kb) 216.72 kb ████▃▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷┌─────┬                                   ╷
         gts large (control) ├┤     │───────────────────────────────────┤
                             ╵└─────┴                                   ╵
                             ╷┌─────┬                                     ╷
      gts large (experiment) ├┤     │─────────────────────────────────────┤
                             ╵└─────┴                                     ╵
                             └                                            ┘
                             235.11 µs         352.91 µs          470.70 µs

summary
  gts large (control)
   1.01x faster than gts large (experiment)

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Updates ember/template-require-iframe-title to more accurately reject non-string “title” literals (null/undefined/boolean/number) and introduces an option to allow whitespace-only static titles while keeping the default behavior strict.

Changes:

  • Reject additional Glimmer literal AST node types for title={{...}} and title="{{...}}".
  • Add allowWhitespaceOnlyTitle rule option to permit whitespace-only static titles.
  • Extend documentation and tests, plus add a non-CI “peer parity” audit fixture.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
tests/lib/rules/template-require-iframe-title.js Adds test coverage for new invalid literals and the whitespace-only option behavior.
tests/audit/iframe-title/peer-parity.js Adds an offline audit fixture encoding current behavior vs peer plugins.
lib/rules/template-require-iframe-title.js Implements isInvalidTitleLiteral() and the allowWhitespaceOnlyTitle option + updated checks.
docs/rules/template-require-iframe-title.md Documents new option and clarifies rule rationale and references.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/rules/template-require-iframe-title.js Outdated
Comment thread lib/rules/template-require-iframe-title.js Outdated
Comment thread lib/rules/template-require-iframe-title.js Outdated
Comment thread lib/rules/template-require-iframe-title.js Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/rules/template-require-iframe-title.js
Comment thread lib/rules/template-require-iframe-title.js Outdated
Comment thread docs/rules/template-require-iframe-title.md Outdated
Comment thread tests/audit/iframe-title/peer-parity.js Outdated
Comment thread tests/lib/rules/template-require-iframe-title.js
@johanrd johanrd force-pushed the fix/iframe-title-value-checks branch from 06219b7 to 1fd8b3b Compare April 22, 2026 17:08
@johanrd johanrd requested a review from Copilot April 24, 2026 08:36
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/rules/template-require-iframe-title.js Outdated
Comment thread tests/lib/rules/template-require-iframe-title.js Outdated
Comment thread docs/rules/template-require-iframe-title.md
Comment thread tests/audit/iframe-title/peer-parity.js Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/rules/template-require-iframe-title.js Outdated
Comment thread tests/lib/rules/template-require-iframe-title.js Outdated
Comment thread tests/audit/iframe-title/peer-parity.js Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@johanrd
Copy link
Copy Markdown
Owner Author

johanrd commented Apr 24, 2026

@copilot can you check all references to wcag and w3 to check if the actual spec says what the inline reference / paragraph says? bonus points for adding precise link to the section in the spec

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

The rule flagged yield-only templates by asserting
`templateNodes.length === 1 && isYieldOnly(templateNodes[0])`. That
works only while the parser strips template comments out of the body,
which ember-estree 0.4.2 happened to do. Upstream ember-estree 0.4.3
(#31) began keeping MustacheCommentStatement nodes in the body, so a
template like

    <template>{{! some comment }}{{yield}}</template>

now yields a body of length 2 and silently stops being flagged — which
is also why PR ember-cli#2735 (upgrade to ember-eslint-parser 0.11) fails this
rule's CI.

Ignoring comment and whitespace-only text nodes before the length check
aligns with:

- upstream ember-template-lint's `no-yield-only.js` (unchanged there
  since comments-in-body is the native Glimmer AST shape anyway)
- the sibling rule `template-no-bare-yield.js`, which already uses the
  same `isEmptyNode` filter

The fix is a no-op on the currently-pinned [email protected] and
fixes the rule under any parser version that preserves comment nodes in
the template body. The rule also now correctly catches yield-only
templates that contain HTML comments (`<!-- x -->{{yield}}`), which it
was silently missing before.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

…only-filter-comments

fix: ignore comment + whitespace nodes in template-no-yield-only
@johanrd johanrd requested a review from Copilot April 25, 2026 04:40
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

… add allowWhitespaceOnlyTitle opt-out

Treat literal AST values that don't produce a meaningful accessible name
as invalid `<iframe title>` values:

- `GlimmerBooleanLiteral` / `GlimmerNullLiteral` / `GlimmerUndefinedLiteral` /
  `GlimmerNumberLiteral` → flagged with a new `invalidTitleLiteral` messageId.
  Coerce-to-string runtime values like "true" / "null" / "42" don't
  describe the frame contents, regardless of framework behavior.
- `GlimmerStringLiteral` resolving to empty / whitespace → flagged as
  `emptyTitle` (resolution shared with the text-node case via a
  `processStaticTitle` helper). Closes a bypass that jsx-a11y already
  catches via `getLiteralPropValue`.

Both literal classes apply to the bare-mustache `title={{x}}` form AND
the single-part concat `title="{{x}}"` form.

Whitespace-only static title (`title="   "`) is now opt-out via a new
`allowWhitespaceOnlyTitle: true` schema option. ACCNAME 1.2 §4.3.2 step
2I (Tooltip) does not whitespace-trim — so a 3-space accessible name is
spec-assigned. The check stays on by default as authoring hygiene; teams
that want strict spec parity can opt out. Empty-string `title=""` and
the non-string-literal cases above are not affected by this option —
they are always flagged as correctness.

Refs:
- WCAG SC 4.1.2: https://www.w3.org/TR/UNDERSTANDING-WCAG20/ensure-compat-rsv.html
- ACCNAME 1.2 §4.3.2: https://www.w3.org/TR/accname-1.2/#computation-steps
@johanrd johanrd force-pushed the fix/iframe-title-value-checks branch from 5dd3c75 to 19e44c4 Compare April 25, 2026 06:15
@johanrd johanrd requested a review from Copilot April 25, 2026 13:49
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

…tle option

Whitespace-only titles are never intentional and the option added API
surface with no real-world use case. Always flag empty/whitespace title
values as errors.
@johanrd johanrd closed this Apr 25, 2026
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.

3 participants