diff --git a/apps/oxlint/src/snapshots/fixtures__issue_11644_-c .oxlintrc.json@oxlint.snap b/apps/oxlint/src/snapshots/fixtures__issue_11644_-c .oxlintrc.json@oxlint.snap index ed83557cb465c..67f6465492117 100644 --- a/apps/oxlint/src/snapshots/fixtures__issue_11644_-c .oxlintrc.json@oxlint.snap +++ b/apps/oxlint/src/snapshots/fixtures__issue_11644_-c .oxlintrc.json@oxlint.snap @@ -6,7 +6,7 @@ arguments: -c .oxlintrc.json working directory: fixtures/issue_11644 ---------- Found 0 warnings and 0 errors. -Finished in ms on 1 file with 164 rules using 1 threads. +Finished in ms on 1 file with 165 rules using 1 threads. ---------- CLI result: LintSucceeded ---------- diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs index 22cc2d7791d2a..851b412f98b3d 100644 --- a/crates/oxc_linter/src/generated/rule_runner_impls.rs +++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs @@ -1805,6 +1805,14 @@ impl RuleRunner for crate::rules::jsx_a11y::no_redundant_roles::NoRedundantRoles const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run; } +impl RuleRunner + for crate::rules::jsx_a11y::no_static_element_interactions::NoStaticElementInteractions +{ + const NODE_TYPES: Option<&AstTypesBitset> = + Some(&AstTypesBitset::from_types(&[AstType::JSXOpeningElement])); + const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run; +} + impl RuleRunner for crate::rules::jsx_a11y::prefer_tag_over_role::PreferTagOverRole { const NODE_TYPES: Option<&AstTypesBitset> = Some(&AstTypesBitset::from_types(&[AstType::JSXOpeningElement])); diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 9217d61935025..aebbc6eb2abfb 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -561,6 +561,7 @@ pub(crate) mod jsx_a11y { pub mod no_distracting_elements; pub mod no_noninteractive_tabindex; pub mod no_redundant_roles; + pub mod no_static_element_interactions; pub mod prefer_tag_over_role; pub mod role_has_required_aria_props; pub mod role_supports_aria_props; @@ -982,6 +983,7 @@ oxc_macros::declare_all_lint_rules! { jsx_a11y::media_has_caption, jsx_a11y::mouse_events_have_key_events, jsx_a11y::no_noninteractive_tabindex, + jsx_a11y::no_static_element_interactions, jsx_a11y::no_access_key, jsx_a11y::no_aria_hidden_on_focusable, jsx_a11y::no_autofocus, diff --git a/crates/oxc_linter/src/rules/jsx_a11y/no_static_element_interactions.rs b/crates/oxc_linter/src/rules/jsx_a11y/no_static_element_interactions.rs new file mode 100644 index 0000000000000..3d7b73e62da99 --- /dev/null +++ b/crates/oxc_linter/src/rules/jsx_a11y/no_static_element_interactions.rs @@ -0,0 +1,344 @@ +use cow_utils::CowUtils; +use schemars::JsonSchema; +use serde::Deserialize; + +use oxc_ast::{ + AstKind, + ast::{JSXAttributeItem, JSXAttributeValue}, +}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::{CompactStr, GetSpan, Span}; + +use crate::{ + AstNode, + context::LintContext, + globals::HTML_TAG, + rule::{DefaultRuleConfig, Rule}, + utils::{ + get_element_type, has_jsx_prop, has_jsx_prop_ignore_case, is_hidden_from_screen_reader, + is_interactive_element, is_presentation_role, + }, +}; + +fn no_static_element_interactions_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Static HTML elements with event handlers require a role.") + .with_help("Add a role attribute to this element, or use a semantic HTML element instead.") + .with_label(span) +} + +const DEFAULT_HANDLERS: &[&str] = + &["onClick", "onMouseDown", "onMouseUp", "onKeyPress", "onKeyDown", "onKeyUp"]; + +#[derive(Debug, Default, Clone, Deserialize)] +pub struct NoStaticElementInteractions(Box); + +impl std::ops::Deref for NoStaticElementInteractions { + type Target = NoStaticElementInteractionsConfig; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug, Default, Clone, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", default)] +pub struct NoStaticElementInteractionsConfig { + /// An array of event handler names that should trigger this rule (e.g., `onClick`, `onKeyDown`). + handlers: Option>, + /// If `true`, role attribute values that are JSX expressions (e.g., `role={ROLE}`) are allowed. + /// If `false`, only string literal role values are permitted. + allow_expression_values: bool, +} + +declare_oxc_lint!( + /// ### What it does + /// + /// Enforces that static HTML elements with event handlers must have appropriate ARIA roles. + /// + /// ### Why is this bad? + /// + /// Static HTML elements do not have semantic meaning in accessibility contexts. + /// When these elements receive click or keyboard event handlers, they must declare a role + /// to indicate their interactive purpose to assistive technologies. + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```jsx + ///
{}} /> + /// + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```jsx + ///