From ad8b0cbdcb7f49456affcf86dd36200b5814bf8f Mon Sep 17 00:00:00 2001 From: keita hino Date: Sat, 13 Jan 2024 18:58:32 +0900 Subject: [PATCH 1/2] feat(linter): eslint-plugin-jsx-a11y aria-activedescendant-has-tabindex --- crates/oxc_linter/src/rules.rs | 2 + .../aria_activedescendant_has_tabindex.rs | 147 ++++++++++++++++++ .../aria_activedescendant_has_tabindex.snap | 19 +++ 3 files changed, 168 insertions(+) create mode 100644 crates/oxc_linter/src/rules/jsx_a11y/aria_activedescendant_has_tabindex.rs create mode 100644 crates/oxc_linter/src/snapshots/aria_activedescendant_has_tabindex.snap diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 969eb8e12f0fe..9b002d4cd0db2 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -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; @@ -507,6 +508,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, diff --git a/crates/oxc_linter/src/rules/jsx_a11y/aria_activedescendant_has_tabindex.rs b/crates/oxc_linter/src/rules/jsx_a11y/aria_activedescendant_has_tabindex.rs new file mode 100644 index 0000000000000..00b3729c2707d --- /dev/null +++ b/crates/oxc_linter/src/rules/jsx_a11y/aria_activedescendant_has_tabindex.rs @@ -0,0 +1,147 @@ +use oxc_ast::{ + ast::{JSXAttribute, JSXAttributeItem}, + 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 + /// + /// + /// + /// + ///
+ /// + ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// + /// + /// + /// + /// // Bad + ///
+ /// ``` + 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; + } + + ctx.diagnostic(AriaActivedescendantHasTabindexDiagnostic(jsx_opening_el.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";", None, None, None), + (r";", None, None, None), + (r";", None, None, None), + (r";", None, None, None), + ( + r";", + None, + Some(settings()), + None, + ), + (r"
;", None, None, None), + (r";", None, None, None), + (r"
;", None, None, None), + (r"
;", None, None, None), + (r"
;", None, None, None), + (r"
;", None, None, None), + (r";", None, None, None), + (r";", None, None, None), + (r";", None, None, None), + (r";", None, None, None), + (r"
;", None, None, None), + (r"
;", None, None, None), + (r";", None, None, None), + ]; + + let fail = vec![ + (r"
;", None, None, None), + (r";", None, Some(settings()), None), + ]; + + Tester::new(AriaActivedescendantHasTabindex::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/aria_activedescendant_has_tabindex.snap b/crates/oxc_linter/src/snapshots/aria_activedescendant_has_tabindex.snap new file mode 100644 index 0000000000000..90c515df0ef79 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/aria_activedescendant_has_tabindex.snap @@ -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 │
; + · ────────────────────────────────────── + ╰──── + 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 │ ; + · ────────────────────────────────────────────────── + ╰──── + help: An element that manages focus with `aria-activedescendant` must have a tabindex. + + From de5111e32ebba7dc43cc097588bb97afb20b2b73 Mon Sep 17 00:00:00 2001 From: keita hino Date: Sun, 14 Jan 2024 10:06:09 +0900 Subject: [PATCH 2/2] fix: highlight only the div element --- .../rules/jsx_a11y/aria_activedescendant_has_tabindex.rs | 8 ++++++-- .../src/snapshots/aria_activedescendant_has_tabindex.snap | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/oxc_linter/src/rules/jsx_a11y/aria_activedescendant_has_tabindex.rs b/crates/oxc_linter/src/rules/jsx_a11y/aria_activedescendant_has_tabindex.rs index 00b3729c2707d..7b604dbd6ef40 100644 --- a/crates/oxc_linter/src/rules/jsx_a11y/aria_activedescendant_has_tabindex.rs +++ b/crates/oxc_linter/src/rules/jsx_a11y/aria_activedescendant_has_tabindex.rs @@ -1,5 +1,5 @@ use oxc_ast::{ - ast::{JSXAttribute, JSXAttributeItem}, + ast::{JSXAttribute, JSXAttributeItem, JSXElementName}, AstKind, }; use oxc_diagnostics::{ @@ -87,7 +87,11 @@ impl Rule for AriaActivedescendantHasTabindex { return; } - ctx.diagnostic(AriaActivedescendantHasTabindexDiagnostic(jsx_opening_el.span)); + let JSXElementName::Identifier(identifier) = &jsx_opening_el.name else { + return; + }; + + ctx.diagnostic(AriaActivedescendantHasTabindexDiagnostic(identifier.span)); } } diff --git a/crates/oxc_linter/src/snapshots/aria_activedescendant_has_tabindex.snap b/crates/oxc_linter/src/snapshots/aria_activedescendant_has_tabindex.snap index 90c515df0ef79..ad20d3880b1d5 100644 --- a/crates/oxc_linter/src/snapshots/aria_activedescendant_has_tabindex.snap +++ b/crates/oxc_linter/src/snapshots/aria_activedescendant_has_tabindex.snap @@ -5,14 +5,14 @@ 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 │
; - · ────────────────────────────────────── + · ─── ╰──── 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 │ ; - · ────────────────────────────────────────────────── + · ─────────────── ╰──── help: An element that manages focus with `aria-activedescendant` must have a tabindex.