Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ working directory:
"settings": {
"jsx-a11y": {
"polymorphicPropName": null,
"components": {}
"components": {},
"attributes": {}
},
"next": {
"rootDir": []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ working directory:
"settings": {
"jsx-a11y": {
"polymorphicPropName": null,
"components": {}
"components": {},
"attributes": {}
},
"next": {
"rootDir": []
Expand Down
19 changes: 19 additions & 0 deletions crates/oxc_linter/src/config/settings/jsx_a11y.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,23 @@ pub struct JSXA11yPluginSettings {
/// ```
#[serde(default)]
pub components: FxHashMap<CompactStr, CompactStr>,

/// Map of attribute names to their DOM equivalents.
/// This is useful for non-React frameworks that use different attribute names.
///
/// Example:
///
/// ```json
/// {
/// "settings": {
/// "jsx-a11y": {
/// "attributes": {
/// "for": ["htmlFor", "for"]
/// }
/// }
/// }
/// }
/// ```
#[serde(default)]
pub attributes: FxHashMap<CompactStr, Vec<CompactStr>>,
}
37 changes: 37 additions & 0 deletions crates/oxc_linter/src/config/settings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,5 +125,42 @@ mod test {
let settings = OxlintSettings::default();
assert!(settings.jsx_a11y.polymorphic_prop_name.is_none());
assert!(settings.jsx_a11y.components.is_empty());
assert!(settings.jsx_a11y.attributes.is_empty());
}

#[test]
fn test_parse_jsx_a11y_attributes() {
let settings = OxlintSettings::deserialize(&serde_json::json!({
"jsx-a11y": {
"attributes": {
"for": ["htmlFor", "for"],
"class": ["className"]
}
}
}))
.unwrap();

let for_attrs = &settings.jsx_a11y.attributes["for"];
assert_eq!(for_attrs.len(), 2);
assert_eq!(for_attrs[0], "htmlFor");
assert_eq!(for_attrs[1], "for");

let class_attrs = &settings.jsx_a11y.attributes["class"];
assert_eq!(class_attrs.len(), 1);
assert_eq!(class_attrs[0], "className");

assert_eq!(settings.jsx_a11y.attributes.get("nonexistent"), None);
}

#[test]
fn test_parse_jsx_a11y_attributes_empty() {
let settings = OxlintSettings::deserialize(&serde_json::json!({
"jsx-a11y": {
"attributes": {}
}
}))
.unwrap();

assert!(settings.jsx_a11y.attributes.is_empty());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,14 @@ impl Rule for LabelHasAssociatedControl {
return;
}

let has_html_for = has_jsx_prop(&element.opening_element, "htmlFor").is_some();
let has_html_for = if let Some(attributes) = ctx.settings().jsx_a11y.attributes.get("for") {
attributes
.iter()
.any(|attr| has_jsx_prop(&element.opening_element, attr.as_str()).is_some())
} else {
has_jsx_prop(&element.opening_element, "htmlFor").is_some()
};

let has_control = self.has_nested_control(element, ctx);

if !self.has_accessible_label(element, ctx) {
Expand Down Expand Up @@ -406,6 +413,18 @@ fn test() {
})
}

fn attributes_settings() -> serde_json::Value {
serde_json::json!({
"settings": {
"jsx-a11y": {
"attributes": {
"for": ["htmlFor", "for"]
}
}
}
})
}

let pass = vec![
(
r#"<label htmlFor="js_id"><span><span><span>A label</span></span></span></label>"#,
Expand Down Expand Up @@ -934,6 +953,32 @@ fn test() {
None,
None,
),
// Test for 'for' attribute with attributes setting
(
r#"<label for="js_id">A label</label>"#,
Some(serde_json::json!([{ "assert": "htmlFor" }])),
Some(attributes_settings()),
),
(
r#"<label for="js_id" aria-label="A label" />"#,
Some(serde_json::json!([{ "assert": "htmlFor" }])),
Some(attributes_settings()),
),
(
r#"<label for="js_id">A label</label>"#,
Some(serde_json::json!([{ "assert": "either" }])),
Some(attributes_settings()),
),
(
r#"<label for="js_id" aria-label="A label" />"#,
Some(serde_json::json!([{ "assert": "either" }])),
Some(attributes_settings()),
),
(
r#"<label for="js_id" aria-label="A label"><input /></label>"#,
Some(serde_json::json!([{ "assert": "both" }])),
Some(attributes_settings()),
),
];

let fail = vec![
Expand Down Expand Up @@ -1611,6 +1656,16 @@ fn test() {
}])),
None,
),
(
r#"<label for="js_id">A label</label>"#,
Some(serde_json::json!([{ "assert": ["htmlFor"] }])),
None,
),
(
r#"<label for="js_id" aria-label="A label" />"#,
Some(serde_json::json!([{ "assert": ["htmlFor"] }])),
None,
),
];

Tester::new(LabelHasAssociatedControl::NAME, LabelHasAssociatedControl::PLUGIN, pass, fail)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -622,3 +622,17 @@ source: crates/oxc_linter/src/tester.rs
· ───────────────────────────────────────────────────────
╰────
help: Ensure the label either has text inside it or is accessibly labelled using an attribute such as `aria-label`, or `aria-labelledby`. You can mark more attributes as accessible labels by configuring the `labelAttributes` option.

⚠ eslint-plugin-jsx-a11y(label-has-associated-control): A form label must be associated with a control.
╭─[label_has_associated_control.tsx:1:1]
1 │ <label for="js_id">A label</label>
· ───────────────────
╰────
help: Either give the label a `htmlFor` attribute with the id of the associated control, or wrap the label around the control.

⚠ eslint-plugin-jsx-a11y(label-has-associated-control): A form label must be associated with a control.
╭─[label_has_associated_control.tsx:1:1]
1 │ <label for="js_id" aria-label="A label" />
· ──────────────────────────────────────────
╰────
help: Either give the label a `htmlFor` attribute with the id of the associated control, or wrap the label around the control.
17 changes: 15 additions & 2 deletions crates/oxc_linter/src/snapshots/schema_json.snap
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ expression: json
"default": {
"jsx-a11y": {
"polymorphicPropName": null,
"components": {}
"components": {},
"attributes": {}
},
"next": {
"rootDir": []
Expand Down Expand Up @@ -256,6 +257,17 @@ expression: json
"description": "Configure JSX A11y plugin rules.\n\nSee\n[eslint-plugin-jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y#configurations)'s\nconfiguration for a full reference.",
"type": "object",
"properties": {
"attributes": {
"description": "Map of attribute names to their DOM equivalents.\nThis is useful for non-React frameworks that use different attribute names.\n\nExample:\n\n```json\n{\n\"settings\": {\n\"jsx-a11y\": {\n\"attributes\": {\n\"for\": [\"htmlFor\", \"for\"]\n}\n}\n}\n}\n```",
"default": {},
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
}
},
"components": {
"description": "To have your custom components be checked as DOM elements, you can\nprovide a mapping of your component names to the DOM element name.\n\nExample:\n\n```json\n{\n\"settings\": {\n\"jsx-a11y\": {\n\"components\": {\n\"Link\": \"a\",\n\"IconButton\": \"button\"\n}\n}\n}\n}\n```",
"default": {},
Expand Down Expand Up @@ -474,7 +486,8 @@ expression: json
"jsx-a11y": {
"default": {
"polymorphicPropName": null,
"components": {}
"components": {},
"attributes": {}
},
"allOf": [
{
Expand Down
17 changes: 15 additions & 2 deletions npm/oxlint/configuration_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@
"default": {
"jsx-a11y": {
"polymorphicPropName": null,
"components": {}
"components": {},
"attributes": {}
},
"next": {
"rootDir": []
Expand Down Expand Up @@ -252,6 +253,17 @@
"description": "Configure JSX A11y plugin rules.\n\nSee\n[eslint-plugin-jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y#configurations)'s\nconfiguration for a full reference.",
"type": "object",
"properties": {
"attributes": {
"description": "Map of attribute names to their DOM equivalents.\nThis is useful for non-React frameworks that use different attribute names.\n\nExample:\n\n```json\n{\n\"settings\": {\n\"jsx-a11y\": {\n\"attributes\": {\n\"for\": [\"htmlFor\", \"for\"]\n}\n}\n}\n}\n```",
"default": {},
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
}
},
"components": {
"description": "To have your custom components be checked as DOM elements, you can\nprovide a mapping of your component names to the DOM element name.\n\nExample:\n\n```json\n{\n\"settings\": {\n\"jsx-a11y\": {\n\"components\": {\n\"Link\": \"a\",\n\"IconButton\": \"button\"\n}\n}\n}\n}\n```",
"default": {},
Expand Down Expand Up @@ -470,7 +482,8 @@
"jsx-a11y": {
"default": {
"polymorphicPropName": null,
"components": {}
"components": {},
"attributes": {}
},
"allOf": [
{
Expand Down
24 changes: 24 additions & 0 deletions tasks/website/src/linter/snapshots/schema_markdown.snap
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,30 @@ See
configuration for a full reference.


#### settings.jsx-a11y.attributes

type: `Record<string, array>`

default: `{}`

Map of attribute names to their DOM equivalents.
This is useful for non-React frameworks that use different attribute names.

Example:

```json
{
"settings": {
"jsx-a11y": {
"attributes": {
"for": ["htmlFor", "for"]
}
}
}
}
```


#### settings.jsx-a11y.components

type: `Record<string, string>`
Expand Down
Loading