Skip to content
Merged
8 changes: 4 additions & 4 deletions COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | βœ… | | |
Expand All @@ -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 | βœ… | | |
Expand Down
45 changes: 45 additions & 0 deletions docs/rules/card-needs-accessible-name.md
Original file line number Diff line number Diff line change
@@ -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.

<!-- end auto-generated rule header -->

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>
<CardHeader>
<Text weight="semibold">Card title</Text>
</CardHeader>
</Card>
```

### Compliant

```jsx
<Card aria-label="Product details">
<CardHeader>
<Text weight="semibold">Card title</Text>
</CardHeader>
</Card>

<Card aria-labelledby="card-title">
<CardHeader>
<Text id="card-title" weight="semibold">Card title</Text>
</CardHeader>
</Card>
```

## 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)
37 changes: 37 additions & 0 deletions docs/rules/infolabel-needs-labelling.md
Original file line number Diff line number Diff line change
@@ -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.

<!-- end auto-generated rule header -->

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
<InfoLabel />
```

### Compliant

```jsx
<InfoLabel aria-label="Additional information" />

<InfoLabel aria-labelledby="info-text">
<span id="info-text">Help text</span>
</InfoLabel>

<InfoLabel>Help information</InfoLabel>
```

## 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)
37 changes: 37 additions & 0 deletions docs/rules/menu-button-needs-labelling.md
Original file line number Diff line number Diff line change
@@ -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.

<!-- end auto-generated rule header -->

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
<MenuButton />
```

### Compliant

```jsx
<MenuButton aria-label="Menu options" />

<MenuButton aria-labelledby="menu-label">
<span id="menu-label">Options</span>
</MenuButton>

<MenuButton>Options</MenuButton>
```

## 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)
2 changes: 1 addition & 1 deletion lib/applicableComponents/buttonBasedComponents.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down
6 changes: 6 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
26 changes: 26 additions & 0 deletions lib/rules/buttons/menu-button-needs-labelling.ts
Original file line number Diff line number Diff line change
@@ -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
})
);
26 changes: 26 additions & 0 deletions lib/rules/card-needs-accessible-name.ts
Original file line number Diff line number Diff line change
@@ -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
})
);
3 changes: 3 additions & 0 deletions lib/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand Down
26 changes: 26 additions & 0 deletions lib/rules/infolabel-needs-labelling.ts
Original file line number Diff line number Diff line change
@@ -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
})
);
2 changes: 1 addition & 1 deletion lib/util/labelUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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})`,
Expand Down
43 changes: 43 additions & 0 deletions tests/lib/rules/buttons/menu-button-needs-labelling.test.ts
Original file line number Diff line number Diff line change
@@ -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: [
`<MenuButton aria-label="Menu options" />`,
`<MenuButton>Options</MenuButton>`,
`<><Label id="menu-label">Menu</Label><MenuButton aria-labelledby="menu-label" /></>`,
`<Tooltip content="Menu options" relationship="label"><MenuButton /></Tooltip>`,
`<MenuButton><img alt="Menu icon" /></MenuButton>`,
`<MenuButton><MenuIcon /></MenuButton>`
],
invalid: [
{
code: `<MenuButton />`,
errors: [{ messageId: "menuButtonNeedsLabelling" }]
},
{
code: `<MenuButton></MenuButton>`,
errors: [{ messageId: "menuButtonNeedsLabelling" }]
},
{
code: `<MenuButton aria-label="" />`,
errors: [{ messageId: "menuButtonNeedsLabelling" }]
},
{
code: `<><Label id="wrong-id">Options</Label><MenuButton aria-labelledby="menu-label" /></>`,
errors: [{ messageId: "menuButtonNeedsLabelling" }]
}
]
});
Loading
Loading