-
-
Notifications
You must be signed in to change notification settings - Fork 964
fix(biome_html_analyze): consolidate a11y helpers and fix aria-hidden behavior #8837
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c66ce63
b345567
02bd109
8dd95a9
a40031e
fee99c5
2434aec
7395933
fc78ca6
208f65f
4002ece
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,22 +1,186 @@ | ||
| //! Shared accessibility helper functions for HTML lint rules. | ||
| //! | ||
| //! This module provides reusable utilities for checking accessibility-related | ||
| //! attributes and element states. All helpers follow a layered architecture: | ||
| //! | ||
| //! 1. **Core helpers** (private): Low-level attribute value extraction and checks | ||
| //! 2. **Element helpers** (public): Higher-level checks on HTML elements | ||
| //! 3. **Type-specific variants** (public): Optimized versions that avoid cloning | ||
|
|
||
| use biome_html_syntax::element_ext::AnyHtmlTagElement; | ||
| use biome_html_syntax::{AnyHtmlElement, HtmlAttribute}; | ||
|
|
||
| // ============================================================================ | ||
| // Core attribute value helpers (private) | ||
| // ============================================================================ | ||
|
|
||
| /// 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") | ||
| }) | ||
|
Comment on lines
+17
to
+28
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: Per the WAI-ARIA specification,
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 -50Repository: 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 3Repository: biomejs/biome Length of output: 50371 Align docs with the lax validation approach. The implementation intentionally accepts any non-empty value other than 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 |
||
| } | ||
rahuld109 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /// Checks if an attribute value equals `"true"` exactly (case-sensitive). | ||
| fn is_strict_true_value(attribute: &HtmlAttribute) -> bool { | ||
| attribute.value().is_some_and(|value| value == "true") | ||
| } | ||
|
|
||
| /// Checks if an attribute has a non-empty value after trimming whitespace. | ||
| /// | ||
| /// Returns `false` for attributes with no value, empty strings, or whitespace-only values. | ||
| fn has_non_empty_value(attribute: &HtmlAttribute) -> bool { | ||
| attribute | ||
| .value() | ||
| .is_some_and(|value| !value.trim().is_empty()) | ||
rahuld109 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| /// Check the element is hidden from screen reader. | ||
| /// Checks if an attribute value matches the expected string (case-insensitive). | ||
| /// | ||
| /// Useful for checking HTML attribute values like `type="hidden"` or `role="button"` | ||
| /// where the comparison should be case-insensitive per HTML spec. | ||
| pub(crate) fn attribute_value_equals_ignore_case( | ||
| attribute: &HtmlAttribute, | ||
| expected: &str, | ||
| ) -> bool { | ||
| attribute | ||
| .value() | ||
| .is_some_and(|value| value.eq_ignore_ascii_case(expected)) | ||
| } | ||
|
|
||
| // ============================================================================ | ||
| // Element-level helpers (public) | ||
| // ============================================================================ | ||
|
|
||
| /// 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, | ||
| } | ||
| } | ||
rahuld109 marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+62
to
86
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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 -20Repository: 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 1Repository: 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 2Repository: 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 1Repository: biomejs/biome Length of output: 83 🏁 Script executed: # Find Rust files in biome_html_analyze
fd . crates/biome_html_analyze -e rs | head -20Repository: 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 1Repository: biomejs/biome Length of output: 485 🏁 Script executed: # Look for other tag name equality checks
rg '== "input"' crates/biome_html_analyze/ -B 2 -A 2Repository: 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 1Repository: 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 2Repository: 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 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 |
||
|
|
||
| /// Strict check: returns `true` only when `aria-hidden="true"` (case-sensitive). | ||
| /// | ||
| /// Unlike [`is_aria_hidden_value_truthy`], this only matches the exact string `"true"`. | ||
| /// | ||
| /// Ref: <https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-hidden> | ||
| pub(crate) fn is_aria_hidden_true(element: &AnyHtmlElement) -> bool { | ||
rahuld109 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| element | ||
| .find_attribute_by_name("aria-hidden") | ||
| .is_some_and(|attr| is_strict_true_value(&attr)) | ||
| } | ||
|
|
||
| /// Returns the `aria-hidden` attribute if it has a truthy value. | ||
| /// | ||
| /// Returns `Some` if present and not `"false"` (case-insensitive), `None` otherwise. | ||
| /// Useful for code fixes that need to reference the attribute node. | ||
| /// | ||
| /// Ref: <https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-hidden> | ||
| pub(crate) fn get_truthy_aria_hidden_attribute(element: &AnyHtmlElement) -> Option<HtmlAttribute> { | ||
| let attribute = element.find_attribute_by_name("aria-hidden")?; | ||
| if is_aria_hidden_value_truthy(&attribute) { | ||
| Some(attribute) | ||
| } else { | ||
| None | ||
| } | ||
| } | ||
|
|
||
| /// Returns `true` if the element has the named attribute with a non-empty value. | ||
| /// | ||
| /// Whitespace-only values are considered empty. | ||
| pub(crate) fn has_non_empty_attribute(element: &AnyHtmlElement, name: &str) -> bool { | ||
| element | ||
| .find_attribute_by_name(name) | ||
| .is_some_and(|attr| has_non_empty_value(&attr)) | ||
| } | ||
|
|
||
| /// Returns `true` if the element has an accessible name via `aria-label`, | ||
| /// `aria-labelledby`, or `title` attributes. | ||
| pub(crate) fn has_accessible_name(element: &AnyHtmlElement) -> bool { | ||
| has_non_empty_attribute(element, "aria-label") | ||
| || has_non_empty_attribute(element, "aria-labelledby") | ||
| || has_non_empty_attribute(element, "title") | ||
| } | ||
|
|
||
| /// Checks if an [`HtmlElement`] has a truthy `aria-hidden` attribute. | ||
| /// | ||
| /// [`HtmlElement`]: biome_html_syntax::HtmlElement | ||
| pub(crate) fn html_element_has_truthy_aria_hidden( | ||
rahuld109 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| element: &biome_html_syntax::HtmlElement, | ||
| ) -> bool { | ||
| element | ||
| .find_attribute_by_name("aria-hidden") | ||
| .is_some_and(|attr| is_aria_hidden_value_truthy(&attr)) | ||
| } | ||
|
|
||
| /// Checks if an [`HtmlSelfClosingElement`] has a truthy `aria-hidden` attribute. | ||
| /// | ||
| /// [`HtmlSelfClosingElement`]: biome_html_syntax::HtmlSelfClosingElement | ||
| pub(crate) fn html_self_closing_element_has_truthy_aria_hidden( | ||
rahuld109 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| element: &biome_html_syntax::HtmlSelfClosingElement, | ||
| ) -> bool { | ||
| element | ||
| .find_attribute_by_name("aria-hidden") | ||
| .is_some_and(|attr| is_aria_hidden_value_truthy(&attr)) | ||
| } | ||
|
|
||
| /// Checks if an [`HtmlSelfClosingElement`] has an accessible name via `aria-label`, | ||
| /// `aria-labelledby`, or `title` attributes. | ||
| /// | ||
| /// [`HtmlSelfClosingElement`]: biome_html_syntax::HtmlSelfClosingElement | ||
| pub(crate) fn html_self_closing_element_has_accessible_name( | ||
| element: &biome_html_syntax::HtmlSelfClosingElement, | ||
| ) -> bool { | ||
| let has_aria_label = element | ||
| .find_attribute_by_name("aria-label") | ||
| .is_some_and(|attr| has_non_empty_value(&attr)); | ||
| let has_aria_labelledby = element | ||
| .find_attribute_by_name("aria-labelledby") | ||
| .is_some_and(|attr| has_non_empty_value(&attr)); | ||
| let has_title = element | ||
| .find_attribute_by_name("title") | ||
| .is_some_and(|attr| has_non_empty_value(&attr)); | ||
| has_aria_label || has_aria_labelledby || has_title | ||
| } | ||
|
|
||
| /// Checks if an [`HtmlSelfClosingElement`] has the named attribute with a non-empty value. | ||
| /// | ||
| /// [`HtmlSelfClosingElement`]: biome_html_syntax::HtmlSelfClosingElement | ||
| pub(crate) fn html_self_closing_element_has_non_empty_attribute( | ||
| element: &biome_html_syntax::HtmlSelfClosingElement, | ||
| name: &str, | ||
| ) -> bool { | ||
| element | ||
| .find_attribute_by_name(name) | ||
| .is_some_and(|attr| has_non_empty_value(&attr)) | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| #[path = "a11y_tests.rs"] | ||
| mod tests; | ||
Uh oh!
There was an error while loading. Please reload this page.