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 `