Skip to content

add counter badge rule #107

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

Merged
merged 9 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ Any use of third-party trademarks or logos are subject to those third-party's po
| [checkbox-needs-labelling](docs/rules/checkbox-needs-labelling.md) | Accessibility: Checkbox without label must have an accessible and visual label: aria-labelledby | ✅ | | |
| [combobox-needs-labelling](docs/rules/combobox-needs-labelling.md) | All interactive elements must have an accessible name | ✅ | | |
| [compound-button-needs-labelling](docs/rules/compound-button-needs-labelling.md) | Accessibility: Compound buttons must have accessible labelling: title, aria-label, aria-labelledby, aria-describedby | ✅ | | |
| [counter-badge-needs-count](docs/rules/counter-badge-needs-count.md) | | | | 🔧 |
| [dialogbody-needs-title-content-and-actions](docs/rules/dialogbody-needs-title-content-and-actions.md) | A DialogBody should have a header(DialogTitle), content(DialogContent), and footer(DialogActions) | ✅ | | |
| [dialogsurface-needs-aria](docs/rules/dialogsurface-needs-aria.md) | DialogueSurface need accessible labelling: aria-describedby on DialogueSurface and aria-label or aria-labelledby(if DialogueTitle is missing) | ✅ | | |
| [dropdown-needs-labelling](docs/rules/dropdown-needs-labelling.md) | Accessibility: Dropdown menu must have an id and it needs to be linked via htmlFor of a Label | ✅ | | |
Expand Down
38 changes: 38 additions & 0 deletions docs/rules/counter-badge-needs-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# @microsoft/fluentui-jsx-a11y/counter-badge-needs-count

🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).

<!-- end auto-generated rule header -->

A counter badge is a badge that displays a numerical count.

## Rule Details

Ensure that the `CounterBadge` component is accessible.

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

```jsx
<CounterBadge appearance="filled" size="extra-large" />
```

```jsx
<CounterBadge icon={<PasteIcon />} />
```


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

If the badge contains a custom icon, that icon must be given alternative text with aria-label, unless it is purely presentational:

```jsx
<CounterBadge icon={<PasteIcon aria-label="paste" />} count={5} />
```

```jsx
<CounterBadge dot />
```

## Further Reading

<https://react.fluentui.dev/?path=/docs/components-badge-badge--docs>
3 changes: 2 additions & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ module.exports = {
"dialogbody-needs-title-content-and-actions": require("./rules/dialogbody-needs-title-content-and-actions"),
"dialogsurface-needs-aria": require("./rules/dialogsurface-needs-aria"),
"spinner-needs-labelling": require("./rules/spinner-needs-labelling"),
"badge-needs-accessible-name": require("./rules/badge-needs-accessible-name")
"badge-needs-accessible-name": require("./rules/badge-needs-accessible-name"),
"counter-badge-needs-count": require("./rules/counter-badge-needs-count")
},
configs: {
recommended: {
Expand Down
88 changes: 88 additions & 0 deletions lib/rules/counter-badge-needs-count.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

"use strict";

var elementType = require("jsx-ast-utils").elementType;
const { getProp } = require("jsx-ast-utils");
var hasProp = require("jsx-ast-utils").hasProp;

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
messages: {
counterBadgeNeedsCount: "CounterBadge: needs numerical count. Add numerical count.",
counterBadgeIconNeedsLabelling: "The icon inside <CounterBadge> must have an aria-label attribute."
},
type: "problem", // `problem`, `suggestion`, or `layout`
docs: {
description: "",
recommended: false,
url: "https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/img_role" // URL to the documentation page for this rule
},
fixable: "code", // Or `code` or `whitespace`
schema: [] // Add a schema if the rule has options
},

create(context) {
return {
// visitor functions for different types of nodes
JSXElement(node) {
const openingElement = node.openingElement;

if (elementType(openingElement) !== "CounterBadge") {
return;
}

const hasDot = hasProp(openingElement.attributes, "dot");

if (hasDot) {
return;
}

const hasIconProp = hasProp(openingElement.attributes, "icon");

if (hasIconProp) {
const iconProp = getProp(openingElement.attributes, "icon");

if (iconProp) {
const iconElement = iconProp.value.expression;

const ariaLabelAttr = hasProp(iconElement.openingElement.attributes, "aria-label");

if (!ariaLabelAttr) {
context.report({
node,
messageId: "counterBadgeIconNeedsLabelling",
fix(fixer) {
const ariaLabelFix = fixer.insertTextAfter(iconElement.openingElement.name, ' aria-label=""');
return ariaLabelFix;
}
});
}
}
}

const hasCount = hasProp(openingElement.attributes, "count");

if (hasCount) {
return;
}

context.report({
node,
messageId: "counterBadgeNeedsCount",
fix(fixer) {
const countFix = fixer.insertTextAfter(openingElement.name, " count={0}");
return countFix;
}
});
}
};
}
};

62 changes: 62 additions & 0 deletions tests/lib/rules/counter-badge-needs-count.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

"use strict";

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const rule = require("../../../lib/rules/counter-badge-needs-count"),
RuleTester = require("eslint").RuleTester;

RuleTester.setDefaultConfig({
parserOptions: {
ecmaVersion: 6,
ecmaFeatures: {
jsx: true
}
}
});

//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------

const ruleTester = new RuleTester();
ruleTester.run("counter-badge-needs-count", rule, {
valid: [
`<CounterBadge count={5} appearance="filled" />`,
`<CounterBadge icon={<PasteIcon aria-label="paste" />} count={1} />`,
`<CounterBadge dot />`,
`<div><CounterBadge count={10} appearance="filled" /></div>`
],

invalid: [
{
code: `<CounterBadge appearance="filled" size="extra-large" />`,
errors: [{ messageId: "counterBadgeNeedsCount" }],
output: `<CounterBadge count={0} appearance="filled" size="extra-large" />`
},
{
code: `<CounterBadge icon={<PasteIcon />} />`,
errors: [
{
messageId: "counterBadgeIconNeedsLabelling"
},
{ messageId: "counterBadgeNeedsCount" }
],
output: `<CounterBadge count={0} icon={<PasteIcon aria-label="" />} />`
},
{
code: `<CounterBadge icon={<PasteIcon />} count={100} />`,
errors: [{ messageId: "counterBadgeIconNeedsLabelling" }],
output: `<CounterBadge icon={<PasteIcon aria-label="" />} count={100} />`
},
{
code: `<CounterBadge></CounterBadge>`,
errors: [{ messageId: "counterBadgeNeedsCount" }],
output: `<CounterBadge count={0}></CounterBadge>`
}
]
});
Loading