Skip to content

fix(biome_html_analyze): consolidate a11y helpers and fix aria-hidden behavior#8837

Merged
dyc3 merged 11 commits intobiomejs:nextfrom
rahuld109:refactor/consolidate-a11y-helpers
Jan 25, 2026
Merged

fix(biome_html_analyze): consolidate a11y helpers and fix aria-hidden behavior#8837
dyc3 merged 11 commits intobiomejs:nextfrom
rahuld109:refactor/consolidate-a11y-helpers

Conversation

@rahuld109
Copy link
Contributor

@rahuld109 rahuld109 commented Jan 22, 2026

Summary

Consolidates duplicated accessibility helper functions into a shared a11y.rs module and fixes incorrect aria-hidden attribute handling.

Bug Fix

Fixed aria-hidden attribute behavior to match ARIA spec:

  • aria-hidden (no value) → now correctly treated as visible (was incorrectly hidden)
  • aria-hidden="" or whitespace → now correctly treated as visible
  • aria-hidden="true" → hidden (unchanged)
  • aria-hidden="false" → visible (unchanged)

Per ARIA spec, aria-hidden is not an HTML boolean attribute - it requires explicit "true" or "false" values. Tested in Chrome DevTools accessibility tree to confirm behavior.

Refactoring

Following @Netail's suggestion in #8155 (comment), helpers are added to biome_html_analyze/src/a11y.rs.

New method in biome_html_syntax:

  • HtmlAttribute::value() - extracts value from attribute initializer

Core helpers: is_aria_hidden_value_truthy(), is_strict_true_value(), has_non_empty_value(), attribute_value_equals_ignore_case()

Element-level helpers: is_hidden_from_screen_reader(), is_aria_hidden_true(), get_truthy_aria_hidden_attribute(), has_non_empty_attribute(), has_accessible_name()

Type-specific variants: For performance in recursive code

Test plan

  • All existing tests pass
  • 69 unit tests for a11y helper functions
  • Snapshot updates reflect corrected aria-hidden behavior

@changeset-bot
Copy link

changeset-bot bot commented Jan 22, 2026

⚠️ No Changeset found

Latest commit: 4002ece

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@rahuld109 rahuld109 changed the base branch from main to next January 22, 2026 22:37
@github-actions github-actions bot added A-Linter Area: linter L-HTML Language: HTML and super languages labels Jan 22, 2026
@codspeed-hq
Copy link

codspeed-hq bot commented Jan 22, 2026

Merging this PR will not alter performance

✅ 3 untouched benchmarks
🆕 1 new benchmark
⏩ 152 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
🆕 html_analyzer[index_1033418810622582172.html] N/A 397.8 µs N/A

Comparing rahuld109:refactor/consolidate-a11y-helpers (4002ece) with next (883ea1d)

Open in CodSpeed

Footnotes

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

@rahuld109 rahuld109 force-pushed the refactor/consolidate-a11y-helpers branch from 0c05609 to 7b5ee6a Compare January 22, 2026 22:59
Extracts duplicated a11y helper logic into shared utilities in `a11y.rs`:

- `is_aria_hidden_true()`: strict aria-hidden="true" check
- `get_truthy_aria_hidden_attribute()`: returns attribute if truthy
- `has_non_empty_attribute()`: checks non-empty trimmed attribute value
- `has_accessible_name()`: checks aria-label or title attributes

Added type-specific variants to avoid cloning in recursive functions:
- `html_element_has_truthy_aria_hidden()`
- `html_self_closing_element_has_truthy_aria_hidden()`
- `html_self_closing_element_has_accessible_name()`
- `html_self_closing_element_has_non_empty_attribute()`

