diff --git a/COVERAGE.md b/COVERAGE.md index 3a8ebc0..77bab8e 100644 --- a/COVERAGE.md +++ b/COVERAGE.md @@ -37,16 +37,16 @@ We currently cover the following components: - [x] Input - [x] Label - [x] Link - - [] Menu - - [] Menu - - [] MenuList - - [] MessageBar + - [x] Menu + - [x] Menu + - [x] MenuList + - [x] MessageBar - [N/A] Overflow - [] Persona - [] Popover - [N/A] Portal - [x] ProgressBar - - [] Rating + - [x] Rating - [] RatingDisplay - [x] Radio - [x] RadioGroup diff --git a/docs/rules/rating-needs-name.md b/docs/rules/rating-needs-name.md new file mode 100644 index 0000000..7b45529 --- /dev/null +++ b/docs/rules/rating-needs-name.md @@ -0,0 +1,36 @@ +# Accessibility: Ratings must have accessible labelling: name, aria-label, aria-labelledby or itemLabel which generates aria-label (`@microsoft/fluentui-jsx-a11y/rating-needs-name`) + +All interactive elements must have an accessible name. + +## Rule Details + +This rule aims to enforce that a Rating element must have an accessible label associated with it. + +Examples of **incorrect** code for this rule: + +```js + + + +``` + +Examples of **correct** code for this rule: + +```js + + `Rating of ${number} starts`} /> + +``` + +### Options + +FluentUI supports receiving a function that will add the aria-label to the element with the number. This prop is called itemLabel. +If this is not the desired route, a name, aria-label or aria-labelledby can be added instead. + +## When Not To Use It + +You might want to turn this rule off if you don't intend for this component to be read by screen readers. + +## Further Reading + +https://www.w3.org/TR/html-aria/ diff --git a/lib/rules/rating-needs-name.ts b/lib/rules/rating-needs-name.ts new file mode 100644 index 0000000..b3fd0fd --- /dev/null +++ b/lib/rules/rating-needs-name.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +"use strict"; + +import { ESLintUtils, TSESTree } from "@typescript-eslint/utils"; +import { hasNonEmptyProp } from "../util/hasNonEmptyProp"; +import { elementType } from "jsx-ast-utils"; +import { hasAssociatedLabelViaAriaLabelledBy } from "../util/labelUtils"; + +const rule = ESLintUtils.RuleCreator.withoutDocs({ + defaultOptions: [], + meta: { + // possible error messages for the rule + messages: { + missingAriaLabel: 'Accessibility - ratings must have an accessible name or an itemLabel that generates an aria label' + }, + // "problem" means the rule is identifying code that either will cause an error or may cause a confusing behavior: https://eslint.org/docs/latest/developer-guide/working-with-rules + type: "problem", + // docs for the rule + docs: { + description: "Accessibility: Ratings must have accessible labelling: name, aria-label, aria-labelledby or itemLabel which generates aria-label", + recommended: "strict", + url: "https://www.w3.org/TR/html-aria/" // URL to the documentation page for this rule + }, + schema: [] + }, + + create(context) { + return { + // visitor functions for different types of nodes + JSXOpeningElement(node) { + // if it is not a listed component, return + if ( + elementType(node) !== "Rating" + ) { + return; + } + + // wrapped in Label tag, labelled with htmlFor, labelled with aria-labelledby + if ( + hasNonEmptyProp(node.attributes, "itemLabel") || + hasNonEmptyProp(node.attributes, "name") || + hasNonEmptyProp(node.attributes, "aria-label") || + hasAssociatedLabelViaAriaLabelledBy(node, context) + ) { + return; + } + + context.report({ + node, + messageId: `missingAriaLabel` + }); + } + }; + } +}); diff --git a/tests/lib/rules/rating-needs-name.test.ts b/tests/lib/rules/rating-needs-name.test.ts new file mode 100644 index 0000000..77b763a --- /dev/null +++ b/tests/lib/rules/rating-needs-name.test.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +import ruleTester from "./helper/ruleTester"; + +import rule from "../../../lib/rules/rating-needs-name"; + +ruleTester.setDefaultConfig({ + parserOptions: { + ecmaVersion: 6, + ecmaFeatures: { + jsx: true + } + } +}); + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +ruleTester.run("rating-needs-name", rule, { + valid: [ + // give me some code that won't trigger a warning + '', + '', + '', + '<>', + '', + '', + '', + '<>' + ], + + invalid: [ + { + code: "", + errors: [{ messageId: "missingAriaLabel" }] + }, + { + code: "", + errors: [{ messageId: "missingAriaLabel" }] + } + ] +});