Skip to content

Commit

Permalink
Merge pull request #116 from joliveira12/user/joliveira/rating-needs-…
Browse files Browse the repository at this point in the history
…name

Added a rule to reinforce the use of an accessible name on the Rating component
  • Loading branch information
aubreyquinn authored Sep 27, 2024
2 parents 20e8ff9 + 29b51cb commit 3d3279d
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 5 deletions.
10 changes: 5 additions & 5 deletions COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions docs/rules/rating-needs-name.md
Original file line number Diff line number Diff line change
@@ -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

<Rating />

```

Examples of **correct** code for this rule:

```js

<Rating itemLabel={number => `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/)
3 changes: 3 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -63,6 +64,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,
Expand Down Expand Up @@ -96,6 +98,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",
Expand Down
60 changes: 60 additions & 0 deletions lib/rules/rating-needs-name.ts
Original file line number Diff line number Diff line change
@@ -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;
41 changes: 41 additions & 0 deletions tests/lib/rules/rating-needs-name.test.ts
Original file line number Diff line number Diff line change
@@ -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
'<Rating itemLabel={itemLabel} />',
'<Rating name="Rating" />',
'<Rating aria-label="Rating" />',
'<><Label id="label-id">Rating</Label><Rating aria-labelledby="label-id" /></>',
'<Rating itemLabel={itemLabel}></Rating>',
'<Rating name="Rating"></Rating>',
'<Rating aria-label="Rating"></Rating>',
'<><Label id="label-id">Rating</Label><Rating aria-labelledby="label-id"></Rating></>'
],

invalid: [
{
code: "<Rating />",
errors: [{ messageId: "missingAriaLabel" }]
},
{
code: "<Rating></Rating>",
errors: [{ messageId: "missingAriaLabel" }]
}
]
});

0 comments on commit 3d3279d

Please sign in to comment.