Skip to content

feat(linter/jsx-a11y): implement no-noninteractive-element-to-interactive-role#21264

Merged
camchenry merged 11 commits into
oxc-project:mainfrom
pedrotainha:feat/jsx-a11y-no-noninteractive-element-to-interactive-role
May 5, 2026
Merged

feat(linter/jsx-a11y): implement no-noninteractive-element-to-interactive-role#21264
camchenry merged 11 commits into
oxc-project:mainfrom
pedrotainha:feat/jsx-a11y-no-noninteractive-element-to-interactive-role

Conversation

@pedrotainha

Copy link
Copy Markdown
Contributor

Summary

Implements the jsx-a11y/no-noninteractive-element-to-interactive-role rule, which enforces that non-interactive HTML elements (e.g., <h1>, <li>, <article>) are not assigned interactive ARIA roles (e.g., button, link, menuitem).

Related umbrella: #1141

Implementation details

  • Uses the shared is_non_interactive_element and is_interactive_role utilities from crate::utils::react
  • Supports configurable allowed role overrides per element (e.g., { "ul": ["menu", "tablist"] })
  • Default config matches the eslint-plugin-jsx-a11y recommended config
  • Tests ported from the original eslint-plugin-jsx-a11y test suite, covering both recommended (with allowed overrides) and strict (no overrides) modes

Note on INTERACTIVE_ROLES gap

While porting the tests, I noticed that the shared INTERACTIVE_ROLES list in utils/react.rs is missing grid, tablist, tree, and treegrid — all of which are interactive widget roles per WAI-ARIA and are treated as interactive by the original eslint-plugin-jsx-a11y.

The existing no_noninteractive_tabindex rule works around this with its own INTERACTIVE_HTML_ROLES constant (29 entries, including these 4).

For this PR, I added a local ADDITIONAL_INTERACTIVE_ROLES constant to align with the original rule's behavior without modifying shared utilities.

Question for maintainers: Would it make sense to open a separate issue/PR to add these 4 roles to the shared INTERACTIVE_ROLES list? That would bring it in line with the WAI-ARIA spec and the original eslint-plugin-jsx-a11y, and allow no_noninteractive_tabindex to drop its local override as well.

AI Disclosure

This implementation was assisted by Claude Code (Opus 4.6). All code has been personally reviewed, tested, and validated against the original rule's logic and test suite.

Test plan

  • cargo lintgen — regenerates correctly (719 variants)
  • cargo test -p oxc_linter -- no_noninteractive_element_to_interactive_role — all tests pass
  • Snapshot generated and validated
  • All test cases from the original eslint-plugin-jsx-a11y ported (recommended + strict modes)

@pedrotainha pedrotainha requested a review from camc314 as a code owner April 9, 2026 14:43
@github-actions github-actions Bot added A-linter Area - Linter C-enhancement Category - New feature or request labels Apr 9, 2026
@camc314 camc314 changed the title feat(linter): implement jsx-a11y/no-noninteractive-element-to-interactive-role feat(linter/jsx-a11y): implement no-noninteractive-element-to-interactive-role Apr 9, 2026
@camc314 camc314 self-assigned this Apr 9, 2026
camc314
camc314 previously requested changes Apr 9, 2026

@camc314 camc314 left a comment

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.

hi @pedrotainha, thanks for this PR - CI is failing, please take a look.

@pedrotainha

Copy link
Copy Markdown
Contributor Author

hi @pedrotainha, thanks for this PR - CI is failing, please take a look.

Trying to replicate the pipeline checks in my local environment to prevent these failures at pr creation. Thanks

@pedrotainha pedrotainha force-pushed the feat/jsx-a11y-no-noninteractive-element-to-interactive-role branch from 95c9e53 to 8abe0ac Compare April 9, 2026 15:15
@pedrotainha pedrotainha marked this pull request as draft April 9, 2026 15:23
@pedrotainha pedrotainha force-pushed the feat/jsx-a11y-no-noninteractive-element-to-interactive-role branch 2 times, most recently from a229612 to a4865a7 Compare April 9, 2026 17:03
@pedrotainha pedrotainha marked this pull request as ready for review April 9, 2026 17:03
@pedrotainha

Copy link
Copy Markdown
Contributor Author

hi @pedrotainha, thanks for this PR - CI is failing, please take a look.

Meanwhile, as stated in the pr body:

"Question for maintainers: Would it make sense to open a separate issue/PR to add these 4 roles to the shared INTERACTIVE_ROLES list? That would bring it in line with the WAI-ARIA spec and the original eslint-plugin-jsx-a11y, and allow no_noninteractive_tabindex to drop its local override as well."

Does this make sense for you? I can open an issue and PR after this PR

