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
+```
+
+## 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`,
+ `<>>`,
+ ``,
+ `
`,
+ ``
+ ],
+ 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: [
+ ``,
+ `<>>`,
+ `
`,
+ ``
+ ],
+ 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`,
+ `?`,
+ `âšī¸`,
+ `<>>`,
+ ``,
+ `
`,
+ ``
+ ],
+ 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" }]
+ }
+ ]
+});