Updated rules to use shared helpers:
- use_alt_text
- use_anchor_content
- use_html_lang
- use_iframe_title
- no_svg_without_title
- no_redundant_alt
@rahuld109 rahuld109 force-pushed the refactor/consolidate-a11y-helpers branch from 7b5ee6a to c66ce63 Compare January 22, 2026 23:04
autofix-ci bot and others added 2 commits January 22, 2026 23:09
Extract duplicated attribute-checking patterns into core helpers:
- is_truthy_aria_hidden_value()
- is_strict_true_value()
- has_non_empty_value()

All public helpers now delegate to these core functions.
@rahuld109 rahuld109 force-pushed the refactor/consolidate-a11y-helpers branch from 114386e to 02bd109 Compare January 22, 2026 23:30
@rahuld109 rahuld109 marked this pull request as ready for review January 22, 2026 23:54
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 23, 2026

Walkthrough

This PR adds a centralized ARIA/HTML accessibility helper module at crates/biome_html_analyze/src/a11y.rs (truthy aria-hidden checks, case-insensitive attribute comparisons, non-empty attribute checks, accessible-name detection), exports AnyHtmlElement and HtmlAttribute within the module scope, and introduces several pub(crate) helper functions used across lints. Multiple a11y lint rules were refactored to call these helpers (no_redundant_alt, no_svg_without_title, use_alt_text, use_anchor_content, use_html_lang, use_iframe_title). It adds comprehensive tests at crates/biome_html_analyze/src/a11y_tests.rs and a HtmlAttribute::value() accessor in crates/biome_html_syntax/src/attr_ext.rs.

Possibly related PRs

Suggested labels

A-Diagnostic

Suggested reviewers

  • ematipico
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarises the main changes: consolidating a11y helpers and fixing aria-hidden behaviour—both primary objectives of this PR.
Description check ✅ Passed The description comprehensively covers the bug fixes, refactoring work, new helpers, and test plan—all directly related to the changeset.

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

✨ Finishing touches
  • 📝 Generate docstrings

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.

❤️ Share

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

Copy link
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: 1

🤖 Fix all issues with AI agents
In `@crates/biome_html_analyze/src/a11y.rs`:
- Around line 92-95: The accessible-name checks currently only test "aria-label"
and "title"; update both has_accessible_name and
html_self_closing_element_has_accessible_name to also consider "aria-labelledby"
as a valid source by adding a has_non_empty_attribute(element,
"aria-labelledby") OR into their conditionals so elements with a non-empty
aria-labelledby are treated as having an accessible name.

…tion

Add `get_attribute_string_value()` as the single source of truth for
extracting attribute values. All other helpers now delegate to this
fundamental function, eliminating the remaining duplication.

Also adds public `attribute_value_equals_ignore_case()` helper and
updates `is_hidden_from_screen_reader()` and `has_type_image_attribute()`
to use the shared helpers.
Include `aria-labelledby` as a valid accessible name source per ARIA
specs in both `has_accessible_name()` and
`html_self_closing_element_has_accessible_name()`.
@rahuld109 rahuld109 requested a review from Netail January 23, 2026 00:06
Copy link
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.

I finished my review at mid way of the code. There's a lot of slop in here. Docs mention "yes" and "1" as truth values... or sometimes the implementation doesn't reflect the docs.

Please make sure to improve the quality of the prompts you give to the AI agent, refer to the real docs such as MDN as source of truth, and please add tests!!!

@github-actions github-actions bot added the A-Parser Area: parser label Jan 23, 2026
- Move get_attribute_string_value to HtmlAttribute::value() in attr_ext.rs
- Rename is_truthy_aria_hidden_value to is_aria_hidden_value_truthy
- Fix aria-hidden documentation per MDN spec
- Remove AI-generated verbose language from docs
- Add unit tests for a11y helper functions
@rahuld109 rahuld109 force-pushed the refactor/consolidate-a11y-helpers branch from c9e3464 to 7395933 Compare January 23, 2026 18:17
Copy link
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: 1

