Skip to content

Commit d1e5499

Browse files
authored
refactor(lint): a11y utils to reduce duplicate function definitions (#5405)
1 parent e7b712e commit d1e5499

File tree

5 files changed

+72
-78
lines changed

5 files changed

+72
-78
lines changed

crates/biome_aria/src/roles.rs

+14
Original file line numberDiff line numberDiff line change
@@ -270,4 +270,18 @@ impl AriaRoles {
270270
},
271271
}
272272
}
273+
274+
/// Check if the element's implicit ARIA semantics have been removed.
275+
///
276+
/// Ref:
277+
/// - https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/presentation_role
278+
/// - https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/v6.10.0/src/util/isPresentationRole.js
279+
pub fn is_presentation_role(&self, element: &impl Element) -> bool {
280+
if let Some(attribute) = element.find_attribute_by_name(|n| n == "role") {
281+
if let Some(value) = attribute.value() {
282+
return matches!(value.as_ref(), "presentation" | "none");
283+
}
284+
}
285+
false
286+
}
273287
}

crates/biome_js_analyze/src/a11y.rs

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use biome_js_syntax::jsx_ext::AnyJsxElement;
2+
3+
/// Check the element is hidden from screen reader.
4+
///
5+
/// Ref:
6+
/// - https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-hidden
7+
/// - https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/hidden
8+
/// - https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/v6.10.0/src/util/isHiddenFromScreenReader.js
9+
pub(crate) fn is_hidden_from_screen_reader(element: &AnyJsxElement) -> bool {
10+
let is_aria_hidden = element.has_truthy_attribute("aria-hidden");
11+
if is_aria_hidden {
12+
return true;
13+
}
14+
15+
match element.name_value_token().ok() {
16+
Some(name) if name.text_trimmed() == "input" => {
17+
let is_input_hidden = element
18+
.find_attribute_by_name("type")
19+
.and_then(|attribute| attribute.as_static_value())
20+
.and_then(|value| value.as_string_constant().map(|value| value == "hidden"))
21+
.unwrap_or_default();
22+
is_input_hidden
23+
}
24+
_ => false,
25+
}
26+
}
27+
28+
/// Check if the element is `contentEditable`
29+
///
30+
/// Ref:
31+
/// - https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/contenteditable
32+
/// - https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/v6.10.0/src/util/isContentEditable.js
33+
pub(crate) fn is_content_editable(element: &AnyJsxElement) -> bool {
34+
element
35+
.find_attribute_by_name("contentEditable")
36+
.and_then(|attribute| attribute.as_static_value())
37+
.and_then(|value| value.as_string_constant().map(|value| value == "true"))
38+
.unwrap_or_default()
39+
}

crates/biome_js_analyze/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use biome_suppression::{SuppressionDiagnostic, parse_suppression_comment};
1616
use std::ops::Deref;
1717
use std::sync::{Arc, LazyLock};
1818

19+
mod a11y;
1920
pub mod assist;
2021
mod ast_utils;
2122
pub mod globals;

crates/biome_js_analyze/src/lint/nursery/no_noninteractive_element_interactions.rs

