Skip to content

Commit bf01891

Browse files
Merge branch 'main' of https://github.com/microsoft/eslint-plugin-fluentui-jsx-a11y into users/sidhshar/accessibility-rules-for-infolabel-menubutton-splitbutton-and-card-components
2 parents e87634b + 2f2d469 commit bf01891

18 files changed

+1460
-59
lines changed

CONTRIBUTING.md

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any addi
1616
## Table of Contents
1717

1818
[Dev Environment](#dev-environment)
19+
[Rule Factory System](#rule-factory-system)
20+
[Creating New Rules](#creating-new-rules)
21+
[Utility Functions](#utility-functions)
1922
[Pull requests](#pull-requests)
2023

2124
## Dev Environment
@@ -61,6 +64,205 @@ To ensure a consistent and productive development environment, install the follo
6164
- [Prettier ESLint](https://marketplace.visualstudio.com/items?itemName=rvest.vs-code-prettier-eslint) — Format code with Prettier and ESLint integration.
6265
- [markdownlint](https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint) — Linting and style checks for Markdown files.
6366

67+
## Rule Factory System
68+
69+
This plugin uses a powerful rule factory system that provides consistent behavior across accessibility rules. The factory system is built around the `ruleFactory` function in `lib/util/ruleFactory.ts` and several utility functions for validating accessible labeling.
70+
71+
### Core Concept
72+
73+
The rule factory centralizes common accessibility validation patterns, making it easy to create new rules with consistent behavior. Instead of implementing validation logic from scratch, rules can leverage the factory's built-in utilities.
74+
75+
### Architecture
76+
77+
```sh
78+
ruleFactory(config) → ESLint Rule
79+
80+
hasAccessibleLabel(opening, element, context, config) → boolean
81+
82+
Utility Functions:
83+
├── hasAssociatedLabelViaAriaLabelledBy(opening, context)
84+
├── hasAssociatedLabelViaHtmlFor(opening, context)
85+
├── hasAssociatedLabelViaAriaDescribedby(opening, context)
86+
├── hasLabeledChild(opening, context)
87+
├── hasTextContentChild(element)
88+
└── isInsideLabelTag(context)
89+
```
90+
91+
## Creating New Rules
92+
93+
### Using the Rule Factory
94+
95+
For most accessibility rules, use the rule factory:
96+
97+
```typescript
98+
import { ruleFactory, LabeledControlConfig } from '../util/ruleFactory';
99+
100+
const rule = ruleFactory({
101+
component: 'YourComponent', // string or regex pattern
102+
message: 'YourComponent needs accessible labeling',
103+
104+
// Validation options (all optional, default false)
105+
allowTextContentChild: true, // Allow text content in children
106+
allowLabeledChild: true, // Allow images with alt, icons, etc.
107+
allowHtmlFor: true, // Allow htmlFor/id label association
108+
allowLabelledBy: true, // Allow aria-labelledby
109+
allowDescribedBy: false, // Allow aria-describedby (discouraged as primary)
110+
allowWrappingLabel: true, // Allow wrapping in <Label> tag
111+
allowTooltipParent: false, // Allow parent <Tooltip>
112+
allowFieldParent: true, // Allow parent <Field>
113+
114+
// Property validation
115+
labelProps: ['aria-label'], // Props that provide labeling
116+
requiredProps: ['role'], // Props that must be present
117+
});
118+
119+
export default rule;
120+
```
121+
122+
### Configuration Options
123+
124+
| Option | Description | Example Use Cases |
125+
|--------|-------------|-------------------|
126+
| `allowTextContentChild` | Allows text content in child elements | Buttons, links with text |
127+
| `allowLabeledChild` | Allows accessible child content (images with alt, icons, aria-labeled elements) | Icon buttons, image buttons |
128+
| `allowHtmlFor` | Allows label association via `htmlFor`/`id` | Form inputs, interactive controls |
129+
| `allowLabelledBy` | Allows `aria-labelledby` references | Complex components referencing external labels |
130+
| `allowDescribedBy` | Allows `aria-describedby` (discouraged for primary labeling) | Rare cases where description suffices |
131+
| `allowWrappingLabel` | Allows element to be wrapped in `<Label>` | Form controls |
132+
| `allowTooltipParent` | Allows parent `<Tooltip>` as accessible name | Simple tooltips (use sparingly) |
133+
| `allowFieldParent` | Allows parent `<Field>` component | FluentUI form fields |
134+
135+
### Custom Rules
136+
137+
For complex validation that doesn't fit the factory pattern:
138+
139+
```typescript
140+
import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
141+
import { JSXOpeningElement } from "estree-jsx";
142+
143+
const rule = ESLintUtils.RuleCreator.withoutDocs({
144+
defaultOptions: [],
145+
meta: {
146+
messages: {
147+
customMessage: "Custom validation message"
148+
},
149+
type: "problem",
150+
schema: []
151+
},
152+
create(context) {
153+
return {
154+
JSXOpeningElement(node: TSESTree.JSXOpeningElement) {
155+
// Custom validation logic
156+
if (needsValidation(node)) {
157+
context.report({
158+
node,
159+
messageId: "customMessage"
160+
});
161+
}
162+
}
163+
};
164+
}
165+
});
166+
```
167+
168+
## Utility Functions
169+
170+
### hasLabeledChild
171+
172+
The `hasLabeledChild` utility detects accessible child content and is one of the most powerful validation functions:
173+
174+
```typescript
175+
import { hasLabeledChild } from '../util/hasLabeledChild';
176+
177+
// Usage in rules
178+
if (hasLabeledChild(openingElement, context)) {
179+
return; // Element has accessible child content
180+
}
181+
```
182+
183+
**Detects:**
184+
185+
1. **Images with alt text:**
186+
```jsx
187+
<Button><img alt="Save document" /></Button>
188+
<Button><Image alt="User profile" /></Button>
189+
```
190+
191+
2. **SVG elements with accessible attributes:**
192+
```jsx
193+
<Button><svg title="Close" /></Button>
194+
<Button><svg aria-label="Menu" /></Button>
195+
<Button><svg aria-labelledby="icon-label" /></Button>
196+
```
197+
198+
3. **Elements with role="img" and labeling:**
199+
```jsx
200+
<Button><span role="img" aria-label="Celebration">🎉</span></Button>
201+
```
202+
203+
4. **FluentUI Icon components:**
204+
```jsx
205+
<Button><SaveIcon /></Button>
206+
<Button><Icon iconName="Save" /></Button>
207+
<Button><MyCustomIcon /></Button>
208+
```
209+
210+
5. **Any element with aria-label or title:**
211+
```jsx
212+
<Button><div aria-label="Status indicator" /></Button>
213+
<Button><span title="Tooltip text" /></Button>
214+
```
215+
216+
6. **Elements with validated aria-labelledby:**
217+
```jsx
218+
<Button><span aria-labelledby="save-label" /></Button>
219+
<Label id="save-label">Save Document</Label>
220+
```
221+
222+
**Key Features:**
223+
224+
- **Source code validation:** Validates that `aria-labelledby` references point to actual elements with matching IDs
225+
- **Deep traversal:** Uses `flattenChildren` to find labeled content in nested structures
226+
- **Case insensitive:** Handles variations like `IMG`, `SVG`, `CLOSEICON`
227+
- **Error handling:** Gracefully handles malformed JSX and missing context
228+
229+
### Other Utility Functions
230+
231+
- **`hasAssociatedLabelViaAriaLabelledBy(opening, context)`** - Validates `aria-labelledby` references
232+
- **`hasAssociatedLabelViaHtmlFor(opening, context)`** - Validates `htmlFor`/`id` label associations
233+
- **`hasAssociatedLabelViaAriaDescribedby(opening, context)`** - Validates `aria-describedby` references
234+
- **`hasTextContentChild(element)`** - Checks for meaningful text content in children
235+
- **`isInsideLabelTag(context)`** - Checks if element is wrapped in a `<Label>` tag
236+
- **`hasNonEmptyProp(attributes, propName)`** - Validates non-empty attribute values
237+
- **`hasDefinedProp(attributes, propName)`** - Checks if attribute is present
238+
239+
### Writing Tests
240+
241+
Use the comprehensive test patterns established in the codebase:
242+
243+
```typescript
244+
import { hasLabeledChild } from "../../../../lib/util/hasLabeledChild";
245+
import { TSESLint } from "@typescript-eslint/utils";
246+
247+
describe("hasLabeledChild", () => {
248+
const mockContext = (sourceText = ""): TSESLint.RuleContext<string, unknown[]> => ({
249+
getSourceCode: () => ({
250+
getText: () => sourceText,
251+
text: sourceText
252+
})
253+
} as unknown as TSESLint.RuleContext<string, unknown[]>);
254+
255+
it("validates aria-labelledby references", () => {
256+
const element = createElementWithChild("div", [
257+
createJSXAttribute("aria-labelledby", "existing-label")
258+
]);
259+
const contextWithLabel = mockContext('<Label id="existing-label">Label Text</Label>');
260+
261+
expect(hasLabeledChild(element, contextWithLabel)).toBe(true);
262+
});
263+
});
264+
```
265+
64266
## To create a new ESLint rule
65267
66268
If you want to create a new ESLint rule:

0 commit comments

Comments
 (0)