|
1 | 1 | // Copyright (c) Microsoft Corporation. |
2 | 2 | // Licensed under the MIT License. |
3 | 3 |
|
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"; |
6 | 7 |
|
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; |
10 | 29 |
|
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 | +}); |
0 commit comments