Skip to content

Commit b413557

Browse files
author
Iryna Vasylenko
committed
Refactor the rule
1 parent 07398b5 commit b413557

File tree

6 files changed

+127
-104
lines changed

6 files changed

+127
-104
lines changed

README.md

Lines changed: 37 additions & 37 deletions
Large diffs are not rendered by default.

docs/rules/tag-dismissible-needs-labelling.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
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`)
1+
# This rule aims to ensure that dismissible Tag components have proper accessibility labelling: either aria-label on dismissIcon or aria-label on Tag with role='presentation' on dismissIcon (`@microsoft/fluentui-jsx-a11y/tag-dismissible-needs-labelling`)
22

33
💼 This rule is enabled in the ✅ `recommended` config.
44

lib/index.ts

Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -13,43 +13,6 @@ 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-
},
5316
configs: {
5417
recommended: {
5518
rules: {
@@ -90,10 +53,47 @@ module.exports = {
9053
"@microsoft/fluentui-jsx-a11y/visual-label-better-than-aria-suggestion": "warn"
9154
}
9255
}
56+
},
57+
rules: {
58+
"accordion-header-needs-labelling": rules.accordionHeaderNeedsLabelling,
59+
"accordion-item-needs-header-and-panel": rules.accordionItemNeedsHeaderAndPanel,
60+
"avatar-needs-name": rules.avatarNeedsName,
61+
"avoid-using-aria-describedby-for-primary-labelling": rules.avoidUsingAriaDescribedByForPrimaryLabelling,
62+
"badge-needs-accessible-name": rules.badgeNeedsAccessibleName,
63+
"breadcrumb-needs-labelling": rules.breadcrumbNeedsLabelling,
64+
"checkbox-needs-labelling": rules.checkboxNeedsLabelling,
65+
"combobox-needs-labelling": rules.comboboxNeedsLabelling,
66+
"compound-button-needs-labelling": rules.compoundButtonNeedsLabelling,
67+
"counter-badge-needs-count": rules.counterBadgeNeedsCount,
68+
"dialogbody-needs-title-content-and-actions": rules.dialogbodyNeedsTitleContentAndActions,
69+
"dialogsurface-needs-aria": rules.dialogsurfaceNeedsAria,
70+
"dropdown-needs-labelling": rules.dropdownNeedsLabelling,
71+
"field-needs-labelling": rules.fieldNeedsLabelling,
72+
"image-button-missing-aria": rules.imageButtonMissingAria,
73+
"input-components-require-accessible-name": rules.inputComponentsRequireAccessibleName,
74+
"link-missing-labelling": rules.linkMissingLabelling,
75+
"menu-item-needs-labelling": rules.menuItemNeedsLabelling,
76+
"no-empty-buttons": rules.noEmptyButtons,
77+
"no-empty-components": rules.noEmptyComponents,
78+
"prefer-aria-over-title-attribute": rules.preferAriaOverTitleAttribute,
79+
"progressbar-needs-labelling": rules.progressbarNeedsLabelling,
80+
"radio-button-missing-label": rules.radioButtonMissingLabel,
81+
"radiogroup-missing-label": rules.radiogroupMissingLabel,
82+
"rating-needs-name": rules.ratingNeedsName,
83+
"spin-button-needs-labelling": rules.spinButtonNeedsLabelling,
84+
"spin-button-unrecommended-labelling": rules.spinButtonUnrecommendedLabelling,
85+
"spinner-needs-labelling": rules.spinnerNeedsLabelling,
86+
"switch-needs-labelling": rules.switchNeedsLabelling,
87+
"tablist-and-tabs-need-labelling": rules.tablistAndTabsNeedLabelling,
88+
"tag-dismissible-needs-labelling": rules.tagDismissibleNeedsLabelling,
89+
"tag-needs-name": rules.tagNeedsName,
90+
"toolbar-missing-aria": rules.toolbarMissingAria,
91+
"tooltip-not-recommended": rules.tooltipNotRecommended,
92+
"visual-label-better-than-aria-suggestion": rules.visualLabelBetterThanAriaSuggestion
9393
}
9494
};
9595

9696
// import processors
9797
module.exports.processors = {
9898
// add your processors here
99-
};
99+
};

lib/rules/tag-dismissible-needs-labelling.ts

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,20 @@
33

44
import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
55
import { elementType, hasProp, getProp, getPropValue } from "jsx-ast-utils";
6+
import { hasNonEmptyProp } from "../util/hasNonEmptyProp";
67
import { JSXOpeningElement, JSXAttribute } from "estree-jsx";
78

89
//------------------------------------------------------------------------------
910
// Utility Functions
1011
//------------------------------------------------------------------------------
1112

1213
/**
13-
* Checks if a value is a non-empty string (same logic as hasNonEmptyProp for strings)
14+
* Checks if a value is a non-empty string
1415
*/
1516
const isNonEmptyString = (value: any): boolean => {
1617
return typeof value === "string" && value.trim().length > 0;
1718
};
1819

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-
2720
//------------------------------------------------------------------------------
2821
// Rule Definition
2922
//------------------------------------------------------------------------------
@@ -33,14 +26,16 @@ const rule = ESLintUtils.RuleCreator.withoutDocs({
3326
type: "problem",
3427
docs: {
3528
description:
36-
"This rule aims to ensure that dismissible Tag components have an aria-label on the dismiss button",
37-
recommended: false
29+
"This rule aims to ensure that dismissible Tag components have proper accessibility labelling: either aria-label on dismissIcon or aria-label on Tag with role on dismissIcon",
30+
recommended: "strict",
31+
url: "https://react.fluentui.dev/?path=/docs/components-tag-tag--docs"
3832
},
3933
fixable: undefined,
4034
schema: [],
4135
messages: {
42-
missingDismissLabel: "Accessibility: Dismissible Tag must have dismissIcon with aria-label"
43-
},
36+
missingDismissLabel:
37+
"Accessibility: Dismissible Tag must have either aria-label on dismissIcon or aria-label on Tag with role on dismissIcon"
38+
}
4439
},
4540
create(context) {
4641
return {
@@ -59,9 +54,8 @@ const rule = ESLintUtils.RuleCreator.withoutDocs({
5954
return;
6055
}
6156

62-
// Check if dismissible Tag has dismissIcon with aria-label
57+
// Check if dismissible Tag has proper accessibility labelling
6358
const dismissIconProp = getProp(openingElement.attributes as JSXAttribute[], "dismissIcon");
64-
6559
if (!dismissIconProp) {
6660
context.report({
6761
node,
@@ -70,10 +64,23 @@ const rule = ESLintUtils.RuleCreator.withoutDocs({
7064
return;
7165
}
7266

73-
// Get the dismissIcon value and check if it has valid aria-label
7467
const dismissIconValue = getPropValue(dismissIconProp);
75-
76-
if (!hasNonEmptyObjectProperty(dismissIconValue, "aria-label")) {
68+
69+
// Check if dismissIcon has aria-label
70+
const dismissIconHasAriaLabel =
71+
dismissIconValue && typeof dismissIconValue === "object" && isNonEmptyString((dismissIconValue as any)["aria-label"]);
72+
73+
// Check if dismissIcon has role
74+
const dismissIconHasRole =
75+
dismissIconValue && typeof dismissIconValue === "object" && isNonEmptyString((dismissIconValue as any)["role"]);
76+
77+
// Check if Tag has aria-label (required when dismissIcon has role)
78+
const tagHasAriaLabel = hasNonEmptyProp(openingElement.attributes, "aria-label");
79+
// Valid patterns:
80+
// Option 1: dismissIcon has aria-label
81+
// Option 2: Tag has aria-label and dismissIcon has role
82+
const hasValidLabelling = dismissIconHasAriaLabel || (tagHasAriaLabel && dismissIconHasRole);
83+
if (!hasValidLabelling) {
7784
context.report({
7885
node,
7986
messageId: `missingDismissLabel`

tests/lib/rules/tag-dismissible-needs-labelling.test.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,42 @@ ruleTester.run("tag-dismissible-needs-labelling", rule as unknown as Rule.RuleMo
1717
// Valid cases for dismissible Tag component
1818
// Non-dismissible tags should be ignored
1919
"<Tag>Regular tag</Tag>",
20-
'<Tag icon={<SettingsIcon />}>Tag with icon</Tag>',
21-
22-
// Dismissible tags with proper labelling
20+
"<Tag icon={<SettingsIcon />}>Tag with icon</Tag>",
21+
// Option 1: dismissIcon with aria-label
2322
'<Tag dismissible dismissIcon={{ "aria-label": "remove" }}>Dismissible tag</Tag>',
24-
'<Tag dismissible dismissIcon={{ "aria-label": "close" }} icon={<CalendarMonthRegular />}>Tag with icon</Tag>'
23+
'<Tag dismissible dismissIcon={{ "aria-label": "close" }} icon={<CalendarMonthRegular />}>Tag with icon</Tag>',
24+
// Option 2: Tag with aria-label and dismissIcon with role
25+
'<Tag dismissible aria-label="Dismissible tag" dismissIcon={{ role: "presentation" }}>Dismissible tag</Tag>'
2526
],
2627

2728
invalid: [
2829
// Invalid cases for dismissible Tag component
2930
{
30-
code: '<Tag dismissible>Dismissible tag</Tag>',
31+
code: "<Tag dismissible>Dismissible tag</Tag>",
3132
errors: [{ messageId: "missingDismissLabel" }]
3233
},
3334
{
34-
code: '<Tag dismissible dismissIcon={{}}>Dismissible tag</Tag>',
35+
code: "<Tag dismissible dismissIcon={{}}>Dismissible tag</Tag>",
3536
errors: [{ messageId: "missingDismissLabel" }]
3637
},
3738
{
3839
code: '<Tag dismissible dismissIcon={{ "aria-label": "" }}>Dismissible tag</Tag>',
3940
errors: [{ messageId: "missingDismissLabel" }]
41+
},
42+
// Missing aria-label on Tag when dismissIcon has role
43+
{
44+
code: '<Tag dismissible dismissIcon={{ role: "presentation" }}>Dismissible tag</Tag>',
45+
errors: [{ messageId: "missingDismissLabel" }]
46+
},
47+
// Empty aria-label on Tag with dismissIcon role
48+
{
49+
code: '<Tag dismissible aria-label="" dismissIcon={{ role: "presentation" }}>Dismissible tag</Tag>',
50+
errors: [{ messageId: "missingDismissLabel" }]
51+
},
52+
// Tag has aria-label but dismissIcon has empty role
53+
{
54+
code: '<Tag dismissible aria-label="Dismissible tag" dismissIcon={{ role: "" }}>Dismissible tag</Tag>',
55+
errors: [{ messageId: "missingDismissLabel" }]
4056
}
4157
]
4258
});

tests/lib/rules/tag-needs-name.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ ruleTester.run("tag-needs-name", rule as unknown as Rule.RuleModule, {
2020
"<Tag>Some text</Tag>",
2121
'<Tag aria-label="Accessible tag name"></Tag>',
2222
'<Tag aria-label="Tag label">Some text</Tag>',
23-
'<Tag icon={<SettingsIcon />}>Tag with icon and text</Tag>',
23+
"<Tag icon={<SettingsIcon />}>Tag with icon and text</Tag>",
2424
'<Tag icon={<SettingsIcon />} aria-label="Settings tag"></Tag>'
2525
],
2626

@@ -35,15 +35,15 @@ ruleTester.run("tag-needs-name", rule as unknown as Rule.RuleModule, {
3535
errors: [{ messageId: "missingAriaLabel" }]
3636
},
3737
{
38-
code: '<Tag aria-label=""></Tag>',
38+
code: '<Tag aria-label="" ></Tag>',
3939
errors: [{ messageId: "missingAriaLabel" }]
4040
},
4141
{
42-
code: '<Tag icon={<SettingsIcon />}></Tag>',
42+
code: "<Tag icon={<SettingsIcon />}></Tag>",
4343
errors: [{ messageId: "missingAriaLabel" }]
4444
},
4545
{
46-
code: '<Tag icon={<SettingsIcon />} />',
46+
code: "<Tag icon={<SettingsIcon />} />",
4747
errors: [{ messageId: "missingAriaLabel" }]
4848
}
4949
]

0 commit comments

Comments
 (0)