feat(biome_html_analyze): port useAriaActivedescendantWithTabindex to HTML#9617
Conversation
🦋 Changeset detectedLatest commit: 1734a54 The changes in this PR will be included in the next version bump. This PR includes changesets to release 14 packages
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 |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds an HTML accessibility lint rule Suggested reviewers
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
crates/biome_html_analyze/src/lint/a11y/use_aria_activedescendant_with_tabindex.rs (1)
68-75: Minor: redundant element name extraction.
is_anchor_with_hrefre-extracts the element name, but the caller (run) has already computed it. You could pass the element name as a parameter to avoid the duplication.♻️ Proposed refactor
-fn is_anchor_with_href(element: &AnyHtmlElement) -> bool { - let name = element.name().ok(); - let name_text = name.as_ref().and_then(|n| n.value_token().ok()); - let Some(name_str) = name_text.as_ref().map(|t| t.text_trimmed()) else { - return false; - }; - name_str == "a" && element.find_attribute_by_name("href").is_some() +fn is_anchor_with_href(element: &AnyHtmlElement, element_name: &str) -> bool { + element_name == "a" && element.find_attribute_by_name("href").is_some() }Then update the call site at line 102:
if is_anchor_with_href(element, element_name) {🤖 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/use_aria_activedescendant_with_tabindex.rs` around lines 68 - 75, The helper is_anchor_with_href currently re-reads the element name redundantly; modify its signature to accept the precomputed element_name (e.g., change to is_anchor_with_href(element: &AnyHtmlElement, element_name: &str)) and update the caller in run to pass element_name (replace is_anchor_with_href(element) with is_anchor_with_href(element, element_name)); inside is_anchor_with_href, use the passed element_name to check equality to "a" and keep the existing href attribute presence check via element.find_attribute_by_name("href").is_some().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In
`@crates/biome_html_analyze/src/lint/a11y/use_aria_activedescendant_with_tabindex.rs`:
- Around line 68-75: The helper is_anchor_with_href currently re-reads the
element name redundantly; modify its signature to accept the precomputed
element_name (e.g., change to is_anchor_with_href(element: &AnyHtmlElement,
element_name: &str)) and update the caller in run to pass element_name (replace
is_anchor_with_href(element) with is_anchor_with_href(element, element_name));
inside is_anchor_with_href, use the passed element_name to check equality to "a"
and keep the existing href attribute presence check via
element.find_attribute_by_name("href").is_some().
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: e7f9f1ef-f507-4d28-a83e-41567b8f0537
📒 Files selected for processing (3)
crates/biome_html_analyze/src/lint/a11y/use_aria_activedescendant_with_tabindex.rscrates/biome_html_analyze/tests/specs/a11y/useAriaActivedescendantWithTabindex/invalid.htmlcrates/biome_html_analyze/tests/specs/a11y/useAriaActivedescendantWithTabindex/valid.html
| /// HTML elements that are natively interactive (focusable without tabindex). | ||
| const INTERACTIVE_ELEMENTS: &[&str] = &[ | ||
| "button", "input", "select", "textarea", | ||
| ]; | ||
|
|
||
| /// Check if the HTML element is natively interactive. | ||
| fn is_interactive_element(element_name: &str) -> bool { | ||
| INTERACTIVE_ELEMENTS.contains(&element_name) | ||
| } | ||
|
|
||
| /// Check if the element is an anchor with an href (which is natively focusable). | ||
| fn is_anchor_with_href(element: &AnyHtmlElement, element_name: &str) -> bool { | ||
| element_name == "a" && element.find_attribute_by_name("href").is_some() | ||
| } |
There was a problem hiding this comment.
nit: move all helpers below the impl Rule block
| const INTERACTIVE_ELEMENTS: &[&str] = &[ | ||
| "button", "input", "select", "textarea", | ||
| ]; | ||
|
|
||
| /// Check if the HTML element is natively interactive. | ||
| fn is_interactive_element(element_name: &str) -> bool { | ||
| INTERACTIVE_ELEMENTS.contains(&element_name) | ||
| } |
There was a problem hiding this comment.
this check must be case insensitive in HTML files, but case sensitive in HTML-ish template languages (vue, svelte, astro)
There was a problem hiding this comment.
@coderabbitai add this to your learnings: When checking element names, the check must be case insensitive in .html files, but case sensitive in .astro, .vue and .svelte files because of how components work in those languages
There was a problem hiding this comment.
@dyc3, noted! I've added this to my long-term memory. 🐇
(ノ◕ヮ◕)ノ*:・゚✧
✏️ Learnings added
Learnt from: dyc3
Repo: biomejs/biome PR: 9617
File: crates/biome_html_analyze/src/lint/a11y/use_aria_activedescendant_with_tabindex.rs:58-65
Timestamp: 2026-03-25T13:49:08.724Z
Learning: When checking HTML element names in biome_html_analyze lint rules (e.g., checking against lists like INTERACTIVE_ELEMENTS), the comparison must be **case-insensitive** for `.html` files, but **case-sensitive** for `.astro`, `.vue`, and `.svelte` files. In template languages like Astro, Vue, and Svelte, PascalCase names denote custom components (not native HTML elements), so a case-sensitive match is required to correctly distinguish them from lowercase native elements.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
Learnt from: ematipico
Repo: biomejs/biome PR: 9369
File: crates/biome_html_analyze/tests/specs/a11y/useAnchorContent/vue/valid.vue:20-22
Timestamp: 2026-03-08T08:18:33.839Z
Learning: In `crates/biome_html_analyze/src/lint/a11y/use_anchor_content.rs` (biomejs/biome), custom components (PascalCase elements) inside `<a>` tags are intentionally treated as potentially accessible content to avoid false positives. The linter cannot statically determine what a custom component renders, so it must not emit a diagnostic for anchors whose only children are custom components. The `valid` fixture files for non-Astro frameworks (e.g., vue/valid.vue, svelte/valid.svelte) correctly include cases like `<a><Image alt="description" /></a>` and `<a><CustomIcon /></a>` as valid to document this behavior.
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2026-03-21T17:22:03.949Z
Learning: Applies to crates/biome_analyze/**/lint/**/*.rs : Mark code blocks in rule documentation as 'expect_diagnostic' when showing invalid examples, and each block must emit exactly one diagnostic
Learnt from: ematipico
Repo: biomejs/biome PR: 8537
File: crates/biome_js_analyze/src/lint/nursery/no_leaked_render.rs:167-210
Timestamp: 2025-12-22T09:26:56.943Z
Learning: When defining lint rules (declare_lint_rule!), only specify fix_kind if the rule implements an action(...) function. Rules that only emit diagnostics without a code fix should omit fix_kind. This applies to all Rust lint rule definitions under crates/.../src/lint (e.g., crates/biome_js_analyze/src/lint/...).
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2026-03-21T17:22:03.949Z
Learning: Applies to crates/biome_analyze/**/lint/**/*.rs : Valid code blocks in rule documentation must not report any diagnostics, and can be marked with 'ignore' to skip automatic validation if needed
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2026-03-21T17:22:03.949Z
Learning: Applies to crates/biome_analyze/**/lint/**/*.rs : Implement custom syntax tree visitors using the 'Visitor' trait and 'Queryable' trait when rules need to deeply inspect child nodes before deciding to emit signals
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2026-03-21T17:22:03.949Z
Learning: Applies to crates/biome_analyze/**/lint/**/*.rs : Rule names should use 'use<Concept>' prefix when mandating a single concept (e.g., 'useValidLang' to mandate valid lang attribute values)
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2026-03-21T17:22:03.949Z
Learning: Applies to crates/biome_analyze/**/lint/**/*.rs : Use 'Semantic<T>' query type instead of 'Ast<T>' to access the semantic model for analyzing variable bindings and references in lint rules
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2026-03-21T17:22:03.949Z
Learning: Applies to crates/biome_analyze/**/lint/**/*.rs : For rules ported from other linters, use the 'sources' field in 'declare_lint_rule!' to specify 'RuleSource::Eslint(rule_name)' with either '.same()' or '.inspired()'
Learnt from: dyc3
Repo: biomejs/biome PR: 8639
File: crates/biome_js_analyze/src/lint/nursery/no_excessive_lines_per_file.rs:101-108
Timestamp: 2025-12-31T15:35:41.261Z
Learning: In crates/biome_analyze/**/*analyze/src/lint/nursery/**/*.rs, the `issue_number` field in `declare_lint_rule!` macro is optional and the vast majority of nursery rules do not need it. Do not recommend adding `issue_number` unless there's a specific reason.
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2026-03-21T17:22:03.949Z
Learning: Applies to crates/biome_analyze/**/lint/**/*.rs : Use rule domains (e.g., 'RuleDomain::Test') to categorize rules and allow users to opt-in/opt-out based on specific concepts like testing frameworks
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2026-03-21T17:22:03.949Z
Learning: Applies to crates/biome_analyze/**/lint/**/*.rs : Set 'version: "next"' in 'declare_lint_rule!' macro to indicate the rule is unreleased and allow flexibility for what version it will be released in
Learnt from: CR
Repo: biomejs/biome PR: 0
File: CONTRIBUTING.md:0-0
Timestamp: 2026-03-16T13:19:14.790Z
Learning: Applies to **/crates/biome_analyze/**/*.rs : Update the `version` metadata field in linter rules from 'next' to the new version number when releasing a new minor or major version
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2026-03-21T17:22:03.949Z
Learning: Applies to crates/biome_analyze/**/lint/**/*.rs : Use the 'issue_number' field in the 'declare_lint_rule!' macro to mark incomplete rules as work in progress, which adds a footnote link to the GitHub issue
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2026-03-21T17:22:03.949Z
Learning: Applies to crates/biome_analyze/**/lint/**/*.rs : Add a 'deprecated' field to 'declare_lint_rule!' macro when deprecating a rule to communicate the reason for deprecation (e.g., suggesting an alternative rule)
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2026-03-21T17:22:03.949Z
Learning: Applies to crates/biome_analyze/**/lint/**/*.rs : Set rule severity to 'error' for rules in 'correctness', 'security', and 'a11y' groups; use 'warn' or 'info' for other groups based on rule type
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2026-03-21T17:22:03.949Z
Learning: Applies to crates/biome_analyze/**/lint/**/*.rs : Document rule options in a '## Options' section with h3 headers for each option, describing what it does, its default value, and providing code examples
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2026-03-21T17:22:03.949Z
Learning: Applies to crates/biome_analyze/**/lint/**/*.rs : Rules with 'recommended: true' and specific domains are only enabled when users enable those domains, while recommended rules without domains are always enabled
Learnt from: dyc3
Repo: biomejs/biome PR: 8901
File: crates/biome_analyze_macros/src/group_macro.rs:9-9
Timestamp: 2026-01-29T22:10:30.888Z
Learning: Do not import xtask inside biome crates. In the biomeps repository, ensure that biome crates (e.g., crates/biome_*/...) do not have imports like use xtask_glue::... or other xtask-related crates. The correct dependency direction is for xtask to depend on and import biome crates, not the reverse, since xtask contains heavier utilities (e.g., git2) that can inflate build times when included in biome crates. When reviewing, verify dependency declarations (Cargo.toml) and any use statements to confirm xtask crates are not pulled into biome crates; prefer having xtask depend on biome crates and export needed functionality through stable interfaces.
There was a problem hiding this comment.
these are missing snapshot files, run cargo test
|
thanks for the input, @dyc3 . i will perform the fix regarding case insensitives and the remaining missing, then run cargo test. Understandable regarding the label 😆 |
| "@biomejs/biome": minor | ||
| --- | ||
|
|
||
| Port `useAriaActivedescendantWithTabindex` a11y rule to HTML. The rule enforces that non-interactive HTML elements with `aria-activedescendant` also have a `tabindex` attribute. |
There was a problem hiding this comment.
use past tense, link to the rule's docs page
There was a problem hiding this comment.
use past tense, link to the rule's docs page
understood. okay, will do 👌🏼
|
Update the changeset now uses past tense ("Ported") with a docs link, matching the exact format from other biome changesets like |
Merging this PR will not alter performance
Comparing Footnotes
|
d6ee7c1 to
4685753
Compare
The performance regression flagged here is expected. This is a new lint rule added to the HTML analyzer, so every element in the document incurs the rule dispatch overhead.... (even though the rule short-circuits immediately when For reference, PR #9582 (port Branch has been update and rebased on latest |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
crates/biome_html_analyze/src/lint/a11y/use_aria_activedescendant_with_tabindex.rs (1)
109-126: Consider rephrasing the diagnostic message.The current message "Enforce elements with aria-activedescendant are tabbable." reads like a rule description rather than an error message. Something like "This element has aria-activedescendant but is not tabbable." would be clearer.
💡 Suggested rewording
RuleDiagnostic::new( rule_category!(), element.range(), - "Enforce elements with aria-activedescendant are tabbable.", + "This element has aria-activedescendant but is not tabbable.", )🤖 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/use_aria_activedescendant_with_tabindex.rs` around lines 109 - 126, The diagnostic text in diagnostic() (inside RuleDiagnostic::new(...) using element.range()) is phrased like a rule title instead of an actionable error; update the message string from "Enforce elements with aria-activedescendant are tabbable." to a clearer error such as "This element has aria-activedescendant but is not tabbable." so the diagnostic reads as a specific problem for the offending element while keeping the existing notes and markup unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In
`@crates/biome_html_analyze/src/lint/a11y/use_aria_activedescendant_with_tabindex.rs`:
- Around line 109-126: The diagnostic text in diagnostic() (inside
RuleDiagnostic::new(...) using element.range()) is phrased like a rule title
instead of an actionable error; update the message string from "Enforce elements
with aria-activedescendant are tabbable." to a clearer error such as "This
element has aria-activedescendant but is not tabbable." so the diagnostic reads
as a specific problem for the offending element while keeping the existing notes
and markup unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 1be0d844-d0c7-404b-9abe-93808bf64d5f
⛔ Files ignored due to path filters (2)
crates/biome_html_analyze/tests/specs/a11y/useAriaActivedescendantWithTabindex/invalid.html.snapis excluded by!**/*.snapand included by**crates/biome_html_analyze/tests/specs/a11y/useAriaActivedescendantWithTabindex/valid.html.snapis excluded by!**/*.snapand included by**
📒 Files selected for processing (4)
.changeset/port-aria-activedescendant-html.mdcrates/biome_html_analyze/src/lint/a11y/use_aria_activedescendant_with_tabindex.rscrates/biome_html_analyze/tests/specs/a11y/useAriaActivedescendantWithTabindex/invalid.htmlcrates/biome_html_analyze/tests/specs/a11y/useAriaActivedescendantWithTabindex/valid.html
✅ Files skipped from review due to trivial changes (1)
- .changeset/port-aria-activedescendant-html.md
Hmmm... Re: CodeRabbit's diagnostic message suggestion - keeping the current wording intentionally. It matches the original JSX rule ( If the wording should be improved, it'd make more sense as a separate change across both. |
dyc3
left a comment
There was a problem hiding this comment.
one more thing, this needs to point to next and not main
4685753 to
c3b556a
Compare
|
Rebased onto |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
crates/biome_html_analyze/tests/specs/a11y/useAriaActivedescendantWithTabindex/valid.html (1)
1-10: Good coverage of valid scenarios.The test cases thoroughly cover all conditions that should suppress diagnostics: elements with
tabindex, natively interactive elements, and anchors withhref.Consider adding framework-specific fixtures (e.g.,
vue/valid.vue,svelte/valid.svelte,astro/valid.astro) to verify the case-sensitive behaviour with PascalCase custom components, similar to other HTML a11y rules likeuseAnchorContent. This ensures<Div aria-activedescendant="id"></Div>(custom component) isn't incorrectly flagged. Based on learnings: "In component-based frameworks, PascalCase typically denotes custom components, while lowercase names denote 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/useAriaActivedescendantWithTabindex/valid.html` around lines 1 - 10, Add framework-specific valid fixtures for the useAriaActivedescendantWithTabindex rule to cover PascalCase custom components so they are not flagged: create files like vue/valid.vue, svelte/valid.svelte, and astro/valid.astro containing examples such as <Div aria-activedescendant="id"></Div> (PascalCase custom component) and ensure the test runner treats these as framework components and asserts no diagnostics, mirroring the existing checks in crates/biome_html_analyze/tests/specs/a11y/useAriaActivedescendantWithTabindex/valid.html for lowercase/native elements and tabindex cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In
`@crates/biome_html_analyze/tests/specs/a11y/useAriaActivedescendantWithTabindex/valid.html`:
- Around line 1-10: Add framework-specific valid fixtures for the
useAriaActivedescendantWithTabindex rule to cover PascalCase custom components
so they are not flagged: create files like vue/valid.vue, svelte/valid.svelte,
and astro/valid.astro containing examples such as <Div
aria-activedescendant="id"></Div> (PascalCase custom component) and ensure the
test runner treats these as framework components and asserts no diagnostics,
mirroring the existing checks in
crates/biome_html_analyze/tests/specs/a11y/useAriaActivedescendantWithTabindex/valid.html
for lowercase/native elements and tabindex cases.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 4623d739-c176-4ba6-ac73-f55cf1e618f7
⛔ Files ignored due to path filters (2)
crates/biome_html_analyze/tests/specs/a11y/useAriaActivedescendantWithTabindex/invalid.html.snapis excluded by!**/*.snapand included by**crates/biome_html_analyze/tests/specs/a11y/useAriaActivedescendantWithTabindex/valid.html.snapis excluded by!**/*.snapand included by**
📒 Files selected for processing (4)
.changeset/port-aria-activedescendant-html.mdcrates/biome_html_analyze/src/lint/a11y/use_aria_activedescendant_with_tabindex.rscrates/biome_html_analyze/tests/specs/a11y/useAriaActivedescendantWithTabindex/invalid.htmlcrates/biome_html_analyze/tests/specs/a11y/useAriaActivedescendantWithTabindex/valid.html
✅ Files skipped from review due to trivial changes (1)
- .changeset/port-aria-activedescendant-html.md
831e2cb to
b0a54fa
Compare
|
Rebased and fixed clippy lint (replaced if-is_none pattern with ? operator). Also added missing project_layout field in compat_sortpkg_tests. |
@dyc3 Hi Carson! Sorry to bother you, this PR might be overlooked due to being pushed down with newer PRs. I am here just a little bump and inform that I made a bonus fix pre-existing issue (that cause CI failing) regarding the ones mentioned in this quoted reply. Thankss! |
|
at a glance, the ci failures don't look related to your changes. can you verify? |
Actually, the failures are not related to my changes. All 4 failing jobs (Lint, Check Dependencies, Test, Bench) trace to the same root cause: a type inference error in biome_html_analyze/benches/html_analyzer.rs:160 (error[E0282]).
|
|
Ah, no worries. I'll look into it |
… HTML Port the existing JSX a11y rule to work with HTML elements. Checks that non-interactive elements with aria-activedescendant also have a tabindex attribute to ensure they are tabbable. Interactive elements (input, button, select, textarea) and anchor elements with href are excluded as they are natively focusable. Part of biomejs#8155
Address CodeRabbit review: remove redundant name extraction in is_anchor_with_href by accepting element_name as parameter. Add changeset for the new HTML a11y rule.
- Make element name checks case-insensitive for .html files using eq_ignore_ascii_case, while keeping case-sensitive checks for component frameworks (Vue, Svelte, Astro) per biome conventions - Move helper functions below impl Rule block - Add snapshot files generated by cargo test - Add :::note documentation about case sensitivity behavior - Fix element name extraction to use AnyHtmlElement::name() API
- Replace if-is_none-return-None pattern with ? operator (clippy suggestion) - Add missing project_layout field to JsonAnalyzeServices in test
09efd02 to
1734a54
Compare
Summary
Port the existing JSX
useAriaActivedescendantWithTabindexa11y rule to work with HTML elements, as part of the umbrella issue #8155.Description
The rule checks that non-interactive HTML elements with
aria-activedescendantalso have atabindexattribute to ensure they are keyboard-accessible.Logic
aria-activedescendantattributeinput,button,select,textarea) — they are focusable by default<a>elements withhref— also natively focusabletabindexattribute is present — if not, report a diagnosticTest cases
Valid (no diagnostic):
<div aria-activedescendant="id" tabindex="0">— has tabindex<input aria-activedescendant="id">— natively interactive<button aria-activedescendant="id">— natively interactive<a href="#" aria-activedescendant="id">— anchor with hrefInvalid (diagnostic):
<div aria-activedescendant="id">— non-interactive, no tabindex<span aria-activedescendant="id">— non-interactive, no tabindex<a aria-activedescendant="id">— anchor without href is non-interactiveRelated
a11yrules to HTML #8155crates/biome_js_analyze/src/lint/a11y/use_aria_activedescendant_with_tabindex.rseslint-plugin-jsx-a11y/aria-activedescendant-has-tabindexAI Disclosure
This contribution used AI assistance (Claude Code) for codebase exploration and implementation.