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..fcda35e --- /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 + +- [ARIA in HTML](https://www.w3.org/TR/html-aria/) diff --git a/lib/index.ts b/lib/index.ts index d7ee438..89944eb 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -24,6 +24,7 @@ import tooltipNotRecommended from "./rules/tooltip-not-recommended"; import avatarNeedsName from "./rules/avatar-needs-name"; import radioButtonMissingLabel from "./rules/radio-button-missing-label"; import radiogroupMissingLabel from "./rules/radiogroup-missing-label"; +import ratingNeedsName from "./rules/rating-needs-name"; import dialogbodyNeedsTitleContentAndActions from "./rules/dialogbody-needs-title-content-and-actions"; import dialogsurfaceNeedsAria from "./rules/dialogsurface-needs-aria"; import spinnerNeedsLabelling from "./rules/spinner-needs-labelling"; @@ -62,6 +63,7 @@ module.exports = { "avatar-needs-name": avatarNeedsName, "radio-button-missing-label": radioButtonMissingLabel, "radiogroup-missing-label": radiogroupMissingLabel, + "rating-needs-name": ratingNeedsName, "prefer-aria-over-title-attribute": preferAriaOverTitleAttribute, "dialogbody-needs-title-content-and-actions": dialogbodyNeedsTitleContentAndActions, "dialogsurface-needs-aria": dialogsurfaceNeedsAria, @@ -93,6 +95,7 @@ module.exports = { "@microsoft/fluentui-jsx-a11y/avatar-needs-name": "error", "@microsoft/fluentui-jsx-a11y/radio-button-missing-label": "error", "@microsoft/fluentui-jsx-a11y/radiogroup-missing-label": "error", + "@microsoft/fluentui-jsx-a11y/rating-needs-name": "error", "@microsoft/fluentui-jsx-a11y/prefer-aria-over-title-attribute": "warn", "@microsoft/fluentui-jsx-a11y/dialogbody-needs-title-content-and-actions": "error", "@microsoft/fluentui-jsx-a11y/dialogsurface-needs-aria": "error", diff --git a/lib/rules/rating-needs-name.ts b/lib/rules/rating-needs-name.ts new file mode 100644 index 0000000..1592e33 --- /dev/null +++ b/lib/rules/rating-needs-name.ts @@ -0,0 +1,60 @@ +// 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"; +import { JSXOpeningElement } from "estree-jsx"; + +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: TSESTree.JSXOpeningElement) { + // if it is not a listed component, return + if ( + elementType(node as JSXOpeningElement) !== "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` + }); + } + }; + } +}); + +export default rule; 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..abb58c3 --- /dev/null +++ b/tests/lib/rules/rating-needs-name.test.ts @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +import { Rule } from "eslint"; +import ruleTester from "./helper/ruleTester"; +import rule from "../../../lib/rules/rating-needs-name"; + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +ruleTester.run("rating-needs-name", rule as unknown as Rule.RuleModule, { + valid: [ + // give me some code that won't trigger a warning + '', + '', + '', + '<>', + '', + '', + '', + '<>' + ], + + invalid: [ + { + code: "", + errors: [{ messageId: "missingAriaLabel" }] + }, + { + code: "", + errors: [{ messageId: "missingAriaLabel" }] + } + ] +});