Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,10 @@ We currently cover the following components:
- [x] SpinButton
- [x] Spinner
- [x] SwatchPicker
- [] ColorSwatch
- [] ImageSwatch
- [] EmptySwatch
- [] SwatchPickerRow
- [x] ColorSwatch
- [x] ImageSwatch
- [x] EmptySwatch
- [N/A] SwatchPickerRow
- [x] Switch
- [] SearchBox
- [] Table
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,17 @@ Any use of third-party trademarks or logos are subject to those third-party's po
| [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 | ✅ | | |
| [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 | ✅ | | |
| [compound-button-needs-labelling](docs/rules/compound-button-needs-labelling.md) | Accessibility: Compound buttons must have accessible labelling: title, aria-label, aria-labelledby, aria-describedby | ✅ | | |
| [counter-badge-needs-count](docs/rules/counter-badge-needs-count.md) | | ✅ | | 🔧 |
| [dialogbody-needs-title-content-and-actions](docs/rules/dialogbody-needs-title-content-and-actions.md) | A DialogBody should have a header(DialogTitle), content(DialogContent), and footer(DialogActions) | ✅ | | |
| [dialogsurface-needs-aria](docs/rules/dialogsurface-needs-aria.md) | DialogueSurface need accessible labelling: aria-describedby on DialogueSurface and aria-label or aria-labelledby(if DialogueTitle is missing) | ✅ | | |
| [dropdown-needs-labelling](docs/rules/dropdown-needs-labelling.md) | Accessibility: Dropdown menu must have an id and it needs to be linked via htmlFor of a Label | ✅ | | |
| [emptyswatch-needs-labelling](docs/rules/emptyswatch-needs-labelling.md) | Accessibility: EmptySwatch must have an accessible name via aria-label, Tooltip, aria-labelledby, etc.. | ✅ | | |
| [field-needs-labelling](docs/rules/field-needs-labelling.md) | Accessibility: Field must have label | ✅ | | |
| [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 | ✅ | | |
| [imageswatch-needs-labelling](docs/rules/imageswatch-needs-labelling.md) | Accessibility: ImageSwatch must have an accessible name via aria-label, Tooltip, 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-item-needs-labelling](docs/rules/menu-item-needs-labelling.md) | Accessibility: MenuItem without label must have an accessible and visual label: aria-labelledby | ✅ | | |
Expand Down
5 changes: 5 additions & 0 deletions docs/rules/colorswatch-needs-labelling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Accessibility: ColorSwatch must have an accessible name via aria-label, Tooltip, aria-labelledby, etc. (`@microsoft/fluentui-jsx-a11y/colorswatch-needs-labelling`)

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

<!-- end auto-generated rule header -->
109 changes: 109 additions & 0 deletions docs/rules/emptyswatch-needs-labelling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Accessibility: EmptySwatch must have an accessible name via aria-label, Tooltip, aria-labelledby, etc. (`@microsoft/fluentui-jsx-a11y/emptyswatch-needs-labelling`)

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

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

All interactive elements must have an accessible name.

`EmptySwatch` without a supported label lacks an accessible name for assistive technology users.

## Allowed labelling strategies

- ✅ `aria-label` **on `EmptySwatch`**
- ✅ `aria-labelledby` **on `EmptySwatch`**
- ✅ `htmlFor`/`id` (native `<label htmlFor="…">` ↔ `id="…"` on `EmptySwatch`)
- ✅ Wrapping native `<label>…</label>`
- ✅ `Tooltip` parent with `relationship="label"`
- ✅ Text content child (e.g., `<EmptySwatch>None</EmptySwatch>`)
- ❌ `Field` parent (not allowed for `EmptySwatch`)
- ❌ Container-only label (e.g., only the surrounding `SwatchPicker` is labelled)

## Ways to fix

- Add `aria-label`/`aria-labelledby` to `EmptySwatch`.
- Use `<label htmlFor="…">` + `id="…"` on `EmptySwatch`.
- Wrap in a native `<label>` or `Tooltip (relationship="label")`.
- Provide meaningful text **as the child** of `EmptySwatch`.

## Rule Details

This rule ensures `EmptySwatch` is labelled using **allowed** mechanisms.

### Examples of **incorrect** code

```jsx
// No label
<SwatchPicker>
<EmptySwatch value="none" />
</SwatchPicker>
```

```jsx
// Container-only label: EmptySwatch itself is unnamed
<SwatchPicker aria-label="Color picker">
<EmptySwatch value="none" />
</SwatchPicker>
```

```jsx
// Not allowed: Field parent labelling
<SwatchPicker>
<Field label="No color">
<EmptySwatch value="none" />
</Field>
</SwatchPicker>
```

### Examples of **correct** code

```jsx
// aria-label
<SwatchPicker>
<EmptySwatch value="none" aria-label="No color" />
</SwatchPicker>
```

```jsx
// aria-labelledby
<>
<span id="empty-no-color">No color</span>
<SwatchPicker>
<EmptySwatch value="none" aria-labelledby="empty-no-color" />
</SwatchPicker>
</>
```

```jsx
// htmlFor/id
<>
<label htmlFor="empty-none">No color</label>
<SwatchPicker>
<EmptySwatch id="empty-none" value="none" />
</SwatchPicker>
</>
```

```jsx
// Wrapping native <label>
<label>
No color
<EmptySwatch value="none" />
</label>
```

```jsx
// Tooltip (acts as a label)
<SwatchPicker>
<Tooltip relationship="label" content="No color">
<EmptySwatch value="none" />
</Tooltip>
</SwatchPicker>
```

```jsx
// Text content child
<SwatchPicker>
<EmptySwatch value="none">No color</EmptySwatch>
</SwatchPicker>
```
108 changes: 108 additions & 0 deletions docs/rules/imageswatch-needs-labelling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Accessibility: ImageSwatch must have an accessible name via aria-label, Tooltip, aria-labelledby, etc. (`@microsoft/fluentui-jsx-a11y/imageswatch-needs-labelling`)

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

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

All interactive elements must have an accessible name.

`ImageSwatch` without a supported label lacks an accessible name for assistive technology users.

## Allowed labelling strategies

- ✅ `aria-label` **on `ImageSwatch`**
- ✅ `aria-labelledby` **on `ImageSwatch`**
- ✅ Wrapping native `<label>…</label>`
- ✅ `Tooltip` parent with `relationship="label"` (e.g., from Fluent UI)
- ❌ `Field` parent (not allowed for `ImageSwatch`)
- ❌ `htmlFor`/`id` (not allowed for `ImageSwatch`)
- ❌ Text content child (not allowed)
- ❌ Container-only label (e.g., only the surrounding `SwatchPicker` is labelled)

## Ways to fix

- Add `aria-label` or `aria-labelledby` **directly** to `ImageSwatch`.
- Wrap the swatch in a native `<label>…</label>` with descriptive text.
- Wrap in a `Tooltip` with `relationship="label"` and meaningful `content`.

## Rule Details

This rule ensures `ImageSwatch` receives a name via **supported** mechanisms.

### Examples of **incorrect** code

```jsx
// No label at all
<SwatchPicker>
<ImageSwatch src="/none.png" value="none" />
</SwatchPicker>
```

```jsx
// Container-only label: ImageSwatch itself is unnamed
<SwatchPicker aria-label="Color picker">
<ImageSwatch src="/none.png" value="none" />
</SwatchPicker>
```

```jsx
// Not allowed for ImageSwatch: htmlFor/id
<>
<label htmlFor="img-none">No color</label>
<SwatchPicker>
<ImageSwatch id="img-none" src="/none.png" value="none" />
</SwatchPicker>
</>
```

```jsx
// Not allowed: text content child
<SwatchPicker>
<ImageSwatch src="/none.png" value="none">No color</ImageSwatch>
</SwatchPicker>
```

```jsx
// Not allowed: Field parent labelling
<SwatchPicker>
<Field label="No color">
<ImageSwatch src="/none.png" value="none" />
</Field>
</SwatchPicker>
```

### Examples of **correct** code

```jsx
// aria-label
<SwatchPicker>
<ImageSwatch src="/none.png" value="none" aria-label="No color" />
</SwatchPicker>
```

```jsx
// aria-labelledby
<>
<span id="img-no-color">No color</span>
<SwatchPicker>
<ImageSwatch src="/none.png" value="none" aria-labelledby="img-no-color" />
</SwatchPicker>
</>
```

```jsx
// Wrapping native <label>
<label>
No color
<ImageSwatch src="/none.png" value="none" />
</label>
```

```jsx
// Tooltip (acts as a label)
<SwatchPicker>
<Tooltip relationship="label" content="No color">
<ImageSwatch src="/none.png" value="none" />
</Tooltip>
</SwatchPicker>
```
6 changes: 6 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,17 @@ module.exports = {
"@microsoft/fluentui-jsx-a11y/badge-needs-accessible-name": "error",
"@microsoft/fluentui-jsx-a11y/breadcrumb-needs-labelling": "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",
"@microsoft/fluentui-jsx-a11y/compound-button-needs-labelling": "error",
"@microsoft/fluentui-jsx-a11y/counter-badge-needs-count": "error",
"@microsoft/fluentui-jsx-a11y/dialogbody-needs-title-content-and-actions": "error",
"@microsoft/fluentui-jsx-a11y/dialogsurface-needs-aria": "error",
"@microsoft/fluentui-jsx-a11y/dropdown-needs-labelling": "error",
"@microsoft/fluentui-jsx-a11y/emptyswatch-needs-labelling": "error",
"@microsoft/fluentui-jsx-a11y/field-needs-labelling": "error",
"@microsoft/fluentui-jsx-a11y/image-button-missing-aria": "error",
"@microsoft/fluentui-jsx-a11y/imageswatch-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-item-needs-labelling": "error",
Expand Down Expand Up @@ -61,14 +64,17 @@ module.exports = {
"badge-needs-accessible-name": rules.badgeNeedsAccessibleName,
"breadcrumb-needs-labelling": rules.breadcrumbNeedsLabelling,
"checkbox-needs-labelling": rules.checkboxNeedsLabelling,
"colorswatch-needs-labelling": rules.colorSwatchNeedsLabelling,
"combobox-needs-labelling": rules.comboboxNeedsLabelling,
"compound-button-needs-labelling": rules.compoundButtonNeedsLabelling,
"counter-badge-needs-count": rules.counterBadgeNeedsCount,
"dialogbody-needs-title-content-and-actions": rules.dialogbodyNeedsTitleContentAndActions,
"dialogsurface-needs-aria": rules.dialogsurfaceNeedsAria,
"dropdown-needs-labelling": rules.dropdownNeedsLabelling,
"emptyswatch-needs-labelling": rules.emptySwatchNeedsLabelling,
"field-needs-labelling": rules.fieldNeedsLabelling,
"image-button-missing-aria": rules.imageButtonMissingAria,
"imageswatch-needs-labelling": rules.imageSwatchNeedsLabelling,
"input-components-require-accessible-name": rules.inputComponentsRequireAccessibleName,
"link-missing-labelling": rules.linkMissingLabelling,
"menu-item-needs-labelling": rules.menuItemNeedsLabelling,
Expand Down
26 changes: 26 additions & 0 deletions lib/rules/colorswatch-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: "ColorSwatch",
messageId: "noUnlabeledColorSwatch",
description: "Accessibility: ColorSwatch must have an accessible name via aria-label, Tooltip, aria-labelledby, etc..",
labelProps: ["aria-label"],
allowFieldParent: true,
allowHtmlFor: true,
allowLabelledBy: false,
allowWrappingLabel: true,
allowTooltipParent: true,
allowDescribedBy: false,
allowLabeledChild: false,
allowTextContentChild: true
})
);
26 changes: 26 additions & 0 deletions lib/rules/emptyswatch-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: "EmptySwatch",
messageId: "noUnlabeledEmptySwatch",
description: "Accessibility: EmptySwatch must have an accessible name via aria-label, Tooltip, aria-labelledby, etc..",
labelProps: ["aria-label"],
allowFieldParent: false,
allowHtmlFor: true,
allowLabelledBy: true,
allowWrappingLabel: true,
allowTooltipParent: true,
allowDescribedBy: false,
allowLabeledChild: false,
allowTextContentChild: true
})
);
26 changes: 26 additions & 0 deletions lib/rules/imageswatch-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: "ImageSwatch",
messageId: "noUnlabeledImageSwatch",
description: "Accessibility: ImageSwatch must have an accessible name via aria-label, Tooltip, aria-labelledby, etc..",
labelProps: ["aria-label"],
allowFieldParent: false,
allowHtmlFor: false,
allowLabelledBy: true,
allowWrappingLabel: true,
allowTooltipParent: true,
allowDescribedBy: false,
allowLabeledChild: false,
allowTextContentChild: false
})
);
3 changes: 3 additions & 0 deletions lib/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export { default as progressbarNeedsLabelling } from "./progressbar-needs-labell
export { default as radioButtonMissingLabel } from "./radio-button-missing-label";
export { default as radiogroupMissingLabel } from "./radiogroup-missing-label";
export { default as ratingNeedsName } from "./rating-needs-name";
export { default as emptySwatchNeedsLabelling } from "./emptyswatch-needs-labelling";
export { default as colorSwatchNeedsLabelling } from "./colorswatch-needs-labelling";
export { default as imageSwatchNeedsLabelling } from "./imageswatch-needs-labelling";
export { default as spinButtonNeedsLabelling } from "./spin-button-needs-labelling";
export { default as spinButtonUnrecommendedLabelling } from "./spin-button-unrecommended-labelling";
export { default as spinnerNeedsLabelling } from "./spinner-needs-labelling";
Expand Down
Loading
Loading