Skip to content

feat(html_analyze): implement noNoninteractiveElementToInteractiveRole#10022

Merged
Netail merged 2 commits intobiomejs:nextfrom
Netail:feat/port-no-noninteractive-element-to-interactive-role
Apr 17, 2026
Merged

feat(html_analyze): implement noNoninteractiveElementToInteractiveRole#10022
Netail merged 2 commits intobiomejs:nextfrom
Netail:feat/port-no-noninteractive-element-to-interactive-role

Conversation

@Netail
Copy link
Copy Markdown
Member

@Netail Netail commented Apr 17, 2026

Summary

Port noNoninteractiveElementToInteractiveRole over to HTML

Related #8155

Test Plan

Unit tests

Docs

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 17, 2026

🦋 Changeset detected

Latest commit: 419e9a6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 14 packages
Name Type
@biomejs/biome Minor
@biomejs/cli-win32-x64 Minor
@biomejs/cli-win32-arm64 Minor
@biomejs/cli-darwin-x64 Minor
@biomejs/cli-darwin-arm64 Minor
@biomejs/cli-linux-x64 Minor
@biomejs/cli-linux-arm64 Minor
@biomejs/cli-linux-x64-musl Minor
@biomejs/cli-linux-arm64-musl Minor
@biomejs/wasm-web Minor
@biomejs/wasm-bundler Minor
@biomejs/wasm-nodejs Minor
@biomejs/backend-jsonrpc Patch
@biomejs/js-api Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@Netail Netail mentioned this pull request Apr 17, 2026
32 tasks
@github-actions github-actions bot added A-Linter Area: linter L-HTML Language: HTML and super languages labels Apr 17, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 17, 2026

Walkthrough

Adds a new HTML accessibility lint rule noNoninteractiveElementToInteractiveRole that flags statically-known interactive ARIA roles applied to non-interactive HTML elements, skipping custom components and allowing treeitem on <li>. Reports target the role attribute range, suggest replacing with div/span, and provide an unsafe fix to remove the attribute. Includes a changeset and comprehensive tests (valid/invalid) for HTML, Astro, Vue and Svelte. Also adds is_custom_component to element utilities.

Possibly related PRs

Suggested labels

A-Parser

Suggested reviewers

  • dyc3
  • ematipico
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: implementing a new HTML lint rule for accessibility.
Description check ✅ Passed The description relates directly to the changeset, explaining the rule port and referencing the related issue with test plans included.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
crates/biome_html_analyze/tests/specs/a11y/noNoninteractiveElementToInteractiveRole/svelte/valid.svelte (1)

4-5: Add a PascalCase component + role valid case to lock in case-sensitivity.

You already cover custom components here, but not through the role code path. Adding one or two PascalCase examples with interactive roles would better guard against false positives in Svelte templates.

Suggested fixture addition
 <TestComponent onClick={doFoo} />
 <Button onClick={doFoo} />
+<TestComponent role="button" />
+<Button role="menuitem" />