@pedrotainha pedrotainha force-pushed the feat/jsx-a11y-no-noninteractive-element-to-interactive-role branch 2 times, most recently from 6affe24 to 5e53aa5 Compare April 10, 2026 17:33
@pedrotainha

Copy link
Copy Markdown
Contributor Author

@camc314 can you approve the pipeline to run the checks?

@codspeed-hq

codspeed-hq Bot commented Apr 10, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 4 untouched benchmarks
⏩ 47 skipped benchmarks1


Comparing pedrotainha:feat/jsx-a11y-no-noninteractive-element-to-interactive-role (b46e3d3) with main (fbb8f22)

Open in CodSpeed

Footnotes

  1. 47 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.

@pedrotainha pedrotainha force-pushed the feat/jsx-a11y-no-noninteractive-element-to-interactive-role branch from 5e53aa5 to d632093 Compare April 10, 2026 22:28
@pedrotainha

Copy link
Copy Markdown
Contributor Author

@camc314 now fixed in last commit

@pedrotainha pedrotainha force-pushed the feat/jsx-a11y-no-noninteractive-element-to-interactive-role branch 2 times, most recently from 02a734b to f5f7109 Compare April 12, 2026 21:33
pedrotainha and others added 6 commits April 13, 2026 14:55
…tive-role

Implements the `jsx-a11y/no-noninteractive-element-to-interactive-role` rule
which enforces that non-interactive HTML elements are not assigned interactive
ARIA roles.

Uses the shared `is_non_interactive_element` and `is_interactive_role` utilities.
Adds `grid`, `tablist`, `tree`, `treegrid` as additional interactive roles to
align with the original eslint-plugin-jsx-a11y rule and the WAI-ARIA spec.

Supports configurable allowed role overrides per element. Default config matches
the eslint-plugin-jsx-a11y recommended config (e.g., `ul` allows `menu`,
`tablist`, etc.).

Tests ported from the original eslint-plugin-jsx-a11y test suite, covering both
recommended (with allowed overrides) and strict (no overrides) modes.

Closes oxc-project#21195

AI Disclosure: This implementation was assisted by Claude Code (Opus 4.6).
All code has been personally reviewed, tested, and validated against the
original rule's logic and test suite.
…ent-to-interactive-role

- Use oxc_str::CompactStr instead of oxc_span::CompactStr (private in MSRV 1.92.0)
- Add explicit type annotation in closure for MSRV compatibility
…fy comment

Revert the .DS_Store .gitignore addition (belongs in a separate PR).
Reformulate the ADDITIONAL_INTERACTIVE_ROLES comment to remove the
misleading mention of progressbar.
… for no-noninteractive-element-to-interactive-role

Collapse nested if into single let-chain to satisfy clippy::collapsible_if.
Add Deserialize + JsonSchema derives and config declaration to fix
rule_configuration_documentation_test.
@pedrotainha pedrotainha force-pushed the feat/jsx-a11y-no-noninteractive-element-to-interactive-role branch from f5f7109 to 337c1e6 Compare April 13, 2026 13:55
@pedrotainha

pedrotainha commented Apr 13, 2026

Copy link
Copy Markdown
Contributor Author

@camc314 can you approve the pipeline to run the checks?

Hello, any chance of approving again the checks please?

@pedrotainha pedrotainha requested a review from camc314 April 13, 2026 16:28
@pedrotainha

Copy link
Copy Markdown
Contributor Author

@camc314 do you have time to review this PR? I set a goal for myself to contribute to the a11y rules.
Thanks

@camchenry camchenry self-assigned this Apr 23, 2026
@pedrotainha

Copy link
Copy Markdown
Contributor Author

@camchenry the pending checks take longer time? Or does it need some action/approval?

@camchenry

camchenry commented Apr 30, 2026

Copy link
Copy Markdown
Member

Question for maintainers: Would it make sense to open a separate issue/PR to add these 4 roles to the shared INTERACTIVE_ROLES list? That would bring it in line with the WAI-ARIA spec and the original eslint-plugin-jsx-a11y, and allow no_noninteractive_tabindex to drop its local override as well.

yes, this sounds like a good idea to me. if there's anything missing that is in the ARIA spec, then we should implement it in oxlint.

@camchenry

Copy link
Copy Markdown
Member

@camchenry the pending checks take longer time? Or does it need some action/approval?

I think it needs a rebase, but it should be fine.

@camchenry

camchenry commented Apr 30, 2026

Copy link
Copy Markdown
Member

@pedrotainha actually, this needs a rebase. it will require a few small fixes like adding version = "next" to the lint rule declaration. do you mind doing that? overall I think this looks pretty good though in terms of implementation

