mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
feat(linter): eslint-plugin-jsx-a11y aria-activedescendant-has-tabindex (#2012)
Part of: #1141 Based on: - docs: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/aria-activedescendant-has-tabindex.md - code: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/src/rules/aria-activedescendant-has-tabindex.js - test: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/__tests__/src/rules/aria-activedescendant-has-tabindex-test.js
This commit is contained in:
parent
60a927d8f5
commit
198f0e5d73
3 changed files with 172 additions and 0 deletions
|
|
@ -243,6 +243,7 @@ mod jsx_a11y {
|
||||||
pub mod alt_text;
|
pub mod alt_text;
|
||||||
pub mod anchor_has_content;
|
pub mod anchor_has_content;
|
||||||
pub mod anchor_is_valid;
|
pub mod anchor_is_valid;
|
||||||
|
pub mod aria_activedescendant_has_tabindex;
|
||||||
pub mod aria_props;
|
pub mod aria_props;
|
||||||
pub mod aria_role;
|
pub mod aria_role;
|
||||||
pub mod aria_unsupported_elements;
|
pub mod aria_unsupported_elements;
|
||||||
|
|
@ -510,6 +511,7 @@ oxc_macros::declare_all_lint_rules! {
|
||||||
jsx_a11y::alt_text,
|
jsx_a11y::alt_text,
|
||||||
jsx_a11y::anchor_has_content,
|
jsx_a11y::anchor_has_content,
|
||||||
jsx_a11y::anchor_is_valid,
|
jsx_a11y::anchor_is_valid,
|
||||||
|
jsx_a11y::aria_activedescendant_has_tabindex,
|
||||||
jsx_a11y::aria_props,
|
jsx_a11y::aria_props,
|
||||||
jsx_a11y::aria_unsupported_elements,
|
jsx_a11y::aria_unsupported_elements,
|
||||||
jsx_a11y::click_events_have_key_events,
|
jsx_a11y::click_events_have_key_events,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
use oxc_ast::{
|
||||||
|
ast::{JSXAttribute, JSXAttributeItem, JSXElementName},
|
||||||
|
AstKind,
|
||||||
|
};
|
||||||
|
use oxc_diagnostics::{
|
||||||
|
miette::{self, Diagnostic},
|
||||||
|
thiserror::Error,
|
||||||
|
};
|
||||||
|
use oxc_macros::declare_oxc_lint;
|
||||||
|
use oxc_span::Span;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
context::LintContext,
|
||||||
|
globals::HTML_TAG,
|
||||||
|
rule::Rule,
|
||||||
|
utils::{get_element_type, has_jsx_prop_lowercase, is_interactive_element, parse_jsx_value},
|
||||||
|
AstNode,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Error, Diagnostic)]
|
||||||
|
#[error("eslint-plugin-jsx-a11y(aria-activedescendant-has-tabindex): Enforce elements with aria-activedescendant are tabbable.")]
|
||||||
|
#[diagnostic(
|
||||||
|
severity(warning),
|
||||||
|
help("An element that manages focus with `aria-activedescendant` must have a tabindex.")
|
||||||
|
)]
|
||||||
|
struct AriaActivedescendantHasTabindexDiagnostic(#[label] pub Span);
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct AriaActivedescendantHasTabindex;
|
||||||
|
|
||||||
|
declare_oxc_lint!(
|
||||||
|
/// ### What it does
|
||||||
|
///
|
||||||
|
/// Enforce elements with aria-activedescendant are tabbable.
|
||||||
|
///
|
||||||
|
/// ### Example
|
||||||
|
/// ```jsx
|
||||||
|
/// // Good
|
||||||
|
/// <CustomComponent />
|
||||||
|
/// <CustomComponent aria-activedescendant={someID} />
|
||||||
|
/// <CustomComponent aria-activedescendant={someID} tabIndex={0} />
|
||||||
|
/// <CustomComponent aria-activedescendant={someID} tabIndex={-1} />
|
||||||
|
/// <div />
|
||||||
|
/// <input />
|
||||||
|
/// <div tabIndex={0} />
|
||||||
|
/// <div aria-activedescendant={someID} tabIndex={0} />
|
||||||
|
/// <div aria-activedescendant={someID} tabIndex="0" />
|
||||||
|
/// <div aria-activedescendant={someID} tabIndex={1} />
|
||||||
|
/// <div aria-activedescendant={someID} tabIndex={-1} />
|
||||||
|
/// <div aria-activedescendant={someID} tabIndex="-1" />
|
||||||
|
/// <input aria-activedescendant={someID} />
|
||||||
|
/// <input aria-activedescendant={someID} tabIndex={0} />
|
||||||
|
/// <input aria-activedescendant={someID} tabIndex={-1} />
|
||||||
|
///
|
||||||
|
/// // Bad
|
||||||
|
/// <div aria-activedescendant={someID} />
|
||||||
|
/// ```
|
||||||
|
AriaActivedescendantHasTabindex,
|
||||||
|
correctness
|
||||||
|
);
|
||||||
|
|
||||||
|
impl Rule for AriaActivedescendantHasTabindex {
|
||||||
|
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
|
||||||
|
let AstKind::JSXOpeningElement(jsx_opening_el) = node.kind() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if has_jsx_prop_lowercase(jsx_opening_el, "aria-activedescendant").is_none() {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(element_type) = get_element_type(ctx, jsx_opening_el) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !HTML_TAG.contains(&element_type) {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(JSXAttributeItem::Attribute(tab_index_attr)) =
|
||||||
|
has_jsx_prop_lowercase(jsx_opening_el, "tabIndex")
|
||||||
|
{
|
||||||
|
if !is_valid_tab_index_attr(tab_index_attr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if is_interactive_element(&element_type, jsx_opening_el) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let JSXElementName::Identifier(identifier) = &jsx_opening_el.name else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.diagnostic(AriaActivedescendantHasTabindexDiagnostic(identifier.span));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_valid_tab_index_attr(attr: &JSXAttribute) -> bool {
|
||||||
|
attr.value
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|value| parse_jsx_value(value).ok())
|
||||||
|
.map_or(false, |parsed_value| parsed_value < -1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test() {
|
||||||
|
use crate::tester::Tester;
|
||||||
|
|
||||||
|
fn settings() -> serde_json::Value {
|
||||||
|
serde_json::json!({
|
||||||
|
"jsx-a11y": {
|
||||||
|
"components": {
|
||||||
|
"CustomComponent": "div",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let pass = vec![
|
||||||
|
(r"<CustomComponent />;", None, None, None),
|
||||||
|
(r"<CustomComponent aria-activedescendant={someID} />;", None, None, None),
|
||||||
|
(r"<CustomComponent aria-activedescendant={someID} tabIndex={0} />;", None, None, None),
|
||||||
|
(r"<CustomComponent aria-activedescendant={someID} tabIndex={-1} />;", None, None, None),
|
||||||
|
(
|
||||||
|
r"<CustomComponent aria-activedescendant={someID} tabIndex={0} />;",
|
||||||
|
None,
|
||||||
|
Some(settings()),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(r"<div />;", None, None, None),
|
||||||
|
(r"<input />;", None, None, None),
|
||||||
|
(r"<div tabIndex={0} />;", None, None, None),
|
||||||
|
(r"<div aria-activedescendant={someID} tabIndex={0} />;", None, None, None),
|
||||||
|
(r"<div aria-activedescendant={someID} tabIndex='0' />;", None, None, None),
|
||||||
|
(r"<div aria-activedescendant={someID} tabIndex={1} />;", None, None, None),
|
||||||
|
(r"<input aria-activedescendant={someID} />;", None, None, None),
|
||||||
|
(r"<input aria-activedescendant={someID} tabIndex={1} />;", None, None, None),
|
||||||
|
(r"<input aria-activedescendant={someID} tabIndex={0} />;", None, None, None),
|
||||||
|
(r"<input aria-activedescendant={someID} tabIndex={-1} />;", None, None, None),
|
||||||
|
(r"<div aria-activedescendant={someID} tabIndex={-1} />;", None, None, None),
|
||||||
|
(r"<div aria-activedescendant={someID} tabIndex='-1' />;", None, None, None),
|
||||||
|
(r"<input aria-activedescendant={someID} tabIndex={-1} />;", None, None, None),
|
||||||
|
];
|
||||||
|
|
||||||
|
let fail = vec![
|
||||||
|
(r"<div aria-activedescendant={someID} />;", None, None, None),
|
||||||
|
(r"<CustomComponent aria-activedescendant={someID} />;", None, Some(settings()), None),
|
||||||
|
];
|
||||||
|
|
||||||
|
Tester::new(AriaActivedescendantHasTabindex::NAME, pass, fail).test_and_snapshot();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
---
|
||||||
|
source: crates/oxc_linter/src/tester.rs
|
||||||
|
expression: aria_activedescendant_has_tabindex
|
||||||
|
---
|
||||||
|
⚠ eslint-plugin-jsx-a11y(aria-activedescendant-has-tabindex): Enforce elements with aria-activedescendant are tabbable.
|
||||||
|
╭─[aria_activedescendant_has_tabindex.tsx:1:1]
|
||||||
|
1 │ <div aria-activedescendant={someID} />;
|
||||||
|
· ───
|
||||||
|
╰────
|
||||||
|
help: An element that manages focus with `aria-activedescendant` must have a tabindex.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-jsx-a11y(aria-activedescendant-has-tabindex): Enforce elements with aria-activedescendant are tabbable.
|
||||||
|
╭─[aria_activedescendant_has_tabindex.tsx:1:1]
|
||||||
|
1 │ <CustomComponent aria-activedescendant={someID} />;
|
||||||
|
· ───────────────
|
||||||
|
╰────
|
||||||
|
help: An element that manages focus with `aria-activedescendant` must have a tabindex.
|
||||||
|
|
||||||
|
|
||||||
Loading…
Reference in a new issue