Based on learnings: in .svelte/.vue/.astro, element-name checks should be case-sensitive so PascalCase components are not treated as native HTML elements.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@crates/biome_html_analyze/tests/specs/a11y/noNoninteractiveElementToInteractiveRole/svelte/valid.svelte`
around lines 4 - 5, Add PascalCase custom-component examples that use an
interactive ARIA role so the Svelte parser's role-based path is exercised and
component names aren't mistaken for native elements; specifically, add one or
two entries like a <TestComponent ... role="button" ...> and/or <Button ...
role="switch" ...> variant (using Svelte on:click or onClick as in existing
fixtures) to the valid.svelte spec so the rule treats PascalCase names
case-sensitively and does not flag them as non-interactive native elements in
the role code path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.changeset/major-bats-fail.md:
- Line 5: Update the changeset sentence that describes the new HTML lint rule by
fixing subject-verb agreement: in the line mentioning the rule name
noNoninteractiveElementToInteractiveRole, change “which enforce” to “which
enforces” so the user-facing copy reads correctly.

In
`@crates/biome_html_analyze/src/lint/a11y/no_noninteractive_element_to_interactive_role.rs`:
- Around line 14-18: The rule documentation in
no_noninteractive_element_to_interactive_role.rs incorrectly lists <area> as a
non-interactive element while the new allow-list permits <area role="button"/>
and <area role="menuitem"/>; update the doc comment (the top-level comment in
the no_noninteractive_element_to_interactive_role module) to remove <area> from
the non-interactive examples, or alternatively tighten the rule/tests (the
no_noninteractive_element_to_interactive_role lint logic and its fixtures) so
that <area> is not allowed—preferably remove <area> from the sentence so the
documentation matches the current allow-list and adjust any example
fixtures/comments accordingly.
- Around line 71-97: The rule currently compares element_name.text()
case-sensitively which misses HTML case-insensitivity; when
source_type.is_html() normalize the element name for comparisons (e.g. let
element_name_text = if source_type.is_html() {
element_name.text().to_ascii_lowercase() } else {
element_name.text().to_string() }) and then use element_name_text for the li
exemption and the ROLE_SENSITIVE_ELEMENTS check (replace element_name.text()
uses in the li branch and ROLE_SENSITIVE_ELEMENTS.contains(...) call), leaving
behavior unchanged for Astro/Vue/Svelte; update checks around
source_type.is_html(), element_name.text(), ROLE_SENSITIVE_ELEMENTS,
ctx.aria_roles().is_not_interactive_element and AriaRole::from_roles to use the
normalized value in HTML files and add uppercase-HTML fixtures to cover the
case.

---

Nitpick comments:
In
`@crates/biome_html_analyze/tests/specs/a11y/noNoninteractiveElementToInteractiveRole/svelte/valid.svelte`:
- Around line 4-5: Add PascalCase custom-component examples that use an
interactive ARIA role so the Svelte parser's role-based path is exercised and
component names aren't mistaken for native elements; specifically, add one or
two entries like a <TestComponent ... role="button" ...> and/or <Button ...
role="switch" ...> variant (using Svelte on:click or onClick as in existing
fixtures) to the valid.svelte spec so the rule treats PascalCase names
case-sensitively and does not flag them as non-interactive native elements in
the role code path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e276b173-6f37-46bc-8de8-6541a4d1de2d

📥 Commits

Reviewing files that changed from the base of the PR and between afd57a6 and 4b8ac44.

⛔ Files ignored due to path filters (8)
  • crates/biome_html_analyze/tests/specs/a11y/noNoninteractiveElementToInteractiveRole/astro/invalid.astro.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/noNoninteractiveElementToInteractiveRole/astro/valid.astro.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/noNoninteractiveElementToInteractiveRole/invalid.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/noNoninteractiveElementToInteractiveRole/svelte/invalid.svelte.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/noNoninteractiveElementToInteractiveRole/svelte/valid.svelte.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/noNoninteractiveElementToInteractiveRole/valid.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/noNoninteractiveElementToInteractiveRole/vue/invalid.vue.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/noNoninteractiveElementToInteractiveRole/vue/valid.vue.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (10)
  • .changeset/major-bats-fail.md
  • crates/biome_html_analyze/src/lint/a11y/no_noninteractive_element_to_interactive_role.rs
  • crates/biome_html_analyze/tests/specs/a11y/noNoninteractiveElementToInteractiveRole/astro/invalid.astro
  • crates/biome_html_analyze/tests/specs/a11y/noNoninteractiveElementToInteractiveRole/astro/valid.astro
  • crates/biome_html_analyze/tests/specs/a11y/noNoninteractiveElementToInteractiveRole/invalid.html
  • crates/biome_html_analyze/tests/specs/a11y/noNoninteractiveElementToInteractiveRole/svelte/invalid.svelte
  • crates/biome_html_analyze/tests/specs/a11y/noNoninteractiveElementToInteractiveRole/svelte/valid.svelte
  • crates/biome_html_analyze/tests/specs/a11y/noNoninteractiveElementToInteractiveRole/valid.html
  • crates/biome_html_analyze/tests/specs/a11y/noNoninteractiveElementToInteractiveRole/vue/invalid.vue
  • crates/biome_html_analyze/tests/specs/a11y/noNoninteractiveElementToInteractiveRole/vue/valid.vue

Comment thread .changeset/major-bats-fail.md Outdated
Comment on lines +14 to +18
/// Non-interactive HTML elements indicate _content_ and _containers_ in the user interface.
/// Non-interactive elements include `<main>`, `<area>`, `<h1>` (,`<h2>`, etc), `<img>`, `<li>`, `<ul>` and `<ol>`.
///
/// Interactive HTML elements indicate _controls_ in the user interface.
/// Interactive elements include `<a href>`, `<button>`, `<input>`, `<select>`, `<textarea>`.
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.

⚠️ Potential issue | 🟡 Minor

Keep the rule docs aligned with the new allow-list.

This text calls out <area> as non-interactive, but the new valid fixtures explicitly allow <area role="button" /> and <area role="menuitem" />. Please either drop <area> from this sentence or tighten the implementation/tests so the docs stay truthful.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@crates/biome_html_analyze/src/lint/a11y/no_noninteractive_element_to_interactive_role.rs`
around lines 14 - 18, The rule documentation in
no_noninteractive_element_to_interactive_role.rs incorrectly lists <area> as a
non-interactive element while the new allow-list permits <area role="button"/>
and <area role="menuitem"/>; update the doc comment (the top-level comment in
the no_noninteractive_element_to_interactive_role module) to remove <area> from
the non-interactive examples, or alternatively tighten the rule/tests (the
no_noninteractive_element_to_interactive_role lint logic and its fixtures) so
that <area> is not allowed—preferably remove <area> from the sentence so the
documentation matches the current allow-list and adjust any example
fixtures/comments accordingly.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Because it has a role in the tests

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Apr 17, 2026