@mehm8128

Copy link
Copy Markdown
Contributor

Hi, I added these roles to INTERACTIVE_ROLES list in #21767, so once you rebase main branch to this PR, you'll be able to it without using ADDITIONAL_INTERACTIVE_ROLES

Question for maintainers: Would it make sense to open a separate issue/PR to add these 4 roles to the shared INTERACTIVE_ROLES list? That would bring it in line with the WAI-ARIA spec and the original eslint-plugin-jsx-a11y, and allow no_noninteractive_tabindex to drop its local override as well.

camchenry and others added 4 commits May 5, 2026 08:13
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
@camchenry

Copy link
Copy Markdown
Member

Hi, I added these roles to INTERACTIVE_ROLES list in #21767, so once you rebase main branch to this PR, you'll be able to it without using ADDITIONAL_INTERACTIVE_ROLES

Question for maintainers: Would it make sense to open a separate issue/PR to add these 4 roles to the shared INTERACTIVE_ROLES list? That would bring it in line with the WAI-ARIA spec and the original eslint-plugin-jsx-a11y, and allow no_noninteractive_tabindex to drop its local override as well.

Removed this in 50b61c3

@camchenry camchenry added the 0-merge Merge with Graphite Merge Queue label May 5, 2026
@graphite-app

graphite-app Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Merge activity

  • May 5, 1:04 PM UTC: @pedrotainha we removed the merge queue label because we could not find a Graphite account associated with your GitHub profile.

You must have a Graphite account in order to use the merge queue. Create an account and try again using this link

@graphite-app graphite-app Bot removed the 0-merge Merge with Graphite Merge Queue label May 5, 2026
@camchenry camchenry merged commit 23abd22 into oxc-project:main May 5, 2026
27 checks passed
@pedrotainha

Copy link
Copy Markdown
Contributor Author

@pedrotainha actually, this needs a rebase. it will require a few small fixes like adding version = "next" to the lint rule declaration. do you mind doing that? overall I think this looks pretty good though in terms of implementation

Sorry, only get back to open source now again. I've been busy at work managing other priorities.
Thanks for moving forward

@camchenry

Copy link
Copy Markdown
Member

@pedrotainha actually, this needs a rebase. it will require a few small fixes like adding version = "next" to the lint rule declaration. do you mind doing that? overall I think this looks pretty good though in terms of implementation

Sorry, only get back to open source now again. I've been busy at work managing other priorities. Thanks for moving forward

No worries, thank you for contributing!

camc314 added a commit that referenced this pull request May 11, 2026
# Oxlint
### 💥 BREAKING CHANGES

