Skip to content

Commit 3d3279d

Browse files
authored
Merge pull request #116 from joliveira12/user/joliveira/rating-needs-name
Added a rule to reinforce the use of an accessible name on the Rating component
2 parents 20e8ff9 + 29b51cb commit 3d3279d

File tree

5 files changed

+145
-5
lines changed

5 files changed

+145
-5
lines changed

COVERAGE.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,16 @@ We currently cover the following components:
3737
- [x] Input
3838
- [x] Label
3939
- [x] Link
40-
- [] Menu
41-
- [] Menu
42-
- [] MenuList
43-
- [] MessageBar
40+
- [x] Menu
41+
- [x] Menu
42+
- [x] MenuList
43+
- [x] MessageBar
4444
- [N/A] Overflow
4545
- [] Persona
4646
- [] Popover
4747
- [N/A] Portal
4848
- [x] ProgressBar
49-
- [] Rating
49+
- [x] Rating
5050
- [] RatingDisplay
5151
- [x] Radio
5252
- [x] RadioGroup

docs/rules/rating-needs-name.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Accessibility: Ratings must have accessible labelling: name, aria-label, aria-labelledby or itemLabel which generates aria-label (`@microsoft/fluentui-jsx-a11y/rating-needs-name`)
2+
3+
All interactive elements must have an accessible name.
4+
5+
## Rule Details
6+
7+
This rule aims to enforce that a Rating element must have an accessible label associated with it.
8+
9+
Examples of **incorrect** code for this rule:
10+
11+
```js
12+
13+
<Rating />
14+
15+
```
16+
17+
Examples of **correct** code for this rule:
18+
19+
```js
20+
21+
<Rating itemLabel={number => `Rating of ${number} starts`} />
22+
23+
```
24+
25+
### Options
26+
27+
FluentUI supports receiving a function that will add the aria-label to the element with the number. This prop is called itemLabel.
28+
If this is not the desired route, a name, aria-label or aria-labelledby can be added instead.
29+
30+
## When Not To Use It
31+
32+
You might want to turn this rule off if you don't intend for this component to be read by screen readers.
33+
34+
## Further Reading
35+
36+
- [ARIA in HTML](https://www.w3.org/TR/html-aria/)

lib/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import tooltipNotRecommended from "./rules/tooltip-not-recommended";
2424
import avatarNeedsName from "./rules/avatar-needs-name";
2525
import radioButtonMissingLabel from "./rules/radio-button-missing-label";
2626
import radiogroupMissingLabel from "./rules/radiogroup-missing-label";
27+
import ratingNeedsName from "./rules/rating-needs-name";
2728
import dialogbodyNeedsTitleContentAndActions from "./rules/dialogbody-needs-title-content-and-actions";
2829
import dialogsurfaceNeedsAria from "./rules/dialogsurface-needs-aria";
2930
import spinnerNeedsLabelling from "./rules/spinner-needs-labelling";
@@ -63,6 +64,7 @@ module.exports = {
6364
"avatar-needs-name": avatarNeedsName,
6465
"radio-button-missing-label": radioButtonMissingLabel,
6566
"radiogroup-missing-label": radiogroupMissingLabel,
67+
"rating-needs-name": ratingNeedsName,
6668
"prefer-aria-over-title-attribute": preferAriaOverTitleAttribute,
6769
"dialogbody-needs-title-content-and-actions": dialogbodyNeedsTitleContentAndActions,
6870
"dialogsurface-needs-aria": dialogsurfaceNeedsAria,
@@ -96,6 +98,7 @@ module.exports = {
9698
"@microsoft/fluentui-jsx-a11y/avatar-needs-name": "error",
9799
"@microsoft/fluentui-jsx-a11y/radio-button-missing-label": "error",
98100
"@microsoft/fluentui-jsx-a11y/radiogroup-missing-label": "error",
101+
"@microsoft/fluentui-jsx-a11y/rating-needs-name": "error",
99102
"@microsoft/fluentui-jsx-a11y/prefer-aria-over-title-attribute": "warn",
100103
"@microsoft/fluentui-jsx-a11y/dialogbody-needs-title-content-and-actions": "error",
101104
"@microsoft/fluentui-jsx-a11y/dialogsurface-needs-aria": "error",

lib/rules/rating-needs-name.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
"use strict";
5+
6+
import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
7+
import { hasNonEmptyProp } from "../util/hasNonEmptyProp";
8+
import { elementType } from "jsx-ast-utils";
9+
import { hasAssociatedLabelViaAriaLabelledBy } from "../util/labelUtils";
10+
import { JSXOpeningElement } from "estree-jsx";
11+
12+
const rule = ESLintUtils.RuleCreator.withoutDocs({
13+
defaultOptions: [],
14+
meta: {
15+
// possible error messages for the rule
16+
messages: {
17+
missingAriaLabel: 'Accessibility - ratings must have an accessible name or an itemLabel that generates an aria label'
18+
},
19+
// "problem" means the rule is identifying code that either will cause an error or may cause a confusing behavior: https://eslint.org/docs/latest/developer-guide/working-with-rules
20+
type: "problem",
21+
// docs for the rule
22+
docs: {
23+
description: "Accessibility: Ratings must have accessible labelling: name, aria-label, aria-labelledby or itemLabel which generates aria-label",
24+
recommended: "strict",
25+
url: "https://www.w3.org/TR/html-aria/" // URL to the documentation page for this rule
26+
},
27+
schema: []
28+
},
29+
30+
create(context) {
31+
return {
32+
// visitor functions for different types of nodes
33+
JSXOpeningElement(node: TSESTree.JSXOpeningElement) {
34+
// if it is not a listed component, return
35+
if (
36+
elementType(node as JSXOpeningElement) !== "Rating"
37+
) {
38+
return;
39+
}
40+
41+
// wrapped in Label tag, labelled with htmlFor, labelled with aria-labelledby
42+
if (
43+
hasNonEmptyProp(node.attributes, "itemLabel") ||
44+
hasNonEmptyProp(node.attributes, "name") ||
45+
hasNonEmptyProp(node.attributes, "aria-label") ||
46+
hasAssociatedLabelViaAriaLabelledBy(node, context)
47+
) {
48+
return;
49+
}
50+
51+
context.report({
52+
node,
53+
messageId: `missingAriaLabel`
54+
});
55+
}
56+
};
57+
}
58+
});
59+
60+
export default rule;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
"use strict";
5+
6+
//------------------------------------------------------------------------------
7+
// Requirements
8+
//------------------------------------------------------------------------------
9+
10+
import { Rule } from "eslint";
11+
import ruleTester from "./helper/ruleTester";
12+
import rule from "../../../lib/rules/rating-needs-name";
13+
14+
//------------------------------------------------------------------------------
15+
// Tests
16+
//------------------------------------------------------------------------------
17+
18+
ruleTester.run("rating-needs-name", rule as unknown as Rule.RuleModule, {
19+
valid: [
20+
// give me some code that won't trigger a warning
21+
'<Rating itemLabel={itemLabel} />',
22+
'<Rating name="Rating" />',
23+
'<Rating aria-label="Rating" />',
24+
'<><Label id="label-id">Rating</Label><Rating aria-labelledby="label-id" /></>',
25+
'<Rating itemLabel={itemLabel}></Rating>',
26+
'<Rating name="Rating"></Rating>',
27+
'<Rating aria-label="Rating"></Rating>',
28+
'<><Label id="label-id">Rating</Label><Rating aria-labelledby="label-id"></Rating></>'
29+
],
30+
31+
invalid: [
32+
{
33+
code: "<Rating />",
34+
errors: [{ messageId: "missingAriaLabel" }]
35+
},
36+
{
37+
code: "<Rating></Rating>",
38+
errors: [{ messageId: "missingAriaLabel" }]
39+
}
40+
]
41+
});

0 commit comments

Comments
 (0)