Merging this PR will not alter performance

✅ 67 untouched benchmarks
⏩ 189 skipped benchmarks1


Comparing Netail:feat/port-no-noninteractive-element-to-interactive-role (419e9a6) with next (afd57a6)

Open in CodSpeed

Footnotes

  1. 189 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Copy link
Copy Markdown
Member

@ematipico ematipico left a comment

Choose a reason for hiding this comment

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

Just need to address the bot suggestions. Good one

@github-actions github-actions bot added the A-Parser Area: parser label Apr 17, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
crates/biome_html_analyze/src/lint/a11y/no_noninteractive_element_to_interactive_role.rs (1)

14-16: ⚠️ Potential issue | 🟡 Minor

Align the docs with the implemented allow-list.

Line 15 still lists <area> as non-interactive, but crates/biome_html_analyze/tests/specs/a11y/noNoninteractiveElementToInteractiveRole/valid.html (Lines 6-7) treats <area role="button" /> and <area role="menuitem" /> as valid. Please update the sentence so docs match behaviour.

Suggested doc tweak
-/// Non-interactive elements include `<main>`, `<area>`, `<h1>` (,`<h2>`, etc), `<img>`, `<li>`, `<ul>` and `<ol>`.
+/// Non-interactive elements include `<main>`, `<h1>`, `<h2>`, `<img>`, `<li>`, `<ul>` and `<ol>`.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@crates/biome_html_analyze/src/lint/a11y/no_noninteractive_element_to_interactive_role.rs`
around lines 14 - 16, Update the module doc comment in
no_noninteractive_element_to_interactive_role.rs so it matches the implemented
allow-list: remove `<area>` from the list of non-interactive elements (or
explicitly note that `<area>` is allowed with certain interactive roles),
ensuring the sentence that lists examples (`<main>`, `<area>`, `<h1>`, `<img>`,
`<li>`, `<ul>`, `<ol>`) is changed to match the rule behavior; edit the
docstring above the lint implementation
(no_noninteractive_element_to_interactive_role) accordingly.
🧹 Nitpick comments (1)
crates/biome_html_analyze/src/lint/a11y/no_noninteractive_element_to_interactive_role.rs (1)

