Skip to content

Commit 9b35600

Browse files
committed
feat(linter/jsx-a11y): add support for mapped attributes in label association checks (#12805)
fixes #12507
1 parent b4c9e74 commit 9b35600

File tree

9 files changed

+184
-7
lines changed

9 files changed

+184
-7
lines changed

apps/oxlint/src/snapshots/_-A all --print-config@oxlint.snap

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ working directory:
1616
"settings": {
1717
"jsx-a11y": {
1818
"polymorphicPropName": null,
19-
"components": {}
19+
"components": {},
20+
"attributes": {}
2021
},
2122
"next": {
2223
"rootDir": []

apps/oxlint/src/snapshots/_-c fixtures__print_config__ban_rules__eslintrc.json -A all -D eqeqeq --print-config@oxlint.snap

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ working directory:
2323
"settings": {
2424
"jsx-a11y": {
2525
"polymorphicPropName": null,
26-
"components": {}
26+
"components": {},
27+
"attributes": {}
2728
},
2829
"next": {
2930
"rootDir": []

crates/oxc_linter/src/config/settings/jsx_a11y.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,23 @@ pub struct JSXA11yPluginSettings {
4545
/// ```
4646
#[serde(default)]
4747
pub components: FxHashMap<CompactStr, CompactStr>,
48+
49+
/// Map of attribute names to their DOM equivalents.
50+
/// This is useful for non-React frameworks that use different attribute names.
51+
///
52+
/// Example:
53+
///
54+
/// ```json
55+
/// {
56+
/// "settings": {
57+
/// "jsx-a11y": {
58+
/// "attributes": {
59+
/// "for": ["htmlFor", "for"]
60+
/// }
61+
/// }
62+
/// }
63+
/// }
64+
/// ```
65+
#[serde(default)]
66+
pub attributes: FxHashMap<CompactStr, Vec<CompactStr>>,
4867
}

crates/oxc_linter/src/config/settings/mod.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,5 +125,42 @@ mod test {
125125
let settings = OxlintSettings::default();
126126
assert!(settings.jsx_a11y.polymorphic_prop_name.is_none());
127127
assert!(settings.jsx_a11y.components.is_empty());
128+
assert!(settings.jsx_a11y.attributes.is_empty());
129+
}
130+
131+
#[test]
132+
fn test_parse_jsx_a11y_attributes() {
133+
let settings = OxlintSettings::deserialize(&serde_json::json!({
134+
"jsx-a11y": {
135+
"attributes": {
136+
"for": ["htmlFor", "for"],
137+
"class": ["className"]
138+
}
139+
}
140+
}))
141+
.unwrap();
142+
143+
let for_attrs = &settings.jsx_a11y.attributes["for"];
144+
assert_eq!(for_attrs.len(), 2);
145+
assert_eq!(for_attrs[0], "htmlFor");
146+
assert_eq!(for_attrs[1], "for");
147+
148+
let class_attrs = &settings.jsx_a11y.attributes["class"];
149+
assert_eq!(class_attrs.len(), 1);
150+
assert_eq!(class_attrs[0], "className");
151+
152+
assert_eq!(settings.jsx_a11y.attributes.get("nonexistent"), None);
153+
}
154+
155+
#[test]
156+
fn test_parse_jsx_a11y_attributes_empty() {
157+
let settings = OxlintSettings::deserialize(&serde_json::json!({
158+
"jsx-a11y": {
159+
"attributes": {}
160+
}
161+
}))
162+
.unwrap();
163+
164+
assert!(settings.jsx_a11y.attributes.is_empty());
128165
}
129166
}

crates/oxc_linter/src/rules/jsx_a11y/label_has_associated_control.rs

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,14 @@ impl Rule for LabelHasAssociatedControl {
215215
return;
216216
}
217217

218-
let has_html_for = has_jsx_prop(&element.opening_element, "htmlFor").is_some();
218+
let has_html_for = if let Some(attributes) = ctx.settings().jsx_a11y.attributes.get("for") {
219+
attributes
220+
.iter()
221+
.any(|attr| has_jsx_prop(&element.opening_element, attr.as_str()).is_some())
222+
} else {
223+
has_jsx_prop(&element.opening_element, "htmlFor").is_some()
224+
};
225+
219226
let has_control = self.has_nested_control(element, ctx);
220227

221228
if !self.has_accessible_label(element, ctx) {
@@ -406,6 +413,18 @@ fn test() {
406413
})
407414
}
408415

416+
fn attributes_settings() -> serde_json::Value {
417+
serde_json::json!({
418+
"settings": {
419+
"jsx-a11y": {
420+
"attributes": {
421+
"for": ["htmlFor", "for"]
422+
}
423+
}
424+
}
425+
})
426+
}
427+
409428
let pass = vec![
410429
(
411430
r#"<label htmlFor="js_id"><span><span><span>A label</span></span></span></label>"#,
@@ -934,6 +953,32 @@ fn test() {
934953
None,
935954
None,
936955
),
956+
// Test for 'for' attribute with attributes setting
957+
(
958+
r#"<label for="js_id">A label</label>"#,
959+
Some(serde_json::json!([{ "assert": "htmlFor" }])),
960+
Some(attributes_settings()),
961+
),
962+
(
963+
r#"<label for="js_id" aria-label="A label" />"#,
964+
Some(serde_json::json!([{ "assert": "htmlFor" }])),
965+
Some(attributes_settings()),
966+
),
967+
(
968+
r#"<label for="js_id">A label</label>"#,
969+
Some(serde_json::json!([{ "assert": "either" }])),
970+
Some(attributes_settings()),
971+
),
972+
(
973+
r#"<label for="js_id" aria-label="A label" />"#,
974+
Some(serde_json::json!([{ "assert": "either" }])),
975+
Some(attributes_settings()),
976+
),
977+
(
978+
r#"<label for="js_id" aria-label="A label"><input /></label>"#,
979+
Some(serde_json::json!([{ "assert": "both" }])),
980+
Some(attributes_settings()),
981+
),
937982
];
938983

939984
let fail = vec![
@@ -1611,6 +1656,16 @@ fn test() {
16111656
}])),
16121657
None,
16131658
),
1659+
(
1660+
r#"<label for="js_id">A label</label>"#,
1661+
Some(serde_json::json!([{ "assert": ["htmlFor"] }])),
1662+
None,
1663+
),
1664+
(
1665+
r#"<label for="js_id" aria-label="A label" />"#,
1666+
Some(serde_json::json!([{ "assert": ["htmlFor"] }])),
1667+
None,
1668+
),
16141669
];
16151670

