From 068a866af64aa6a664014af822a97b846f16b500 Mon Sep 17 00:00:00 2001 From: Joel Hawksley Date: Wed, 22 Apr 2026 13:43:29 -0600 Subject: [PATCH 1/3] Implement `a11y-nested-interactive-elements` linter rule Implements the `a11y-nested-interactive-elements` rule from erblint-github's `NestedInteractiveElements`. This rule flags interactive elements (``, `') + }) +}) From c0bec7b56ac1978be85ecac3afd6a43ab963fc9b Mon Sep 17 00:00:00 2001 From: Joel Hawksley Date: Fri, 8 May 2026 14:45:26 -0600 Subject: [PATCH 2/3] Add a11y-nested-interactive-elements to rules README --- javascript/packages/linter/docs/rules/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/javascript/packages/linter/docs/rules/README.md b/javascript/packages/linter/docs/rules/README.md index a506bccaf..cd0a211e3 100644 --- a/javascript/packages/linter/docs/rules/README.md +++ b/javascript/packages/linter/docs/rules/README.md @@ -6,6 +6,7 @@ This page contains documentation for all Herb Linter rules. #### Accessibility +- [`a11y-nested-interactive-elements`](./a11y-nested-interactive-elements.md) - Disallow nesting interactive elements - [`a11y-no-accesskey-attribute`](./a11y-no-accesskey-attribute.md) - Prevent usage of the `accesskey` attribute - [`a11y-no-aria-unsupported-elements`](./a11y-no-aria-unsupported-elements.md) - Prevent usage of ARIA on unsupported elements - [`a11y-no-autofocus-attribute`](./a11y-no-autofocus-attribute.md) - Prevent usage of the `autofocus` attribute From f18828d71140c38327ddd84a75b717d13253ef1e Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Sun, 7 Jun 2026 12:15:41 +0200 Subject: [PATCH 3/3] Cleanup --- .../rules/a11y-nested-interactive-elements.md | 3 +- .../rules/a11y-nested-interactive-elements.ts | 14 ++-- .../packages/linter/src/rules/rule-utils.ts | 7 ++ .../a11y-nested-interactive-elements.test.ts | 75 +++++++++++++++++++ 4 files changed, 91 insertions(+), 8 deletions(-) diff --git a/javascript/packages/linter/docs/rules/a11y-nested-interactive-elements.md b/javascript/packages/linter/docs/rules/a11y-nested-interactive-elements.md index 3870b0238..6958b1967 100644 --- a/javascript/packages/linter/docs/rules/a11y-nested-interactive-elements.md +++ b/javascript/packages/linter/docs/rules/a11y-nested-interactive-elements.md @@ -60,5 +60,6 @@ Nesting interactive elements produces invalid HTML, and assistive technologies, ## References - [erblint-github: NestedInteractiveElements](https://github.com/github/erblint-github/blob/main/lib/erblint-github/linters/github/accessibility/nested_interactive_elements.rb) -- [Deque University: nested-interactive](https://dequeuniversity.com/rules/axe/4.2/nested-interactive) +- [erblint-github docs](https://github.com/github/erblint-github/blob/main/docs/rules/accessibility/nested-interactive-elements.md) +- [Deque University: nested-interactive](https://dequeuniversity.com/rules/axe/4.8/nested-interactive) - [Accessibility Insights](https://accessibilityinsights.io/info-examples/web/nested-interactive/) diff --git a/javascript/packages/linter/src/rules/a11y-nested-interactive-elements.ts b/javascript/packages/linter/src/rules/a11y-nested-interactive-elements.ts index 23996ce3b..5e4be1e8b 100644 --- a/javascript/packages/linter/src/rules/a11y-nested-interactive-elements.ts +++ b/javascript/packages/linter/src/rules/a11y-nested-interactive-elements.ts @@ -38,16 +38,16 @@ class NestedInteractiveElementsVisitor extends ElementStackVisitor { } private findInteractiveAncestor(childTagName: string): string | null { - const ancestor = this.currentElement + for (const ancestor of this.ancestors) { + const ancestorTag = getTagLocalName(ancestor) - if (!ancestor) return null + if (!ancestorTag || !INTERACTIVE_ELEMENTS.has(ancestorTag)) continue + if (ancestorTag === "summary" && childTagName === "a") continue - const ancestorTag = getTagLocalName(ancestor) - if (!ancestorTag || !INTERACTIVE_ELEMENTS.has(ancestorTag)) return null - - if (ancestorTag === "summary" && childTagName === "a") return null + return ancestorTag + } - return ancestorTag + return null } } diff --git a/javascript/packages/linter/src/rules/rule-utils.ts b/javascript/packages/linter/src/rules/rule-utils.ts index 2e70e27e1..c585fb207 100644 --- a/javascript/packages/linter/src/rules/rule-utils.ts +++ b/javascript/packages/linter/src/rules/rule-utils.ts @@ -226,6 +226,13 @@ export abstract class ElementStackVisitor { assertOffenses('') }) + + test("fails for anchor deeply nested inside button", () => { + expectError('Found `` nested inside of `') + }) + + test("fails for input deeply nested inside anchor", () => { + expectError('Found `` nested inside of ``. Nesting interactive elements produces invalid HTML, and assistive technologies, such as screen readers, might ignore or respond unexpectedly to such nested controls.') + + assertOffenses('
') + }) + + test("fails for input with dynamic type inside button", () => { + expectError('Found `` nested inside of `') + }) + + test("passes for div with role=button containing anchor", () => { + expectNoOffenses('
Link
') + }) + + test("fails for tag.a nested inside tag.button", () => { + expectError('Found `` nested inside of `') + }) + + test("fails for link_to block with nested button", () => { + expectError('Found `<% end %>') + }) + + test("fails for link_to block with nested tag.button", () => { + expectError('Found `') + }) })