diff --git a/COVERAGE.md b/COVERAGE.md index 872b914..834a913 100644 --- a/COVERAGE.md +++ b/COVERAGE.md @@ -13,13 +13,13 @@ We currently cover the following components: - [x] Button - [x] Button - [X] CompoundButton - - [] MenuButton + - [x] MenuButton - [X] MenuItem - [] SplitButton - [x] ToggleButton - [] ToolbarToggleButton - - [] Card - - [] Card + - [x] Card + - [x] Card - [] CardFooter - [] CardHeader - [] CardPreview @@ -37,7 +37,7 @@ We currently cover the following components: - [x] Field - [N/A] FluentProvider - [] Image - - [] InfoLabel + - [x] InfoLabel - [x] Input - [x] Label - [x] Link diff --git a/README.md b/README.md index eead4c4..999ad2b 100644 --- a/README.md +++ b/README.md @@ -178,6 +178,7 @@ Any use of third-party trademarks or logos are subject to those third-party's po | [avoid-using-aria-describedby-for-primary-labelling](docs/rules/avoid-using-aria-describedby-for-primary-labelling.md) | aria-describedby provides additional context and is not meant for primary labeling. | ✅ | | | | [badge-needs-accessible-name](docs/rules/badge-needs-accessible-name.md) | | ✅ | | 🔧 | | [breadcrumb-needs-labelling](docs/rules/breadcrumb-needs-labelling.md) | All interactive elements must have an accessible name | ✅ | | | +| [card-needs-accessible-name](docs/rules/card-needs-accessible-name.md) | Accessibility: Interactive Card must have an accessible name via aria-label, aria-labelledby, etc. | ✅ | | | | [checkbox-needs-labelling](docs/rules/checkbox-needs-labelling.md) | Accessibility: Checkbox without label must have an accessible and visual label: aria-labelledby | ✅ | | | | [colorswatch-needs-labelling](docs/rules/colorswatch-needs-labelling.md) | Accessibility: ColorSwatch must have an accessible name via aria-label, Tooltip, aria-labelledby, etc.. | ✅ | | | | [combobox-needs-labelling](docs/rules/combobox-needs-labelling.md) | All interactive elements must have an accessible name | ✅ | | | @@ -191,8 +192,10 @@ Any use of third-party trademarks or logos are subject to those third-party's po | [image-button-missing-aria](docs/rules/image-button-missing-aria.md) | Accessibility: Image buttons must have accessible labelling: title, aria-label, aria-labelledby, aria-describedby | ✅ | | | | [image-needs-alt](docs/rules/image-needs-alt.md) | Accessibility: Image must have alt attribute with a meaningful description of the image. If the image is decorative, use alt="". | ✅ | | | | [imageswatch-needs-labelling](docs/rules/imageswatch-needs-labelling.md) | Accessibility: ImageSwatch must have an accessible name via aria-label, Tooltip, aria-labelledby, etc.. | ✅ | | | +| [infolabel-needs-labelling](docs/rules/infolabel-needs-labelling.md) | Accessibility: InfoLabel must have an accessible name via aria-label, text content, aria-labelledby, etc. | ✅ | | | | [input-components-require-accessible-name](docs/rules/input-components-require-accessible-name.md) | Accessibility: Input fields must have accessible labelling: aria-label, aria-labelledby or an associated label | ✅ | | | | [link-missing-labelling](docs/rules/link-missing-labelling.md) | Accessibility: Image links must have an accessible name. Add either text content, labelling to the image or labelling to the link itself. | ✅ | | 🔧 | +| [menu-button-needs-labelling](docs/rules/menu-button-needs-labelling.md) | Accessibility: MenuButton must have an accessible name via aria-label, text content, aria-labelledby, etc. | ✅ | | | | [menu-item-needs-labelling](docs/rules/menu-item-needs-labelling.md) | Accessibility: MenuItem without label must have an accessible and visual label: aria-labelledby | ✅ | | | | [no-empty-buttons](docs/rules/no-empty-buttons.md) | Accessibility: Button, ToggleButton, SplitButton, MenuButton, CompoundButton must either text content or icon or child component | ✅ | | | | [no-empty-components](docs/rules/no-empty-components.md) | FluentUI components should not be empty | ✅ | | | diff --git a/docs/rules/card-needs-accessible-name.md b/docs/rules/card-needs-accessible-name.md new file mode 100644 index 0000000..25fedaf --- /dev/null +++ b/docs/rules/card-needs-accessible-name.md @@ -0,0 +1,45 @@ +# Accessibility: Interactive Card must have an accessible name via aria-label, aria-labelledby, etc (`@microsoft/fluentui-jsx-a11y/card-needs-accessible-name`) + +đŸ’ŧ This rule is enabled in the ✅ `recommended` config. + + + +Interactive Card components must have an accessible name for screen readers. + +## Rule Details + +This rule enforces that Card components have proper accessible names when they are interactive (clickable). + +### Noncompliant + +```jsx + + + Card title + + +``` + +### Compliant + +```jsx + + + Card title + + + + + + Card title + + +``` + +## When Not To Use + +If the Card is purely decorative and not interactive, this rule is not necessary. + +## Accessibility guidelines + +- [WCAG 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value.html) diff --git a/docs/rules/infolabel-needs-labelling.md b/docs/rules/infolabel-needs-labelling.md new file mode 100644 index 0000000..ddc01b4 --- /dev/null +++ b/docs/rules/infolabel-needs-labelling.md @@ -0,0 +1,37 @@ +# Accessibility: InfoLabel must have an accessible name via aria-label, text content, aria-labelledby, etc (`@microsoft/fluentui-jsx-a11y/infolabel-needs-labelling`) + +đŸ’ŧ This rule is enabled in the ✅ `recommended` config. + + + +InfoLabel components must have accessible labelling for screen readers. + +## Rule Details + +This rule enforces that InfoLabel components have proper accessible names through aria-label, aria-labelledby, or text content. + +### Noncompliant + +```jsx + +``` + +### Compliant + +```jsx + + + + Help text + + +Help information +``` + +## When Not To Use + +If the InfoLabel is purely decorative, this rule may not be necessary. + +## Accessibility guidelines + +- [WCAG 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value.html) diff --git a/docs/rules/menu-button-needs-labelling.md b/docs/rules/menu-button-needs-labelling.md new file mode 100644 index 0000000..d2dca03 --- /dev/null +++ b/docs/rules/menu-button-needs-labelling.md @@ -0,0 +1,37 @@ +# Accessibility: MenuButton must have an accessible name via aria-label, text content, aria-labelledby, etc (`@microsoft/fluentui-jsx-a11y/menu-button-needs-labelling`) + +đŸ’ŧ This rule is enabled in the ✅ `recommended` config. + + + +MenuButton components must have accessible labelling for screen readers. + +## Rule Details + +This rule enforces that MenuButton components have proper accessible names through aria-label, aria-labelledby, or text content. + +### Noncompliant + +```jsx + +``` + +### Compliant + +```jsx + + + + Options + + +Options +``` + +## When Not To Use + +This rule should always be used for MenuButton components as they are interactive elements. + +## Accessibility guidelines + +- [WCAG 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value.html) diff --git a/lib/applicableComponents/buttonBasedComponents.js b/lib/applicableComponents/buttonBasedComponents.js index 469972e..ac3116d 100644 --- a/lib/applicableComponents/buttonBasedComponents.js +++ b/lib/applicableComponents/buttonBasedComponents.js @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -const applicableComponents = ["Button", "ToggleButton", "CompoundButton"]; +const applicableComponents = ["Button", "ToggleButton", "CompoundButton", "MenuButton", "SplitButton"]; module.exports = { applicableComponents diff --git a/lib/index.ts b/lib/index.ts index 9de9152..f3a13e3 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -22,6 +22,7 @@ module.exports = { "@microsoft/fluentui-jsx-a11y/avoid-using-aria-describedby-for-primary-labelling": "error", "@microsoft/fluentui-jsx-a11y/badge-needs-accessible-name": "error", "@microsoft/fluentui-jsx-a11y/breadcrumb-needs-labelling": "error", + "@microsoft/fluentui-jsx-a11y/card-needs-accessible-name": "error", "@microsoft/fluentui-jsx-a11y/checkbox-needs-labelling": "error", "@microsoft/fluentui-jsx-a11y/colorswatch-needs-labelling": "error", "@microsoft/fluentui-jsx-a11y/combobox-needs-labelling": "error", @@ -35,8 +36,10 @@ module.exports = { "@microsoft/fluentui-jsx-a11y/image-button-missing-aria": "error", "@microsoft/fluentui-jsx-a11y/image-needs-alt": "error", "@microsoft/fluentui-jsx-a11y/imageswatch-needs-labelling": "error", + "@microsoft/fluentui-jsx-a11y/infolabel-needs-labelling": "error", "@microsoft/fluentui-jsx-a11y/input-components-require-accessible-name": "error", "@microsoft/fluentui-jsx-a11y/link-missing-labelling": "error", + "@microsoft/fluentui-jsx-a11y/menu-button-needs-labelling": "error", "@microsoft/fluentui-jsx-a11y/menu-item-needs-labelling": "error", "@microsoft/fluentui-jsx-a11y/no-empty-buttons": "error", "@microsoft/fluentui-jsx-a11y/no-empty-components": "error", @@ -66,6 +69,7 @@ module.exports = { "avoid-using-aria-describedby-for-primary-labelling": rules.avoidUsingAriaDescribedByForPrimaryLabelling, "badge-needs-accessible-name": rules.badgeNeedsAccessibleName, "breadcrumb-needs-labelling": rules.breadcrumbNeedsLabelling, + "card-needs-accessible-name": rules.cardNeedsAccessibleName, "checkbox-needs-labelling": rules.checkboxNeedsLabelling, "colorswatch-needs-labelling": rules.colorSwatchNeedsLabelling, "combobox-needs-labelling": rules.comboboxNeedsLabelling, @@ -79,8 +83,10 @@ module.exports = { "image-button-missing-aria": rules.imageButtonMissingAria, "image-needs-alt": rules.imageNeedsAlt, "imageswatch-needs-labelling": rules.imageSwatchNeedsLabelling, + "infolabel-needs-labelling": rules.infoLabelNeedsLabelling, "input-components-require-accessible-name": rules.inputComponentsRequireAccessibleName, "link-missing-labelling": rules.linkMissingLabelling, + "menu-button-needs-labelling": rules.menuButtonNeedsLabelling, "menu-item-needs-labelling": rules.menuItemNeedsLabelling, "no-empty-buttons": rules.noEmptyButtons, "no-empty-components": rules.noEmptyComponents, diff --git a/lib/rules/buttons/menu-button-needs-labelling.ts b/lib/rules/buttons/menu-button-needs-labelling.ts new file mode 100644 index 0000000..e1cc841 --- /dev/null +++ b/lib/rules/buttons/menu-button-needs-labelling.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ESLintUtils } from "@typescript-eslint/utils"; +import { makeLabeledControlRule } from "../../util/ruleFactory"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +export default ESLintUtils.RuleCreator.withoutDocs( + makeLabeledControlRule({ + component: "MenuButton", + messageId: "menuButtonNeedsLabelling", + description: "Accessibility: MenuButton must have an accessible name via aria-label, text content, aria-labelledby, etc.", + labelProps: ["aria-label"], + allowFieldParent: false, + allowHtmlFor: false, + allowLabelledBy: true, + allowWrappingLabel: true, + allowTooltipParent: true, + allowDescribedBy: false, + allowLabeledChild: true, + allowTextContentChild: true + }) +); diff --git a/lib/rules/card-needs-accessible-name.ts b/lib/rules/card-needs-accessible-name.ts new file mode 100644 index 0000000..6095db2 --- /dev/null +++ b/lib/rules/card-needs-accessible-name.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ESLintUtils } from "@typescript-eslint/utils"; +import { makeLabeledControlRule } from "../util/ruleFactory"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +export default ESLintUtils.RuleCreator.withoutDocs( + makeLabeledControlRule({ + component: "Card", + messageId: "cardNeedsAccessibleName", + description: "Accessibility: Interactive Card must have an accessible name via aria-label, aria-labelledby, etc.", + labelProps: ["aria-label"], + allowFieldParent: false, + allowHtmlFor: false, + allowLabelledBy: true, + allowWrappingLabel: false, + allowTooltipParent: true, + allowDescribedBy: false, + allowLabeledChild: true, + allowTextContentChild: true + }) +); diff --git a/lib/rules/index.ts b/lib/rules/index.ts index 219d9bd..83bc9e1 100644 --- a/lib/rules/index.ts +++ b/lib/rules/index.ts @@ -7,6 +7,7 @@ export { default as avatarNeedsName } from "./avatar-needs-name"; export { default as avoidUsingAriaDescribedByForPrimaryLabelling } from "./avoid-using-aria-describedby-for-primary-labelling"; export { default as badgeNeedsAccessibleName } from "./badge-needs-accessible-name"; export { default as breadcrumbNeedsLabelling } from "./breadcrumb-needs-labelling"; +export { default as cardNeedsAccessibleName } from "./card-needs-accessible-name"; export { default as checkboxNeedsLabelling } from "./checkbox-needs-labelling"; export { default as comboboxNeedsLabelling } from "./combobox-needs-labelling"; export { default as compoundButtonNeedsLabelling } from "./buttons/compound-button-needs-labelling"; @@ -17,8 +18,10 @@ export { default as dropdownNeedsLabelling } from "./dropdown-needs-labelling"; export { default as fieldNeedsLabelling } from "./field-needs-labelling"; export { default as imageButtonMissingAria } from "./buttons/image-button-missing-aria"; export { default as imageNeedsAlt } from "./image-needs-alt"; +export { default as infoLabelNeedsLabelling } from "./infolabel-needs-labelling"; export { default as inputComponentsRequireAccessibleName } from "./input-components-require-accessible-name"; export { default as linkMissingLabelling } from "./link-missing-labelling"; +export { default as menuButtonNeedsLabelling } from "./buttons/menu-button-needs-labelling"; export { default as menuItemNeedsLabelling } from "./menu-item-needs-labelling"; export { default as noEmptyButtons } from "./buttons/no-empty-buttons"; export { default as noEmptyComponents } from "./no-empty-components"; diff --git a/lib/rules/infolabel-needs-labelling.ts b/lib/rules/infolabel-needs-labelling.ts new file mode 100644 index 0000000..3587e10 --- /dev/null +++ b/lib/rules/infolabel-needs-labelling.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ESLintUtils } from "@typescript-eslint/utils"; +import { makeLabeledControlRule } from "../util/ruleFactory"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +export default ESLintUtils.RuleCreator.withoutDocs( + makeLabeledControlRule({ + component: "InfoLabel", + messageId: "infoLabelNeedsLabelling", + description: "Accessibility: InfoLabel must have an accessible name via aria-label, text content, aria-labelledby, etc.", + labelProps: ["aria-label"], + allowFieldParent: false, + allowHtmlFor: false, + allowLabelledBy: true, + allowWrappingLabel: false, + allowTooltipParent: true, + allowDescribedBy: false, + allowLabeledChild: true, + allowTextContentChild: true + }) +); diff --git a/lib/util/labelUtils.ts b/lib/util/labelUtils.ts index b7a4b96..e41435d 100644 --- a/lib/util/labelUtils.ts +++ b/lib/util/labelUtils.ts @@ -42,7 +42,7 @@ const idLiteralDouble = '"([^"]*)"'; const idLiteralSingle = "'([^']*)'"; const exprStringDouble = '\\{\\s*"([^"]*)"\\s*\\}'; const exprStringSingle = "\\{\\s*'([^']*)'\\s*\\}"; -const exprIdentifier = "\\{\\s*([A-Za-z_$][A-Za-l0-9_$]*)\\s*\\}"; +const exprIdentifier = "\\{\\s*([A-Za-z_$][A-Za-z0-9_$]*)\\s*\\}"; // FIXED: l -> z const idOrExprRegex = new RegExp( `(?:${idLiteralDouble}|${idLiteralSingle}|${exprStringDouble}|${exprStringSingle}|${exprIdentifier})`, diff --git a/tests/lib/rules/buttons/menu-button-needs-labelling.test.ts b/tests/lib/rules/buttons/menu-button-needs-labelling.test.ts new file mode 100644 index 0000000..113dfb0 --- /dev/null +++ b/tests/lib/rules/buttons/menu-button-needs-labelling.test.ts @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +import { Rule } from "eslint"; +import ruleTester from "../helper/ruleTester"; +import rule from "../../../../lib/rules/buttons/menu-button-needs-labelling"; + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +ruleTester.run("menu-button-needs-labelling", rule as unknown as Rule.RuleModule, { + valid: [ + ``, + `Options`, + `<>`, + ``, + `Menu icon`, + `` + ], + invalid: [ + { + code: ``, + errors: [{ messageId: "menuButtonNeedsLabelling" }] + }, + { + code: ``, + errors: [{ messageId: "menuButtonNeedsLabelling" }] + }, + { + code: ``, + errors: [{ messageId: "menuButtonNeedsLabelling" }] + }, + { + code: `<>`, + errors: [{ messageId: "menuButtonNeedsLabelling" }] + } + ] +}); diff --git a/tests/lib/rules/card-needs-accessible-name.test.ts b/tests/lib/rules/card-needs-accessible-name.test.ts new file mode 100644 index 0000000..4cddd19 --- /dev/null +++ b/tests/lib/rules/card-needs-accessible-name.test.ts @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +import { Rule } from "eslint"; +import ruleTester from "./helper/ruleTester"; +import rule from "../../../lib/rules/card-needs-accessible-name"; + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +ruleTester.run("card-needs-accessible-name", rule as unknown as Rule.RuleModule, { + valid: [ + ``, + `<>`, + `Product image`, + `` + ], + invalid: [ + { + code: ``, + errors: [{ messageId: "cardNeedsAccessibleName" }] + }, + { + code: ``, + errors: [{ messageId: "cardNeedsAccessibleName" }] + }, + { + code: ``, + errors: [{ messageId: "cardNeedsAccessibleName" }] + }, + { + code: `<>`, + errors: [{ messageId: "cardNeedsAccessibleName" }] + } + ] +}); diff --git a/tests/lib/rules/infolabel-needs-labelling.test.ts b/tests/lib/rules/infolabel-needs-labelling.test.ts new file mode 100644 index 0000000..ac1ee2a --- /dev/null +++ b/tests/lib/rules/infolabel-needs-labelling.test.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +import { Rule } from "eslint"; +import ruleTester from "./helper/ruleTester"; +import rule from "../../../lib/rules/infolabel-needs-labelling"; + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +ruleTester.run("infolabel-needs-labelling", rule as unknown as Rule.RuleModule, { + valid: [ + ``, + `Help text`, + `?`, + `â„šī¸`, + `<>`, + ``, + `Help icon`, + `` + ], + invalid: [ + { + code: ``, + errors: [{ messageId: "infoLabelNeedsLabelling" }] + }, + { + code: ``, + errors: [{ messageId: "infoLabelNeedsLabelling" }] + }, + { + code: ``, + errors: [{ messageId: "infoLabelNeedsLabelling" }] + }, + { + code: ``, + errors: [{ messageId: "infoLabelNeedsLabelling" }] + }, + { + code: `<>`, + errors: [{ messageId: "infoLabelNeedsLabelling" }] + }, + { + code: ``, + errors: [{ messageId: "infoLabelNeedsLabelling" }] + }, + { + code: ``, + errors: [{ messageId: "infoLabelNeedsLabelling" }] + } + ] +});