Skip to content
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

feat(lint): useSemanticElements #2867

Merged
merged 9 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

38 changes: 32 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 @@ -146,6 +146,7 @@ define_categories! {
"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/useNumberToFixedDigitsArgument": "https://biomejs.dev/linter/rules/use-number-to-fixed-digits-argument",
"lint/nursery/useSemanticElements": "https://biomejs.dev/linter/rules/use-semantic-elements",
"lint/nursery/useSortedClasses": "https://biomejs.dev/linter/rules/use-sorted-classes",
"lint/nursery/useThrowNewError": "https://biomejs.dev/linter/rules/use-throw-new-error",
"lint/nursery/useTopLevelRegex": "https://biomejs.dev/linter/rules/use-top-level-regex",
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.

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

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

declare_rule! {
/// It detects the use of `role` attributes in JSX elements and suggests using semantic elements instead.
///
/// The `role` attribute is used to define the purpose of an element, but it should be used as a last resort. Using semantic elements like `<button>`, `<input>`, `<textarea>`, `<a>`, `<img>`, `<table>`, `<article>`, `<section>`, `<nav>`, `<aside>`, `<header>`, `<footer>`, `<main>`, `<figure>`, `<figcaption>`, `<details>`, `<summary>`, `<dialog>`, `<menu>`, `<menuitem>`, `<fieldset>`, `<legend>`, `<caption>`, `<colgroup>`, `<col>`, `<optgroup>`, `<option>`, `<select>`, `<datalist>`, `<output>`, `<progress>`, `<meter>`, `<time>`, `<audio>`, `<video>`, `<track>`, `<source>`, `<embed>`, `<object>`, `<param>`, `<iframe>`, `<canvas>`, `<map>`, `<area>`, `<svg>`, `<math>` are more accessible and provide better semantics.
///
///
/// ## Examples
///
/// ### Invalid
///
/// ```jsx,expect_diagnostic
/// <div role="checkbox">
/// ```
///
/// ```jsx,expect_diagnostic
/// <div role="img">
/// ```
///
/// ### Valid
///
/// ```jsx
/// <div>...</div>
/// <header>...</header>
/// <img alt="" src="image.jpg" />
/// ```
///
pub UseSemanticElements {
version: "next",
name: "useSemanticElements",
language: "jsx",
sources: &[RuleSource::EslintJsxA11y("prefer-tag-over-role")],
recommended: true,
}
}

impl Rule for UseSemanticElements {
type Query = Aria<JsxOpeningElement>;
type State = AnyJsxAttribute;
type Signals = Option<Self::State>;
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let node = ctx.query();
let role_attributes = node.find_attribute_by_name("role").unwrap();

if let Some(attr) = role_attributes {
let extract_attributes = ctx.extract_attributes(&node.attributes());

let element = node.name().ok()?.as_jsx_name()?.value_token().ok()?;
let element_name = element.text_trimmed();
let is_not_interative =
AriaRoles.is_not_interactive_element(element_name, extract_attributes);
Copy link
Member

Choose a reason for hiding this comment

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

@fujiyamaorange why do we need to check that the element/role is not interactive? I don't get this part.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As far as I remember, there are some proper cases with the role attribute.
Regarding, role="checkbox", it's allowed to use this attribute when it also has an aria-checked attribute in an element.

https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/checkbox_role

if is_not_interative {
return Some(AnyJsxAttribute::from(attr));
}
}

None
}

fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
Some(
RuleDiagnostic::new(
rule_category!(),
state.range(),
markup! {
"The element with this role can be changed to a DOM element that already this role."
},
)
.footer_list(
markup! {
"For examples and more information, see" <Hyperlink href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles">"WAI-ARIA Roles"</Hyperlink>
},
&["<button>", "<input>", "<textarea>", "<a>", "<img>", "<table>", "<article>", "<section>", "<nav>", "<aside>", "<header>", "<footer>", "<main>", "<figure>", "<figcaption>", "<details>", "<summary>", "<dialog>", "<menu>", "<menuitem>", "<fieldset>", "<legend>", "<caption>", "<colgroup>", "<col>", "<optgroup>", "<option>", "<select>", "<datalist>", "<output>", "<progress>", "<meter>", "<time>", "<audio>", "<video>", "<track>", "<source>", "<embed>", "<object>", "<param>", "<iframe>", "<canvas>", "<map>", "<area>", "<svg>", "<math>"]
),
)
}
}
2 changes: 2 additions & 0 deletions crates/biome_js_analyze/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,8 @@ pub type UseOptionalChain =
pub type UseRegexLiterals =
<lint::complexity::use_regex_literals::UseRegexLiterals as biome_analyze::Rule>::Options;
pub type UseSelfClosingElements = < lint :: style :: use_self_closing_elements :: UseSelfClosingElements as biome_analyze :: Rule > :: Options ;
pub type UseSemanticElements =
<lint::nursery::use_semantic_elements::UseSemanticElements as biome_analyze::Rule>::Options;
pub type UseShorthandArrayType =
<lint::style::use_shorthand_array_type::UseShorthandArrayType as biome_analyze::Rule>::Options;
pub type UseShorthandAssign =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<>
<div role="checkbox" ></div>
<div role="radio" ></div>
<div role="option" ></div>
<div role="combobox" ></div>
<div role="heading" ></div>
<div role="separator" ></div>
<div role="button" ></div>
<div role="article" ></div>
<div role="dialog" ></div>
<div role="alert" ></div>
<div role="alertdialog" ></div>
<div role="cell" ></div>
<div role="columnheader" ></div>
<div role="definition" ></div>
<div role="figure" ></div>
<div role="form" ></div>
<div role="graphics-document" ></div>
<div role="graphics-object" ></div>
<div role="grid" ></div>
<div role="gridcell" ></div>
<div role="group" ></div>
<div role="img" ></div>
<div role="link" ></div>
<div role="list" ></div>
<div role="listbox" ></div>
<div role="listitem" ></div>
<div role="navigation" ></div>
<div role="row" ></div>
<div role="rowgroup" ></div>
<div role="rowheader" ></div>
<div role="search" ></div>
<div role="searchbox" ></div>
<div role="table" ></div>
<div role="term" ></div>
<div role="textbox" ></div>
<div role="generic" ></div>
<div role="caption" ></div>
<div role="main" ></div>
<div role="time" ></div>
<div role="p" ></div>
<div role="aside" ></div>
<div role="blockquote" ></div>
<div role="associationlist" ></div>
<div role="status" ></div>
<div role="contentinfo" ></div>
<div role="region" ></div>
</>
Loading
Loading