mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 04:08:41 +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 anchor_has_content;
|
||||
pub mod anchor_is_valid;
|
||||
pub mod aria_activedescendant_has_tabindex;
|
||||
pub mod aria_props;
|
||||
pub mod aria_role;
|
||||
pub mod aria_unsupported_elements;
|
||||
|
|
@ -510,6 +511,7 @@ oxc_macros::declare_all_lint_rules! {
|
|||
jsx_a11y::alt_text,
|
||||
jsx_a11y::anchor_has_content,
|
||||
jsx_a11y::anchor_is_valid,
|
||||
jsx_a11y::aria_activedescendant_has_tabindex,
|
||||
jsx_a11y::aria_props,
|
||||
jsx_a11y::aria_unsupported_elements,
|
||||
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