@@ -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
66268If you want to create a new ESLint rule:
0 commit comments