+6-53
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use crate::services::aria::Aria;
1+
use crate::{
2+
a11y::{is_content_editable, is_hidden_from_screen_reader},
3+
services::aria::Aria,
4+
};
25
use biome_analyze::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule};
36
use biome_console::markup;
47
use biome_js_syntax::jsx_ext::AnyJsxElement;
@@ -96,8 +99,8 @@ impl Rule for NoNoninteractiveElementInteractions {
9699

97100
if !has_handler_props(element)
98101
|| is_content_editable(element)
99-
|| has_presentation_role(element)
100-
|| is_hidden_from_screen_reader(element)?
102+
|| aria_roles.is_presentation_role(element)
103+
|| is_hidden_from_screen_reader(element)
101104
|| has_interactive_role
102105
{
103106
return None;
@@ -163,53 +166,3 @@ fn has_handler_props(element: &AnyJsxElement) -> bool {
163166
.iter()
164167
.any(|handler| element.find_attribute_by_name(handler).is_some())
165168
}
166-
167-
/// Check if the element's implicit ARIA semantics have been removed.
168-
/// See https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/presentation_role
169-
///
170-
/// Ref: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/v6.10.0/src/util/isPresentationRole.js
171-
fn has_presentation_role(element: &AnyJsxElement) -> bool {
172-
if let Some(attribute) = element.find_attribute_by_name("role") {
173-
let value = attribute.as_static_value();
174-
if let Some(value) = value {
175-
return matches!(value.as_string_constant(), Some("presentation" | "none"));
176-
}
177-
}
178-
false
179-
}
180-
181-
/// Check the element is hidden from screen reader.
182-
/// See
183-
/// - https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-hidden
184-
/// - https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/hidden
185-
///
186-
/// Ref: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/v6.10.0/src/util/isHiddenFromScreenReader.js
187-
fn is_hidden_from_screen_reader(element_name: &AnyJsxElement) -> Option<bool> {
188-
let is_aria_hidden = element_name.has_truthy_attribute("aria-hidden");
189-
190-
let name = element_name.name_value_token().ok()?;
191-
192-
let is_input_hidden = if name.text_trimmed() == "input" {
193-
element_name
194-
.find_attribute_by_name("type")
195-
.and_then(|attribute| attribute.as_static_value())
196-
.and_then(|value| value.as_string_constant().map(|value| value == "hidden"))
197-
.unwrap_or_default()
198-
} else {
199-
false
200-
};
201-
202-
Some(is_aria_hidden || is_input_hidden)
203-
}
204-
205-
/// Check if the element is `contentEditable`
206-
/// See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/contenteditable
207-
///
208-
/// Ref: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/v6.10.0/src/util/isContentEditable.js
209-
fn is_content_editable(element: &AnyJsxElement) -> bool {
210-
element
211-
.find_attribute_by_name("contentEditable")
212-
.and_then(|attribute| attribute.as_static_value())
213-
.and_then(|value| value.as_string_constant().map(|value| value == "true"))
214-
.unwrap_or_default()
215-
}

crates/biome_js_analyze/src/lint/nursery/no_static_element_interactions.rs

+12-25
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::a11y::is_hidden_from_screen_reader;
12
use crate::services::aria::Aria;
23
use biome_analyze::context::RuleContext;
34
use biome_analyze::{Rule, RuleDiagnostic, RuleSource, declare_lint_rule};
@@ -42,6 +43,11 @@ declare_lint_rule! {
4243
/// </>
4344
/// ```
4445
///
46+
/// Custom components are not checked.
47+
/// ```jsx
48+
/// <TestComponent onClick={doFoo} />
49+
/// ```
50+
///
4551
pub NoStaticElementInteractions {
4652
version: "1.9.0",
4753
name: "noStaticElementInteractions",
@@ -91,11 +97,13 @@ impl Rule for NoStaticElementInteractions {
9197

9298
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
9399
let node = ctx.query();
94-
let element_name = node.name().ok()?.as_jsx_name()?.value_token().ok()?;
95-
let element_name = element_name.text_trimmed();
96100

97-
// Check if the element is hidden from screen readers.
98-
if is_hidden_from_screen_reader(node, element_name) {
101+
// Custom components are not checked because we do not know what DOM will be used.
102+
if node.is_custom_component() {
103+
return None;
104+
}
105+
106+
if is_hidden_from_screen_reader(node) {
99107
return None;
100108
}
101109

@@ -140,24 +148,3 @@ impl Rule for NoStaticElementInteractions {
140148
))
141149
}
142150
}
143-
144-
/**
145-
* Returns boolean indicating that the aria-hidden prop
146-
* is present or the value is true. Will also return true if
147-
* there is an input with type='hidden'.
148-
*
149-
* <div aria-hidden /> is equivalent to the DOM as <div aria-hidden=true />.
150-
* ref: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/src/util/isHiddenFromScreenReader.js
151-
*/
152-
fn is_hidden_from_screen_reader(node: &AnyJsxElement, element_name: &str) -> bool {
153-
node.find_attribute_by_name("aria-hidden")
154-
.is_some_and(|attr| {
155-
attr.as_static_value()
156-
.is_none_or(|val| val.text() == "true")
157-
})// <div aria-hidden />
158-
|| (element_name == "input"
159-
&& node.find_attribute_by_name("type").is_some_and(|attr| {
160-
attr.as_static_value()
161-
.is_some_and(|val| val.text() == "hidden")
162-
})) // <input type="hidden" />
163-
}

0 commit comments

Comments
 (0)