Skip to content

Commit cd6d850

Browse files
committed
Updating rule
1 parent e3ad83c commit cd6d850

File tree

4 files changed

+165
-94
lines changed

4 files changed

+165
-94
lines changed

docs/rules/split-button-needs-labelling.md

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,33 @@ Please add label, or aria-labelledby.
1616

1717
This rule aims to...
1818

19-
Examples of **incorrect** code for this rule:
19+
Example of **incorrect** code for this rule:
2020

2121
```jsx
22-
<SplitButton> Example</SplitButton>
22+
<SplitButton
23+
menuButton={triggerProps}
24+
primaryActionButton={primaryActionButtonProps}
25+
/>
2326
```
2427

25-
```jsx
26-
<SplitButton disabled> Example</SplitButton>
27-
```
28-
29-
3028
Examples of **correct** code for this rule:
3129

3230
```jsx
33-
<SplitButton aria-label="My button">Example</SplitButton>
31+
<SplitButton
32+
menuButton={triggerProps}
33+
primaryActionButton={primaryActionButtonProps}
34+
>
35+
Example
36+
</SplitButton>
3437
```
3538

3639
```jsx
37-
<SplitButton disabled aria-label="My button">Disabled State</SplitButton>
40+
<SplitButton
41+
menuButton={triggerProps}
42+
primaryActionButton={{
43+
ref: setPrimaryActionButtonRef,
44+
"aria-label": "With calendar icon only",
45+
}}
46+
icon={<CalendarMonthRegular />}
47+
/>
3848
```
Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,65 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
import { ESLintUtils } from "@typescript-eslint/utils";
5-
import { makeLabeledControlRule } from "../util/ruleFactory";
4+
import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
5+
import { elementType } from "jsx-ast-utils";
6+
import { JSXOpeningElement } from "estree-jsx";
67

7-
//------------------------------------------------------------------------------
8-
// Rule Definition
9-
//------------------------------------------------------------------------------
8+
export default ESLintUtils.RuleCreator.withoutDocs({
9+
meta: {
10+
type: "problem",
11+
docs: {
12+
description: "Accessibility: SplitButton must have text content or accessible name on primaryActionButton prop.",
13+
recommended: "strict",
14+
url: "https://www.w3.org/WAI/standards-guidelines/act/rules/97a4e1/"
15+
},
16+
messages: {
17+
noUnlabeledSplitButton:
18+
"SplitButton must have text content, or an accessible name on the primaryActionButton prop (e.g., aria-label). Labeling the SplitButton itself is not valid."
19+
},
20+
schema: []
21+
},
22+
defaultOptions: [],
23+
create(context) {
24+
return {
25+
JSXElement(node: TSESTree.JSXElement) {
26+
const opening = node.openingElement;
27+
const name = elementType(opening as unknown as JSXOpeningElement);
28+
if (name !== "SplitButton") return;
1029

11-
export default ESLintUtils.RuleCreator.withoutDocs(
12-
makeLabeledControlRule({
13-
component: "SplitButton",
14-
messageId: "noUnlabeledSplitButton",
15-
description: "Accessibility: SplitButton must have an accessible name via title, aria-label",
16-
labelProps: ["aria-label"],
17-
allowFieldParent: true,
18-
allowHtmlFor: false,
19-
allowLabelledBy: true,
20-
allowWrappingLabel: false,
21-
allowTooltipParent: false,
22-
allowDescribedBy: false,
23-
allowLabeledChild: false
24-
})
25-
);
30+
// 1. Check for any non-empty text child
31+
const hasTextContent = node.children.some(child => child.type === "JSXText" && child.value.trim().length > 0);
32+
if (hasTextContent) return;
33+
34+
// 2. Check for primaryActionButton prop with aria-label
35+
const primaryActionButtonProp = opening.attributes.find(
36+
attr => attr.type === "JSXAttribute" && attr.name.name === "primaryActionButton"
37+
);
38+
if (
39+
primaryActionButtonProp &&
40+
primaryActionButtonProp.type === "JSXAttribute" &&
41+
primaryActionButtonProp.value &&
42+
primaryActionButtonProp.value.type === "JSXExpressionContainer"
43+
) {
44+
const expr = primaryActionButtonProp.value.expression;
45+
// Only handle object literals
46+
if (expr.type === "ObjectExpression") {
47+
const hasAriaLabel = expr.properties.some(
48+
prop =>
49+
prop.type === "Property" &&
50+
((prop.key.type === "Identifier" && prop.key.name === "aria-label") ||
51+
(prop.key.type === "Literal" && prop.key.value === "aria-label")) &&
52+
prop.value.type === "Literal" &&
53+
typeof prop.value.value === "string" &&
54+
prop.value.value.trim().length > 0
55+
);
56+
if (hasAriaLabel) return;
57+
}
58+
}
59+
60+
// 3. Otherwise, report error
61+
context.report({ node: opening, messageId: "noUnlabeledSplitButton" });
62+
}
63+
};
64+
}
65+
});

tests/lib/rules/split-button-needs-labelling-test.ts

Lines changed: 0 additions & 65 deletions
This file was deleted.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { Rule } from "eslint";
5+
import ruleTester from "./helper/ruleTester";
6+
import rule from "../../../lib/rules/split-button-needs-labelling";
7+
// -----------------------------------------------------------------------------
8+
// Tests
9+
// -----------------------------------------------------------------------------
10+
11+
ruleTester.run("split-button-needs-labelling", rule as unknown as Rule.RuleModule, {
12+
valid: [
13+
// 1) aria-label on the SplitButton
14+
15+
`<SplitButton
16+
menuButton={triggerProps}
17+
primaryActionButton={primaryActionButtonProps}
18+
>
19+
Example
20+
</SplitButton>
21+
`,
22+
// 2) with primaryActionButton prop having aria-label
23+
`
24+
<SplitButton
25+
menuButton={triggerProps}
26+
primaryActionButton={{
27+
ref: setPrimaryActionButtonRef,
28+
"aria-label": "With calendar icon only",
29+
}}
30+
icon={<CalendarMonthRegular />}
31+
/>
32+
`
33+
],
34+
35+
invalid: [
36+
// Unlabeled SplitButton
37+
{
38+
code: `
39+
<SplitButton
40+
menuButton={triggerProps}
41+
primaryActionButton={primaryActionButtonProps}
42+
/>
43+
`,
44+
errors: [{ messageId: "noUnlabeledSplitButton" }]
45+
},
46+
// SplitButton empty aria-label
47+
{
48+
code: `
49+
<SplitButton
50+
menuButton={triggerProps}
51+
primaryActionButton={{
52+
ref: setPrimaryActionButtonRef,
53+
"aria-label": "",
54+
}}
55+
/>
56+
`,
57+
errors: [{ messageId: "noUnlabeledSplitButton" }]
58+
},
59+
// SplitButton with aria-label null
60+
{
61+
code: `
62+
<SplitButton
63+
menuButton={triggerProps}
64+
primaryActionButton={{
65+
ref: setPrimaryActionButtonRef,
66+
"aria-label": null,
67+
}}
68+
/>
69+
`,
70+
errors: [{ messageId: "noUnlabeledSplitButton" }]
71+
},
72+
// SplitButton with aria-label undefined
73+
{
74+
code: `
75+
<SplitButton
76+
menuButton={triggerProps}
77+
primaryActionButton={{
78+
ref: setPrimaryActionButtonRef,
79+
"aria-label": undefined,
80+
}}
81+
/>
82+
`,
83+
errors: [{ messageId: "noUnlabeledSplitButton" }]
84+
}
85+
]
86+
});

0 commit comments

Comments
 (0)