Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/fix-use-semantic-elements-false-positive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@biomejs/biome": patch
---

Fixed [#5212](https://github.com/biomejs/biome/issues/5212): [`useSemanticElements`](https://biomejs.dev/linter/rules/use-semantic-elements/) no longer reports a diagnostic when a semantic element already has its corresponding role attribute (e.g. `<nav role="navigation">`, `<footer role="contentinfo">`). These cases are now correctly left to [`noRedundantRoles`](https://biomejs.dev/linter/rules/no-redundant-roles/).
40 changes: 36 additions & 4 deletions crates/biome_js_analyze/src/lint/a11y/use_semantic_elements.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ declare_lint_rule! {
/// <p>&#x1F408; &#x1F602;</p>
/// </div>
/// ```
///
/// Semantic elements with a matching role are not flagged (see [noRedundantRoles](https://biomejs.dev/linter/rules/no-redundant-roles/)):
///
/// ```jsx
/// <>
/// <nav role="navigation"></nav>
/// <footer role="contentinfo"></footer>
/// </>;
/// ```
pub UseSemanticElements {
version: "1.8.0",
name: "useSemanticElements",
Expand Down Expand Up @@ -95,11 +104,34 @@ impl Rule for UseSemanticElements {
}

let role = AriaRole::from_roles(role_value)?;
if role.base_html_elements().is_empty() && role.related_html_elements().is_empty() {
None
} else {
Some(role_attribute)
let semantic_elements = role.base_html_elements();
let related_elements = role.related_html_elements();
if semantic_elements.is_empty() && related_elements.is_empty() {
return None;
}

// If the current element is already a semantic element for this role
// (matching both the tag name and any required attributes),
// don't flag it. That case is handled by `noRedundantRoles`.
let element_name = node.name_value_token().ok()?;
let element_name = element_name.text_trimmed();
let is_already_semantic =
semantic_elements
.iter()
.chain(related_elements.iter())
.any(|instance| {
instance.element.as_str() == element_name
&& instance.attributes.iter().all(|required_attr| {
node.find_attribute_by_name(required_attr.attribute.as_str())
.and_then(|attr| attr.as_static_value())
.is_some_and(|value| value.text() == required_attr.value)
})
});
if is_already_semantic {
return None;
}

Some(role_attribute)
}

fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,12 @@
<div role="status" ></div>
<div role="contentinfo" ></div>
<div role="region" ></div>

{/* Constrained elements: tag matches but required attributes are missing */}
<input role="checkbox" ></input>
<input role="radio" ></input>
<input role="searchbox" ></input>
<input role="textbox" ></input>
<th role="columnheader" ></th>
<th role="rowheader" ></th>
</>
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ expression: invalid.jsx
<div role="status" ></div>
<div role="contentinfo" ></div>
<div role="region" ></div>

{/* Constrained elements: tag matches but required attributes are missing */}
<input role="checkbox" ></input>
<input role="radio" ></input>
<input role="searchbox" ></input>
<input role="textbox" ></input>
<th role="columnheader" ></th>
<th role="rowheader" ></th>
</>

```
Expand Down Expand Up @@ -502,25 +510,6 @@ invalid.jsx:31:10 lint/a11y/useSemanticElements ━━━━━━━━━━
i For examples and more information, see WAI-ARIA Roles


```

```
invalid.jsx:32:10 lint/a11y/useSemanticElements ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× The elements with this role can be changed to the following elements:
<div>
<span>

30 │ <div role="term" ></div>
31 │ <div role="textbox" ></div>
> 32 │ <div role="generic" ></div>
│ ^^^^^^^^^^^^^^
33 │ <div role="caption" ></div>
34 │ <div role="main" ></div>

i For examples and more information, see WAI-ARIA Roles


```

```
Expand Down Expand Up @@ -662,7 +651,7 @@ invalid.jsx:40:10 lint/a11y/useSemanticElements ━━━━━━━━━━
> 40 │ <div role="contentinfo" ></div>
│ ^^^^^^^^^^^^^^^^^^
41 │ <div role="region" ></div>
42 │ </>
42 │

i For examples and more information, see WAI-ARIA Roles

Expand All @@ -679,8 +668,116 @@ invalid.jsx:41:10 lint/a11y/useSemanticElements ━━━━━━━━━━
40 │ <div role="contentinfo" ></div>
> 41 │ <div role="region" ></div>
│ ^^^^^^^^^^^^^
42 │ </>
43 │
42 │
43 │ {/* Constrained elements: tag matches but required attributes are missing */}

i For examples and more information, see WAI-ARIA Roles


```

```
invalid.jsx:44:12 lint/a11y/useSemanticElements ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× The elements with this role can be changed to the following elements:
<input type="checkbox">

43 │ {/* Constrained elements: tag matches but required attributes are missing */}
> 44 │ <input role="checkbox" ></input>
│ ^^^^^^^^^^^^^^^
45 │ <input role="radio" ></input>
46 │ <input role="searchbox" ></input>

i For examples and more information, see WAI-ARIA Roles


```

```
invalid.jsx:45:12 lint/a11y/useSemanticElements ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× The elements with this role can be changed to the following elements:
<input type="radio">

43 │ {/* Constrained elements: tag matches but required attributes are missing */}
44 │ <input role="checkbox" ></input>
> 45 │ <input role="radio" ></input>
│ ^^^^^^^^^^^^
46 │ <input role="searchbox" ></input>
47 │ <input role="textbox" ></input>

i For examples and more information, see WAI-ARIA Roles


```

```
invalid.jsx:46:12 lint/a11y/useSemanticElements ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× The elements with this role can be changed to the following elements:
<input type="search">

44 │ <input role="checkbox" ></input>
45 │ <input role="radio" ></input>
> 46 │ <input role="searchbox" ></input>
│ ^^^^^^^^^^^^^^^^
47 │ <input role="textbox" ></input>
48 │ <th role="columnheader" ></th>

i For examples and more information, see WAI-ARIA Roles


```

```
invalid.jsx:47:12 lint/a11y/useSemanticElements ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× The elements with this role can be changed to the following elements:
<input type="text">
<textarea>

45 │ <input role="radio" ></input>
46 │ <input role="searchbox" ></input>
> 47 │ <input role="textbox" ></input>
│ ^^^^^^^^^^^^^^
48 │ <th role="columnheader" ></th>
49 │ <th role="rowheader" ></th>

i For examples and more information, see WAI-ARIA Roles


```

```
invalid.jsx:48:9 lint/a11y/useSemanticElements ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× The elements with this role can be changed to the following elements:
<th scope="col">

46 │ <input role="searchbox" ></input>
47 │ <input role="textbox" ></input>
> 48 │ <th role="columnheader" ></th>
│ ^^^^^^^^^^^^^^^^^^^
49 │ <th role="rowheader" ></th>
50 │ </>

i For examples and more information, see WAI-ARIA Roles


```

```
invalid.jsx:49:9 lint/a11y/useSemanticElements ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× The elements with this role can be changed to the following elements:
<th scope="row">

47 │ <input role="textbox" ></input>
48 │ <th role="columnheader" ></th>
> 49 │ <th role="rowheader" ></th>
│ ^^^^^^^^^^^^^^^^
50 │ </>
51 │

i For examples and more information, see WAI-ARIA Roles

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -502,25 +502,6 @@ invalid_self_closing.jsx:31:10 lint/a11y/useSemanticElements ━━━━━━
i For examples and more information, see WAI-ARIA Roles


```

```
invalid_self_closing.jsx:32:10 lint/a11y/useSemanticElements ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× The elements with this role can be changed to the following elements:
<div>
<span>

30 │ <div role="term" />
31 │ <div role="textbox" />
> 32 │ <div role="generic" />
│ ^^^^^^^^^^^^^^
33 │ <div role="caption" />
34 │ <div role="main" />

i For examples and more information, see WAI-ARIA Roles


```

```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,24 @@ export const Component2 = () => (
{children}
</Card>
</>

{/* Semantic elements with a matching role should not be flagged (issue #5212) */}
<>
<nav role="navigation"></nav>
<footer role="contentinfo"></footer>
<aside role="complementary"></aside>
<article role="article"></article>
<button role="button"></button>
<form role="form"></form>
<main role="main"></main>
<table role="table"></table>
<hr role="separator" />
{/* Constrained elements: tag + matching attributes should not be flagged */}
<input role="checkbox" type="checkbox" />
<input role="radio" type="radio" />
<input role="searchbox" type="search" />
<input role="textbox" type="text" />
<th role="columnheader" scope="col"></th>
<th role="rowheader" scope="row"></th>
</>

Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,27 @@ export const Component2 = () => (
</Card>
</>

{/* Semantic elements with a matching role should not be flagged (issue #5212) */}
<>
<nav role="navigation"></nav>
<footer role="contentinfo"></footer>
<aside role="complementary"></aside>
<article role="article"></article>
<button role="button"></button>
<form role="form"></form>
<main role="main"></main>
<table role="table"></table>
<hr role="separator" />
{/* Constrained elements: tag + matching attributes should not be flagged */}
<input role="checkbox" type="checkbox" />
<input role="radio" type="radio" />
<input role="searchbox" type="search" />
<input role="textbox" type="text" />
<th role="columnheader" scope="col"></th>
<th role="rowheader" scope="row"></th>
</>


```

_Note: The parser emitted 4 diagnostics which are not shown here._