Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added a rule to reinforce the use of an accessible name on the Rating component #116

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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" }]
}
]
});
Loading