🤖 Fix all issues with AI agents
In `@crates/biome_html_analyze/src/a11y.rs`:
- Around line 17-28: The function is_aria_hidden_value_truthy currently treats
missing attributes as truthy; change it so None and empty-string values return
false. Use HtmlAttribute::value() and implement map_or(false, |v| { if
v.trim().is_empty() { false } else { !v.eq_ignore_ascii_case("false") } }) so
only non-empty values that are not "false" (including explicit "true" or
unrecognized strings) are treated as truthy.

Empty strings, whitespace-only values, and missing attribute values
are now considered falsy for aria-hidden. Only non-empty values that
are not "false" (case-insensitive) are truthy. This aligns with
jsx-a11y's stricter interpretation of the ARIA spec.
@rahuld109 rahuld109 force-pushed the refactor/consolidate-a11y-helpers branch from 62684be to 208f65f Compare January 23, 2026 21:55
Copy link
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: 1

🤖 Fix all issues with AI agents
In `@crates/biome_html_analyze/src/a11y.rs`:
- Around line 72-83: Replace the case-sensitive check using
element.has_truthy_attribute("aria-hidden") inside is_hidden_from_screen_reader
with a call that locates the "aria-hidden" attribute via
element.find_attribute_by_name("aria-hidden") and passes that attribute to
is_aria_hidden_value_truthy(&attr) so the function uses the same
case-insensitive, trimmed semantics as other helpers; keep the rest of
is_hidden_from_screen_reader (including the input[type=hidden] check that uses
attribute_value_equals_ignore_case and the name_value_token logic) unchanged.
🧹 Nitpick comments (1)
crates/biome_html_analyze/src/a11y_tests.rs (1)

221-226: Test name is slightly misleading.

The test is named multiple_sources_first_wins, but has_accessible_name uses boolean OR logic—it doesn't prioritise one source over another. Consider renaming to something like multiple_sources_any_valid or multiple_sources_returns_true.

♻️ Suggested rename
     #[test]
