Skip to content

Commit

Permalink
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 0 deletions.
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
@@ -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.


0 comments on commit 198f0e5

Please sign in to comment.