49-56: Add rule source metadata for this ESLint port.

Given this rule is a port, adding a sources entry will improve provenance and biome migrate eslint mapping.

As per coding guidelines: Use RuleSource::Eslint("rule-name").same() when porting an ESLint rule with matching behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@crates/biome_html_analyze/src/lint/a11y/no_noninteractive_element_to_interactive_role.rs`
around lines 49 - 56, The rule declaration for
NoNoninteractiveElementToInteractiveRole is missing ESLint provenance metadata;
add a sources field to the struct initialization using
RuleSource::Eslint("no-noninteractive-element-to-interactive-role").same() so
the port maps back to the original ESLint rule. Update the
NoNoninteractiveElementToInteractiveRole initializer to include sources:
vec![RuleSource::Eslint("no-noninteractive-element-to-interactive-role").same()],
making sure RuleSource is in scope.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@crates/biome_html_analyze/src/lint/a11y/no_noninteractive_element_to_interactive_role.rs`:
- Around line 14-16: Update the module doc comment in
no_noninteractive_element_to_interactive_role.rs so it matches the implemented
allow-list: remove `<area>` from the list of non-interactive elements (or
explicitly note that `<area>` is allowed with certain interactive roles),
ensuring the sentence that lists examples (`<main>`, `<area>`, `<h1>`, `<img>`,
`<li>`, `<ul>`, `<ol>`) is changed to match the rule behavior; edit the
docstring above the lint implementation
(no_noninteractive_element_to_interactive_role) accordingly.

---

Nitpick comments:
In
`@crates/biome_html_analyze/src/lint/a11y/no_noninteractive_element_to_interactive_role.rs`:
- Around line 49-56: The rule declaration for
NoNoninteractiveElementToInteractiveRole is missing ESLint provenance metadata;
add a sources field to the struct initialization using
RuleSource::Eslint("no-noninteractive-element-to-interactive-role").same() so
the port maps back to the original ESLint rule. Update the
NoNoninteractiveElementToInteractiveRole initializer to include sources:
vec![RuleSource::Eslint("no-noninteractive-element-to-interactive-role").same()],
making sure RuleSource is in scope.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 23d09f50-e536-4a07-888f-6343ec03cf12

📥 Commits

Reviewing files that changed from the base of the PR and between 4b8ac44 and 419e9a6.

⛔ Files ignored due to path filters (2)
  • crates/biome_html_analyze/tests/specs/a11y/noNoninteractiveElementToInteractiveRole/invalid.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/noNoninteractiveElementToInteractiveRole/valid.html.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (5)
  • .changeset/major-bats-fail.md
  • crates/biome_html_analyze/src/lint/a11y/no_noninteractive_element_to_interactive_role.rs
  • crates/biome_html_analyze/tests/specs/a11y/noNoninteractiveElementToInteractiveRole/invalid.html
  • crates/biome_html_analyze/tests/specs/a11y/noNoninteractiveElementToInteractiveRole/valid.html
  • crates/biome_html_syntax/src/element_ext.rs
✅ Files skipped from review due to trivial changes (1)
  • .changeset/major-bats-fail.md

@Netail Netail merged commit 3422d71 into biomejs:next Apr 17, 2026
29 checks passed
@Netail Netail deleted the feat/port-no-noninteractive-element-to-interactive-role branch April 17, 2026 19:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Linter Area: linter A-Parser Area: parser L-HTML Language: HTML and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants