Skip to content

Commit 07398b5

Browse files
author
Iryna Vasylenko
committed
Fix merge conflicts
1 parent 3977d44 commit 07398b5

File tree

9 files changed

+392
-39
lines changed

9 files changed

+392
-39
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,10 @@ Any use of third-party trademarks or logos are subject to those third-party's po
137137
| [spin-button-needs-labelling](docs/rules/spin-button-needs-labelling.md) | Accessibility: SpinButtons must have an accessible label | ✅ | | |
138138
| [spin-button-unrecommended-labelling](docs/rules/spin-button-unrecommended-labelling.md) | Accessibility: Unrecommended accessibility labelling - SpinButton | ✅ | | |
139139
| [spinner-needs-labelling](docs/rules/spinner-needs-labelling.md) | Accessibility: Spinner must have either aria-label or label, aria-live and aria-busy attributes | ✅ | | |
140-
| [swatchpicker-needs-labelling](docs/rules/swatchpicker-needs-labelling.md) | Accessibility: SwatchPicker must have an accessible name via aria-label, aria-labelledby, Field component, etc.. | ✅ | | |
141140
| [switch-needs-labelling](docs/rules/switch-needs-labelling.md) | Accessibility: Switch must have an accessible label | ✅ | | |
142141
| [tablist-and-tabs-need-labelling](docs/rules/tablist-and-tabs-need-labelling.md) | This rule aims to ensure that Tabs with icons but no text labels have an accessible name and that Tablist is properly labeled. | ✅ | | |
142+
| [tag-dismissible-needs-labelling](docs/rules/tag-dismissible-needs-labelling.md) | This rule aims to ensure that dismissible Tag components have an aria-label on the dismiss button | ✅ | | |
143+
| [tag-needs-name](docs/rules/tag-needs-name.md) | This rule aims to ensure that Tag component have an accessible name via text content, aria-label, or aria-labelledby. | ✅ | | |
143144
| [toolbar-missing-aria](docs/rules/toolbar-missing-aria.md) | Accessibility: Toolbars need accessible labelling: aria-label or aria-labelledby | ✅ | | |
144145
| [tooltip-not-recommended](docs/rules/tooltip-not-recommended.md) | Accessibility: Prefer text content or aria over a tooltip for these components MenuItem, SpinButton | ✅ | | |
145146
| [visual-label-better-than-aria-suggestion](docs/rules/visual-label-better-than-aria-suggestion.md) | Visual label is better than an aria-label because sighted users can't read the aria-label text. | | ✅ | |
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# This rule aims to ensure that dismissible Tag components have an aria-label on the dismiss button (`@microsoft/fluentui-jsx-a11y/tag-dismissible-needs-labelling`)
2+
3+
💼 This rule is enabled in the ✅ `recommended` config.
4+
5+
<!-- end auto-generated rule header -->
6+
7+
All interactive elements must have an accessible name.
8+
9+
Dismissible Tag components render a dismiss/close button that must have an accessible name for screen reader users.
10+
11+
When a Tag has the `dismissible` prop, it must provide a `dismissIcon` with an `aria-label`.
12+
13+
<https://react.fluentui.dev/?path=/docs/components-tag-tag--docs>
14+
15+
## Rule Details
16+
17+
This rule aims to ensure that dismissible Tag components have an aria-label on the dismiss button.
18+
19+
Examples of **incorrect** code for this rule:
20+
21+
```jsx
22+
<Tag dismissible>Dismissible tag</Tag>
23+
```
24+
25+
```jsx
26+
<Tag dismissible dismissIcon={{}}>Dismissible tag</Tag>
27+
```
28+
29+
```jsx
30+
<Tag dismissible dismissIcon={{ "aria-label": "" }}>Dismissible tag</Tag>
31+
```
32+
33+
Examples of **correct** code for this rule:
34+
35+
```jsx
36+
<Tag>Regular tag</Tag>
37+
```
38+
39+
```jsx
40+
<Tag icon={<SettingsIcon />}>Tag with icon</Tag>
41+
```
42+
43+
```jsx
44+
<Tag dismissible dismissIcon={{ "aria-label": "remove" }}>Dismissible tag</Tag>
45+
```
46+
47+
```jsx
48+
<Tag dismissible dismissIcon={{ "aria-label": "close" }} icon={<CalendarMonthRegular />}>Tag with icon</Tag>
49+
```

docs/rules/tag-needs-name.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# This rule aims to ensure that Tag component have an accessible name via text content, aria-label, or aria-labelledby (`@microsoft/fluentui-jsx-a11y/tag-needs-name`)
2+
3+
💼 This rule is enabled in the ✅ `recommended` config.
4+
5+
<!-- end auto-generated rule header -->
6+
7+
All interactive elements must have an accessible name.
8+
9+
Tag components need an accessible name for screen reader users.
10+
11+
Please provide text content, aria-label, or aria-labelledby.
12+
13+
<https://react.fluentui.dev/?path=/docs/components-tag-tag--docs>
14+
15+
## Rule Details
16+
17+
This rule aims to ensure that Tag components have an accessible name via text content, aria-label, or aria-labelledby.
18+
19+
Examples of **incorrect** code for this rule:
20+
21+
```jsx
22+
<Tag></Tag>
23+
```
24+
25+
```jsx
26+
<Tag />
27+
```
28+
29+
```jsx
30+
<Tag aria-label=""></Tag>
31+
```
32+
33+
```jsx
34+
<Tag icon={<SettingsIcon />}></Tag>
35+
```
36+
37+
```jsx
38+
<Tag icon={<SettingsIcon />} />
39+
```
40+
41+
Examples of **correct** code for this rule:
42+
43+
```jsx
44+
<Tag>Tag with some text</Tag>
45+
```
46+
47+
```jsx
48+
<Tag aria-label="Accessible tag name"></Tag>
49+
```
50+
51+
```jsx
52+
<Tag aria-label="Tag label">Some text</Tag>
53+
```
54+
55+
```jsx
56+
<Tag icon={<SettingsIcon />}>Tag with icon and text</Tag>
57+
```
58+
59+
```jsx
60+
<Tag icon={<SettingsIcon />} aria-label="Settings tag"></Tag>
61+
```

lib/index.ts

Lines changed: 40 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,43 @@ import * as rules from "./rules";
1313

1414
// import all rules in lib/rules
1515
module.exports = {
16+
rules: {
17+
"accordion-header-needs-labelling": rules.accordionHeaderNeedsLabelling,
18+
"accordion-item-needs-header-and-panel": rules.accordionItemNeedsHeaderAndPanel,
19+
"avatar-needs-name": rules.avatarNeedsName,
20+
"avoid-using-aria-describedby-for-primary-labelling": rules.avoidUsingAriaDescribedByForPrimaryLabelling,
21+
"badge-needs-accessible-name": rules.badgeNeedsAccessibleName,
22+
"breadcrumb-needs-labelling": rules.breadcrumbNeedsLabelling,
23+
"checkbox-needs-labelling": rules.checkboxNeedsLabelling,
24+
"combobox-needs-labelling": rules.comboboxNeedsLabelling,
25+
"compound-button-needs-labelling": rules.compoundButtonNeedsLabelling,
26+
"counter-badge-needs-count": rules.counterBadgeNeedsCount,
27+
"dialogbody-needs-title-content-and-actions": rules.dialogbodyNeedsTitleContentAndActions,
28+
"dialogsurface-needs-aria": rules.dialogsurfaceNeedsAria,
29+
"dropdown-needs-labelling": rules.dropdownNeedsLabelling,
30+
"field-needs-labelling": rules.fieldNeedsLabelling,
31+
"image-button-missing-aria": rules.imageButtonMissingAria,
32+
"input-components-require-accessible-name": rules.inputComponentsRequireAccessibleName,
33+
"link-missing-labelling": rules.linkMissingLabelling,
34+
"menu-item-needs-labelling": rules.menuItemNeedsLabelling,
35+
"no-empty-buttons": rules.noEmptyButtons,
36+
"no-empty-components": rules.noEmptyComponents,
37+
"prefer-aria-over-title-attribute": rules.preferAriaOverTitleAttribute,
38+
"progressbar-needs-labelling": rules.progressbarNeedsLabelling,
39+
"radio-button-missing-label": rules.radioButtonMissingLabel,
40+
"radiogroup-missing-label": rules.radiogroupMissingLabel,
41+
"rating-needs-name": rules.ratingNeedsName,
42+
"spin-button-needs-labelling": rules.spinButtonNeedsLabelling,
43+
"spin-button-unrecommended-labelling": rules.spinButtonUnrecommendedLabelling,
44+
"spinner-needs-labelling": rules.spinnerNeedsLabelling,
45+
"switch-needs-labelling": rules.switchNeedsLabelling,
46+
"tablist-and-tabs-need-labelling": rules.tablistAndTabsNeedLabelling,
47+
"tag-dismissible-needs-labelling": rules.tagDismissibleNeedsLabelling,
48+
"tag-needs-name": rules.tagNeedsName,
49+
"toolbar-missing-aria": rules.toolbarMissingAria,
50+
"tooltip-not-recommended": rules.tooltipNotRecommended,
51+
"visual-label-better-than-aria-suggestion": rules.visualLabelBetterThanAriaSuggestion
52+
},
1653
configs: {
1754
recommended: {
1855
rules: {
@@ -44,54 +81,19 @@ module.exports = {
4481
"@microsoft/fluentui-jsx-a11y/spin-button-needs-labelling": "error",
4582
"@microsoft/fluentui-jsx-a11y/spin-button-unrecommended-labelling": "error",
4683
"@microsoft/fluentui-jsx-a11y/spinner-needs-labelling": "error",
47-
"@microsoft/fluentui-jsx-a11y/swatchpicker-needs-labelling": "error",
4884
"@microsoft/fluentui-jsx-a11y/switch-needs-labelling": "error",
4985
"@microsoft/fluentui-jsx-a11y/tablist-and-tabs-need-labelling": "error",
86+
"@microsoft/fluentui-jsx-a11y/tag-dismissible-needs-labelling": "error",
87+
"@microsoft/fluentui-jsx-a11y/tag-needs-name": "error",
5088
"@microsoft/fluentui-jsx-a11y/toolbar-missing-aria": "error",
5189
"@microsoft/fluentui-jsx-a11y/tooltip-not-recommended": "error",
5290
"@microsoft/fluentui-jsx-a11y/visual-label-better-than-aria-suggestion": "warn"
5391
}
5492
}
55-
},
56-
rules: {
57-
"accordion-header-needs-labelling": rules.accordionHeaderNeedsLabelling,
58-
"accordion-item-needs-header-and-panel": rules.accordionItemNeedsHeaderAndPanel,
59-
"avatar-needs-name": rules.avatarNeedsName,
60-
"avoid-using-aria-describedby-for-primary-labelling": rules.avoidUsingAriaDescribedByForPrimaryLabelling,
61-
"badge-needs-accessible-name": rules.badgeNeedsAccessibleName,
62-
"breadcrumb-needs-labelling": rules.breadcrumbNeedsLabelling,
63-
"checkbox-needs-labelling": rules.checkboxNeedsLabelling,
64-
"combobox-needs-labelling": rules.comboboxNeedsLabelling,
65-
"compound-button-needs-labelling": rules.compoundButtonNeedsLabelling,
66-
"counter-badge-needs-count": rules.counterBadgeNeedsCount,
67-
"dialogbody-needs-title-content-and-actions": rules.dialogbodyNeedsTitleContentAndActions,
68-
"dialogsurface-needs-aria": rules.dialogsurfaceNeedsAria,
69-
"dropdown-needs-labelling": rules.dropdownNeedsLabelling,
70-
"field-needs-labelling": rules.fieldNeedsLabelling,
71-
"image-button-missing-aria": rules.imageButtonMissingAria,
72-
"input-components-require-accessible-name": rules.inputComponentsRequireAccessibleName,
73-
"link-missing-labelling": rules.linkMissingLabelling,
74-
"menu-item-needs-labelling": rules.menuItemNeedsLabelling,
75-
"no-empty-buttons": rules.noEmptyButtons,
76-
"no-empty-components": rules.noEmptyComponents,
77-
"prefer-aria-over-title-attribute": rules.preferAriaOverTitleAttribute,
78-
"progressbar-needs-labelling": rules.progressbarNeedsLabelling,
79-
"radio-button-missing-label": rules.radioButtonMissingLabel,
80-
"radiogroup-missing-label": rules.radiogroupMissingLabel,
81-
"rating-needs-name": rules.ratingNeedsName,
82-
"spin-button-needs-labelling": rules.spinButtonNeedsLabelling,
83-
"spin-button-unrecommended-labelling": rules.spinButtonUnrecommendedLabelling,
84-
"spinner-needs-labelling": rules.spinnerNeedsLabelling,
85-
"swatchpicker-needs-labelling": rules.swatchpickerNeedsLabelling,
86-
"switch-needs-labelling": rules.switchNeedsLabelling,
87-
"tablist-and-tabs-need-labelling": rules.tablistAndTabsNeedLabelling,
88-
"toolbar-missing-aria": rules.toolbarMissingAria,
89-
"tooltip-not-recommended": rules.tooltipNotRecommended,
90-
"visual-label-better-than-aria-suggestion": rules.visualLabelBetterThanAriaSuggestion
9193
}
9294
};
9395

9496
// import processors
9597
module.exports.processors = {
9698
// add your processors here
97-
};
99+
};

lib/rules/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ export { default as spinnerNeedsLabelling } from "./spinner-needs-labelling";
3232
export { default as swatchpickerNeedsLabelling } from "./swatchpicker-needs-labelling";
3333
export { default as switchNeedsLabelling } from "./switch-needs-labelling";
3434
export { default as tablistAndTabsNeedLabelling } from "./tablist-and-tabs-need-labelling";
35+
export { default as tagDismissibleNeedsLabelling } from "./tag-dismissible-needs-labelling";
36+
export { default as tagNeedsName } from "./tag-needs-name";
3537
export { default as toolbarMissingAria } from "./toolbar-missing-aria";
3638
export { default as tooltipNotRecommended } from "./tooltip-not-recommended";
3739
export { default as visualLabelBetterThanAriaSuggestion } from "./visual-label-better-than-aria-suggestion";
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
5+
import { elementType, hasProp, getProp, getPropValue } from "jsx-ast-utils";
6+
import { JSXOpeningElement, JSXAttribute } from "estree-jsx";
7+
8+
//------------------------------------------------------------------------------
9+
// Utility Functions
10+
//------------------------------------------------------------------------------
11+
12+
/**
13+
* Checks if a value is a non-empty string (same logic as hasNonEmptyProp for strings)
14+
*/
15+
const isNonEmptyString = (value: any): boolean => {
16+
return typeof value === "string" && value.trim().length > 0;
17+
};
18+
19+
/**
20+
* Checks if an object has a non-empty string property
21+
*/
22+
const hasNonEmptyObjectProperty = (obj: any, propertyName: string): boolean => {
23+
if (!obj || typeof obj !== "object") return false;
24+
return isNonEmptyString(obj[propertyName]);
25+
};
26+
27+
//------------------------------------------------------------------------------
28+
// Rule Definition
29+
//------------------------------------------------------------------------------
30+
const rule = ESLintUtils.RuleCreator.withoutDocs({
31+
defaultOptions: [],
32+
meta: {
33+
type: "problem",
34+
docs: {
35+
description:
36+
"This rule aims to ensure that dismissible Tag components have an aria-label on the dismiss button",
37+
recommended: false
38+
},
39+
fixable: undefined,
40+
schema: [],
41+
messages: {
42+
missingDismissLabel: "Accessibility: Dismissible Tag must have dismissIcon with aria-label"
43+
},
44+
},
45+
create(context) {
46+
return {
47+
// visitor functions for different types of nodes
48+
JSXElement(node: TSESTree.JSXElement) {
49+
const openingElement = node.openingElement;
50+
51+
// if it is not a Tag, return
52+
if (elementType(openingElement as JSXOpeningElement) !== "Tag") {
53+
return;
54+
}
55+
56+
// Check if Tag has dismissible prop
57+
const isDismissible = hasProp(openingElement.attributes as JSXAttribute[], "dismissible");
58+
if (!isDismissible) {
59+
return;
60+
}
61+
62+
// Check if dismissible Tag has dismissIcon with aria-label
63+
const dismissIconProp = getProp(openingElement.attributes as JSXAttribute[], "dismissIcon");
64+
65+
if (!dismissIconProp) {
66+
context.report({
67+
node,
68+
messageId: `missingDismissLabel`
69+
});
70+
return;
71+
}
72+
73+
// Get the dismissIcon value and check if it has valid aria-label
74+
const dismissIconValue = getPropValue(dismissIconProp);
75+
76+
if (!hasNonEmptyObjectProperty(dismissIconValue, "aria-label")) {
77+
context.report({
78+
node,
79+
messageId: `missingDismissLabel`
80+
});
81+
}
82+
}
83+
};
84+
}
85+
});
86+
87+
export default rule;

