Skip to content

Commit 46cb3f3

Browse files
committed
Updating rule to use with factory function
1 parent 8b1e607 commit 46cb3f3

File tree

2 files changed

+63
-58
lines changed

2 files changed

+63
-58
lines changed

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

Lines changed: 55 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,64 +2,64 @@
22
// Licensed under the MIT License.
33

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

8-
export default ESLintUtils.RuleCreator.withoutDocs({
9-
meta: {
10-
type: "problem",
11-
docs: {
7+
export default ESLintUtils.RuleCreator.withoutDocs(
8+
makeLabeledControlRule(
9+
{
10+
component: "SplitButton",
11+
messageId: "noUnlabeledSplitButton",
1212
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/"
13+
labelProps: [],
14+
allowFieldParent: false,
15+
allowHtmlFor: false,
16+
allowLabelledBy: false,
17+
allowWrappingLabel: false,
18+
allowTooltipParent: false,
19+
allowDescribedBy: false,
20+
allowLabeledChild: false,
21+
allowTextContentChild: false
1522
},
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;
29-
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;
23+
isSplitButtonAccessiblyLabeled
24+
)
25+
);
3326

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-
}
27+
/**
28+
* Custom accessibility checker for SplitButton:
29+
* 1. Accessible if it has any non-empty text child.
30+
* 2. If not, must have primaryActionButton prop with aria-label.
31+
* 3. All other labeling strategies are invalid.
32+
*/
33+
export function isSplitButtonAccessiblyLabeled(node: TSESTree.JSXElement): boolean {
34+
// 1. Check for any non-empty text child
35+
const hasTextContent = node.children.some(child => child.type === "JSXText" && child.value.trim().length > 0);
36+
if (hasTextContent) return true;
5937

60-
// 3. Otherwise, report error
61-
context.report({ node: opening, messageId: "noUnlabeledSplitButton" });
62-
}
63-
};
38+
// 2. Check for primaryActionButton prop with aria-label
39+
const opening = node.openingElement;
40+
const primaryActionButtonProp = opening.attributes.find(
41+
attr => attr.type === "JSXAttribute" && attr.name.name === "primaryActionButton"
42+
);
43+
if (
44+
primaryActionButtonProp &&
45+
primaryActionButtonProp.type === "JSXAttribute" &&
46+
primaryActionButtonProp.value &&
47+
primaryActionButtonProp.value.type === "JSXExpressionContainer"
48+
) {
49+
const expr = primaryActionButtonProp.value.expression;
50+
// Only handle object literals
51+
if (expr.type === "ObjectExpression") {
52+
const hasAriaLabel = expr.properties.some(
53+
prop =>
54+
prop.type === "Property" &&
55+
((prop.key.type === "Identifier" && prop.key.name === "aria-label") ||
56+
(prop.key.type === "Literal" && prop.key.value === "aria-label")) &&
57+
prop.value.type === "Literal" &&
58+
typeof prop.value.value === "string" &&
59+
prop.value.value.trim().length > 0
60+
);
61+
if (hasAriaLabel) return true;
62+
}
6463
}
65-
});
64+
return false;
65+
}

lib/util/ruleFactory.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,11 @@ export function hasAccessibleLabel(
8282
* Factory for a minimal, strongly-configurable ESLint rule that enforces
8383
* accessible labeling on a specific JSX element/component.
8484
*/
85-
export function makeLabeledControlRule(config: LabeledControlConfig): TSESLint.RuleModule<string, []> {
85+
86+
// eslint-disable-next-line no-unused-vars
87+
type CustomChecker = (node: TSESTree.JSXElement, context: TSESLint.RuleContext<string, []>) => boolean;
88+
89+
export function makeLabeledControlRule(config: LabeledControlConfig, customChecker?: CustomChecker): TSESLint.RuleModule<string, []> {
8690
return {
8791
meta: {
8892
type: "problem",
@@ -105,8 +109,9 @@ export function makeLabeledControlRule(config: LabeledControlConfig): TSESLint.R
105109

106110
if (!matches) return;
107111

108-
if (!hasAccessibleLabel(opening, node, context, config)) {
109-
// report on the opening tag for better location
112+
const isAccessible = customChecker ? customChecker(node, context) : hasAccessibleLabel(opening, node, context, config);
113+
114+
if (!isAccessible) {
110115
context.report({ node: opening, messageId: config.messageId });
111116
}
112117
}

0 commit comments

Comments
 (0)