-    fn multiple_sources_first_wins() {
+    fn multiple_sources_returns_true() {
         let element =
             parse_first_element(r#"<div aria-label="Label" aria-labelledby="id" title="T"></div>"#);
         assert!(has_accessible_name(&element));
     }

@ematipico
Copy link
Member

Here's another suggestion, the PR description is outdated.

Behavior is identical - this is purely a refactoring

It's not because you fixed some bugs, and the description doesn't reflect that

@rahuld109 rahuld109 changed the title refactor(biome_html_analyze): consolidate a11y helper functions fix(biome_html_analyze): consolidate a11y helpers and fix aria-hidden behavior Jan 25, 2026
…n_from_screen_reader

Replace has_truthy_attribute with find_attribute_by_name + is_aria_hidden_value_truthy
for consistent case-insensitive, trimmed semantics across all aria-hidden checks.
@rahuld109 rahuld109 force-pushed the refactor/consolidate-a11y-helpers branch from 07505b8 to 4002ece Compare January 25, 2026 07:25
Copy link
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: 2

🤖 Fix all issues with AI agents
In `@crates/biome_html_analyze/src/a11y.rs`:
- Around line 62-86: The tag-name check in is_hidden_from_screen_reader is
case-sensitive and should be made case-insensitive; update the match arm that
currently does `name.text_trimmed() == "input"` to use
`name.text_trimmed().eq_ignore_ascii_case("input")` so variants like `<INPUT>`
are detected; keep the surrounding logic (the subsequent check of the "type"
attribute via attribute_value_equals_ignore_case and the aria-hidden branch)
unchanged.
- Around line 17-28: The doc comment for is_aria_hidden_value_truthy
inaccurately implies strict ARIA spec validation; update it to state that the
function intentionally uses a permissive check (accepts any non-empty value
except case-insensitive "false", e.g., "yes" or "1" are treated truthy) while
noting the spec defines "true"/"false". Modify the comment to describe this
lenient behavior, explain which inputs are considered falsy (missing, empty,
whitespace-only, or "false"), and keep or add the MDN ref for context; reference
the function name is_aria_hidden_value_truthy so reviewers can locate the
change.

Comment on lines +17 to +28
/// Checks if an `aria-hidden` attribute has a truthy value.
///
/// Per ARIA spec, `aria-hidden` accepts only `"true"` or `"false"` as valid values.
/// Returns `true` only for non-empty values that are not `"false"` (case-insensitive).
/// Missing values, empty strings, and whitespace-only values are considered falsy.
///
/// Ref: <https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-hidden>
fn is_aria_hidden_value_truthy(attribute: &HtmlAttribute) -> bool {
attribute.value().is_some_and(|value| {
let trimmed = value.trim();
!trimmed.is_empty() && !trimmed.eq_ignore_ascii_case("false")
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

ARIA aria-hidden attribute specification valid values

💡 Result:

Per the WAI-ARIA specification, aria-hidden accepts these valid values (tokens):

  • "true" — element is hidden from the accessibility API
  • "false" — element is exposed to the accessibility API as if rendered
  • "undefined" (default) — user agent determines hidden state based on whether it is rendered (same effect as omitting the attribute) [1][2]

Any other value is invalid. [1]

Sources: WAI-ARIA 1.2 spec [1], MDN reference [2].


🏁 Script executed:

# Check the complete function and surrounding context
cd crates/biome_html_analyze/src && cat -n a11y.rs | head -50

Repository: biomejs/biome

Length of output: 2689


🏁 Script executed:

# Search for tests related to aria-hidden to understand intended behaviour
rg "aria-hidden" --type rust -C 3

Repository: biomejs/biome

Length of output: 50371


Align docs with the lax validation approach.

The implementation intentionally accepts any non-empty value other than "false" (per the tests accepting "yes" and "1"), but the doc comment claims the ARIA spec accepts only "true"/"false". Whilst the spec does define those two values, the function is more permissive. Update the docs to clarify this intentional leniency rather than implying strict spec compliance:

Suggested doc adjustment
-/// Per ARIA spec, `aria-hidden` accepts only `"true"` or `"false"` as valid values.
-/// Returns `true` only for non-empty values that are not `"false"` (case-insensitive).
+/// `aria-hidden` is defined with `"true"`/`"false"` values per ARIA spec.
+/// This helper is permissive: treats any non-empty value other than `"false"` as truthy.
🤖 Prompt for AI Agents
In `@crates/biome_html_analyze/src/a11y.rs` around lines 17 - 28, The doc comment
for is_aria_hidden_value_truthy inaccurately implies strict ARIA spec
validation; update it to state that the function intentionally uses a permissive
check (accepts any non-empty value except case-insensitive "false", e.g., "yes"
or "1" are treated truthy) while noting the spec defines "true"/"false". Modify
the comment to describe this lenient behavior, explain which inputs are
considered falsy (missing, empty, whitespace-only, or "false"), and keep or add
the MDN ref for context; reference the function name is_aria_hidden_value_truthy
so reviewers can locate the change.

Comment on lines +62 to 86
/// Returns `true` if the element is hidden from assistive technologies.
///
/// An element is hidden from screen readers when:
/// - It has a truthy `aria-hidden` attribute
/// - It is an `<input type="hidden">` element
///
/// Ref:
/// - https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-hidden
/// - https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/hidden
/// - https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/v6.10.0/src/util/isHiddenFromScreenReader.js
/// - <https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-hidden>
/// - <https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/hidden>
/// - <https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/v6.10.0/src/util/isHiddenFromScreenReader.js>
pub(crate) fn is_hidden_from_screen_reader(element: &AnyHtmlTagElement) -> bool {
let is_aria_hidden = element.has_truthy_attribute("aria-hidden");
if is_aria_hidden {
if element
.find_attribute_by_name("aria-hidden")
.is_some_and(|attr| is_aria_hidden_value_truthy(&attr))
{
return true;
}

match element.name_value_token().ok() {
Some(name) if name.text_trimmed() == "input" => element
.find_attribute_by_name("type")
.and_then(|attribute| attribute.initializer()?.value().ok()?.string_value())
.is_some_and(|value| value.text() == "hidden"),
.is_some_and(|attr| attribute_value_equals_ignore_case(&attr, "hidden")),
_ => false,
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's understand what name_value_token() returns and how it's used
# Check the AnyHtmlTagElement implementation and name_value_token method
fd -e rs crates/biome_html_analyze | head -20

Repository: biomejs/biome

Length of output: 462


🏁 Script executed:

# Search for name_value_token implementation and usage patterns
rg "name_value_token" --type rs -A 3 -B 1

Repository: biomejs/biome

Length of output: 83


🏁 Script executed:

# Look for other tag name comparisons in the a11y.rs file and related files
rg "== [\"']input[\"']|== [\"'].*[\"']" crates/biome_html_analyze/src/ --type rs -B 2 -A 2

Repository: biomejs/biome

Length of output: 83


🏁 Script executed:

# Check how HTML tag names are handled elsewhere in the codebase - look for case-insensitive patterns
rg "eq_ignore_ascii_case|to_lowercase|to_lowercase_ascii" crates/biome_html_analyze/ --type rs -B 1 -A 1

Repository: biomejs/biome

Length of output: 83


🏁 Script executed:

# Find Rust files in biome_html_analyze
fd . crates/biome_html_analyze -e rs | head -20

Repository: biomejs/biome

Length of output: 1196


🏁 Script executed:

# Search for name_value_token usage and implementation
rg "name_value_token" crates/biome_html_analyze/ -A 3 -B 1

Repository: biomejs/biome

Length of output: 485


🏁 Script executed:

# Look for other tag name equality checks
rg '== "input"' crates/biome_html_analyze/ -B 2 -A 2

Repository: biomejs/biome

Length of output: 485


🏁 Script executed:

# Check for case-insensitive comparisons in HTML analysis code
rg "eq_ignore_ascii_case|to_lowercase" crates/biome_html_analyze/ -B 1 -A 1

Repository: biomejs/biome

Length of output: 8518


🏁 Script executed:

# Search for how other HTML linters/analyzers handle tag names - look at related implementations
rg 'text_trimmed|name.*==' crates/biome_html_analyze/src/a11y.rs -B 2 -A 2

Repository: biomejs/biome

Length of output: 295


Make the tag name check case-insensitive to match codebase patterns.

HTML tag names are case-insensitive. Every other HTML tag check in the codebase uses eq_ignore_ascii_case() (e.g. use_anchor_content.rs uses it for "input" checks), but this function uses case-sensitive comparison. Source code preserves the original case, so <INPUT> won't match.

Suggested fix
-        Some(name) if name.text_trimmed() == "input" => element
+        Some(name) if name.text_trimmed().eq_ignore_ascii_case("input") => element
🤖 Prompt for AI Agents
In `@crates/biome_html_analyze/src/a11y.rs` around lines 62 - 86, The tag-name
check in is_hidden_from_screen_reader is case-sensitive and should be made
case-insensitive; update the match arm that currently does `name.text_trimmed()
== "input"` to use `name.text_trimmed().eq_ignore_ascii_case("input")` so
variants like `<INPUT>` are detected; keep the surrounding logic (the subsequent
check of the "type" attribute via attribute_value_equals_ignore_case and the
aria-hidden branch) unchanged.

@ematipico ematipico requested a review from dyc3 January 25, 2026 08:18
@dyc3 dyc3 merged commit 3d427b8 into biomejs:next Jan 25, 2026
16 checks passed
@rahuld109 rahuld109 deleted the refactor/consolidate-a11y-helpers branch January 26, 2026 02:39
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.

4 participants