lib/rules/tag-needs-name.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
5+
import { elementType } from "jsx-ast-utils";
6+
import { hasNonEmptyProp } from "../util/hasNonEmptyProp";
7+
import { hasTextContentChild } from "../util/hasTextContentChild";
8+
import { hasAssociatedLabelViaAriaLabelledBy } from "../util/labelUtils";
9+
import { JSXOpeningElement } from "estree-jsx";
10+
11+
//------------------------------------------------------------------------------
12+
// Rule Definition
13+
//------------------------------------------------------------------------------
14+
15+
const rule = ESLintUtils.RuleCreator.withoutDocs({
16+
defaultOptions: [],
17+
meta: {
18+
type: "problem",
19+
docs: {
20+
description:
21+
"This rule aims to ensure that Tag component have an accessible name via text content, aria-label, or aria-labelledby.",
22+
recommended: "strict",
23+
url: "https://react.fluentui.dev/?path=/docs/components-tag-tag--docs"
24+
},
25+
fixable: undefined,
26+
schema: [],
27+
messages: {
28+
missingAriaLabel: "Accessibility: Tag must have an accessible name"
29+
}
30+
},
31+
create(context) {
32+
return {
33+
// visitor functions for different types of nodes
34+
JSXElement(node: TSESTree.JSXElement) {
35+
const openingElement = node.openingElement;
36+
37+
// if it is not a Tag, return
38+
if (elementType(openingElement as JSXOpeningElement) !== "Tag") {
39+
return;
40+
}
41+
42+
// Check if tag has any accessible name
43+
const hasTextContent = hasTextContentChild(node);
44+
const hasAriaLabel = hasNonEmptyProp(openingElement.attributes, "aria-label");
45+
const hasAriaLabelledBy = hasAssociatedLabelViaAriaLabelledBy(openingElement, context);
46+
const hasAccessibleName = hasTextContent || hasAriaLabel || hasAriaLabelledBy;
47+
48+
if (!hasAccessibleName) {
49+
context.report({
50+
node,
51+
messageId: `missingAriaLabel`
52+
});
53+
}
54+
}
55+
};
56+
}
57+
});
58+
59+
export default rule;

0 commit comments

Comments
 (0)