Skip to content

Commit 8111277

Browse files
authored
Merge pull request #92 from microsoft/user/aubreyquinn/badge
Added a new rule for the Badge component
2 parents 915a776 + 9994400 commit 8111277

File tree

6 files changed

+248
-7
lines changed

6 files changed

+248
-7
lines changed

COVERAGE.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ We currently cover the following components:
66
- [x] Avatar
77
- [x] AvatarGroup
88
- [] Badge
9+
- [x] Badge
10+
- [] CounterBadge
11+
- [N/A] PresenceBadge
12+
- [x] Breadcrumb
913
- [x] Button
1014
- [x] Button
1115
- [X] CompoundButton
@@ -69,7 +73,6 @@ We currently cover the following components:
6973
- [x] Toolbar
7074
- [x] Tooltip
7175
- [] Tree
72-
- [x] Breadcrumb
7376
- [x] Datepicker
7477
- [] Calendar
7578
- [] Timepicker

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ Any use of third-party trademarks or logos are subject to those third-party's po
151151
| [accordion-header-needs-labelling](docs/rules/accordion-header-needs-labelling.md) | The accordion header is a button and it needs an accessibile name e.g. text content, aria-label, aria-labelledby. | ✅ | | |
152152
| [accordion-item-needs-header-and-panel](docs/rules/accordion-item-needs-header-and-panel.md) | An AccordionItem needs exactly one header and one panel | ✅ | | |
153153
| [avatar-needs-name](docs/rules/avatar-needs-name.md) | Accessibility: Avatar must have an accessible labelling: name, aria-label, aria-labelledby | ✅ | | |
154+
| [badge-needs-accessible-name](docs/rules/badge-needs-accessible-name.md) | | ✅ | | 🔧 |
154155
| [breadcrumb-needs-labelling](docs/rules/breadcrumb-needs-labelling.md) | All interactive elements must have an accessible name | ✅ | | |
155156
| [checkbox-needs-labelling](docs/rules/checkbox-needs-labelling.md) | Accessibility: Checkbox without label must have an accessible and visual label: aria-labelledby | ✅ | | |
156157
| [combobox-needs-labelling](docs/rules/combobox-needs-labelling.md) | All interactive elements must have an accessible name | ✅ | | |
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# @microsoft/fluentui-jsx-a11y/badge-needs-accessible-name
2+
3+
💼 This rule is enabled in the ✅ `recommended` config.
4+
5+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6+
7+
<!-- end auto-generated rule header -->
8+
9+
Badge information should be surfaced as part of the control that it is associated with, because, badges themselves do not receive focus meaning they are not directly accessible by screen readers. If the combination of icon and badge communicates some meaningful information, that information should be surfaced in another way through screenreader or tooltip on the component the badge is associated with.
10+
11+
Badge content is exposed as text, and is treated by screen readers as if it were inline content of the control it is associated with.
12+
13+
## Rule Details
14+
15+
Ensure that the `Badge` component is accessible.
16+
17+
Examples of **incorrect** code for this rule:
18+
19+
```jsx
20+
<Badge icon={<PasteIcon />} />
21+
```
22+
23+
```jsx
24+
<Badge appearance="filled" color="brand" />} />
25+
```
26+
27+
```jsx
28+
<Badge size="extra-large" />
29+
```
30+
31+
Examples of **correct** code for this rule:
32+
33+
If the badge contains a custom icon, that icon must be given alternative text with aria-label, unless it is purely presentational:
34+
35+
```jsx
36+
<Badge icon={<PasteIcon aria-label="paste" />} />
37+
```
38+
39+
Badge shouldn't rely only on color information. Include meaningful descriptions when using color to represent meaning in a badge. If relying on color only ensure that non-visual information is included in the parent's label or description. Alternatively, mark up the Badge as an image with a label:
40+
41+
```jsx
42+
<Badge role="img" aria-label="Active" appearance="filled" color="brand" />} />
43+
```
44+
45+
```jsx
46+
<Badge appearance="tint">999+</Badge>
47+
```
48+
49+
## Further Reading
50+
51+
<https://react.fluentui.dev/?path=/docs/components-badge-badge--docs>
52+

