diff --git a/COVERAGE.md b/COVERAGE.md
index c9c1c32..d65a83b 100644
--- a/COVERAGE.md
+++ b/COVERAGE.md
@@ -63,7 +63,11 @@ We currently cover the following components:
- [N/A] SkeletonItem
- [x] SpinButton
- [x] Spinner
- - [] SwatchPicker
+ - [x] SwatchPicker
+ - [] ColorSwatch
+ - [] ImageSwatch
+ - [] EmptySwatch
+ - [] SwatchPickerRow
- [x] Switch
- [] SearchBox
- [] Table
diff --git a/README.md b/README.md
index 23c6a89..29e74b0 100644
--- a/README.md
+++ b/README.md
@@ -137,6 +137,7 @@ Any use of third-party trademarks or logos are subject to those third-party's po
| [spin-button-needs-labelling](docs/rules/spin-button-needs-labelling.md) | Accessibility: SpinButtons must have an accessible label | ✅ | | |
| [spin-button-unrecommended-labelling](docs/rules/spin-button-unrecommended-labelling.md) | Accessibility: Unrecommended accessibility labelling - SpinButton | ✅ | | |
| [spinner-needs-labelling](docs/rules/spinner-needs-labelling.md) | Accessibility: Spinner must have either aria-label or label, aria-live and aria-busy attributes | ✅ | | |
+| [swatchpicker-needs-labelling](docs/rules/swatchpicker-needs-labelling.md) | Accessibility: SwatchPicker must have an accessible name via aria-label, aria-labelledby, Field component, etc.. | ✅ | | |
| [switch-needs-labelling](docs/rules/switch-needs-labelling.md) | Accessibility: Switch must have an accessible label | ✅ | | |
| [tablist-and-tabs-need-labelling](docs/rules/tablist-and-tabs-need-labelling.md) | This rule aims to ensure that Tabs with icons but no text labels have an accessible name and that Tablist is properly labeled. | ✅ | | |
| [toolbar-missing-aria](docs/rules/toolbar-missing-aria.md) | Accessibility: Toolbars need accessible labelling: aria-label or aria-labelledby | ✅ | | |
diff --git a/docs/rules/swatchpicker-needs-labelling.md b/docs/rules/swatchpicker-needs-labelling.md
new file mode 100644
index 0000000..01829b8
--- /dev/null
+++ b/docs/rules/swatchpicker-needs-labelling.md
@@ -0,0 +1,60 @@
+# Accessibility: SwatchPicker must have an accessible name via aria-label, aria-labelledby, Field component, etc. (`@microsoft/fluentui-jsx-a11y/swatchpicker-needs-labelling`)
+
+💼 This rule is enabled in the ✅ `recommended` config.
+
+
+
+All interactive elements must have an accessible name.
+
+SwatchPicker without a label or accessible labeling lack an accessible name for assistive technology users.
+
+
+
+## Ways to fix
+
+- Add an aria-label or aria-labelledby attribute to the SwatchPicker tag. You can also use the Field component.
+
+## Rule Details
+
+This rule aims to make SwatchPickers accessible.
+
+Examples of **incorrect** code for this rule:
+
+```jsx
+
+
+```
+
+```jsx
+
+
+```
+
+Examples of **correct** code for this rule:
+
+```jsx
+
+
+```
+
+```jsx
+
+
+
+
+```
+
+```jsx
+
+
+
+
+
+
+```
diff --git a/lib/index.ts b/lib/index.ts
index 67f9823..bbf4bfc 100644
--- a/lib/index.ts
+++ b/lib/index.ts
@@ -44,6 +44,7 @@ module.exports = {
"@microsoft/fluentui-jsx-a11y/spin-button-needs-labelling": "error",
"@microsoft/fluentui-jsx-a11y/spin-button-unrecommended-labelling": "error",
"@microsoft/fluentui-jsx-a11y/spinner-needs-labelling": "error",
+ "@microsoft/fluentui-jsx-a11y/swatchpicker-needs-labelling": "error",
"@microsoft/fluentui-jsx-a11y/switch-needs-labelling": "error",
"@microsoft/fluentui-jsx-a11y/tablist-and-tabs-need-labelling": "error",
"@microsoft/fluentui-jsx-a11y/toolbar-missing-aria": "error",
@@ -81,6 +82,7 @@ module.exports = {
"spin-button-needs-labelling": rules.spinButtonNeedsLabelling,
"spin-button-unrecommended-labelling": rules.spinButtonUnrecommendedLabelling,
"spinner-needs-labelling": rules.spinnerNeedsLabelling,
+ "swatchpicker-needs-labelling": rules.swatchpickerNeedsLabelling,
"switch-needs-labelling": rules.switchNeedsLabelling,
"tablist-and-tabs-need-labelling": rules.tablistAndTabsNeedLabelling,
"toolbar-missing-aria": rules.toolbarMissingAria,
diff --git a/lib/rules/index.ts b/lib/rules/index.ts
index ec59318..bfdbe7b 100644
--- a/lib/rules/index.ts
+++ b/lib/rules/index.ts
@@ -29,6 +29,7 @@ export { default as ratingNeedsName } from "./rating-needs-name";
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";
+export { default as swatchpickerNeedsLabelling } from "./swatchpicker-needs-labelling";
export { default as switchNeedsLabelling } from "./switch-needs-labelling";
export { default as tablistAndTabsNeedLabelling } from "./tablist-and-tabs-need-labelling";
export { default as toolbarMissingAria } from "./toolbar-missing-aria";
diff --git a/lib/rules/radiogroup-missing-label.ts b/lib/rules/radiogroup-missing-label.ts
index eb585f2..a74a3c9 100644
--- a/lib/rules/radiogroup-missing-label.ts
+++ b/lib/rules/radiogroup-missing-label.ts
@@ -35,12 +35,12 @@ const rule = ESLintUtils.RuleCreator.withoutDocs({
return {
// visitor functions for different types of nodes
JSXOpeningElement(node: TSESTree.JSXOpeningElement) {
- // if it is not a Checkbox, return
+ // if it is not a RadioGroup, return
if (elementType(node as JSXOpeningElement) !== "RadioGroup") {
return;
}
- // if the Checkbox has a label, if the Switch has an associated label, return
+ // if the RadioGroup has a label, return
if (
hasFieldParent(context) ||
hasNonEmptyProp(node.attributes, "label") ||
diff --git a/lib/rules/swatchpicker-needs-labelling.ts b/lib/rules/swatchpicker-needs-labelling.ts
new file mode 100644
index 0000000..eeb163e
--- /dev/null
+++ b/lib/rules/swatchpicker-needs-labelling.ts
@@ -0,0 +1,22 @@
+// 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: "SwatchPicker",
+ labelProps: ["aria-label"],
+ allowFieldParent: true,
+ allowFor: false,
+ allowLabelledBy: true,
+ allowWrappingLabel: false,
+ messageId: "noUnlabeledSwatchPicker",
+ description: "Accessibility: SwatchPicker must have an accessible name via aria-label, aria-labelledby, Field component, etc.."
+ })
+);
diff --git a/lib/util/ruleFactory.ts b/lib/util/ruleFactory.ts
new file mode 100644
index 0000000..fe5dda1
--- /dev/null
+++ b/lib/util/ruleFactory.ts
@@ -0,0 +1,117 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { TSESLint, TSESTree } from "@typescript-eslint/utils";
+import { hasNonEmptyProp } from "./hasNonEmptyProp";
+import { hasAssociatedLabelViaAriaLabelledBy, isInsideLabelTag, hasAssociatedLabelViaHtmlFor } from "./labelUtils";
+import { hasFieldParent } from "./hasFieldParent";
+import { elementType } from "jsx-ast-utils";
+import { JSXOpeningElement } from "estree-jsx";
+
+export type LabeledControlConfig = {
+ component: string | RegExp;
+ labelProps: string[]; // e.g. ["label", "aria-label"]
+ allowFieldParent: boolean; // e.g.
+ allowFor: boolean; // htmlFor
+ allowLabelledBy: boolean; // aria-labelledby
+ allowWrappingLabel: boolean; //
+ messageId: string;
+ description: string;
+};
+
+/**
+ * Returns `true` if the JSX opening element is considered **accessibly labelled**
+ * per the rule configuration. This function centralizes all supported labelling
+ * strategies so the rule stays small and testable.
+ *
+ * The supported strategies (gated by `config` flags) are:
+ * 1) A parent ``-like wrapper that provides the label context (`allowFieldParent`).
+ * 2) A non-empty inline prop such as `aria-label` or `title` (`labelProps`).
+ * 3) Being wrapped by a `