diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs index 6b35c5672a997..345415717e1cd 100644 --- a/crates/oxc_linter/src/generated/rule_runner_impls.rs +++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs @@ -1806,6 +1806,11 @@ impl RuleRunner for crate::rules::jsx_a11y::no_distracting_elements::NoDistracti const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run; } +impl RuleRunner for crate::rules::jsx_a11y::no_noninteractive_element_interactions::NoNoninteractiveElementInteractions { + const NODE_TYPES: Option<&AstTypesBitset> = Some(&AstTypesBitset::from_types(&[AstType::JSXOpeningElement])); + const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run; +} + impl RuleRunner for crate::rules::jsx_a11y::no_noninteractive_tabindex::NoNoninteractiveTabindex { 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 e888a64d1b9b5..79c697bafaaf4 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -563,6 +563,7 @@ pub(crate) mod jsx_a11y { pub mod no_aria_hidden_on_focusable; pub mod no_autofocus; pub mod no_distracting_elements; + pub mod no_noninteractive_element_interactions; pub mod no_noninteractive_tabindex; pub mod no_redundant_roles; pub mod no_static_element_interactions; @@ -997,6 +998,7 @@ oxc_macros::declare_all_lint_rules! { jsx_a11y::lang, jsx_a11y::media_has_caption, jsx_a11y::mouse_events_have_key_events, + jsx_a11y::no_noninteractive_element_interactions, jsx_a11y::no_noninteractive_tabindex, jsx_a11y::no_static_element_interactions, jsx_a11y::no_access_key, diff --git a/crates/oxc_linter/src/rules/jsx_a11y/no_noninteractive_element_interactions.rs b/crates/oxc_linter/src/rules/jsx_a11y/no_noninteractive_element_interactions.rs new file mode 100644 index 0000000000000..099de0525f610 --- /dev/null +++ b/crates/oxc_linter/src/rules/jsx_a11y/no_noninteractive_element_interactions.rs @@ -0,0 +1,695 @@ +use cow_utils::CowUtils; +use oxc_ast::{ + AstKind, + ast::{JSXAttributeItem, JSXAttributeValue}, +}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::{CompactStr, GetSpan, Span}; +use schemars::JsonSchema; +use serde::Deserialize; + +use crate::{ + AstNode, + context::LintContext, + globals::HTML_TAG, + rule::{DefaultRuleConfig, Rule}, + utils::{ + get_element_type, get_string_literal_prop_value, has_jsx_prop, has_jsx_prop_ignore_case, + is_hidden_from_screen_reader, is_presentation_role, + }, +}; + +use oxc_ast::ast::JSXOpeningElement; + +fn no_noninteractive_element_interactions_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn( + "Non-interactive elements should not be assigned mouse or keyboard event listeners.", + ) + .with_help("Add an interactive role or use a semantic HTML element instead.") + .with_label(span) +} + +const DEFAULT_HANDLERS: &[&str] = + &["onClick", "onMouseDown", "onMouseUp", "onKeyPress", "onKeyDown", "onKeyUp"]; + +fn is_content_editable(node: &JSXOpeningElement) -> bool { + has_jsx_prop_ignore_case(node, "contentEditable") + .and_then(|item| get_string_literal_prop_value(item)) + .is_some_and(|value| value == "true") +} + +fn is_null_handler_value(value: &JSXAttributeValue) -> bool { + matches!( + value, + JSXAttributeValue::ExpressionContainer(container) + if matches!(&container.expression, oxc_ast::ast::JSXExpression::NullLiteral(_)) + ) +} + +#[derive(Debug, Default, Clone, Deserialize)] +pub struct NoNoninteractiveElementInteractions(Box); + +impl std::ops::Deref for NoNoninteractiveElementInteractions { + type Target = NoNoninteractiveElementInteractionsConfig; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug, Default, Clone, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", default)] +pub struct NoNoninteractiveElementInteractionsConfig { + /// An array of event handler names that should trigger this rule (e.g., `onClick`, `onKeyDown`). + handlers: Option>, +} + +declare_oxc_lint!( + /// ### What it does + /// + /// Enforces that non-interactive HTML elements do not have interactive handlers assigned. + /// + /// ### Why is this bad? + /// + /// Non-interactive HTML elements and non-interactive ARIA roles indicate content intended solely for display, + /// not for user interaction. Attaching mouse or keyboard event handlers to these elements creates + /// accessibility violations by misleading users of assistive technologies. + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```jsx + ///
  • {}} /> + ///
    {}} /> + ///

    {}} /> + ///
    {}} /> + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```jsx + ///
    {}} role="button" /> + ///