lib/index.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ module.exports = {
4040
"avoid-using-aria-describedby-for-primary-labelling": require("./rules/avoid-using-aria-describedby-for-primary-labelling"),
4141
"dialogbody-needs-title-content-and-actions": require("./rules/dialogbody-needs-title-content-and-actions"),
4242
"dialogsurface-needs-aria": require("./rules/dialogsurface-needs-aria"),
43-
"spinner-needs-labelling": require("./rules/spinner-needs-labelling")
43+
"spinner-needs-labelling": require("./rules/spinner-needs-labelling"),
44+
"badge-needs-accessible-name": require("./rules/badge-needs-accessible-name")
4445
},
4546
configs: {
4647
recommended: {
@@ -68,13 +69,10 @@ module.exports = {
6869
"@microsoft/fluentui-jsx-a11y/radio-button-missing-label": "error",
6970
"@microsoft/fluentui-jsx-a11y/radiogroup-missing-label": "error",
7071
"@microsoft/fluentui-jsx-a11y/prefer-aria-over-title-attribute": "warn",
71-
<<<<<<< HEAD
72-
"@microsoft/fluentui-jsx-a11y/avoid-using-aria-describedby-for-primary-labelling": "warn"
73-
=======
72+
"@microsoft/fluentui-jsx-a11y/avoid-using-aria-describedby-for-primary-labelling": "warn",
7473
"@microsoft/fluentui-jsx-a11y/dialogbody-needs-title-content-and-actions": "error",
7574
"@microsoft/fluentui-jsx-a11y/dialogsurface-needs-aria": "error",
7675
"@microsoft/fluentui-jsx-a11y/spinner-needs-labelling": "error"
77-
>>>>>>> origin/main
7876
}
7977
}
8078
}
@@ -84,4 +82,3 @@ module.exports = {
8482
module.exports.processors = {
8583
// add your processors here
8684
};
87-
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
"use strict";
5+
6+
var elementType = require("jsx-ast-utils").elementType;
7+
const { getPropValue, getProp } = require("jsx-ast-utils");
8+
const { hasTextContentChild } = require("../util/hasTextContentChild");
9+
var hasProp = require("jsx-ast-utils").hasProp;
10+
11+
//------------------------------------------------------------------------------
12+
// Rule Definition
13+
//------------------------------------------------------------------------------
14+
15+
/** @type {import('eslint').Rule.RuleModule} */
16+
module.exports = {
17+
meta: {
18+
// possible error messages for the rule
19+
messages: {
20+
badgeNeedsAccessibleName: "Badge: needs accessible name. Add text content or a labelled image.",
21+
colourOnlyBadgesNeedAttributes: 'Color-only <Badge> must have role="img" and an aria-label attribute.',
22+
badgeIconNeedsLabelling: "The icon inside <Badge> must have an aria-label attribute."
23+
},
24+
type: "problem", // `problem`, `suggestion`, or `layout`
25+
docs: {
26+
description: "",
27+
recommended: false,
28+
url: "https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/img_role" // URL to the documentation page for this rule
29+
},
30+
fixable: "code", // Or `code` or `whitespace`
31+
schema: [] // Add a schema if the rule has options
32+
},
33+
34+
create(context) {
35+
return {
36+
// visitor functions for different types of nodes
37+
JSXElement(node) {
38+
const openingElement = node.openingElement;
39+
40+
// If it's not a Badge component, return early
41+
if (elementType(openingElement) !== "Badge") {
42+
return;
43+
}
44+
45+
const hasTextContent = hasTextContentChild(node);
46+
47+
// Check if Badge has text content and return early if it does
48+
if (hasTextContent) {
49+
return;
50+
}
51+
52+
// Check if Badge has an icon
53+
const hasIconProp = hasProp(openingElement.attributes, "icon");
54+
55+
if (hasIconProp) {
56+
const iconProp = getProp(openingElement.attributes, "icon");
57+
58+
if (iconProp) {
59+
const iconElement = iconProp.value.expression;
60+
61+
// Check if the icon has an aria-label
62+
const ariaLabelAttr = hasProp(iconElement.openingElement.attributes, "aria-label");
63+
64+
// Report an error if aria-label is missing
65+
if (!ariaLabelAttr) {
66+
context.report({
67+
node,
68+
messageId: "badgeIconNeedsLabelling",
69+
fix(fixer) {
70+
const ariaLabelFix = fixer.insertTextAfter(iconElement.openingElement.name, ' aria-label=""');
71+
return ariaLabelFix;
72+
}
73+
});
74+
}
75+
}
76+
}
77+
78+
// Simplified logic to check for a color-only Badge (no icon, no text)
79+
const hasColorProp = hasProp(openingElement.attributes, "color");
80+
const hasRole = getPropValue(getProp(openingElement.attributes, "role")) === "img";
81+
const hasAriaLabel = hasProp(openingElement.attributes, "aria-label");
82+
83+
// If it's color-only, ensure it has role="img" and aria-label
84+
if (!hasIconProp && !(hasRole && hasAriaLabel)) {
85+
if (hasColorProp) {
86+
context.report({
87+
node,
88+
messageId: "colourOnlyBadgesNeedAttributes",
89+
fix(fixer) {
90+
const fixes = [];
91+
92+
// Fix role by adding role="img"
93+
if (!hasRole) {
94+
fixes.push(fixer.insertTextAfter(openingElement.name, ' role="img"'));
95+
}
96+
97+
// Fix aria-label by adding aria-label=""
98+
if (!hasAriaLabel) {
99+
fixes.push(fixer.insertTextAfter(openingElement.name, ' aria-label=""'));
100+
}
101+
102+
return fixes;
103+
}
104+
});
105+
} else {
106+
context.report({
107+
node,
108+
messageId: "badgeNeedsAccessibleName"
109+
});
110+
}
111+
}
112+
}
113+
};
114+
}
115+
};
116+
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
"use strict";
5+
6+
//------------------------------------------------------------------------------
7+
// Requirements
8+
//------------------------------------------------------------------------------
9+
10+
const rule = require("../../../lib/rules/badge-needs-accessible-name"),
11+
RuleTester = require("eslint").RuleTester;
12+
13+
RuleTester.setDefaultConfig({
14+
parserOptions: {
15+
ecmaVersion: 6,
16+
ecmaFeatures: {
17+
jsx: true
18+
}
19+
}
20+
});
21+
22+
//------------------------------------------------------------------------------
23+
// Tests
24+
//------------------------------------------------------------------------------
25+
26+
const ruleTester = new RuleTester();
27+
ruleTester.run("badge-needs-accessible-name", rule, {
28+
valid: [
29+
`<Badge role="img" aria-label="Active" appearance="filled" color="brand" />`,
30+
`<Badge icon={<PasteIcon aria-label="paste" />} />`,
31+
`<div><Badge appearance="filled">999+</Badge></div>`
32+
],
33+
34+
invalid: [
35+
{
36+
code: `<Badge appearance="filled" color="brand" />`,
37+
errors: [{ messageId: "colourOnlyBadgesNeedAttributes" }],
38+
output: `<Badge role="img" aria-label="" appearance="filled" color="brand" />`
39+
},
40+
{
41+
code: `<Badge appearance="filled" color="brand" aria-label="Active" />`,
42+
errors: [{ messageId: "colourOnlyBadgesNeedAttributes" }],
43+
output: `<Badge role="img" appearance="filled" color="brand" aria-label="Active" />`
44+
},
45+
{
46+
code: `<Badge appearance="filled" color="brand" role="img" />`,
47+
errors: [{ messageId: "colourOnlyBadgesNeedAttributes" }],
48+
output: `<Badge aria-label="" appearance="filled" color="brand" role="img" />`
49+
},
50+
{
51+
code: `<Badge icon={<PasteIcon />} />`,
52+
errors: [{ messageId: "badgeIconNeedsLabelling" }],
53+
output: `<Badge icon={<PasteIcon aria-label="" />} />`
54+
},
55+
{
56+
code: `<Badge appearance="filled" color="brand"></Badge>`,
57+
errors: [{ messageId: "colourOnlyBadgesNeedAttributes" }],
58+
output: `<Badge role="img" aria-label="" appearance="filled" color="brand"></Badge>`
59+
},
60+
{
61+
code: `<Badge></Badge>`,
62+
errors: [{ messageId: "badgeNeedsAccessibleName" }],
63+
output: null
64+
},
65+
{
66+
code: `<Badge size="medium" appearance="filled" />`,
67+
errors: [{ messageId: "badgeNeedsAccessibleName" }],
68+
output: null
69+
}
70+
]
71+
});
72+

0 commit comments

Comments
 (0)