Skip to content

Commit

Permalink
feat: noAriaHiddenOnFocusable rule (#822)
Browse files Browse the repository at this point in the history
  • Loading branch information
vasucp1207 authored Nov 23, 2023
1 parent f32bb9e commit cc60a55
Show file tree
Hide file tree
Showing 16 changed files with 493 additions and 57 deletions.
1 change: 1 addition & 0 deletions crates/biome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ define_categories! {
"lint/correctness/useValidForDirection": "https://biomejs.dev/linter/rules/use-valid-for-direction",
"lint/correctness/useYield": "https://biomejs.dev/linter/rules/use-yield",
"lint/nursery/noApproximativeNumericConstant": "https://biomejs.dev/linter/rules/no-approximative-numeric-constant",
"lint/nursery/noAriaHiddenOnFocusable": "https://biomejs.dev/linter/rules/no-aria-hidden-on-focusable",
"lint/nursery/noDefaultExport": "https://biomejs.dev/lint/rules/no-default-export",
"lint/nursery/noDuplicateJsonKeys": "https://biomejs.dev/linter/rules/no-duplicate-json-keys",
"lint/nursery/noEmptyBlockStatements": "https://biomejs.dev/linter/rules/no-empty-block-statements",
Expand Down
2 changes: 2 additions & 0 deletions crates/biome_js_analyze/src/aria_analyzers/nursery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
use biome_analyze::declare_group;

pub(crate) mod no_aria_hidden_on_focusable;
pub(crate) mod no_interactive_element_to_noninteractive_role;
pub(crate) mod use_aria_activedescendant_with_tabindex;
pub(crate) mod use_valid_aria_role;
Expand All @@ -10,6 +11,7 @@ declare_group! {
pub (crate) Nursery {
name : "nursery" ,
rules : [
self :: no_aria_hidden_on_focusable :: NoAriaHiddenOnFocusable ,
self :: no_interactive_element_to_noninteractive_role :: NoInteractiveElementToNoninteractiveRole ,
self :: use_aria_activedescendant_with_tabindex :: UseAriaActivedescendantWithTabindex ,
self :: use_valid_aria_role :: UseValidAriaRole ,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
use crate::{aria_services::Aria, JsRuleAction};
use biome_analyze::{
context::RuleContext, declare_rule, ActionCategory, FixKind, Rule, RuleDiagnostic,
};
use biome_console::markup;
use biome_diagnostics::Applicability;
use biome_js_syntax::jsx_ext::AnyJsxElement;
use biome_rowan::{AstNode, BatchMutationExt};

declare_rule! {
/// Enforce that aria-hidden="true" is not set on focusable elements.
///
/// `aria-hidden="true"` can be used to hide purely decorative content from screen reader users.
/// A focusable element with `aria-hidden="true"` can be reached by keyboard.
/// This can lead to confusion or unexpected behavior for screen reader users.
///
/// Source: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/no-aria-hidden-on-focusable.md
///
/// ## Example
///
/// ### Invalid
///
/// ```js,expect_diagnostic
/// <div aria-hidden="true" tabIndex="0" />
/// ```
///
/// ```js, expect_diagnostic
/// <a href="/" aria-hidden="true" />
/// ```
///
/// ## Valid
///
/// ```js
/// <button aria-hidden="true" tabIndex="-1" />
/// ```
///
/// ```js
/// <div aria-hidden="true"><a href="#"></a></div>
/// ```
///
/// ## Resources
///
/// - [aria-hidden elements do not contain focusable elements](https://dequeuniversity.com/rules/axe/html/4.4/aria-hidden-focus)
/// - [Element with aria-hidden has no content in sequential focus navigation](https://www.w3.org/WAI/standards-guidelines/act/rules/6cfa84/proposed/)
/// - [MDN aria-hidden](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-hidden)
///
pub(crate) NoAriaHiddenOnFocusable {
version: "1.3.0",
name: "noAriaHiddenOnFocusable",
recommended: true,
fix_kind: FixKind::Unsafe,
}
}

impl Rule for NoAriaHiddenOnFocusable {
type Query = Aria<AnyJsxElement>;
type State = ();
type Signals = Option<Self::State>;
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let node = ctx.query();
let aria_roles = ctx.aria_roles();
let element_name = node.name().ok()?.as_jsx_name()?.value_token().ok()?;

if node.is_element() {
let aria_hidden_attr = node.find_attribute_by_name("aria-hidden")?;
let attr_static_val = aria_hidden_attr.as_static_value()?;
let attr_text = attr_static_val.text();

let attributes = ctx.extract_attributes(&node.attributes());

if attr_text == "false" {
return None;
}

if let Some(tabindex_attr) = node.find_attribute_by_name("tabIndex") {
if let Some(tabindex_static) = tabindex_attr.as_static_value() {
let tabindex_text = tabindex_static.text();
let tabindex_val = tabindex_text.trim().parse::<i32>();

if let Ok(num) = tabindex_val {
return (num >= 0).then_some(());
}
}
}

if !aria_roles.is_not_interactive_element(element_name.text_trimmed(), attributes) {
return Some(());
}
}

None
}

fn diagnostic(ctx: &RuleContext<Self>, _: &Self::State) -> Option<RuleDiagnostic> {
let node = ctx.query();
Some(
RuleDiagnostic::new(
rule_category!(),
node.range(),
markup! {
"Disallow "<Emphasis>"aria-hidden=\"true\""</Emphasis>" from being set on focusable elements."
},
)
.note(markup! {
""<Emphasis>"aria-hidden"</Emphasis>" should not be set to "<Emphasis>"true"</Emphasis>" on focusable elements because this can lead to confusing behavior for screen reader users."
}),
)
}

fn action(ctx: &RuleContext<Self>, _: &Self::State) -> Option<JsRuleAction> {
let node = ctx.query();
let mut mutation = ctx.root().begin();
let aria_hidden_attr = node.find_attribute_by_name("aria-hidden")?;
mutation.remove_node(aria_hidden_attr);
Some(JsRuleAction {
category: ActionCategory::QuickFix,
applicability: Applicability::MaybeIncorrect,
message: markup! { "Remove the aria-hidden attribute from the element." }.to_owned(),
mutation,
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<>
<div aria-hidden="true" tabIndex="0" />
<input aria-hidden="true" />
<a href="/" aria-hidden="true" />
<button aria-hidden="true" />
<textarea aria-hidden="true" />
<p tabIndex="0" aria-hidden="true">text</p>
</>
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: invalid.jsx
---
# Input
```js
<>
<div aria-hidden="true" tabIndex="0" />
<input aria-hidden="true" />
<a href="/" aria-hidden="true" />
<button aria-hidden="true" />
<textarea aria-hidden="true" />
<p tabIndex="0" aria-hidden="true">text</p>
</>
```

# Diagnostics
```
invalid.jsx:2:3 lint/nursery/noAriaHiddenOnFocusable FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! Disallow aria-hidden="true" from being set on focusable elements.
1 │ <>
> 2 │ <div aria-hidden="true" tabIndex="0" />
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3 │ <input aria-hidden="true" />
4 │ <a href="/" aria-hidden="true" />
i aria-hidden should not be set to true on focusable elements because this can lead to confusing behavior for screen reader users.
i Unsafe fix: Remove the aria-hidden attribute from the element.
2 │ ··<div·aria-hidden="true"·tabIndex="0"·/>
│ -------------------
```

```
invalid.jsx:3:3 lint/nursery/noAriaHiddenOnFocusable FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! Disallow aria-hidden="true" from being set on focusable elements.
1 │ <>
2 │ <div aria-hidden="true" tabIndex="0" />
> 3 │ <input aria-hidden="true" />
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4 │ <a href="/" aria-hidden="true" />
5 │ <button aria-hidden="true" />
i aria-hidden should not be set to true on focusable elements because this can lead to confusing behavior for screen reader users.
i Unsafe fix: Remove the aria-hidden attribute from the element.
3 │ ··<input·aria-hidden="true"·/>
│ -------------------
```

```
invalid.jsx:4:3 lint/nursery/noAriaHiddenOnFocusable FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! Disallow aria-hidden="true" from being set on focusable elements.
2 │ <div aria-hidden="true" tabIndex="0" />
3 │ <input aria-hidden="true" />
> 4 │ <a href="/" aria-hidden="true" />
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
5 │ <button aria-hidden="true" />
6 │ <textarea aria-hidden="true" />
i aria-hidden should not be set to true on focusable elements because this can lead to confusing behavior for screen reader users.
i Unsafe fix: Remove the aria-hidden attribute from the element.
4 │ ··<a·href="/"·aria-hidden="true"·/>
│ -------------------
```

```
invalid.jsx:5:3 lint/nursery/noAriaHiddenOnFocusable FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! Disallow aria-hidden="true" from being set on focusable elements.
3 │ <input aria-hidden="true" />
4 │ <a href="/" aria-hidden="true" />
> 5 │ <button aria-hidden="true" />
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
6 │ <textarea aria-hidden="true" />
7 │ <p tabIndex="0" aria-hidden="true">text</p>
i aria-hidden should not be set to true on focusable elements because this can lead to confusing behavior for screen reader users.
i Unsafe fix: Remove the aria-hidden attribute from the element.
5 │ ··<button·aria-hidden="true"·/>
│ -------------------
```

```
invalid.jsx:6:3 lint/nursery/noAriaHiddenOnFocusable FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! Disallow aria-hidden="true" from being set on focusable elements.
4 │ <a href="/" aria-hidden="true" />
5 │ <button aria-hidden="true" />
> 6 │ <textarea aria-hidden="true" />
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
7 │ <p tabIndex="0" aria-hidden="true">text</p>
8 │ </>
i aria-hidden should not be set to true on focusable elements because this can lead to confusing behavior for screen reader users.
i Unsafe fix: Remove the aria-hidden attribute from the element.
6 │ ··<textarea·aria-hidden="true"·/>
│ -------------------
```

```
invalid.jsx:7:3 lint/nursery/noAriaHiddenOnFocusable FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! Disallow aria-hidden="true" from being set on focusable elements.
5 │ <button aria-hidden="true" />
6 │ <textarea aria-hidden="true" />
> 7 │ <p tabIndex="0" aria-hidden="true">text</p>
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
8 │ </>
i aria-hidden should not be set to true on focusable elements because this can lead to confusing behavior for screen reader users.
i Unsafe fix: Remove the aria-hidden attribute from the element.
7 │ ··<p·tabIndex="0"·aria-hidden="true">text</p>
│ ------------------
```


Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<>
<div aria-hidden="true" />
<img aria-hidden="true" />
<a aria-hidden="false" href="#" />
<button aria-hidden="true" tabIndex="-1" />
<a href="/" />
<div aria-hidden="true"><a href="#"></a></div>
<button />
</>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: valid.jsx
---
# Input
```js
<>
<div aria-hidden="true" />
<img aria-hidden="true" />
<a aria-hidden="false" href="#" />
<button aria-hidden="true" tabIndex="-1" />
<a href="/" />
<div aria-hidden="true"><a href="#"></a></div>
<button />
</>
```


Loading

0 comments on commit cc60a55

Please sign in to comment.