diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs
index 662909719357c..5fd2ef8e7a577 100644
--- a/crates/oxc_linter/src/rules.rs
+++ b/crates/oxc_linter/src/rules.rs
@@ -254,6 +254,7 @@ mod jsx_a11y {
pub mod no_autofocus;
pub mod no_distracting_elements;
pub mod prefer_tag_over_role;
+ pub mod role_has_required_aria_props;
pub mod role_support_aria_props;
pub mod scope;
pub mod tab_index_no_positive;
@@ -509,6 +510,7 @@ oxc_macros::declare_all_lint_rules! {
jsx_a11y::no_aria_hidden_on_focusable,
jsx_a11y::no_autofocus,
jsx_a11y::prefer_tag_over_role,
+ jsx_a11y::role_has_required_aria_props,
jsx_a11y::scope,
jsx_a11y::tab_index_no_positive,
jsx_a11y::aria_role,
diff --git a/crates/oxc_linter/src/rules/jsx_a11y/role_has_required_aria_props.rs b/crates/oxc_linter/src/rules/jsx_a11y/role_has_required_aria_props.rs
new file mode 100644
index 0000000000000..a8c8187744a57
--- /dev/null
+++ b/crates/oxc_linter/src/rules/jsx_a11y/role_has_required_aria_props.rs
@@ -0,0 +1,136 @@
+use crate::{context::LintContext, rule::Rule, utils::has_jsx_prop_lowercase, AstNode};
+use oxc_ast::{
+ ast::{JSXAttributeItem, JSXAttributeValue},
+ AstKind,
+};
+use oxc_diagnostics::{
+ miette::{self, Diagnostic},
+ thiserror::{self, Error},
+};
+use oxc_macros::declare_oxc_lint;
+use oxc_span::Span;
+use phf::{phf_map, phf_set};
+
+#[derive(Debug, Error, Diagnostic)]
+#[error(
+ "eslint-plugin-jsx-a11y(role-has-required-aria-props): `{role}` role is missing required aria props `{props}`."
+)]
+#[diagnostic(
+ severity(warning),
+ help("Add missing aria props `{props}` to the element with `{role}` role.")
+)]
+struct RoleHasRequiredAriaPropsDiagnostic {
+ #[label]
+ pub span: Span,
+ pub role: String,
+ pub props: String,
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct RoleHasRequiredAriaProps;
+declare_oxc_lint!(
+ /// ### What it does
+ /// Enforces that elements with ARIA roles must have all required attributes for that role.
+ ///
+ /// ### Why is this bad?
+ /// Certain ARIA roles require specific attributes to express necessary semantics for assistive technology.
+ ///
+ /// ### Example
+ /// ```javascript
+ /// // Bad
+ ///
+ ///
+ /// // Good
+ ///
+ /// ```
+ RoleHasRequiredAriaProps,
+ correctness
+);
+
+static ROLE_TO_REQUIRED_ARIA_PROPS: phf::Map<&'static str, phf::Set<&'static str>> = phf_map! {
+ "checkbox" => phf_set!{"aria-checked"},
+ "radio" => phf_set!{"aria-checked"},
+ "combobox" => phf_set!{"aria-controls", "aria-expanded"},
+ "tab" => phf_set!{"aria-selected"},
+ "slider" => phf_set!{"aria-valuemax", "aria-valuemin", "aria-valuenow"},
+ "scrollbar" => phf_set!{"aria-valuemax", "aria-valuemin", "aria-valuenow", "aria-orientation", "aria-controls"},
+ "heading" => phf_set!{"aria-level"},
+ "option" => phf_set!{"aria-selected"},
+};
+
+impl Rule for RoleHasRequiredAriaProps {
+ fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
+ if let AstKind::JSXOpeningElement(jsx_el) = node.kind() {
+ let Some(role_prop) = has_jsx_prop_lowercase(jsx_el, "role") else { return };
+ let JSXAttributeItem::Attribute(attr) = role_prop else { return };
+ let Some(JSXAttributeValue::StringLiteral(role_values)) = &attr.value else { return };
+ let roles = role_values.value.split_whitespace();
+ for role in roles {
+ if let Some(props) = ROLE_TO_REQUIRED_ARIA_PROPS.get(role) {
+ for prop in props {
+ if has_jsx_prop_lowercase(jsx_el, prop).is_none() {
+ ctx.diagnostic(RoleHasRequiredAriaPropsDiagnostic {
+ span: attr.span,
+ role: role.into(),
+ props: (*prop).into(),
+ });
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+#[test]
+fn test() {
+ use crate::rules::RoleHasRequiredAriaProps;
+ use crate::tester::Tester;
+
+ fn settings() -> serde_json::Value {
+ serde_json::json!({
+ "jsx-a11y": {
+ "components": {
+ "MyComponent": "div",
+ }
+ }
+ })
+ }
+
+ let pass = vec![
+ ("", None, None, None),
+ ("", None, None, None),
+ ("", None, None, None),
+ ("", None, None, None),
+ ("", None, None, None),
+ ("", None, None, None),
+ ("", None, None, None),
+ ("", None, None, None),
+ ("", None, None, None),
+ ("", None, None, None),
+ ("", None, Some(settings()), None),
+ ];
+
+ let fail = vec![
+ ("", None, None, None),
+ ("", None, None, None),
+ ("", None, None, None),
+ ("", None, None, None),
+ ("", None, None, None),
+ ("", None, None, None),
+ ("", None, None, None),
+ ("", None, None, None),
+ ("", None, None, None),
+ ("", None, None, None),
+ ("", None, None, None),
+ ("", None, None, None),
+ ("", None, None, None),
+ ("", None, None, None),
+ ("", None, None, None),
+ ("", None, None, None),
+ ("", None, None, None),
+ ("", None, Some(settings()), None),
+ ];
+
+ Tester::new_with_settings(RoleHasRequiredAriaProps::NAME, pass, fail).test_and_snapshot();
+}
diff --git a/crates/oxc_linter/src/snapshots/role_has_required_aria_props.snap b/crates/oxc_linter/src/snapshots/role_has_required_aria_props.snap
new file mode 100644
index 0000000000000..dde7b73253812
--- /dev/null
+++ b/crates/oxc_linter/src/snapshots/role_has_required_aria_props.snap
@@ -0,0 +1,271 @@
+---
+source: crates/oxc_linter/src/tester.rs
+expression: role_has_required_aria_props
+---
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `slider` role is missing required aria props `aria-valuenow`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ─────────────
+ ╰────
+ help: Add missing aria props `aria-valuenow` to the element with `slider` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `slider` role is missing required aria props `aria-valuemin`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ─────────────
+ ╰────
+ help: Add missing aria props `aria-valuemin` to the element with `slider` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `slider` role is missing required aria props `aria-valuemax`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ─────────────
+ ╰────
+ help: Add missing aria props `aria-valuemax` to the element with `slider` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `slider` role is missing required aria props `aria-valuenow`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ─────────────
+ ╰────
+ help: Add missing aria props `aria-valuenow` to the element with `slider` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `slider` role is missing required aria props `aria-valuemin`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ─────────────
+ ╰────
+ help: Add missing aria props `aria-valuemin` to the element with `slider` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `slider` role is missing required aria props `aria-valuenow`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ─────────────
+ ╰────
+ help: Add missing aria props `aria-valuenow` to the element with `slider` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `checkbox` role is missing required aria props `aria-checked`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ───────────────
+ ╰────
+ help: Add missing aria props `aria-checked` to the element with `checkbox` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `checkbox` role is missing required aria props `aria-checked`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ───────────────
+ ╰────
+ help: Add missing aria props `aria-checked` to the element with `checkbox` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `checkbox` role is missing required aria props `aria-checked`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ───────────────
+ ╰────
+ help: Add missing aria props `aria-checked` to the element with `checkbox` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `checkbox` role is missing required aria props `aria-checked`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ───────────────
+ ╰────
+ help: Add missing aria props `aria-checked` to the element with `checkbox` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `combobox` role is missing required aria props `aria-controls`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ───────────────
+ ╰────
+ help: Add missing aria props `aria-controls` to the element with `combobox` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `combobox` role is missing required aria props `aria-expanded`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ───────────────
+ ╰────
+ help: Add missing aria props `aria-expanded` to the element with `combobox` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `combobox` role is missing required aria props `aria-controls`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ───────────────
+ ╰────
+ help: Add missing aria props `aria-controls` to the element with `combobox` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `combobox` role is missing required aria props `aria-expanded`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ───────────────
+ ╰────
+ help: Add missing aria props `aria-expanded` to the element with `combobox` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `combobox` role is missing required aria props `aria-controls`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ───────────────
+ ╰────
+ help: Add missing aria props `aria-controls` to the element with `combobox` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `combobox` role is missing required aria props `aria-expanded`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ───────────────
+ ╰────
+ help: Add missing aria props `aria-expanded` to the element with `combobox` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `scrollbar` role is missing required aria props `aria-orientation`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ────────────────
+ ╰────
+ help: Add missing aria props `aria-orientation` to the element with `scrollbar` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `scrollbar` role is missing required aria props `aria-valuemax`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ────────────────
+ ╰────
+ help: Add missing aria props `aria-valuemax` to the element with `scrollbar` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `scrollbar` role is missing required aria props `aria-valuemin`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ────────────────
+ ╰────
+ help: Add missing aria props `aria-valuemin` to the element with `scrollbar` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `scrollbar` role is missing required aria props `aria-controls`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ────────────────
+ ╰────
+ help: Add missing aria props `aria-controls` to the element with `scrollbar` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `scrollbar` role is missing required aria props `aria-valuenow`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ────────────────
+ ╰────
+ help: Add missing aria props `aria-valuenow` to the element with `scrollbar` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `scrollbar` role is missing required aria props `aria-orientation`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ────────────────
+ ╰────
+ help: Add missing aria props `aria-orientation` to the element with `scrollbar` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `scrollbar` role is missing required aria props `aria-valuemin`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ────────────────
+ ╰────
+ help: Add missing aria props `aria-valuemin` to the element with `scrollbar` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `scrollbar` role is missing required aria props `aria-controls`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ────────────────
+ ╰────
+ help: Add missing aria props `aria-controls` to the element with `scrollbar` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `scrollbar` role is missing required aria props `aria-valuenow`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ────────────────
+ ╰────
+ help: Add missing aria props `aria-valuenow` to the element with `scrollbar` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `scrollbar` role is missing required aria props `aria-orientation`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ────────────────
+ ╰────
+ help: Add missing aria props `aria-orientation` to the element with `scrollbar` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `scrollbar` role is missing required aria props `aria-controls`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ────────────────
+ ╰────
+ help: Add missing aria props `aria-controls` to the element with `scrollbar` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `scrollbar` role is missing required aria props `aria-valuenow`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ────────────────
+ ╰────
+ help: Add missing aria props `aria-valuenow` to the element with `scrollbar` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `scrollbar` role is missing required aria props `aria-orientation`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ────────────────
+ ╰────
+ help: Add missing aria props `aria-orientation` to the element with `scrollbar` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `scrollbar` role is missing required aria props `aria-valuemin`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ────────────────
+ ╰────
+ help: Add missing aria props `aria-valuemin` to the element with `scrollbar` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `scrollbar` role is missing required aria props `aria-controls`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ────────────────
+ ╰────
+ help: Add missing aria props `aria-controls` to the element with `scrollbar` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `scrollbar` role is missing required aria props `aria-orientation`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ────────────────
+ ╰────
+ help: Add missing aria props `aria-orientation` to the element with `scrollbar` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `scrollbar` role is missing required aria props `aria-valuemax`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ────────────────
+ ╰────
+ help: Add missing aria props `aria-valuemax` to the element with `scrollbar` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `scrollbar` role is missing required aria props `aria-controls`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ────────────────
+ ╰────
+ help: Add missing aria props `aria-controls` to the element with `scrollbar` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `heading` role is missing required aria props `aria-level`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ──────────────
+ ╰────
+ help: Add missing aria props `aria-level` to the element with `heading` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `option` role is missing required aria props `aria-selected`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ─────────────
+ ╰────
+ help: Add missing aria props `aria-selected` to the element with `option` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `combobox` role is missing required aria props `aria-controls`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ───────────────
+ ╰────
+ help: Add missing aria props `aria-controls` to the element with `combobox` role.
+
+ ⚠ eslint-plugin-jsx-a11y(role-has-required-aria-props): `combobox` role is missing required aria props `aria-expanded`.
+ ╭─[role_has_required_aria_props.tsx:1:1]
+ 1 │
+ · ───────────────
+ ╰────
+ help: Add missing aria props `aria-expanded` to the element with `combobox` role.
+
+