16161671
Tester::new(LabelHasAssociatedControl::NAME, LabelHasAssociatedControl::PLUGIN, pass, fail)

crates/oxc_linter/src/snapshots/jsx_a11y_label_has_associated_control.snap

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,3 +622,17 @@ source: crates/oxc_linter/src/tester.rs
622622
· ───────────────────────────────────────────────────────
623623
╰────
624624
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.
625+
626+
eslint-plugin-jsx-a11y(label-has-associated-control): A form label must be associated with a control.
627+
╭─[label_has_associated_control.tsx:1:1]
628+
1<label for="js_id">A label</label>
629+
· ───────────────────
630+
╰────
631+
help: Either give the label a `htmlFor` attribute with the id of the associated control, or wrap the label around the control.
632+
633+
eslint-plugin-jsx-a11y(label-has-associated-control): A form label must be associated with a control.
634+
╭─[label_has_associated_control.tsx:1:1]
635+
1<label for="js_id" aria-label="A label" />
636+
· ──────────────────────────────────────────
637+
╰────
638+
help: Either give the label a `htmlFor` attribute with the id of the associated control, or wrap the label around the control.

crates/oxc_linter/src/snapshots/schema_json.snap

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ expression: json
8383
"default": {
8484
"jsx-a11y": {
8585
"polymorphicPropName": null,
86-
"components": {}
86+
"components": {},
87+
"attributes": {}
8788
},
8889
"next": {
8990
"rootDir": []
@@ -256,6 +257,17 @@ expression: json
256257
"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.",
257258
"type": "object",
258259
"properties": {
260+
"attributes": {
261+
"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```",
262+
"default": {},
263+
"type": "object",
264+
"additionalProperties": {
265+
"type": "array",
266+
"items": {
267+
"type": "string"
268+
}
269+
}
270+
},
259271
"components": {
260272
"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```",
261273
"default": {},
@@ -474,7 +486,8 @@ expression: json
474486
"jsx-a11y": {
475487
"default": {
476488
"polymorphicPropName": null,
477-
"components": {}
489+
"components": {},
490+
"attributes": {}
478491
},
479492
"allOf": [
480493
{

npm/oxlint/configuration_schema.json

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@
7979
"default": {
8080
"jsx-a11y": {
8181
"polymorphicPropName": null,
82-
"components": {}
82+
"components": {},
83+
"attributes": {}
8384
},
8485
"next": {
8586
"rootDir": []
@@ -252,6 +253,17 @@
252253
"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.",
253254
"type": "object",
254255
"properties": {
256+
"attributes": {
257+
"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```",
258+
"default": {},
259+
"type": "object",
260+
"additionalProperties": {
261+
"type": "array",
262+
"items": {
263+
"type": "string"
264+
}
265+
}
266+
},
255267
"components": {
256268
"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```",
257269
"default": {},
@@ -470,7 +482,8 @@
470482
"jsx-a11y": {
471483
"default": {
472484
"polymorphicPropName": null,
473-
"components": {}
485+
"components": {},
486+
"attributes": {}
474487
},
475488
"allOf": [
476489
{

tasks/website/src/linter/snapshots/schema_markdown.snap

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,30 @@ See
359359
configuration for a full reference.
360360

361361

362+
#### settings.jsx-a11y.attributes
363+
364+
type: `Record<string, array>`
365+
366+
default: `{}`
367+
368+
Map of attribute names to their DOM equivalents.
369+
This is useful for non-React frameworks that use different attribute names.
370+
371+
Example:
372+
373+
```json
374+
{
375+
"settings": {
376+
"jsx-a11y": {
377+
"attributes": {
378+
"for": ["htmlFor", "for"]
379+
}
380+
}
381+
}
382+
}
383+
```
384+
385+
362386
#### settings.jsx-a11y.components
363387

364388
type: `Record<string, string>`

0 commit comments

Comments
 (0)