Skip to content

Commit

Permalink
feat(biome_js_analyze): implement useFocusableInteractive (#2710)
Browse files Browse the repository at this point in the history
Co-authored-by: Emanuele Stoppa <[email protected]>
  • Loading branch information
DerTimonius and ematipico authored May 9, 2024
1 parent 6f80b7a commit 1309451
Show file tree
Hide file tree
Showing 12 changed files with 253 additions and 6 deletions.
10 changes: 10 additions & 0 deletions crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 27 additions & 6 deletions crates/biome_configuration/src/linter/rules.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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 @@ -142,6 +142,7 @@ define_categories! {
"lint/nursery/useConsistentBuiltinInstantiation": "https://biomejs.dev/linter/rules/use-consistent-new-builtin",
"lint/nursery/useDefaultSwitchClause": "https://biomejs.dev/linter/rules/use-default-switch-clause",
"lint/nursery/useExplicitLengthCheck": "https://biomejs.dev/linter/rules/use-explicit-length-check",
"lint/nursery/useFocusableInteractive": "https://biomejs.dev/linter/rules/use-focusable-interactive",
"lint/nursery/useGenericFontNames": "https://biomejs.dev/linter/rules/use-generic-font-names",
"lint/nursery/useImportRestrictions": "https://biomejs.dev/linter/rules/use-import-restrictions",
"lint/nursery/useSortedClasses": "https://biomejs.dev/linter/rules/use-sorted-classes",
Expand Down
2 changes: 2 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

104 changes: 104 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery/use_focusable_interactive.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use biome_analyze::{context::RuleContext, declare_rule, Rule, RuleDiagnostic, RuleSource};
use biome_aria::AriaRoles;
use biome_console::markup;
use biome_js_syntax::{jsx_ext::AnyJsxElement, AnyJsxAttributeValue};
use biome_rowan::AstNode;

use crate::services::aria::Aria;

declare_rule! {
/// Elements with an interactive role and interaction handlers must be focusable.
///
/// HTML elements with interactive roles must have `tabIndex` defined to ensure they are
/// focusable. Without tabIndex, assistive technologies may not recognize these elements as
/// interactive.
/// You could also consider switching from an interactive role to its semantic HTML element
/// instead.
///
/// ## Examples
///
/// ### Invalid
///
/// ```js,expect_diagnostic
/// <div role="button" />
/// ```
///
/// ```js,expect_diagnostic
/// <div role="tab" />
/// ```
///
/// ### Valid
///
/// ```js
/// <div role="button" tabIndex={0} />
/// ```
///
/// ```jsx
/// <div />
/// ```
///
pub UseFocusableInteractive {
version: "next",
name: "useFocusableInteractive",
sources: &[RuleSource::EslintJsxA11y("interactive-support-focus")],
recommended: true,
}
}

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

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let node = ctx.query();
if !node.is_element() {
return None;
}

let element_name = node.name().ok()?.as_jsx_name()?.value_token().ok()?;
let aria_roles = ctx.aria_roles();
let attributes = ctx.extract_attributes(&node.attributes());

if aria_roles.is_not_interactive_element(element_name.text_trimmed(), attributes) {
let role_attribute = node.find_attribute_by_name("role");
if let Some(role_attribute) = role_attribute {
let tabindex_attribute = node.find_attribute_by_name("tabIndex");
let role_attribute_value = role_attribute.initializer()?.value().ok()?;
if attribute_has_interactive_role(&role_attribute_value, aria_roles)?
&& tabindex_attribute.is_none()
{
return Some(role_attribute_value.text());
}
}
}
None
}

fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
let node = ctx.query();
Some(
RuleDiagnostic::new(
rule_category!(),
node.range(),
markup! {
"The HTML element with the interactive role "<Emphasis>{state}</Emphasis>" is not focusable."
},
).note(markup! {
"A non-interactive HTML element that is not focusable may not be reachable for users that rely on keyboard navigation, even with an added role like "<Emphasis>{state}</Emphasis>"."
})
.note(markup! {
"Add a "<Emphasis>"tabIndex"</Emphasis>" attribute to make this element focusable."
}),
)
}
}

/// Checks if the given role attribute value is interactive or not based on ARIA roles.
fn attribute_has_interactive_role(
role_attribute_value: &AnyJsxAttributeValue,
aria_roles: &AriaRoles,
) -> Option<bool> {
Some(aria_roles.is_role_interactive(role_attribute_value.as_static_value()?.text()))
}
1 change: 1 addition & 0 deletions crates/biome_js_analyze/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ pub type UseExportType =
<lint::style::use_export_type::UseExportType as biome_analyze::Rule>::Options;
pub type UseFilenamingConvention = < lint :: style :: use_filenaming_convention :: UseFilenamingConvention as biome_analyze :: Rule > :: Options ;
pub type UseFlatMap = <lint::complexity::use_flat_map::UseFlatMap as biome_analyze::Rule>::Options;
pub type UseFocusableInteractive = < lint :: nursery :: use_focusable_interactive :: UseFocusableInteractive as biome_analyze :: Rule > :: Options ;
pub type UseForOf = <lint::style::use_for_of::UseForOf as biome_analyze::Rule>::Options;
pub type UseFragmentSyntax =
<lint::style::use_fragment_syntax::UseFragmentSyntax as biome_analyze::Rule>::Options;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<div>
<div role="button" />
<div role="tab" />
</div>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: invalid.js
---
# Input
```jsx
<div>
<div role="button" />
<div role="tab" />
</div>;

```

# Diagnostics
```
invalid.js:2:2 lint/nursery/useFocusableInteractive ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! The HTML element with the interactive role "button" is not focusable.
1 │ <div>
> 2 │ <div role="button" />
│ ^^^^^^^^^^^^^^^^^^^^^
3 │ <div role="tab" />
4 │ </div>;
i A non-interactive HTML element that is not focusable may not be reachable for users that rely on keyboard navigation, even with an added role like "button".
i Add a tabIndex attribute to make this element focusable.
```

```
invalid.js:3:2 lint/nursery/useFocusableInteractive ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! The HTML element with the interactive role "tab" is not focusable.
1 │ <div>
2 │ <div role="button" />
> 3 │ <div role="tab" />
│ ^^^^^^^^^^^^^^^^^^
4 │ </div>;
5 │
i A non-interactive HTML element that is not focusable may not be reachable for users that rely on keyboard navigation, even with an added role like "tab".
i Add a tabIndex attribute to make this element focusable.
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<div>
<div role="button" tabIndex={0} />

<div role="menu">
<div role="menuitem" tabIndex="0">
Open
</div>
<div role="menuitem" tabIndex="-1">
Save
</div>
<div role="menuitem" tabIndex="-1">
Close
</div>
</div>
<button />
<div role="h1" />
</div>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: valid.js
---
# Input
```jsx
<div>
<div role="button" tabIndex={0} />

<div role="menu">
<div role="menuitem" tabIndex="0">
Open
</div>
<div role="menuitem" tabIndex="-1">
Save
</div>
<div role="menuitem" tabIndex="-1">
Close
</div>
</div>
<button />
<div role="h1" />
</div>;

```
5 changes: 5 additions & 0 deletions packages/@biomejs/backend-jsonrpc/src/workspace.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions packages/@biomejs/biome/configuration_schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 1309451

Please sign in to comment.