- 00ce512 oxlint/lsp: [**BREAKING**] Don't fix suggestions on fixAll
code actions & command (#22195) (Sysix)

### 🚀 Features

- 0eeceaf linter/no-unused-vars: Rename parameter with initializer
(#22308) (camc314)
- fa0232b linter/no-unused-vars: Add param rename suggestion (#22285)
(Ryota Misumi)
- ae59305 linter/promise/no-promise-in-callback: Add
`exemptDeclarations` option (#22275) (Mikhail Baev)
- 60bed4a linter: Extends `no-redundant-roles` and
`prefer-tag-over-role` support roles (#22069) (mehm8128)
- 545c80f linter/eslint: Implement `prefer-regex-literals` rule (#22192)
(Mikhail Baev)
- cf86d7a linter: Bulk suppression (#19328) (Said Atrahouch)
- 23abd22 linter/jsx-a11y: Implement
no-noninteractive-element-to-interactive-role (#21264) (Pedro Tainha)
- fbb8f22 linter: Support `ignores` in overrides (#22148) (camc314)
- 5a4414d oxlint/lsp: Support `rulesCustomization` lsp option (#21858)
(Sysix)

### 🐛 Bug Fixes

- 610f4c7 linter/no-unused-vars: Avoid renaming captured vars (#22310)
(camc314)
- 6b50f23 oxlint/cli: Load root config by searching up parent
directories (#22272) (Sysix)
- 31a5de7 linter: Rename override `ignores` to `excludeFiles` (#22283)
(camc314)
- 26d5d7b linter: Add missing vitest/valid-describe-callback
functionality (#22279) (camchenry)
- 784530f linter: `valid-title`: detect `String.raw` strings (#22271)
(Sysix)
- 080d90e linter: Move `no-debugger` fix to suggestion (#22256) (Sysix)
- 25b7017 linter: Undocument override `ignores` option (#22213)
(camc314)
- 7bb00dd linter: Fix role-has-required-aria-props (#22097) (mehm8128)
- d25279e linter/disable-directives: Improve parsing of names,
descriptions (#22184) (camc314)
- a59e447 linter/disable-directives: Ignore invalid enable suffixes
(#22179) (camc314)
- aafef0f ci: Disable bulk supression test on big endian (#22175)
(camc314)
- 281daec linter/vue/define-props-destructuring: Add
`only-when-assigned` config opt (#22142) (camc314)
- 46ab679 linter/plugins: Trim leading newline for partial sources
(#20928) (bab)
- 29ff6d9 linter: Update docs for no_alias_methods rule to be
Vitest-specific and add toThrowError alias (#22129) (camchenry)

### ⚡ Performance

- 9414bee linter/role-has-required-aria-props: Avoid intermediate vec
(#22212) (camc314)
- 3883ea3 linter/no-useless-escape: Drop unnecessary Vec collect
(#22171) (connorshea)
- 42c3029 linter/check-property-names: Replace split-collect-pop-join
with rfind (#22172) (connorshea)
- 9551d53 linter: Remove unnecessary Vec collect in CFG edge traversal
(#22167) (connorshea)
- 26fa2fc linter/aria-role: Remove unnecessary string allocations in run
method (#22168) (connorshea)
- c9ce045 linter/getter-return: Remove unnecessary Vec collect in CFG
edge traversal (#22166) (connorshea)
- 72bd846 linter/no-this-in-sfc: Reorder cheap name check, avoid String
allocation (#22164) (connorshea)

### 📚 Documentation

- 4da212a linter/no-unused-vars: Add docs to
`rename_unused_function_parameter` (#22311) (camc314)
- 27c4628 linter/forbid-dom-props: Escape jsx examples in lint rule docs
(#22254) (4MBL)
- 3f81147 linter: Improve the `react/jsx-key` rule docs. (#22162)
(connorshea)
- 07f03cc linter/consistent-return: Add note about `noImplicitReturns`
coverage (#22156) (camc314)
- 7c1e049 oxlint/lsp: Improve autogenerated lsp docs (#22154) (Sysix)
- 87b3e38 linter: Update docs to be vitest-specific for
consistent-test-it (#22128) (camchenry)
# Oxfmt
### 💥 BREAKING CHANGES

- 5c6c390 oxfmt: [**BREAKING**] Respect more git ignore options, align
with Oxlint (#22210) (leaysgur)

### 🚀 Features

- 6e8e818 oxfmt: Experimental .svelte support (#21700) (leaysgur)

### 🐛 Bug Fixes

- e2a20b6 formatter: Add space after commas in import attributes
(#22274) (Leonabcd123)

### ⚡ Performance

- b756682 oxfmt: Optimize nested config prescan (#22232) (Jovi De
Croock)
- f14e81e formatter/sort_imports: Skip sort for single import runs
(#22204) (leaysgur)
- 32255b1 formatter: Process `ImportDeclaration`s in a run (#22079)
(overlookmotel)

### 📚 Documentation

- 4da6f4c formatter: Correct comment (#22217) (overlookmotel)
- ef3507d formatter/sort_imports: Refresh docs (#22203) (leaysgur)

Co-authored-by: Cameron <cameron.clark@hey.com>
camchenry pushed a commit that referenced this pull request May 12, 2026
…2337)

## Summary

Adds the `jsx-a11y/no-noninteractive-element-interactions` rule to
oxlint.

This rule reports mouse/keyboard event handlers on non-interactive
elements or elements with non-interactive ARIA roles, while respecting
interactive elements, interactive/abstract/presentation roles, hidden
content, `contentEditable`, custom component mappings, and handler
exception configuration.

## Notes

- Test coverage is based on the upstream `eslint-plugin-jsx-a11y` rule
fixtures.
- There is an earlier open PR for this same rule at
#17608; this PR is a fresh
implementation/review pass for that rule.
- This follows the structure and conventions from the related
`jsx-a11y/no-noninteractive-element-to-interactive-role` work in
#21264.
- The local test suite intentionally keeps `separator` and `progressbar`
behavior tied to Oxc's existing shared ARIA role utilities for now.
- Upstream expects `role="separator"` to report and `role="progressbar"`
to pass. Oxc's current shared role classification leads to the opposite
behavior for those two cases, so I would like maintainer guidance on
whether this rule should add rule-specific overrides or whether the
shared utilities should change.

## Testing

- `cargo +stable fmt --check`
- `cargo +stable test -p oxc_linter --
no_noninteractive_element_interactions`
- `cargo +stable insta pending-snapshots`
- `git diff --check`

## Disclosure

I used Pi and Codex with GPT-5.5 to help prepare these changes, and
manually reviewed everything before opening this PR.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-linter Area - Linter C-enhancement Category - New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants