├── .markdownlintignore
├── .gitignore
├── .husky
└── pre-push
├── .markdownlint.json
├── .prettierignore
├── HOW_TO_PUBLISH_ON_NPM.md
├── lib
├── applicableComponents
│ ├── dropdownBasedComponents.ts
│ ├── linkBasedComponents.js
│ ├── buttonBasedComponents.js
│ ├── imageBasedComponents.js
│ ├── inputBasedComponents.js
│ ├── labelBasedComponents.ts
│ ├── ariaLabelBasedComponent.js
│ └── disabledFocusableComponents.ts
├── util
│ ├── hasTriggerProp.ts
│ ├── flattenChildren.ts
│ ├── hasTooltipParent.ts
│ ├── hasValidNestedProp.ts
│ ├── hasFieldParent.ts
│ ├── hasLoadingState.ts
│ ├── hasTextContentChild.ts
│ ├── hasDefinedProp.ts
│ └── hasNonEmptyProp.ts
└── rules
│ ├── tag-needs-labelling.ts
│ ├── swatchpicker-needs-labelling.ts
│ ├── card-needs-accessible-name.ts
│ ├── colorswatch-needs-labelling.ts
│ ├── emptyswatch-needs-labelling.ts
│ ├── image-needs-alt.ts
│ ├── imageswatch-needs-labelling.ts
│ ├── infolabel-needs-labelling.ts
│ ├── buttons
│ └── menu-button-needs-labelling.ts
│ ├── field-needs-labelling.ts
│ ├── no-empty-components.ts
│ ├── tooltip-not-recommended.ts
│ ├── toolbar-missing-aria.ts
│ ├── visual-label-better-than-aria-suggestion.ts
│ ├── rating-needs-name.ts
│ ├── spin-button-unrecommended-labelling.ts
│ ├── breadcrumb-needs-labelling.ts
│ ├── spinner-needs-labelling.ts
│ ├── avatar-needs-name.ts
│ ├── accordion-item-needs-header-and-panel.ts
│ ├── spin-button-needs-labelling.ts
│ └── switch-needs-labelling.ts
├── .prettierrc.json
├── .vscode
├── settings.json
└── extensions.json
├── scripts
├── utils
│ └── kebabToKamelCase.js
├── boilerplate
│ ├── util.js
│ ├── doc.js
│ ├── test.js
│ └── rule.js
├── create-rule.md
└── addRuleToIndex.js
├── tests
└── lib
│ └── rules
│ ├── helper
│ └── ruleTester.ts
│ ├── spin-button-unrecommended-labelling.test.ts
│ ├── utils
│ ├── hasLabeledChild.ts
│ ├── hasValidNestedProp.test.ts
│ ├── hasDefinedProp.test.ts
│ ├── flattenChildren.test.ts
│ └── hasTooltipParent.test.ts
│ ├── tooltip-not-recommended.test.ts
│ ├── image-needs-alt.test.ts
│ ├── field-needs-labelling.test.ts
│ ├── rating-needs-name.test.ts
│ ├── card-needs-accessible-name.test.ts
│ ├── breadcrumb-needs-labelling.test.ts
│ ├── buttons
│ ├── menu-button-needs-labelling.test.ts
│ └── compound-button-needs-labelling.test.ts
│ ├── tag-needs-labelling.test.ts
│ ├── counter-badge-needs-count.test.ts
│ ├── avatar-needs-name.test.ts
│ ├── menu-item-needs-labelling.test.ts
│ ├── avoid-using-aria-describedby-for-primary-labelling.test.ts
│ ├── accordion-item-needs-header-and-panel.test.ts
│ ├── visual-label-better-than-aria-suggestion.test.ts
│ ├── toolbar-missing-aria.test.ts
│ ├── tablist-and-tabs-need-labelling.test.ts
│ ├── prefer-aria-over-title-attribute.test.ts
│ ├── spinner-needs-labelling.test.ts
│ ├── dialogbody-needs-title-content-and-actions.test.ts
│ ├── infolabel-needs-labelling.test.ts
│ ├── switch-needs-labelling.test.ts
│ ├── radio-button-missing-label.test.ts
│ ├── radioGroup-missing-label.test.ts
│ ├── badge-needs-accessible-name.test.ts
│ ├── colorswatch-needs-labelling.test.ts
│ ├── spin-button-needs-labelling.test.ts
│ ├── checkbox-needs-labelling.test.ts
│ └── tag-dismissible-needs-labelling.test.ts
├── docs
└── rules
│ ├── colorswatch-needs-labelling.md
│ ├── visual-label-better-than-aria-suggestion.md
│ ├── infolabel-needs-labelling.md
│ ├── menu-button-needs-labelling.md
│ ├── avatar-needs-name.md
│ ├── no-empty-buttons.md
│ ├── no-empty-components.md
│ ├── image-needs-alt.md
│ ├── breadcrumb-needs-labelling.md
│ ├── rating-needs-name.md
│ ├── menu-item-needs-labelling.md
│ ├── counter-badge-needs-count.md
│ ├── tooltip-not-recommended.md
│ ├── toolbar-missing-aria.md
│ ├── combobox-needs-labelling.md
│ ├── card-needs-accessible-name.md
│ ├── switch-needs-labelling.md
│ ├── radio-button-missing-label.md
│ ├── field-needs-labelling.md
│ ├── tag-needs-name.md
│ ├── dropdown-needs-labelling.md
│ ├── image-button-missing-aria.md
│ ├── checkbox-needs-labelling.md
│ ├── spin-button-needs-labelling.md
│ ├── spin-button-unrecommended-labelling.md
│ ├── radiogroup-missing-label.md
│ ├── tag-dismissible-needs-labelling.md
│ ├── input-components-require-accessible-name.md
│ ├── compound-button-needs-labelling.md
│ ├── dialogbody-needs-title-content-and-actions.md
│ ├── link-missing-labelling.md
│ ├── swatchpicker-needs-labelling.md
│ ├── spinner-needs-labelling.md
│ ├── accordion-header-needs-labelling.md
│ ├── tablist-and-tabs-need-labelling.md
│ ├── badge-needs-accessible-name.md
│ ├── accordion-item-needs-header-and-panel.md
│ ├── avoid-using-aria-describedby-for-primary-labelling.md
│ └── prefer-aria-over-title-attribute.md
├── CODE_OF_CONDUCT.md
├── jest.config.js
├── KNOWN_ISSUES.md
├── SUPPORT.md
├── tsconfig.json
├── .github
└── workflows
│ ├── code-coverage.yml
│ ├── node.js.yml
│ └── release-package.yml
├── LICENSE
├── .eslintrc.js
├── COVERAGE.md
└── SECURITY.md
/.markdownlintignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | coverage
--------------------------------------------------------------------------------
/.husky/pre-push:
--------------------------------------------------------------------------------
1 | npm run build
2 | npm run lint
3 | npm test
4 |
--------------------------------------------------------------------------------
/.markdownlint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "markdownlint/style/prettier",
3 | "line-length": false
4 | }
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | *.md
2 | *.txt
3 | *.zip
4 | .gitignore
5 | .npmrc
6 | .prettierignore
7 | node_modules/
8 | package-lock.json
9 | yarn.lock
--------------------------------------------------------------------------------
/HOW_TO_PUBLISH_ON_NPM.md:
--------------------------------------------------------------------------------
1 | # How to Publish on NPM
2 |
3 | ## Commands
4 |
5 | ```sh
6 | npm publish --scope @microsoft --access=public
7 | npm login --scope @microsoft
8 | ```
9 |
--------------------------------------------------------------------------------
/lib/applicableComponents/dropdownBasedComponents.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | const dropdownBasedComponents = ["Dropdown"];
5 |
6 | export { dropdownBasedComponents };
7 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "avoid",
3 | "bracketSameLine": true,
4 | "bracketSpacing": true,
5 | "endOfLine": "auto",
6 | "printWidth": 140,
7 | "semi": true,
8 | "tabWidth": 4,
9 | "trailingComma": "none"
10 | }
11 |
--------------------------------------------------------------------------------
/lib/applicableComponents/linkBasedComponents.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | const linkBasedComponents = ["Link", "a"]; // , "BreadcrumbButton"
5 |
6 | module.exports = {
7 | linkBasedComponents
8 | };
9 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "javascript.updateImportsOnFileMove.enabled": "always",
3 | "editor.formatOnSave": true,
4 | "markdown.validate.enabled": true,
5 | "editor.defaultFormatter": "esbenp.prettier-vscode",
6 | "prettier.requireConfig": true
7 | }
8 |
--------------------------------------------------------------------------------
/scripts/utils/kebabToKamelCase.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | export const kebabToCamelCase = str => {
5 | return str.replace(/[-_](.)/g, (_, char) => char.toUpperCase()).replace(/^(.)/, (_, char) => char.toLowerCase());
6 | };
7 |
--------------------------------------------------------------------------------
/tests/lib/rules/helper/ruleTester.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { RuleTester } from "eslint";
5 | const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } } });
6 |
7 | export default ruleTester;
8 |
--------------------------------------------------------------------------------
/lib/applicableComponents/buttonBasedComponents.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | const applicableComponents = ["Button", "ToggleButton", "CompoundButton", "MenuButton", "SplitButton"];
5 |
6 | module.exports = {
7 | applicableComponents
8 | };
9 |
--------------------------------------------------------------------------------
/lib/applicableComponents/imageBasedComponents.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | const fluentImageComponents = ["Image"];
5 | const imageDomNodes = ["img"];
6 |
7 | module.exports = {
8 | fluentImageComponents,
9 | imageDomNodes
10 | };
11 |
--------------------------------------------------------------------------------
/docs/rules/colorswatch-needs-labelling.md:
--------------------------------------------------------------------------------
1 | # Accessibility: ColorSwatch must have an accessible name via aria-label, Tooltip, aria-labelledby, etc. (`@microsoft/fluentui-jsx-a11y/colorswatch-needs-labelling`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
--------------------------------------------------------------------------------
/lib/applicableComponents/inputBasedComponents.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | const applicableComponents = ["Input", "Slider", "DatePicker", "Textarea", "TextField", "TimePicker", "SearchBox", "Select"];
5 |
6 | module.exports = {
7 | applicableComponents
8 | };
9 |
--------------------------------------------------------------------------------
/lib/applicableComponents/labelBasedComponents.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | const labelBasedComponents = ["Label", "label"];
5 | const elementsUsedAsLabels = ["div", "span", "p", "h1", "h2", "h3", "h4", "h5", "h6"];
6 |
7 | export { labelBasedComponents, elementsUsedAsLabels };
8 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "dbaeumer.vscode-eslint",
4 | "firsttris.vscode-jest-runner",
5 | "esbenp.prettier-vscode",
6 | "rvest.vs-code-prettier-eslint",
7 | "DavidAnson.vscode-markdownlint",
8 | "streetsidesoftware.code-spell-checker-cspell-bundled-dictionaries"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/scripts/boilerplate/util.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Helper method to convert LF line endings to CRLF line endings
3 | * @remarks This is needed to avoid prettier formatting issues on generation of the files
4 | * @param text The text to convert
5 | * @returns The converted text
6 | */
7 | const withCRLF = text => text.replace(/\n/g, "\r\n");
8 |
9 | module.exports = { withCRLF };
10 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Microsoft Open Source Code of Conduct
2 |
3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
4 |
5 | Resources:
6 |
7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns
10 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | module.exports = {
5 | preset: "ts-jest",
6 | testEnvironment: "node",
7 | testMatch: ["**/tests/**/*.test.ts"],
8 | verbose: true,
9 | collectCoverage: true,
10 | coverageThreshold: {
11 | global: {
12 | branches: 80,
13 | functions: 80,
14 | lines: 80,
15 | statements: 80
16 | }
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/lib/applicableComponents/ariaLabelBasedComponent.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | const { applicableComponents: ButtonBasedComponents } = require("./buttonBasedComponents");
5 | const { applicableComponents: InputBasedComponents } = require("./inputBasedComponents");
6 |
7 | const applicableComponents = ["SpinButton", ...ButtonBasedComponents, ...InputBasedComponents];
8 |
9 | module.exports = {
10 | applicableComponents
11 | };
12 |
--------------------------------------------------------------------------------
/KNOWN_ISSUES.md:
--------------------------------------------------------------------------------
1 | # Known Issues
2 |
3 | ## No object props deconstruction
4 |
5 | We currently do not support object props deconstruction.
6 |
7 | e.g.
8 |
9 | ```tsx
10 | const buttonProps = {
11 | icon: ,
12 | aria-label: 'start date'
13 | };
14 |
15 |
16 |
17 | ```
18 |
19 | Unfortunately, these will not be picked up by our linter. However, we hope to support this soon 🚀
20 |
21 | See [issue #149](https://github.com/microsoft/eslint-plugin-fluentui-jsx-a11y/issues/149) for details.
22 |
--------------------------------------------------------------------------------
/scripts/boilerplate/doc.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | const { withCRLF } = require("./util");
5 |
6 | const docBoilerplateGenerator = (name, description) =>
7 | withCRLF(`# ${description} (@microsoft/fluentui-jsx-a11y/${name})
8 |
9 | Write a useful explanation here!
10 |
11 | ## Rule details
12 |
13 | Write more details here!
14 |
15 | \`\`\`jsx
16 |
17 | \`\`\`
18 |
19 | \`\`\`jsx
20 |
21 | \`\`\`
22 |
23 | ## Further Reading
24 | `);
25 | module.exports = docBoilerplateGenerator;
26 |
--------------------------------------------------------------------------------
/SUPPORT.md:
--------------------------------------------------------------------------------
1 | # Support
2 |
3 | ## How to file issues and get help
4 |
5 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing
6 | issues before filing new issues to avoid duplicates. For new issues, file your bug or
7 | feature request as a new Issue.
8 |
9 | For help and questions about using this project, please contact the [MWT Dublin Accessibility V-Team](https://domoreexp.visualstudio.com/Teamspace/_wiki/wikis/Teamspace.wiki/28464/Accessibility-Contacts).
10 |
11 | ## Microsoft Support Policy
12 |
13 | Support for this **PROJECT or PRODUCT** is limited to the resources listed above.
14 |
--------------------------------------------------------------------------------
/lib/util/hasTriggerProp.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { TSESTree } from "@typescript-eslint/utils";
5 | import { hasProp } from "jsx-ast-utils";
6 | import { JSXAttribute } from "estree-jsx";
7 |
8 | /**
9 | * Checks if a component has a specific trigger prop.
10 | * This is useful for rules that only apply when certain props are present
11 | * (e.g., dismissible, expandable, collapsible, etc.)
12 | */
13 | export const hasTriggerProp = (openingElement: TSESTree.JSXOpeningElement, triggerProp: string): boolean => {
14 | return hasProp(openingElement.attributes as JSXAttribute[], triggerProp);
15 | };
16 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "allowUnreachableCode": false,
5 | "allowUnusedLabels": false,
6 | "declaration": true,
7 | "declarationMap": false,
8 | "module": "CommonJS",
9 | "noImplicitReturns": true,
10 | "pretty": true,
11 | "resolveJsonModule": true,
12 | "sourceMap": false,
13 | "lib": ["ES5"],
14 | "strict": true,
15 | "esModuleInterop": true,
16 | "skipLibCheck": true,
17 | "forceConsistentCasingInFileNames": true,
18 | "outDir": "./dist",
19 | "rootDir": ".",
20 | "allowJs": true
21 | },
22 | "include": ["lib", "tests/lib"],
23 | "exclude": ["node_modules", "dist"]
24 | }
25 |
--------------------------------------------------------------------------------
/lib/util/flattenChildren.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { TSESTree } from "@typescript-eslint/types";
5 |
6 | // Flatten the JSX tree structure by recursively collecting all child elements
7 | const flattenChildren = (node: TSESTree.JSXElement): TSESTree.JSXElement[] => {
8 | const flatChildren: TSESTree.JSXElement[] = [];
9 |
10 | if (node.children && node.children.length > 0) {
11 | node.children.forEach(child => {
12 | if (child.type === "JSXElement") {
13 | const jsxChild = child as TSESTree.JSXElement;
14 | flatChildren.push(jsxChild, ...flattenChildren(jsxChild));
15 | }
16 | });
17 | }
18 |
19 | return flatChildren;
20 | };
21 |
22 | export { flattenChildren };
23 |
--------------------------------------------------------------------------------
/.github/workflows/code-coverage.yml:
--------------------------------------------------------------------------------
1 | name: Test Coveralls
2 |
3 | on: ["push", "pull_request"]
4 |
5 | jobs:
6 | build:
7 | name: Build
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v4
11 |
12 | - name: Use Node.js 20.x
13 | uses: actions/setup-node@v4
14 | with:
15 | node-version: 20.x
16 |
17 | - name: npm install, run tests with coverage
18 | run: |
19 | npm install
20 | npm run test:coverage
21 |
22 | - name: Upload coverage to Coveralls
23 | uses: coverallsapp/github-action@v2.3.0
24 | with:
25 | coveralls-token: ${{ secrets.COVERALLS_REPO_TOKEN }}
26 | path-to-lcov: ./coverage/lcov.info
27 |
--------------------------------------------------------------------------------
/scripts/boilerplate/test.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | const { withCRLF } = require("./util");
5 |
6 | const testBoilerplate = name =>
7 | withCRLF(`// Copyright (c) Microsoft Corporation.
8 | // Licensed under the MIT License.
9 |
10 | import { Rule } from "eslint";
11 | import ruleTester from "./helper/ruleTester";
12 | import rule from "../../../lib/rules/${name}";
13 |
14 | // -----------------------------------------------------------------------------
15 | // Tests
16 | // -----------------------------------------------------------------------------
17 |
18 | ruleTester.run("${name}", rule as unknown as Rule.RuleModule, {
19 | valid: [
20 | /* ... */
21 | ],
22 | invalid: [
23 | /* ... */
24 | ]
25 | });
26 | `);
27 | module.exports = testBoilerplate;
28 |
--------------------------------------------------------------------------------
/lib/applicableComponents/disabledFocusableComponents.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | /**
5 | * FluentUI components that support both 'disabled' and 'disabledFocusable' props
6 | * These are components where the rule should apply
7 | */
8 | const disabledFocusableComponents = [
9 | // Button components
10 | "Button",
11 | "ToggleButton",
12 | "CompoundButton",
13 | "MenuButton",
14 | "SplitButton",
15 |
16 | // Form controls
17 | "Checkbox",
18 | "Radio",
19 | "Switch",
20 |
21 | // Input components
22 | "Input",
23 | "Textarea",
24 | "Combobox",
25 | "Dropdown",
26 | "SpinButton",
27 | "Slider",
28 | "DatePicker",
29 | "TimePicker",
30 |
31 | // Other interactive components
32 | "Link",
33 | "Tab"
34 | ] as const;
35 |
36 | export { disabledFocusableComponents };
37 |
--------------------------------------------------------------------------------
/lib/util/hasTooltipParent.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { elementType } from "jsx-ast-utils";
5 | import { TSESLint } from "@typescript-eslint/utils"; // Assuming context comes from TSESLint
6 | import { JSXOpeningElement } from "estree-jsx";
7 |
8 | const hasToolTipParent = (context: TSESLint.RuleContext): boolean => {
9 | const ancestors = context.getAncestors();
10 |
11 | if (!ancestors || ancestors.length === 0) {
12 | return false;
13 | }
14 |
15 | return ancestors.some(
16 | item =>
17 | item.type === "JSXElement" &&
18 | item.openingElement &&
19 | item.openingElement.type === "JSXOpeningElement" &&
20 | elementType(item.openingElement as unknown as JSXOpeningElement) === "Tooltip"
21 | );
22 | };
23 |
24 | export { hasToolTipParent };
25 |
--------------------------------------------------------------------------------
/docs/rules/visual-label-better-than-aria-suggestion.md:
--------------------------------------------------------------------------------
1 | # Visual label is better than an aria-label because sighted users can't read the aria-label text (`@microsoft/fluentui-jsx-a11y/visual-label-better-than-aria-suggestion`)
2 |
3 | ⚠️ This rule _warns_ in the ✅ `recommended` config.
4 |
5 |
6 |
7 | For component like Dropdown, SpinButton, it's good to have a aria-label for screen reader users but visual labels are considered better because they're also useful for sighted user and comes in screen announcement as well.
8 |
9 | ## Rule Details
10 |
11 | This rule aims to encourage the usage of visual labels in place of aria-label
12 |
13 | Examples of **incorrect** code for this rule:
14 |
15 | ```jsx
16 |
17 | ```
18 |
19 | Examples of **correct** code for this rule:
20 |
21 | ```jsx
22 | <>This is the visual label >
23 | ```
24 |
--------------------------------------------------------------------------------
/.github/workflows/node.js.yml:
--------------------------------------------------------------------------------
1 | name: Node.js CI
2 |
3 | on:
4 | push:
5 | branches: ["main"]
6 | pull_request:
7 | branches: ["main"]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 |
13 | strategy:
14 | matrix:
15 | node-version: [16.x, 18.x, 20.x]
16 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
17 |
18 | steps:
19 | - uses: actions/checkout@v4
20 |
21 | - name: Use Node.js ${{ matrix.node-version }}
22 | uses: actions/setup-node@v4
23 | with:
24 | node-version: ${{ matrix.node-version }}
25 | cache: "npm"
26 |
27 | - run: npm ci
28 | - run: npm run build --if-present
29 | - run: npm run lint --if-present
30 | - run: npm test
31 |
32 | - name: Run tests with coverage
33 | run: npm run test:coverage
34 |
--------------------------------------------------------------------------------
/lib/rules/tag-needs-labelling.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { ESLintUtils } from "@typescript-eslint/utils";
5 | import { makeLabeledControlRule } from "../util/ruleFactory";
6 |
7 | //------------------------------------------------------------------------------
8 | // Rule Definition
9 | //------------------------------------------------------------------------------
10 |
11 | export default ESLintUtils.RuleCreator.withoutDocs(
12 | makeLabeledControlRule({
13 | component: "Tag",
14 | messageId: "missingAriaLabel",
15 | description: "Accessibility: Tag must have an accessible name",
16 | labelProps: ["aria-label"],
17 | allowFieldParent: false,
18 | allowHtmlFor: false,
19 | allowLabelledBy: true,
20 | allowWrappingLabel: false,
21 | allowTooltipParent: false,
22 | allowDescribedBy: false,
23 | allowLabeledChild: false,
24 | allowTextContentChild: true
25 | })
26 | );
27 |
--------------------------------------------------------------------------------
/lib/util/hasValidNestedProp.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { TSESTree } from "@typescript-eslint/utils";
5 | import { getProp, getPropValue } from "jsx-ast-utils";
6 | import { JSXAttribute } from "estree-jsx";
7 |
8 | /**
9 | * Checks if a value is a non-empty string
10 | */
11 | const isNonEmptyString = (value: any): boolean => {
12 | return typeof value === "string" && value.trim().length > 0;
13 | };
14 |
15 | /**
16 | * Validates if a component has a specific nested property with a non-empty string value.
17 | */
18 | export const hasValidNestedProp = (openingElement: TSESTree.JSXOpeningElement, propName: string, nestedKey: string): boolean => {
19 | const prop = getProp(openingElement.attributes as JSXAttribute[], propName);
20 | if (!prop) {
21 | return false;
22 | }
23 |
24 | const propValue = getPropValue(prop);
25 | return Boolean(propValue && typeof propValue === "object" && isNonEmptyString((propValue as any)[nestedKey]));
26 | };
27 |
--------------------------------------------------------------------------------
/.github/workflows/release-package.yml:
--------------------------------------------------------------------------------
1 | name: Publish as Github package
2 |
3 | on:
4 | release:
5 | types: [created, prereleased]
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v3
12 | - uses: actions/setup-node@v3
13 | with:
14 | node-version: 16
15 | - run: npm ci
16 | - run: npm run build
17 | - run: npm test
18 |
19 | publish-gpr:
20 | needs: build
21 | runs-on: ubuntu-latest
22 | permissions:
23 | packages: write
24 | contents: read
25 | steps:
26 | - uses: actions/checkout@v3
27 | - uses: actions/setup-node@v3
28 | with:
29 | node-version: 16
30 | registry-url: https://npm.pkg.github.com/
31 | - run: npm ci
32 | - run: npm run build
33 | - run: npm publish
34 | env:
35 | NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}
36 |
--------------------------------------------------------------------------------
/lib/rules/swatchpicker-needs-labelling.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { ESLintUtils } from "@typescript-eslint/utils";
5 | import { makeLabeledControlRule } from "../util/ruleFactory";
6 |
7 | //------------------------------------------------------------------------------
8 | // Rule Definition
9 | //------------------------------------------------------------------------------
10 |
11 | export default ESLintUtils.RuleCreator.withoutDocs(
12 | makeLabeledControlRule({
13 | component: "SwatchPicker",
14 | messageId: "noUnlabeledSwatchPicker",
15 | description: "Accessibility: SwatchPicker must have an accessible name via aria-label, aria-labelledby, Field component, etc..",
16 | labelProps: ["aria-label"],
17 | allowFieldParent: true,
18 | allowHtmlFor: false,
19 | allowLabelledBy: true,
20 | allowWrappingLabel: false,
21 | allowTooltipParent: false,
22 | allowDescribedBy: false,
23 | allowLabeledChild: false
24 | })
25 | );
26 |
--------------------------------------------------------------------------------
/lib/rules/card-needs-accessible-name.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { ESLintUtils } from "@typescript-eslint/utils";
5 | import { makeLabeledControlRule } from "../util/ruleFactory";
6 |
7 | //------------------------------------------------------------------------------
8 | // Rule Definition
9 | //------------------------------------------------------------------------------
10 |
11 | export default ESLintUtils.RuleCreator.withoutDocs(
12 | makeLabeledControlRule({
13 | component: "Card",
14 | messageId: "cardNeedsAccessibleName",
15 | description: "Accessibility: Interactive Card must have an accessible name via aria-label, aria-labelledby, etc.",
16 | labelProps: ["aria-label"],
17 | allowFieldParent: false,
18 | allowHtmlFor: false,
19 | allowLabelledBy: true,
20 | allowWrappingLabel: false,
21 | allowTooltipParent: true,
22 | allowDescribedBy: false,
23 | allowLabeledChild: true,
24 | allowTextContentChild: true
25 | })
26 | );
27 |
--------------------------------------------------------------------------------
/lib/rules/colorswatch-needs-labelling.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { ESLintUtils } from "@typescript-eslint/utils";
5 | import { makeLabeledControlRule } from "../util/ruleFactory";
6 |
7 | //------------------------------------------------------------------------------
8 | // Rule Definition
9 | //------------------------------------------------------------------------------
10 |
11 | export default ESLintUtils.RuleCreator.withoutDocs(
12 | makeLabeledControlRule({
13 | component: "ColorSwatch",
14 | messageId: "noUnlabeledColorSwatch",
15 | description: "Accessibility: ColorSwatch must have an accessible name via aria-label, Tooltip, aria-labelledby, etc..",
16 | labelProps: ["aria-label"],
17 | allowFieldParent: true,
18 | allowHtmlFor: true,
19 | allowLabelledBy: false,
20 | allowWrappingLabel: true,
21 | allowTooltipParent: true,
22 | allowDescribedBy: false,
23 | allowLabeledChild: false,
24 | allowTextContentChild: true
25 | })
26 | );
27 |
--------------------------------------------------------------------------------
/lib/rules/emptyswatch-needs-labelling.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { ESLintUtils } from "@typescript-eslint/utils";
5 | import { makeLabeledControlRule } from "../util/ruleFactory";
6 |
7 | //------------------------------------------------------------------------------
8 | // Rule Definition
9 | //------------------------------------------------------------------------------
10 |
11 | export default ESLintUtils.RuleCreator.withoutDocs(
12 | makeLabeledControlRule({
13 | component: "EmptySwatch",
14 | messageId: "noUnlabeledEmptySwatch",
15 | description: "Accessibility: EmptySwatch must have an accessible name via aria-label, Tooltip, aria-labelledby, etc..",
16 | labelProps: ["aria-label"],
17 | allowFieldParent: false,
18 | allowHtmlFor: true,
19 | allowLabelledBy: true,
20 | allowWrappingLabel: true,
21 | allowTooltipParent: true,
22 | allowDescribedBy: false,
23 | allowLabeledChild: false,
24 | allowTextContentChild: true
25 | })
26 | );
27 |
--------------------------------------------------------------------------------
/lib/rules/image-needs-alt.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { ESLintUtils } from "@typescript-eslint/utils";
5 | import { makeLabeledControlRule } from "../util/ruleFactory";
6 |
7 | //------------------------------------------------------------------------------
8 | // Rule Definition
9 | //------------------------------------------------------------------------------
10 |
11 | const rule = ESLintUtils.RuleCreator.withoutDocs(
12 | makeLabeledControlRule({
13 | component: "Image",
14 | messageId: "imageNeedsAlt",
15 | description:
16 | 'Accessibility: Image must have alt attribute with a meaningful description of the image. If the image is decorative, use alt="".',
17 | requiredProps: ["alt"],
18 | allowFieldParent: false,
19 | allowHtmlFor: false,
20 | allowLabelledBy: false,
21 | allowWrappingLabel: false,
22 | allowTooltipParent: false,
23 | allowDescribedBy: false,
24 | allowLabeledChild: false
25 | })
26 | );
27 |
28 | export default rule;
29 |
--------------------------------------------------------------------------------
/lib/rules/imageswatch-needs-labelling.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { ESLintUtils } from "@typescript-eslint/utils";
5 | import { makeLabeledControlRule } from "../util/ruleFactory";
6 |
7 | //------------------------------------------------------------------------------
8 | // Rule Definition
9 | //------------------------------------------------------------------------------
10 |
11 | export default ESLintUtils.RuleCreator.withoutDocs(
12 | makeLabeledControlRule({
13 | component: "ImageSwatch",
14 | messageId: "noUnlabeledImageSwatch",
15 | description: "Accessibility: ImageSwatch must have an accessible name via aria-label, Tooltip, aria-labelledby, etc..",
16 | labelProps: ["aria-label"],
17 | allowFieldParent: false,
18 | allowHtmlFor: false,
19 | allowLabelledBy: true,
20 | allowWrappingLabel: true,
21 | allowTooltipParent: true,
22 | allowDescribedBy: false,
23 | allowLabeledChild: false,
24 | allowTextContentChild: false
25 | })
26 | );
27 |
--------------------------------------------------------------------------------
/lib/rules/infolabel-needs-labelling.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { ESLintUtils } from "@typescript-eslint/utils";
5 | import { makeLabeledControlRule } from "../util/ruleFactory";
6 |
7 | //------------------------------------------------------------------------------
8 | // Rule Definition
9 | //------------------------------------------------------------------------------
10 |
11 | export default ESLintUtils.RuleCreator.withoutDocs(
12 | makeLabeledControlRule({
13 | component: "InfoLabel",
14 | messageId: "infoLabelNeedsLabelling",
15 | description: "Accessibility: InfoLabel must have an accessible name via aria-label, text content, aria-labelledby, etc.",
16 | labelProps: ["aria-label"],
17 | allowFieldParent: false,
18 | allowHtmlFor: false,
19 | allowLabelledBy: true,
20 | allowWrappingLabel: false,
21 | allowTooltipParent: true,
22 | allowDescribedBy: false,
23 | allowLabeledChild: true,
24 | allowTextContentChild: true
25 | })
26 | );
27 |
--------------------------------------------------------------------------------
/lib/rules/buttons/menu-button-needs-labelling.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { ESLintUtils } from "@typescript-eslint/utils";
5 | import { makeLabeledControlRule } from "../../util/ruleFactory";
6 |
7 | //------------------------------------------------------------------------------
8 | // Rule Definition
9 | //------------------------------------------------------------------------------
10 |
11 | export default ESLintUtils.RuleCreator.withoutDocs(
12 | makeLabeledControlRule({
13 | component: "MenuButton",
14 | messageId: "menuButtonNeedsLabelling",
15 | description: "Accessibility: MenuButton must have an accessible name via aria-label, text content, aria-labelledby, etc.",
16 | labelProps: ["aria-label"],
17 | allowFieldParent: false,
18 | allowHtmlFor: false,
19 | allowLabelledBy: true,
20 | allowWrappingLabel: true,
21 | allowTooltipParent: true,
22 | allowDescribedBy: false,
23 | allowLabeledChild: true,
24 | allowTextContentChild: true
25 | })
26 | );
27 |
--------------------------------------------------------------------------------
/lib/util/hasFieldParent.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { TSESTree } from "@typescript-eslint/types";
5 | import { elementType } from "jsx-ast-utils";
6 | import { TSESLint } from "@typescript-eslint/utils";
7 | import { JSXOpeningElement } from "estree-jsx";
8 |
9 | // Function to check if the current node has a "Field" parent JSXElement
10 | export const hasFieldParent = (context: TSESLint.RuleContext): boolean => {
11 | const ancestors: TSESTree.Node[] = context.getAncestors();
12 |
13 | if (ancestors == null || ancestors.length === 0) {
14 | return false;
15 | }
16 |
17 | let field = false;
18 |
19 | ancestors.forEach(item => {
20 | if (
21 | item.type === "JSXElement" &&
22 | item.openingElement != null &&
23 | item.openingElement.type === "JSXOpeningElement" &&
24 | elementType(item.openingElement as unknown as JSXOpeningElement) === "Field"
25 | ) {
26 | field = true;
27 | }
28 | });
29 |
30 | return field;
31 | };
32 |
--------------------------------------------------------------------------------
/docs/rules/infolabel-needs-labelling.md:
--------------------------------------------------------------------------------
1 | # Accessibility: InfoLabel must have an accessible name via aria-label, text content, aria-labelledby, etc (`@microsoft/fluentui-jsx-a11y/infolabel-needs-labelling`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 | InfoLabel components must have accessible labelling for screen readers.
8 |
9 | ## Rule Details
10 |
11 | This rule enforces that InfoLabel components have proper accessible names through aria-label, aria-labelledby, or text content.
12 |
13 | ### Noncompliant
14 |
15 | ```jsx
16 |
17 | ```
18 |
19 | ### Compliant
20 |
21 | ```jsx
22 |
23 |
24 |
25 | Help text
26 |
27 |
28 | Help information
29 | ```
30 |
31 | ## When Not To Use
32 |
33 | If the InfoLabel is purely decorative, this rule may not be necessary.
34 |
35 | ## Accessibility guidelines
36 |
37 | - [WCAG 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value.html)
38 |
--------------------------------------------------------------------------------
/docs/rules/menu-button-needs-labelling.md:
--------------------------------------------------------------------------------
1 | # Accessibility: MenuButton must have an accessible name via aria-label, text content, aria-labelledby, etc (`@microsoft/fluentui-jsx-a11y/menu-button-needs-labelling`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 | MenuButton components must have accessible labelling for screen readers.
8 |
9 | ## Rule Details
10 |
11 | This rule enforces that MenuButton components have proper accessible names through aria-label, aria-labelledby, or text content.
12 |
13 | ### Noncompliant
14 |
15 | ```jsx
16 |
17 | ```
18 |
19 | ### Compliant
20 |
21 | ```jsx
22 |
23 |
24 |
25 |
26 |
27 |
28 | Options
29 | ```
30 |
31 | ## When Not To Use
32 |
33 | This rule should always be used for MenuButton components as they are interactive elements.
34 |
35 | ## Accessibility guidelines
36 |
37 | - [WCAG 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value.html)
38 |
--------------------------------------------------------------------------------
/docs/rules/avatar-needs-name.md:
--------------------------------------------------------------------------------
1 | # Accessibility: Avatar must have an accessible labelling: name, aria-label, aria-labelledby (`@microsoft/fluentui-jsx-a11y/avatar-needs-name`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 | All interactive elements must have an accessible name.
8 |
9 | Avatar lacks an accessible name without a name or accessible labelling.
10 |
11 |
12 |
13 | ## Rule Details
14 |
15 | This rule aims to prevent an avatar from not having an accessible name.
16 |
17 | Examples of **incorrect** code for this rule:
18 |
19 | ```jsx
20 |
21 |
22 |
23 | Start date
24 |
25 | ```
26 |
27 | Examples of **correct** code for this rule:
28 |
29 | ```jsx
30 |
31 |
32 |
33 | Jane Doe
34 |
35 | ```
36 |
--------------------------------------------------------------------------------
/docs/rules/no-empty-buttons.md:
--------------------------------------------------------------------------------
1 | # Accessibility: Button, ToggleButton, SplitButton, MenuButton, CompoundButton must either text content or icon or child component (`@microsoft/fluentui-jsx-a11y/no-empty-buttons`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 | Buttons must either have text, content or accessible labelling
8 |
9 |
10 |
11 | ## Rule Details
12 |
13 | This rule aims to make a button to have something to generate an aria-label.
14 |
15 | Examples of **incorrect** code for this rule:
16 |
17 | ```jsx
18 |
19 | ```
20 |
21 | ```jsx
22 |
23 | ```
24 |
25 | ```jsx
26 |
27 | ```
28 |
29 | ```jsx
30 |
31 | ```
32 |
33 | Examples of **correct** code for this rule:
34 |
35 | ```jsx
36 | Example
37 | ```
38 |
39 | ```jsx
40 | Example
41 | ```
42 |
43 | ```jsx
44 | } aria-label="Close" />
45 | ```
46 |
47 | ```jsx
48 | }>Button
49 | ```
50 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Microsoft Corporation.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE
22 |
--------------------------------------------------------------------------------
/docs/rules/no-empty-components.md:
--------------------------------------------------------------------------------
1 | # FluentUI components should not be empty (`@microsoft/fluentui-jsx-a11y/no-empty-components`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 | Provide labels to identify all form controls, including text fields, checkboxes, radio buttons, and drop-down menus. In most cases, this is done by using the label element.
8 |
9 |
10 |
11 | ## Rule Details
12 |
13 | This rule aims to prevent missing text and info for users.
14 |
15 | Examples of **incorrect** code for this rule:
16 |
17 | ```jsx
18 |
19 | ```
20 |
21 | ```jsx
22 |
23 | ```
24 |
25 | ```jsx
26 |
27 | ```
28 |
29 | Examples of **correct** code for this rule:
30 |
31 | ```jsx
32 | Small
33 | ```
34 |
35 | ```jsx
36 | Small
37 | ```
38 |
39 | ```jsx
40 | This is an example of the Text component's usage.
41 | ```
42 |
43 | ```jsx
44 |
45 |
46 | Item 1
47 |
48 |
49 | ```
50 |
--------------------------------------------------------------------------------
/docs/rules/image-needs-alt.md:
--------------------------------------------------------------------------------
1 | # Accessibility: Image must have alt attribute with a meaningful description of the image. If the image is decorative, use alt="" (`@microsoft/fluentui-jsx-a11y/image-needs-alt`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 | ## Rule details
8 |
9 | This rule requires all `` components have non-empty alternative text. The `alt` attribute should provide a clear and concise text replacement for the image's content. It should *not* describe the presence of the image itself or the file name of the image. Purely decorative images should have empty `alt` text (`alt=""`).
10 |
11 |
12 | Examples of **incorrect** code for this rule:
13 |
14 | ```jsx
15 |
16 | ```
17 |
18 | ```jsx
19 |
20 | ```
21 |
22 | Examples of **correct** code for this rule:
23 |
24 | ```jsx
25 |
26 | ```
27 |
28 | ```jsx
29 |
30 | ```
31 |
32 | ## Further Reading
33 |
34 | - [` ` Accessibility](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img#accessibility)
35 |
--------------------------------------------------------------------------------
/docs/rules/breadcrumb-needs-labelling.md:
--------------------------------------------------------------------------------
1 | # All interactive elements must have an accessible name (`@microsoft/fluentui-jsx-a11y/breadcrumb-needs-labelling`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 | Provide labels to identify all form controls, including text fields, checkboxes, radio buttons, and drop-down menus. In most cases, this is done by using the label element.
8 |
9 |
10 |
11 | All interactive elements must have an accessible name.
12 |
13 | ## Rule Details
14 |
15 | This rule aims to...
16 |
17 | Examples of **incorrect** code for this rule:
18 |
19 | ```jsx
20 |
21 | Breadcrumb default example
22 |
23 |
24 | ```
25 |
26 | ```jsx
27 |
28 | Breadcrumb default example
29 | ```
30 |
31 | Examples of **correct** code for this rule:
32 |
33 | ```jsx
34 |
35 | ```
36 |
37 | ```jsx
38 |
39 | Breadcrumb default example
40 |
41 |
42 | ```
43 |
--------------------------------------------------------------------------------
/docs/rules/rating-needs-name.md:
--------------------------------------------------------------------------------
1 | # Accessibility: Ratings must have accessible labelling: name, aria-label, aria-labelledby or itemLabel which generates aria-label (`@microsoft/fluentui-jsx-a11y/rating-needs-name`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 | All interactive elements must have an accessible name.
8 |
9 | ## Rule Details
10 |
11 | This rule aims to enforce that a Rating element must have an accessible label associated with it.
12 |
13 | Examples of **incorrect** code for this rule:
14 |
15 | ```js
16 |
17 | ```
18 |
19 | Examples of **correct** code for this rule:
20 |
21 | ```js
22 | `Rating of ${number} starts`} />
23 | ```
24 |
25 | FluentUI supports receiving a function that will add the aria-label to the element with the number. This prop is called itemLabel.
26 | If this is not the desired route, a name, aria-label or aria-labelledby can be added instead.
27 |
28 | ## When Not To Use It
29 |
30 | You might want to turn this rule off if you don't intend for this component to be read by screen readers.
31 |
32 | ## Further Reading
33 |
34 | - [ARIA in HTML](https://www.w3.org/TR/html-aria/)
35 |
--------------------------------------------------------------------------------
/docs/rules/menu-item-needs-labelling.md:
--------------------------------------------------------------------------------
1 | # Accessibility: MenuItem without label must have an accessible and visual label: aria-labelledby (`@microsoft/fluentui-jsx-a11y/menu-item-needs-labelling`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 | Accessibility: MenuItem must have a visual label and it needs to be linked via aria-labelledby
8 |
9 |
10 |
11 | ## Ways to fix
12 |
13 | - Add a label with an id, add the aria-labelledby having same value as id to MenuItem.
14 |
15 | ## Rule Details
16 |
17 | This rule aims to make MenuItem accessible
18 |
19 | Examples of **incorrect** code for this rule:
20 |
21 | ```jsx
22 |
23 | ```
24 |
25 | ```jsx
26 | } onClick={handleClick}>
27 | ```
28 |
29 | ```jsx
30 |
31 | ```
32 |
33 | Examples of **correct** code for this rule:
34 |
35 | ```jsx
36 | <>
37 | Settings
38 | } onClick={handleClick}>
39 | Settings
40 | >
41 | ```
42 |
--------------------------------------------------------------------------------
/docs/rules/counter-badge-needs-count.md:
--------------------------------------------------------------------------------
1 | # @microsoft/fluentui-jsx-a11y/counter-badge-needs-count
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 | 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6 |
7 |
8 |
9 | A counter badge is a badge that displays a numerical count.
10 |
11 | ## Rule Details
12 |
13 | Ensure that the `CounterBadge` component is accessible.
14 |
15 | Examples of **incorrect** code for this rule:
16 |
17 | ```jsx
18 |
19 | ```
20 |
21 | ```jsx
22 | } />
23 | ```
24 |
25 |
26 | Examples of **correct** code for this rule:
27 |
28 | If the badge contains a custom icon, that icon must be given alternative text with aria-label, unless it is purely presentational:
29 |
30 | ```jsx
31 | } count={5} />
32 | ```
33 |
34 | ```jsx
35 |
36 | ```
37 |
38 | ```jsx
39 | 5
40 | ```
41 |
42 | ## Further Reading
43 |
44 |
45 |
--------------------------------------------------------------------------------
/lib/util/hasLoadingState.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { TSESTree } from "@typescript-eslint/utils";
5 | import { hasNonEmptyProp } from "./hasNonEmptyProp";
6 |
7 | /**
8 | * Common prop names that indicate a loading state in FluentUI components
9 | */
10 | const LOADING_STATE_PROPS = ["loading", "isLoading", "pending", "isPending", "busy", "isBusy"] as const;
11 |
12 | /**
13 | * Determines if the component has any loading state indicator prop
14 | * @param attributes - JSX attributes array
15 | * @returns boolean indicating if component has loading state
16 | */
17 | export const hasLoadingState = (attributes: TSESTree.JSXOpeningElement["attributes"]): boolean => {
18 | return LOADING_STATE_PROPS.some(prop => hasNonEmptyProp(attributes, prop));
19 | };
20 |
21 | /**
22 | * Gets the specific loading prop that is present (for better error messages)
23 | * @param attributes - JSX attributes array
24 | * @returns string name of the loading prop found, or null if none
25 | */
26 | export const getLoadingStateProp = (attributes: TSESTree.JSXOpeningElement["attributes"]): string | null => {
27 | return LOADING_STATE_PROPS.find(prop => hasNonEmptyProp(attributes, prop)) ?? null;
28 | };
29 |
--------------------------------------------------------------------------------
/docs/rules/tooltip-not-recommended.md:
--------------------------------------------------------------------------------
1 | # Accessibility: Prefer text content or aria over a tooltip for these components MenuItem, SpinButton (`@microsoft/fluentui-jsx-a11y/tooltip-not-recommended`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 | All interactive elements must have an accessible name.
8 |
9 | Tooltip not recommended for these components: MenuItem, SpinButton, etc.
10 |
11 | Prefer text content or aria over a tooltip for these components.
12 |
13 |
14 |
15 | ## Rule Details
16 |
17 | This rule aims to prevent the usage of Tooltip.
18 |
19 | Examples of **incorrect** code for this rule:
20 |
21 | ```jsx
22 |
23 |
24 |
25 | ```
26 |
27 | ```jsx
28 |
29 |
30 |
31 | ```
32 |
33 | Examples of **correct** code for this rule:
34 |
35 | ```jsx
36 |
37 | More option
38 |
39 |
40 | ```
41 |
42 | ```jsx
43 |
44 | More option
45 |
46 |
47 | ```
48 |
--------------------------------------------------------------------------------
/docs/rules/toolbar-missing-aria.md:
--------------------------------------------------------------------------------
1 | # Accessibility: Toolbars need accessible labelling: aria-label or aria-labelledby (`@microsoft/fluentui-jsx-a11y/toolbar-missing-aria`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 |
8 |
9 | The toolbar role needs an accessible name, especially if there are multiple toolbars on a screen.
10 |
11 | ## Rule Details
12 |
13 | This rule aims to prevent a confusing user experience for tool bars.
14 |
15 | Examples of **incorrect** code for this rule:
16 |
17 | ```jsx
18 |
19 | } name="textOptions" value="bold" />
20 |
21 | ```
22 |
23 | Examples of **correct** code for this rule:
24 |
25 | ```jsx
26 |
27 | } name="textOptions" value="bold" />
28 |
29 | ```
30 |
31 | ```jsx
32 | Subtle
33 |
34 | } name="textOptions" value="bold" />
35 |
36 | ```
37 |
--------------------------------------------------------------------------------
/tests/lib/rules/spin-button-unrecommended-labelling.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | //------------------------------------------------------------------------------
5 | // Requirements
6 | //------------------------------------------------------------------------------
7 |
8 | import { Rule } from "eslint";
9 | import ruleTester from "./helper/ruleTester";
10 | import rule from "../../../lib/rules/spin-button-unrecommended-labelling";
11 |
12 | //------------------------------------------------------------------------------
13 | // Tests
14 | //------------------------------------------------------------------------------
15 |
16 | ruleTester.run("spin-button-unrecommended-labelling", rule as unknown as Rule.RuleModule, {
17 | valid: [],
18 | invalid: [
19 | {
20 | code: ``,
21 | errors: [{ messageId: "unRecommendedlabellingSpinButton" }]
22 | },
23 | {
24 | code: `<>This is a spin button >`,
25 | errors: [{ messageId: "unRecommendedlabellingSpinButton" }]
26 | }
27 | ]
28 | });
29 |
--------------------------------------------------------------------------------
/docs/rules/combobox-needs-labelling.md:
--------------------------------------------------------------------------------
1 | # All interactive elements must have an accessible name (`@microsoft/fluentui-jsx-a11y/combobox-needs-labelling`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 | Provide labels to identify all form controls, including text fields, checkboxes, radio buttons, and drop-down menus. In most cases, this is done by using the label element.
8 |
9 |
10 |
11 | All interactive elements must have an accessible name.
12 |
13 | ## Rule Details
14 |
15 | This rule aims to...
16 |
17 | Examples of **incorrect** code for this rule:
18 |
19 | ```jsx
20 | Best pet
21 | {"Cat"}
22 | ```
23 |
24 | Examples of **correct** code for this rule:
25 |
26 | ```jsx
27 | Best pet
28 |
29 | {"Cat"}
30 |
31 | ```
32 |
33 | ```jsx
34 |
35 |
36 | Option 1
37 | Option 2
38 | Option 3
39 |
40 |
41 | ```
42 |
--------------------------------------------------------------------------------
/docs/rules/card-needs-accessible-name.md:
--------------------------------------------------------------------------------
1 | # Accessibility: Interactive Card must have an accessible name via aria-label, aria-labelledby, etc (`@microsoft/fluentui-jsx-a11y/card-needs-accessible-name`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 | Interactive Card components must have an accessible name for screen readers.
8 |
9 | ## Rule Details
10 |
11 | This rule enforces that Card components have proper accessible names when they are interactive (clickable).
12 |
13 | ### Noncompliant
14 |
15 | ```jsx
16 |
17 |
18 | Card title
19 |
20 |
21 | ```
22 |
23 | ### Compliant
24 |
25 | ```jsx
26 |
27 |
28 | Card title
29 |
30 |
31 |
32 |
33 |
34 | Card title
35 |
36 |
37 | ```
38 |
39 | ## When Not To Use
40 |
41 | If the Card is purely decorative and not interactive, this rule is not necessary.
42 |
43 | ## Accessibility guidelines
44 |
45 | - [WCAG 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value.html)
46 |
--------------------------------------------------------------------------------
/docs/rules/switch-needs-labelling.md:
--------------------------------------------------------------------------------
1 | # Accessibility: Switch must have an accessible label (`@microsoft/fluentui-jsx-a11y/switch-needs-labelling`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 | All interactive elements must have an accessible name.
8 |
9 | Switch components need a visual label.
10 |
11 | Please add label, or aria-labelledby.
12 |
13 |
14 |
15 | ## Rule Details
16 |
17 | This rule aims to...
18 |
19 | Examples of **incorrect** code for this rule:
20 |
21 | ```jsx
22 |
23 | ```
24 |
25 | ```jsx
26 |
27 | This is a switch.
28 |
32 |
33 | ```
34 |
35 | Examples of **correct** code for this rule:
36 |
37 | ```jsx
38 |
39 | ```
40 |
41 | ```jsx
42 |
43 | This is a switch.
44 |
49 |
50 | ```
51 |
52 | ````jsx
53 |
54 |
55 |
56 | ```
57 | ````
58 |
--------------------------------------------------------------------------------
/docs/rules/radio-button-missing-label.md:
--------------------------------------------------------------------------------
1 | # Accessibility: Radio button without label must have an accessible and visual label: aria-labelledby (`@microsoft/fluentui-jsx-a11y/radio-button-missing-label`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 | All interactive elements must have an accessible name.
8 |
9 | Radio button without a label or accessible labeling lack an accessible name.
10 |
11 |
12 |
13 | ## Ways to fix
14 |
15 | - Add a label, aria-label or aria-labelledby attribute to the Radio tag.
16 |
17 | ## Rule Details
18 |
19 | This rule aims to make Radioes accessible.
20 |
21 | Examples of **incorrect** code for this rule:
22 |
23 | ```jsx
24 |
25 |
26 | ```
27 |
28 | ```jsx
29 | This is a switch.
30 |
34 | ```
35 |
36 | Examples of **correct** code for this rule:
37 |
38 | ```jsx
39 |
40 | ```
41 |
42 | ```jsx
43 | This is a Radio.
44 |
49 | ```
50 |
--------------------------------------------------------------------------------
/tests/lib/rules/utils/hasLabeledChild.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { AST_NODE_TYPES, TSESTree } from "@typescript-eslint/types";
5 | import { TSESLint } from "@typescript-eslint/utils";
6 | import { hasLabeledChild } from "../../../../lib/util/hasLabeledChild";
7 |
8 | // helper for loc/range
9 | const mockLocRange = () => ({
10 | loc: {
11 | start: { line: 0, column: 0 },
12 | end: { line: 0, column: 0 }
13 | },
14 | range: [0, 0] as [number, number]
15 | });
16 |
17 | // minimal JSXOpeningElement:
18 | const openingEl: TSESTree.JSXOpeningElement = {
19 | type: AST_NODE_TYPES.JSXOpeningElement,
20 | name: {
21 | type: AST_NODE_TYPES.JSXIdentifier,
22 | name: "img",
23 | ...mockLocRange()
24 | },
25 | attributes: [], // no props
26 | selfClosing: true, //
27 | ...mockLocRange()
28 | };
29 |
30 | // minimal RuleContext mock
31 | const mockContext = {
32 | report: jest.fn(),
33 | getSourceCode: jest.fn()
34 | } as unknown as TSESLint.RuleContext;
35 |
36 | describe("hasLabeledChild", () => {
37 | it("returns false for a self-closing with no labeled children", () => {
38 | expect(hasLabeledChild(openingEl, mockContext)).toBe(false);
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/docs/rules/field-needs-labelling.md:
--------------------------------------------------------------------------------
1 | # Accessibility: Field must have label (`@microsoft/fluentui-jsx-a11y/field-needs-labelling`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 | Field must have `label` prop.
8 |
9 |
10 |
11 | ## Ways to fix
12 |
13 | - Make sure that Field component has following props:
14 | - `label`
15 |
16 | ## Rule Details
17 |
18 | This rule aims to make Field component accessible.
19 |
20 | Examples of **incorrect** code for this rule:
21 |
22 | ```jsx
23 |
24 |
25 |
26 | ```
27 |
28 | ```jsx
29 |
30 |
31 |
32 | ```
33 |
34 | Examples of **correct** code for this rule:
35 |
36 | ```jsx
37 |
38 |
39 |
40 | ```
41 |
42 | ```jsx
43 |
44 |
45 |
46 | ```
47 |
48 | ```jsx
49 |
50 |
51 |
52 | ```
53 |
--------------------------------------------------------------------------------
/docs/rules/tag-needs-name.md:
--------------------------------------------------------------------------------
1 | # Accessibility: Tag must have an accessible name (`@microsoft/fluentui-jsx-a11y/tag-needs-name`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 | All interactive elements must have an accessible name.
8 |
9 | Tag components need an accessible name for screen reader users.
10 |
11 | Please provide text content, aria-label, or aria-labelledby.
12 |
13 |
14 |
15 | ## Rule Details
16 |
17 | This rule aims to ensure that Tag components have an accessible name via text content, aria-label, or aria-labelledby.
18 |
19 | Examples of **incorrect** code for this rule:
20 |
21 | ```jsx
22 |
23 | ```
24 |
25 | ```jsx
26 |
27 | ```
28 |
29 | ```jsx
30 |
31 | ```
32 |
33 | ```jsx
34 | }>
35 | ```
36 |
37 | ```jsx
38 | } />
39 | ```
40 |
41 | Examples of **correct** code for this rule:
42 |
43 | ```jsx
44 | Tag with some text
45 | ```
46 |
47 | ```jsx
48 |
49 | ```
50 |
51 | ```jsx
52 | Some text
53 | ```
54 |
55 | ```jsx
56 | }>Tag with icon and text
57 | ```
58 |
59 | ```jsx
60 | } aria-label="Settings tag">
61 | ```
62 |
--------------------------------------------------------------------------------
/tests/lib/rules/tooltip-not-recommended.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | //------------------------------------------------------------------------------
5 | // Requirements
6 | //------------------------------------------------------------------------------
7 |
8 | import { Rule } from "eslint";
9 | import ruleTester from "./helper/ruleTester";
10 | import rule from "../../../lib/rules/tooltip-not-recommended";
11 |
12 | //------------------------------------------------------------------------------
13 | // Tests
14 | //------------------------------------------------------------------------------
15 |
16 | ruleTester.run("tooltip-not-recommended", rule as unknown as Rule.RuleModule, {
17 | valid: [
18 | // Valid cases
19 | 'More option
',
20 | 'More option
'
21 | ],
22 | invalid: [
23 | // Invalid cases
24 | {
25 | code: ' ',
26 | errors: [{ messageId: "tooltipNotRecommended" }]
27 | },
28 | {
29 | code: ' ',
30 | errors: [{ messageId: "tooltipNotRecommended" }]
31 | }
32 | ]
33 | });
34 |
--------------------------------------------------------------------------------
/docs/rules/dropdown-needs-labelling.md:
--------------------------------------------------------------------------------
1 | # Accessibility: Dropdown menu must have an id and it needs to be linked via htmlFor of a Label (`@microsoft/fluentui-jsx-a11y/dropdown-needs-labelling`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 | Accessibility: Dropdown menu must have a visual label and it needs to be linked via htmlFor aria-labelledby of Label Or Dropdown mush have aria-label
8 | Dropdown having label linked via htmlFor in Label is recommended
9 |
10 |
11 |
12 | ## Ways to fix
13 |
14 | - Add a label with htmlFor, add the id having same value as htmlFor to dropdown.
15 | - Add a label with id, add the aria-labelledby having same value as id to dropdown.
16 | - Add a aria-label to dropdown
17 |
18 | ## Rule Details
19 |
20 | This rule aims to make dropdown accessible
21 |
22 | Examples of **incorrect** code for this rule:
23 |
24 | ```jsx
25 |
26 |
27 | <>
28 |
29 |
30 | >
31 | ```
32 |
33 | Examples of **correct** code for this rule:
34 |
35 | ```jsx
36 | <>
37 |
38 |
39 | >
40 | <>
41 |
42 |
43 | >
44 |
45 | >
46 | ```
47 |
--------------------------------------------------------------------------------
/tests/lib/rules/image-needs-alt.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { Rule } from "eslint";
5 | import ruleTester from "./helper/ruleTester";
6 | import rule from "../../../lib/rules/image-needs-alt";
7 |
8 | // -----------------------------------------------------------------------------
9 | // Tests
10 | // -----------------------------------------------------------------------------
11 |
12 | ruleTester.run("image-needs-alt", rule as unknown as Rule.RuleModule, {
13 | valid: [
14 | // Not an Image
15 | "
",
16 | // Valid string test
17 | ' ',
18 | // Valid expression test
19 | ' ',
20 | // Decorative image with empty alt
21 | ' '
22 | ],
23 | invalid: [
24 | {
25 | // No alt attribute
26 | code: ' ',
27 | errors: [{ messageId: "imageNeedsAlt" }]
28 | },
29 | {
30 | // Null alt attribute
31 | code: ' ',
32 | errors: [{ messageId: "imageNeedsAlt" }]
33 | },
34 | {
35 | // Undefined alt attribute
36 | code: ' ',
37 | errors: [{ messageId: "imageNeedsAlt" }]
38 | }
39 | ]
40 | });
41 |
--------------------------------------------------------------------------------
/docs/rules/image-button-missing-aria.md:
--------------------------------------------------------------------------------
1 | # Accessibility: Image buttons must have accessible labelling: title, aria-label, aria-labelledby, aria-describedby (`@microsoft/fluentui-jsx-a11y/image-button-missing-aria`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 | All interactive elements must have an accessible name.
8 |
9 | Image buttons without additional text content lack an accessible name.
10 |
11 | Please add title, aria-label, aria-labelledby, aria-described by etc.
12 |
13 |
14 |
15 | ## Rule Details
16 |
17 | This rule aims to prevent an icon button from not having an accessible name.
18 |
19 | Examples of **incorrect** code for this rule:
20 |
21 | ```jsx
22 | } />
23 | }>
24 |
25 | Start date
26 | } />
27 | ```
28 |
29 | Examples of **correct** code for this rule:
30 |
31 | ```jsx
32 | } title="Current month" />
33 | } aria-label="Start date" />
34 | }>Start date
35 |
36 | Start date
37 | } aria-labelledby="calendar-1" />
38 |
39 |
40 | } />
41 |
42 | ```
43 |
--------------------------------------------------------------------------------
/docs/rules/checkbox-needs-labelling.md:
--------------------------------------------------------------------------------
1 | # Accessibility: Checkbox without label must have an accessible and visual label: aria-labelledby (`@microsoft/fluentui-jsx-a11y/checkbox-needs-labelling`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 | All interactive elements must have an accessible name.
8 |
9 | Checkboxes without a label or accessible labelling lack an accessible name.
10 |
11 |
12 |
13 | ## Ways to fix
14 |
15 | - Add a label, aria-label or aria-labelledby attribute or text content to the Checkbox tag.
16 |
17 | ## Rule Details
18 |
19 | This rule aims to make Checkboxes accessible.
20 |
21 | Examples of **incorrect** code for this rule:
22 |
23 | ```jsx
24 |
25 |
26 | ```
27 |
28 | ```jsx
29 | This is a switch.
30 |
34 | ```
35 |
36 | Examples of **correct** code for this rule:
37 |
38 | ```jsx
39 |
40 | ```
41 |
42 | ```jsx
43 | This is a checkbox.
44 |
49 | ```
50 |
51 | ```jsx
52 |
53 |
54 |
55 | ```
56 |
--------------------------------------------------------------------------------
/scripts/create-rule.md:
--------------------------------------------------------------------------------
1 | # Rule Generator
2 |
3 | ```bash
4 | $ node scripts/create-rule.js rule-name --author="Your name" --description="Description of the rule"
5 | # OR with npm script alias
6 | $ npm run create -- rule-name -- --author="Auz" --description="A cool rule"
7 | ```
8 |
9 | This script will generate three files with basic boilerplate for the given rule:
10 |
11 | 1. lib/rules/${rule-name}.js
12 | 2. tests/lib/rules/${rule-name}-test.js
13 | 3. docs/rules/${rule-name}.md
14 |
15 | It will also update the following `index.ts` files:
16 |
17 | 1. lib/index.ts
18 | 2. lib/rules/index.ts
19 |
20 | If the rule already exists or is not specified in the correct format, an error will be thrown.
21 |
22 | If we wanted to scaffold a rule for `no-marquee`, we could run:
23 |
24 | ```bash
25 | $ node scripts/create-rule.js no-marquee --author="Ethan Cohen <@evcohen>" --description="Enforce elements are not used."
26 | # OR
27 | $ npm run create -- no-marquee --author="Ethan Cohen <@evcohen>" --description="Enforce elements are not used."
28 | ```
29 |
30 | ## Debug this script
31 |
32 | addRuleToIndex.js
33 |
34 | ```bash
35 | jscodeshift ./lib/index.ts -t ./scripts/addRuleToIndex.js --extensions ts --parser flow --ruleName=rule-name --rulePath=./lib/rules/rule-name.ts
36 | ```
37 |
38 | addRuleToExportIndex.js
39 |
40 | ```bash
41 | jscodeshift ./lib/rules/index.ts -t ./scripts/addRuleToExportIndex.js --extensions ts --parser flow --ruleName=rule-name --exportIndexFilePath=./lib/rules/index.ts
42 | ```
43 |
--------------------------------------------------------------------------------
/docs/rules/spin-button-needs-labelling.md:
--------------------------------------------------------------------------------
1 | # Accessibility: SpinButtons must have an accessible label (`@microsoft/fluentui-jsx-a11y/spin-button-needs-labelling`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 | All interactive elements must have an accessible name.
8 |
9 | Spin Button components need a visual label.
10 |
11 | Please add label, aria-labelledby or htmlFor.
12 |
13 |
14 |
15 | ## Rule Details
16 |
17 | This rule aims to...
18 |
19 | Examples of **incorrect** code for this rule:
20 |
21 | ```jsx
22 |
23 | ```
24 |
25 | ```jsx
26 |
27 | Default SpinButton
28 |
33 | ```
34 |
35 | Examples of **correct** code for this rule:
36 |
37 | ```jsx
38 |
39 | Default SpinButton
40 |
41 |
42 |
43 |
44 | ```
45 |
46 | ```jsx
47 | Default SpinButton
48 |
54 | ```
55 |
56 | ```jsx
57 | Default SpinButton
58 |
64 | ```
65 |
66 | ```jsx
67 |
68 |
69 |
70 | ```
71 |
--------------------------------------------------------------------------------
/tests/lib/rules/field-needs-labelling.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | "use strict";
5 |
6 | //------------------------------------------------------------------------------
7 | // Requirements
8 | //------------------------------------------------------------------------------
9 | import { Rule } from "eslint";
10 | import ruleTester from "./helper/ruleTester";
11 | import rule from "../../../lib/rules/field-needs-labelling";
12 |
13 | //------------------------------------------------------------------------------
14 | // Tests
15 | //------------------------------------------------------------------------------
16 |
17 | ruleTester.run("field-needs-labelling", rule as unknown as Rule.RuleModule, {
18 | valid: [
19 | `
20 |
21 | `,
22 | `
27 |
28 | `
29 | ],
30 | invalid: [
31 | {
32 | code: ` `,
33 | errors: [{ messageId: "noUnlabelledField" }]
34 | },
35 | {
36 | code: ` `,
37 | errors: [{ messageId: "noUnlabelledField" }]
38 | }
39 | ]
40 | });
41 |
--------------------------------------------------------------------------------
/docs/rules/spin-button-unrecommended-labelling.md:
--------------------------------------------------------------------------------
1 | # Accessibility: Unrecommended accessibility labelling - SpinButton (`@microsoft/fluentui-jsx-a11y/spin-button-unrecommended-labelling`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 | All interactive elements must have an accessible name.
8 |
9 | Spin Button components need a visual label.
10 |
11 | Using aria-label or wrapping the SpinButton in a Tooltip component is not recommended.
12 |
13 |
14 |
15 | ## Rule Details
16 |
17 | This rule aims to...
18 |
19 | Examples of **unrecommended** code for this rule:
20 |
21 | ```jsx
22 |
23 | ```
24 |
25 | ```jsx
26 |
27 |
28 |
29 | ```
30 |
31 | Examples of **correct** code for this rule:
32 |
33 | ```jsx
34 |
35 | Default SpinButton
36 |
37 |
38 |
39 |
40 | ```
41 |
42 | ```jsx
43 | Default SpinButton
44 |
50 | ```
51 |
52 | ```jsx
53 | Default SpinButton
54 |
60 | ```
61 |
--------------------------------------------------------------------------------
/lib/util/hasTextContentChild.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { TSESTree } from "@typescript-eslint/types";
5 |
6 | /**
7 | * hasTextContentChild - determines if a component has text content as a child, e.g., Hello , {'Hello'} , {myFunc()} , or {myVar}
8 | * @param {*} node JSXElement
9 | * @returns boolean
10 | */
11 | const hasTextContentChild = (node?: TSESTree.JSXElement) => {
12 | // no children
13 | if (!node) {
14 | return false;
15 | }
16 |
17 | if (!node.children || node.children.length === 0) {
18 | return false;
19 | }
20 |
21 | const result = node.children.filter(element => {
22 | // Check for JSXText with non-whitespace content
23 | if (element.type === "JSXText" && element.value.trim().length > 0) {
24 | return true;
25 | }
26 |
27 | // Check for JSXExpressionContainer with valid expression content
28 | if (
29 | element.type === "JSXExpressionContainer" &&
30 | element.expression &&
31 | ((element.expression.type === "Literal" && String(element.expression.value).trim().length > 0) ||
32 | element.expression.type === "CallExpression" ||
33 | element.expression.type === "Identifier")
34 | ) {
35 | return true;
36 | }
37 |
38 | return false;
39 | });
40 |
41 | return result.length !== 0;
42 | };
43 |
44 | export { hasTextContentChild };
45 |
--------------------------------------------------------------------------------
/docs/rules/radiogroup-missing-label.md:
--------------------------------------------------------------------------------
1 | # Accessibility: RadioGroup without label must have an accessible and visual label: aria-labelledby (`@microsoft/fluentui-jsx-a11y/radiogroup-missing-label`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 | All interactive elements must have an accessible name.
8 |
9 | RadioGroup without a label or accessible labeling lack an accessible name.
10 |
11 |
12 |
13 | ## Ways to fix
14 |
15 | - Add a label, aria-label or aria-labelledby attribute to the RadioGroup tag.
16 |
17 | ## Rule Details
18 |
19 | This rule aims to make Radioes accessible.
20 |
21 | Examples of **incorrect** code for this rule:
22 |
23 | ```jsx
24 |
25 |
26 | ```
27 |
28 | ```jsx
29 | This is a switch.
30 |
33 | ```
34 |
35 | Examples of **correct** code for this rule:
36 |
37 | ```jsx
38 |
39 | ```
40 |
41 | ```jsx
42 | This is a Radio.
43 |
48 | ```
49 |
50 | ```jsx
51 |
52 | ```
53 |
54 | ```jsx
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | ```
63 |
--------------------------------------------------------------------------------
/tests/lib/rules/rating-needs-name.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | "use strict";
5 |
6 | //------------------------------------------------------------------------------
7 | // Requirements
8 | //------------------------------------------------------------------------------
9 |
10 | import { Rule } from "eslint";
11 | import ruleTester from "./helper/ruleTester";
12 | import rule from "../../../lib/rules/rating-needs-name";
13 |
14 | //------------------------------------------------------------------------------
15 | // Tests
16 | //------------------------------------------------------------------------------
17 |
18 | ruleTester.run("rating-needs-name", rule as unknown as Rule.RuleModule, {
19 | valid: [
20 | // give me some code that won't trigger a warning
21 | " ",
22 | ' ',
23 | ' ',
24 | '<>Rating >',
25 | " ",
26 | ' ',
27 | ' ',
28 | '<>Rating >'
29 | ],
30 |
31 | invalid: [
32 | {
33 | code: " ",
34 | errors: [{ messageId: "missingAriaLabel" }]
35 | },
36 | {
37 | code: " ",
38 | errors: [{ messageId: "missingAriaLabel" }]
39 | }
40 | ]
41 | });
42 |
--------------------------------------------------------------------------------
/docs/rules/tag-dismissible-needs-labelling.md:
--------------------------------------------------------------------------------
1 | # This rule aims to ensure that dismissible Tag components have proper accessibility labelling: either aria-label on dismissIcon or aria-label on Tag with role on dismissIcon (`@microsoft/fluentui-jsx-a11y/tag-dismissible-needs-labelling`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 | All interactive elements must have an accessible name.
8 |
9 | Dismissible Tag components render a dismiss/close button that must have an accessible name for screen reader users.
10 |
11 | When a Tag has the `dismissible` prop, it must provide a `dismissIcon` with an `aria-label`.
12 |
13 |
14 |
15 | ## Rule Details
16 |
17 | This rule aims to ensure that dismissible Tag components have an aria-label on the dismiss button.
18 |
19 | Examples of **incorrect** code for this rule:
20 |
21 | ```jsx
22 | Dismissible tag
23 | ```
24 |
25 | ```jsx
26 | Dismissible tag
27 | ```
28 |
29 | ```jsx
30 | Dismissible tag
31 | ```
32 |
33 | Examples of **correct** code for this rule:
34 |
35 | ```jsx
36 | Regular tag
37 | ```
38 |
39 | ```jsx
40 | }>Tag with icon
41 | ```
42 |
43 | ```jsx
44 | Dismissible tag
45 | ```
46 |
47 | ```jsx
48 | }>Tag with icon
49 | ```
50 |
--------------------------------------------------------------------------------
/tests/lib/rules/card-needs-accessible-name.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | //------------------------------------------------------------------------------
5 | // Requirements
6 | //------------------------------------------------------------------------------
7 |
8 | import { Rule } from "eslint";
9 | import ruleTester from "./helper/ruleTester";
10 | import rule from "../../../lib/rules/card-needs-accessible-name";
11 |
12 | //------------------------------------------------------------------------------
13 | // Tests
14 | //------------------------------------------------------------------------------
15 |
16 | ruleTester.run("card-needs-accessible-name", rule as unknown as Rule.RuleModule, {
17 | valid: [
18 | ` `,
19 | `<>Product >`,
20 | ` `,
21 | ` `
22 | ],
23 | invalid: [
24 | {
25 | code: ` `,
26 | errors: [{ messageId: "cardNeedsAccessibleName" }]
27 | },
28 | {
29 | code: ` `,
30 | errors: [{ messageId: "cardNeedsAccessibleName" }]
31 | },
32 | {
33 | code: ` `,
34 | errors: [{ messageId: "cardNeedsAccessibleName" }]
35 | },
36 | {
37 | code: `<>Product >`,
38 | errors: [{ messageId: "cardNeedsAccessibleName" }]
39 | }
40 | ]
41 | });
42 |
--------------------------------------------------------------------------------
/docs/rules/input-components-require-accessible-name.md:
--------------------------------------------------------------------------------
1 | # Accessibility: Input fields must have accessible labelling: aria-label, aria-labelledby or an associated label (`@microsoft/fluentui-jsx-a11y/input-components-require-accessible-name`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 | All interactive elements must have an accessible name.
8 |
9 | ## Rule Details
10 |
11 | This rule aims to prevent that a Input element must have an accessible label associated with it or an accessible label.
12 |
13 | Rule explanation here [https://www.w3.org/WAI/tutorials/forms/labels/](https://www.w3.org/WAI/tutorials/forms/labels/)
14 |
15 | Examples of **incorrect** code for this rule:
16 |
17 | ```jsx
18 | Label name
19 |
20 | ```
21 |
22 | or
23 |
24 | ```jsx
25 | Label name
26 |
27 | ```
28 |
29 | or
30 |
31 | ```jsx
32 | Label name
33 |
34 | ```
35 |
36 | or
37 |
38 | ```jsx
39 |
40 | ```
41 |
42 | or
43 |
44 | ```jsx
45 |
46 |
47 | ```
48 |
49 | Examples of **correct** code for this rule:
50 |
51 | ```jsx
52 |
53 | ```
54 |
55 | or
56 |
57 | ```jsx
58 | Label name
59 |
60 | ```
61 |
62 | or
63 |
64 | ```jsx
65 | Medium Slider
66 |
67 | ```
68 |
69 | or
70 |
71 | ```jsx
72 |
73 |
74 |
75 | ```
76 |
--------------------------------------------------------------------------------
/scripts/boilerplate/rule.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | const { withCRLF } = require("./util");
5 |
6 | const ruleBoilerplate = (name, description) =>
7 | withCRLF(`// Copyright (c) Microsoft Corporation.
8 | // Licensed under the MIT License.
9 |
10 | import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
11 |
12 | //------------------------------------------------------------------------------
13 | // Rule Definition
14 | //------------------------------------------------------------------------------
15 |
16 | // RuleCreator requires a URL or documentation link, but it can be a placeholder
17 | const createRule = ESLintUtils.RuleCreator(name => "https://example.com/rule/${name}");
18 |
19 | const rule = createRule({
20 | name: "${name}",
21 | meta: {
22 | type: "suggestion", // could be "problem", "suggestion", or "layout"
23 | docs: {
24 | description: "${description}",
25 | recommended: "error" // could also be "warn"
26 | },
27 | messages: {
28 | errorMessage: "" // describe the issue
29 | },
30 | schema: [] // no options for this rule
31 | },
32 | defaultOptions: [], // no options needed
33 | create(context) {
34 | return {
35 | // Listen for variable declarations
36 | JSXOpeningElement(node: TSESTree.JSXOpeningElement) {
37 | context.report({
38 | node,
39 | messageId: "errorMessage"
40 | });
41 | }
42 | };
43 | }
44 | });
45 |
46 | export default rule;
47 | `);
48 | module.exports = ruleBoilerplate;
49 |
--------------------------------------------------------------------------------
/docs/rules/compound-button-needs-labelling.md:
--------------------------------------------------------------------------------
1 | # Accessibility: Compound buttons must have accessible labelling: title, aria-label, aria-labelledby, aria-describedby (`@microsoft/fluentui-jsx-a11y/compound-button-needs-labelling`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 | All interactive elements must have an accessible name.
8 | Please add title, aria-label, aria-labelledby, aria-describedBy, secondaryContent etc.
9 |
10 |
11 |
12 | ## Rule Details
13 |
14 | This rule aims to prevent an compound button from not having an accessible name.
15 |
16 | Examples of **incorrect** code for this rule:
17 |
18 | ```jsx
19 |
20 |
21 |
22 | }>
23 |
24 | Start date
25 |
26 | ```
27 |
28 | Examples of **correct** code for this rule:
29 | Please note that without an icon, these buttons are actually not accessible for sighted users
30 |
31 | ```jsx
32 |
33 | } aria-label="Compound example" />
34 | Compound example
35 | } secondaryContent="Compound example" />
36 | <>
37 | Compound example
38 | } />
39 | >
40 |
41 |
42 | }/>
43 |
44 | ```
45 |
--------------------------------------------------------------------------------
/lib/util/hasDefinedProp.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { TSESTree } from "@typescript-eslint/utils";
5 | import { JSXOpeningElement } from "estree-jsx";
6 | import { hasProp, getPropValue, getProp } from "jsx-ast-utils";
7 |
8 | /**
9 | * Determines if the property exists and has a non-nullish value.
10 | * @param attributes The attributes on the visited node
11 | * @param name The name of the prop to check
12 | * @returns Whether the specified prop exists and is not null or undefined
13 | * @example
14 | * //
15 | * hasDefinedProp(attributes, 'src') // true
16 | * //
17 | * hasDefinedProp(attributes, 'src') // true
18 | * //
19 | * hasDefinedProp(attributes, 'src') // false
20 | * //
21 | * hasDefinedProp(attributes, 'src') // false
22 | * //
23 | * hasDefinedProp(attributes, 'src') // false
24 | * //
25 | * hasDefinedProp(attributes, 'src') // false
26 | * //
27 | * hasDefinedProp(attributes, 'src') // false
28 | */
29 | const hasDefinedProp = (attributes: TSESTree.JSXOpeningElement["attributes"], name: string): boolean => {
30 | if (!hasProp(attributes as JSXOpeningElement["attributes"], name)) {
31 | return false;
32 | }
33 |
34 | const prop = getProp(attributes as JSXOpeningElement["attributes"], name);
35 |
36 | // Safely get the value of the prop, handling potential undefined or null values
37 | const propValue = prop ? getPropValue(prop) : undefined;
38 |
39 | // Return true if the prop value is not null or undefined
40 | return propValue !== null && propValue !== undefined;
41 | };
42 |
43 | export { hasDefinedProp };
44 |
--------------------------------------------------------------------------------
/tests/lib/rules/breadcrumb-needs-labelling.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | //------------------------------------------------------------------------------
5 | // Requirements
6 | //------------------------------------------------------------------------------
7 |
8 | import { Rule } from "eslint";
9 | import ruleTester from "./helper/ruleTester";
10 | import rule from "../../../lib/rules/breadcrumb-needs-labelling";
11 |
12 | //------------------------------------------------------------------------------
13 | // Tests
14 | //------------------------------------------------------------------------------
15 |
16 | ruleTester.run("breadcrumb-needs-labelling", rule as unknown as Rule.RuleModule, {
17 | valid: [
18 | // give me some code that won't trigger a warning
19 | ' ',
20 | 'Breadcrumb default example
',
21 | 'Breadcrumb default example
'
22 | ],
23 | invalid: [
24 | {
25 | code: 'Breadcrumb default example
',
26 | errors: [{ messageId: "noUnlabelledBreadcrumb" }]
27 | },
28 | {
29 | code: "Breadcrumb default example ",
30 | errors: [{ messageId: "noUnlabelledBreadcrumb" }]
31 | },
32 | {
33 | code: " ",
34 | errors: [{ messageId: "noUnlabelledBreadcrumb" }]
35 | }
36 | ]
37 | });
38 |
--------------------------------------------------------------------------------
/docs/rules/dialogbody-needs-title-content-and-actions.md:
--------------------------------------------------------------------------------
1 | # A DialogBody should have a header(DialogTitle), content(DialogContent), and footer(DialogActions) (`@microsoft/fluentui-jsx-a11y/dialogbody-needs-title-content-and-actions`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 | Accessibility: A DialogBody should have a header(DialogTitle), content(DialogContent), and footer(DialogActions).
8 |
9 |
10 |
11 | ## Ways to fix
12 |
13 | - Add DialogTitle, DialogContent and DialogActions inside DialogBody component.
14 |
15 | ## Rule Details
16 |
17 | This rule aims to make Dialogs accessible
18 |
19 | Examples of **incorrect** code for this rule:
20 |
21 | ```jsx
22 |
23 | Test
24 |
25 | Close
26 | Do Something
27 |
28 |
29 | ```
30 |
31 | ```jsx
32 |
33 | Dialog title
34 |
35 | Close
36 | Do Something
37 |
38 |
39 | ```
40 |
41 | ```jsx
42 |
43 | Dialog title
44 | Test
45 |
46 | ```
47 |
48 | Examples of **correct** code for this rule:
49 |
50 | ```jsx
51 |
52 | Dialog title
53 | Test
54 |
55 | Close
56 | Do Something
57 |
58 |
59 | ```
60 |
--------------------------------------------------------------------------------
/docs/rules/link-missing-labelling.md:
--------------------------------------------------------------------------------
1 | # Accessibility: Image links must have an accessible name. Add either text content, labelling to the image or labelling to the link itself (`@microsoft/fluentui-jsx-a11y/link-missing-labelling`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 | 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6 |
7 |
8 |
9 | All interactive elements must have an accessible name.
10 |
11 | Links that contain Images without additional text content lack an accessible name.
12 |
13 | Ways to fix:
14 |
15 | 1. Add a title, aria-label or aria-labelledby attribute or text content to the Link element.
16 | 2. Add a alt text, title, aria-label or aria-labelledby attribute to the Image element.
17 |
18 |
19 |
20 | ## Rule Details
21 |
22 | This rule aims to make Image links accessible.
23 |
24 | Examples of **incorrect** code for this rule:
25 |
26 | ```jsx
27 |
28 |
29 |
30 | ```
31 |
32 | Examples of **correct** code for this rule:
33 |
34 | ```jsx
35 | This is a label link
36 |
37 |
38 |
39 |
40 | ```
41 |
--------------------------------------------------------------------------------
/tests/lib/rules/buttons/menu-button-needs-labelling.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | //------------------------------------------------------------------------------
5 | // Requirements
6 | //------------------------------------------------------------------------------
7 |
8 | import { Rule } from "eslint";
9 | import ruleTester from "../helper/ruleTester";
10 | import rule from "../../../../lib/rules/buttons/menu-button-needs-labelling";
11 |
12 | //------------------------------------------------------------------------------
13 | // Tests
14 | //------------------------------------------------------------------------------
15 |
16 | ruleTester.run("menu-button-needs-labelling", rule as unknown as Rule.RuleModule, {
17 | valid: [
18 | ` `,
19 | `Options `,
20 | `<> >`,
21 | ` `,
22 | ` `,
23 | ` `
24 | ],
25 | invalid: [
26 | {
27 | code: ` `,
28 | errors: [{ messageId: "menuButtonNeedsLabelling" }]
29 | },
30 | {
31 | code: ` `,
32 | errors: [{ messageId: "menuButtonNeedsLabelling" }]
33 | },
34 | {
35 | code: ` `,
36 | errors: [{ messageId: "menuButtonNeedsLabelling" }]
37 | },
38 | {
39 | code: `<>Options >`,
40 | errors: [{ messageId: "menuButtonNeedsLabelling" }]
41 | }
42 | ]
43 | });
44 |
--------------------------------------------------------------------------------
/docs/rules/swatchpicker-needs-labelling.md:
--------------------------------------------------------------------------------
1 | # Accessibility: SwatchPicker must have an accessible name via aria-label, aria-labelledby, Field component, etc. (`@microsoft/fluentui-jsx-a11y/swatchpicker-needs-labelling`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 | All interactive elements must have an accessible name.
8 |
9 | SwatchPicker without a label or accessible labeling lack an accessible name for assistive technology users.
10 |
11 |
12 |
13 | ## Ways to fix
14 |
15 | - Add an aria-label or aria-labelledby attribute to the SwatchPicker tag. You can also use the Field component.
16 |
17 | ## Rule Details
18 |
19 | This rule aims to make SwatchPickers accessible.
20 |
21 | Examples of **incorrect** code for this rule:
22 |
23 | ```jsx
24 |
25 |
26 | ```
27 |
28 | ```jsx
29 | This is a switch.
30 |
33 | ```
34 |
35 | Examples of **correct** code for this rule:
36 |
37 | ```jsx
38 | This is a Radio.
39 |
44 | ```
45 |
46 | ```jsx
47 |
48 |
49 |
50 |
51 | ```
52 |
53 | ```jsx
54 |
55 |
56 |
57 |
58 |
59 |
60 | ```
61 |
--------------------------------------------------------------------------------
/tests/lib/rules/tag-needs-labelling.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { Rule } from "eslint";
5 | import ruleTester from "./helper/ruleTester";
6 | import rule from "../../../lib/rules/tag-needs-labelling";
7 |
8 | //------------------------------------------------------------------------------
9 | // Requirements
10 | //------------------------------------------------------------------------------
11 |
12 | //------------------------------------------------------------------------------
13 | // Tests
14 | //------------------------------------------------------------------------------
15 |
16 | ruleTester.run("tag-needs-name", rule as unknown as Rule.RuleModule, {
17 | valid: [
18 | // Valid cases for Tag component
19 | "Tag with some text ",
20 | "Some text ",
21 | ' ',
22 | 'Some text ',
23 | " }>Tag with icon and text",
24 | ' } aria-label="Settings tag">'
25 | ],
26 |
27 | invalid: [
28 | // Invalid cases for Tag component
29 | {
30 | code: " ",
31 | errors: [{ messageId: "missingAriaLabel" }]
32 | },
33 | {
34 | code: " ",
35 | errors: [{ messageId: "missingAriaLabel" }]
36 | },
37 | {
38 | code: ' ',
39 | errors: [{ messageId: "missingAriaLabel" }]
40 | },
41 | {
42 | code: " }>",
43 | errors: [{ messageId: "missingAriaLabel" }]
44 | },
45 | {
46 | code: " } />",
47 | errors: [{ messageId: "missingAriaLabel" }]
48 | }
49 | ]
50 | });
51 |
--------------------------------------------------------------------------------
/lib/util/hasNonEmptyProp.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { TSESTree } from "@typescript-eslint/utils";
5 | import { JSXOpeningElement } from "estree-jsx";
6 | import { hasProp, getPropValue, getProp } from "jsx-ast-utils";
7 |
8 | /**
9 | * Determines if the prop exists and has a non-empty value.
10 | * @param {*} attributes
11 | * @param {*} name
12 | * @returns boolean
13 | */
14 | const hasNonEmptyProp = (attributes: TSESTree.JSXOpeningElement["attributes"], name: string): boolean => {
15 | if (!hasProp(attributes as unknown as JSXOpeningElement["attributes"], name)) {
16 | return false;
17 | }
18 |
19 | const prop = getProp(attributes as unknown as JSXOpeningElement["attributes"], name);
20 |
21 | // Safely get the value of the prop, handling potential undefined or null values
22 | const propValue = prop ? getPropValue(prop) : undefined;
23 |
24 | // Check for various types that `getPropValue` could return
25 | if (propValue === null || propValue === undefined) {
26 | return false;
27 | }
28 |
29 | if (typeof propValue === "boolean" || typeof propValue === "number") {
30 | // Booleans and numbers are considered non-empty if they exist
31 | return true;
32 | }
33 |
34 | if (typeof propValue === "string") {
35 | // For strings, check if it is non-empty
36 | return propValue.trim().length > 0;
37 | }
38 |
39 | // Handle other potential types (e.g., arrays, objects)
40 | if (Array.isArray(propValue)) {
41 | return propValue.length > 0;
42 | }
43 |
44 | if (typeof propValue === "object") {
45 | // Objects are considered non-empty if they have properties
46 | return Object.keys(propValue).length > 0;
47 | }
48 |
49 | // If the type is not handled, return false as a fallback
50 | return false;
51 | };
52 |
53 | export { hasNonEmptyProp };
54 |
--------------------------------------------------------------------------------
/docs/rules/spinner-needs-labelling.md:
--------------------------------------------------------------------------------
1 | # Accessibility: Spinner must have either aria-label or label, aria-live and aria-busy attributes (`@microsoft/fluentui-jsx-a11y/spinner-needs-labelling`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 | Spinner must have either aria-label or label, aria-live and aria-busy attributes.
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | ## Ways to fix
16 |
17 | - Make sure that Spinner component has following attributes:
18 | - aria-live
19 | - aria-busy
20 | - either label or aria-label
21 |
22 | ## Rule Details
23 |
24 | This rule aims to make Spinners accessible.
25 |
26 | Examples of **incorrect** code for this rule:
27 |
28 | ```jsx
29 |
30 | ```
31 |
32 | ```jsx
33 |
37 | ```
38 |
39 | ```jsx
40 |
44 | ```
45 |
46 | ```jsx
47 |
52 | ```
53 |
54 | ```jsx
55 |
60 | ```
61 |
62 | Examples of **correct** code for this rule:
63 |
64 | ```jsx
65 |
71 | ```
72 |
73 | ```jsx
74 |
80 | ```
81 |
82 | ```jsx
83 |
89 | ```
90 |
--------------------------------------------------------------------------------
/tests/lib/rules/counter-badge-needs-count.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | //------------------------------------------------------------------------------
5 | // Requirements
6 | //------------------------------------------------------------------------------
7 |
8 | import { Rule } from "eslint";
9 | import ruleTester from "./helper/ruleTester";
10 | import rule from "../../../lib/rules/counter-badge-needs-count";
11 |
12 | //------------------------------------------------------------------------------
13 | // Tests
14 | //------------------------------------------------------------------------------
15 |
16 | ruleTester.run("counter-badge-needs-count", rule as unknown as Rule.RuleModule, {
17 | valid: [
18 | ` `,
19 | ` } count={1} />`,
20 | ` `,
21 | `5 `,
22 | `
`
23 | ],
24 |
25 | invalid: [
26 | {
27 | code: ` `,
28 | errors: [{ messageId: "counterBadgeNeedsCount" }],
29 | output: ` `
30 | },
31 | {
32 | code: ` } />`,
33 | errors: [
34 | {
35 | messageId: "counterBadgeIconNeedsLabelling"
36 | },
37 | { messageId: "counterBadgeNeedsCount" }
38 | ],
39 | output: ` } />`
40 | },
41 | {
42 | code: ` `,
43 | errors: [{ messageId: "counterBadgeNeedsCount" }],
44 | output: ` `
45 | }
46 | ]
47 | });
48 |
--------------------------------------------------------------------------------
/docs/rules/accordion-header-needs-labelling.md:
--------------------------------------------------------------------------------
1 | # The accordion header is a button and it needs an accessibile name e.g. text content, aria-label, aria-labelledby (`@microsoft/fluentui-jsx-a11y/accordion-header-needs-labelling`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 | The ESLint rule is designed to enforce accessibility standards in React components, specifically ensuring an accordion header is accessible to screen reader users.
8 |
9 | ## Rule Details
10 |
11 | Accordions are common UI components that allow users to expand and collapse sections of content.
12 |
13 | For an accordion to be accessible:
14 |
15 | Each accordion header should be implemented as a button. This is because buttons are inherently accessible, providing keyboard and screen reader support. Making the accordion header a button ensures that users can interact with it using keyboard commands or assistive technologies.
16 |
17 | Each accordion panel should be marked as a region that is controlled by the accordion header. This can be achieved using attributes like aria-controls to link the header to the panel. The panel should be appropriately labeled to ensure it can be identified and understood by users of assistive technologies.
18 |
19 | Examples of **incorrect** code for this rule:
20 |
21 | ```jsx
22 | }>
23 | ```
24 |
25 | ```jsx
26 | } expandIcon={ }>
27 | ```
28 |
29 | ```jsx
30 | Heading 1
31 | } expandIcon={ }>
32 | ```
33 |
34 | Examples of **correct** code for this rule:
35 |
36 | ```jsx
37 | Accordion Header 1
38 | ```
39 |
40 | ```jsx
41 | }>Accordion Header 1
42 | ```
43 |
44 | ## Further Reading
45 |
46 | [ARIA Patterns](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/)
47 |
--------------------------------------------------------------------------------
/docs/rules/tablist-and-tabs-need-labelling.md:
--------------------------------------------------------------------------------
1 | # This rule aims to ensure that Tabs with icons but no text labels have an accessible name and that Tablist is properly labeled (`@microsoft/fluentui-jsx-a11y/tablist-and-tabs-need-labelling`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 | Accessibility:
7 | 1. If the Tab is represented by an icon, it must have an 'aria-label' to describe the Tab.
8 | 2. If the Tablist has a visible label, it should use aria-labelledby to reference that label. If there is no visible label, the Tablist should have a label provided by aria-label.
9 |
10 |
11 |
12 | ## Ways to fix
13 | 1. Add an 'aria-label' to the Tab when it is represented by icon
14 | 2. Add an 'aria-labelledby' to the Tablist to reference a visible label
15 | 3. If there is no visible label, add an 'aria-label' to the Tablist
16 |
17 | ## Rule Details
18 |
19 | This rule aims to ensure that Tabs with icons but no text labels have an accessible name and that Tablist is properly labeled.
20 |
21 | Examples of **incorrect** code for this rule:
22 |
23 | ```jsx
24 | } />
25 |
26 | }>
27 |
28 |
29 | Settings Tab
30 |
31 |
32 |
33 | Settings Label
34 | Settings Tab
35 |
36 |
37 |
38 | Settings Label
39 | Settings Tab
40 |
41 |
42 | ```
43 |
44 | Examples of **correct** code for this rule:
45 |
46 | ```jsx
47 |
48 | } aria-label="Settings" />
49 |
50 | }>Settings
51 |
52 | Settings
53 |
54 |
55 | Settings Tab
56 |
57 |
58 |
59 | Settings Label
60 | Settings Tab
61 |
62 |
63 | ```
64 |
--------------------------------------------------------------------------------
/tests/lib/rules/utils/hasValidNestedProp.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | jest.mock("jsx-ast-utils", () => ({
5 | getProp: jest.fn(),
6 | getPropValue: jest.fn()
7 | }));
8 |
9 | import { getProp, getPropValue } from "jsx-ast-utils";
10 | import { hasValidNestedProp } from "../../../../lib/util/hasValidNestedProp";
11 |
12 | describe("hasValidNestedProp", () => {
13 | const opening = { attributes: [] } as any;
14 |
15 | beforeEach(() => {
16 | (getProp as jest.Mock).mockReset();
17 | (getPropValue as jest.Mock).mockReset();
18 | });
19 |
20 | test("returns false when the prop is not present (e.g. )", () => {
21 | // Example:
22 | (getProp as jest.Mock).mockReturnValue(undefined);
23 | const result = hasValidNestedProp(opening, "dismissIcon", "aria-label");
24 | expect(result).toBe(false);
25 | expect(getProp).toHaveBeenCalledWith(opening.attributes, "dismissIcon");
26 | });
27 |
28 | test("returns false when nested key is missing or empty string", () => {
29 | // Example:
30 | (getProp as jest.Mock).mockReturnValue({});
31 | (getPropValue as jest.Mock).mockReturnValue({});
32 | expect(hasValidNestedProp(opening, "dismissIcon", "aria-label")).toBe(false);
33 |
34 | // Example:
35 | (getPropValue as jest.Mock).mockReturnValue({ "aria-label": " " });
36 | expect(hasValidNestedProp(opening, "dismissIcon", "aria-label")).toBe(false);
37 | });
38 |
39 | test("returns true when nested key is a non-empty string (e.g. )", () => {
40 | // Example:
41 | (getProp as jest.Mock).mockReturnValue({});
42 | (getPropValue as jest.Mock).mockReturnValue({ "aria-label": "Dismiss" });
43 | expect(hasValidNestedProp(opening, "dismissIcon", "aria-label")).toBe(true);
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | "use strict";
5 |
6 | module.exports = {
7 | root: true,
8 | extends: [
9 | "eslint:recommended",
10 | "plugin:eslint-plugin/recommended",
11 | "plugin:node/recommended",
12 | "plugin:prettier/recommended" // Prettier plugin must be last in the extensions
13 | ],
14 | plugins: ["header"],
15 | env: {
16 | node: true,
17 | es6: true
18 | },
19 | parserOptions: {
20 | ecmaVersion: 2021, // Allows the latest ECMAScript features
21 | sourceType: "module" // Ensures `import` and `export` syntax are valid
22 | },
23 | overrides: [
24 | {
25 | files: ["tests/**/*.js"],
26 | env: { mocha: true }
27 | },
28 | {
29 | files: ["tests/**/*.ts"],
30 | env: { jest: true }
31 | },
32 | {
33 | files: ["**/*.ts"],
34 | parser: "@typescript-eslint/parser",
35 | plugins: ["@typescript-eslint"],
36 | parserOptions: {
37 | ecmaVersion: 2021,
38 | sourceType: "module",
39 | project: "./tsconfig.json"
40 | },
41 | rules: {
42 | // Disable Node.js rules that conflict with TypeScript
43 | "node/no-missing-import": "off",
44 | "node/no-unsupported-features/es-syntax": "off",
45 | "node/no-extraneous-import": "off"
46 | }
47 | },
48 | {
49 | files: ["lib/index.ts"],
50 | rules: {
51 | "sort-keys": ["error", "asc", { caseSensitive: true, natural: false }]
52 | }
53 | }
54 | ],
55 | rules: {
56 | "header/header": [2, "line", [" Copyright (c) Microsoft Corporation.", " Licensed under the MIT License."], 2],
57 | "no-console": "warn" // Add this to warn about console statements
58 | },
59 | ignorePatterns: ["node_modules", "dist/", "scripts"]
60 | };
61 |
--------------------------------------------------------------------------------
/docs/rules/badge-needs-accessible-name.md:
--------------------------------------------------------------------------------
1 | # @microsoft/fluentui-jsx-a11y/badge-needs-accessible-name
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 | 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6 |
7 |
8 |
9 | Badge information should be surfaced as part of the control that it is associated with, because, badges themselves do not receive focus meaning they are not directly accessible by screen readers. If the combination of icon and badge communicates some meaningful information, that information should be surfaced in another way through screenreader or tooltip on the component the badge is associated with.
10 |
11 | Badge content is exposed as text, and is treated by screen readers as if it were inline content of the control it is associated with.
12 |
13 | ## Rule Details
14 |
15 | Ensure that the `Badge` component is accessible.
16 |
17 | Examples of **incorrect** code for this rule:
18 |
19 | ```jsx
20 | } />
21 | ```
22 |
23 | ```jsx
24 | } />
25 | ```
26 |
27 | ```jsx
28 |
29 | ```
30 |
31 | Examples of **correct** code for this rule:
32 |
33 | If the badge contains a custom icon, that icon must be given alternative text with aria-label, unless it is purely presentational:
34 |
35 | ```jsx
36 | } />
37 | ```
38 |
39 | Badge shouldn't rely only on color information. Include meaningful descriptions when using color to represent meaning in a badge. If relying on color only ensure that non-visual information is included in the parent's label or description. Alternatively, mark up the Badge as an image with a label:
40 |
41 | ```jsx
42 | } />
43 | ```
44 |
45 | ```jsx
46 | 999+
47 | ```
48 |
49 | ## Further Reading
50 |
51 |
52 |
--------------------------------------------------------------------------------
/tests/lib/rules/avatar-needs-name.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | //------------------------------------------------------------------------------
5 | // Requirements
6 | //------------------------------------------------------------------------------
7 |
8 | import { Rule } from "eslint";
9 | import ruleTester from "./helper/ruleTester";
10 | import rule from "../../../lib/rules/avatar-needs-name";
11 |
12 | //------------------------------------------------------------------------------
13 | // Tests
14 | //------------------------------------------------------------------------------
15 |
16 | ruleTester.run("avatar-needs-name", rule as unknown as Rule.RuleModule, {
17 | valid: [
18 | // give me some code that won't trigger a warning
19 | ' ',
20 | ' ',
21 | '<>Jane Doe >',
22 | ' ',
23 | ' ',
24 | '<>Jane Doe >'
25 | ],
26 |
27 | invalid: [
28 | {
29 | code: " ",
30 | errors: [{ messageId: "missingAriaLabel" }]
31 | },
32 | {
33 | code: " ",
34 | errors: [{ messageId: "missingAriaLabel" }]
35 | },
36 | {
37 | code: " }>",
38 | errors: [{ messageId: "missingAriaLabel" }]
39 | },
40 | {
41 | code: " } />",
42 | errors: [{ messageId: "missingAriaLabel" }]
43 | },
44 | {
45 | code: ' ',
46 | errors: [{ messageId: "missingAriaLabel" }]
47 | },
48 | {
49 | code: ' ',
50 | errors: [{ messageId: "missingAriaLabel" }]
51 | }
52 | ]
53 | });
54 |
--------------------------------------------------------------------------------
/tests/lib/rules/menu-item-needs-labelling.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | //------------------------------------------------------------------------------
5 | // Requirements
6 | //------------------------------------------------------------------------------
7 |
8 | import { Rule } from "eslint";
9 | import ruleTester from "./helper/ruleTester";
10 | import rule from "../../../lib/rules/menu-item-needs-labelling";
11 |
12 | //------------------------------------------------------------------------------
13 | // Tests
14 | //------------------------------------------------------------------------------
15 |
16 | ruleTester.run("menuitem-needs-labelling", rule as unknown as Rule.RuleModule, {
17 | valid: [
18 | // Valid cases
19 | ' } onClick={handleClick}>',
20 | "Settings ",
21 | 'More option
',
22 | 'More option } onClick={handleClick}>Settings
',
23 | ' '
24 | ],
25 | invalid: [
26 | // Invalid cases
27 | {
28 | code: " ",
29 | errors: [{ messageId: "noUnlabelledMenuItem" }]
30 | },
31 | {
32 | code: " } onClick={handleClick}>",
33 | errors: [{ messageId: "noUnlabelledMenuItem" }]
34 | },
35 | {
36 | code: "Settings } onClick={handleClick}>
",
37 | errors: [{ messageId: "noUnlabelledMenuItem" }]
38 | },
39 | {
40 | code: "Settings } onClick={handleClick}> ",
41 | errors: [{ messageId: "noUnlabelledMenuItem" }]
42 | }
43 | ]
44 | });
45 |
--------------------------------------------------------------------------------
/tests/lib/rules/avoid-using-aria-describedby-for-primary-labelling.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | //------------------------------------------------------------------------------
5 | // Requirements
6 | //------------------------------------------------------------------------------
7 |
8 | import { Rule } from "eslint";
9 | import ruleTester from "./helper/ruleTester";
10 | import rule from "../../../lib/rules/avoid-using-aria-describedby-for-primary-labelling";
11 |
12 | //------------------------------------------------------------------------------
13 | // Tests
14 | //------------------------------------------------------------------------------
15 |
16 | ruleTester.run("avoid-using-aria-describedby-for-primary-labelling", rule as unknown as Rule.RuleModule, {
17 | valid: [
18 | '<> } />Click to submit your form. This will save your data. >',
19 | '<>Submit Click to submit your form. This will save your data. >',
20 | '<>Name This field is for your full legal name. >'
21 | ],
22 | invalid: [
23 | {
24 | code: '<> } />Click to submit your form. This will save your data. >',
25 | errors: [{ messageId: "noAriaDescribedbyAsLabel" }]
26 | },
27 | {
28 | code: '<>Name >',
29 | errors: [{ messageId: "noAriaDescribedbyAsLabel" }]
30 | },
31 | {
32 | code: '<>Name >',
33 | errors: [{ messageId: "noAriaDescribedbyAsLabel" }]
34 | }
35 | ]
36 | });
37 |
--------------------------------------------------------------------------------
/tests/lib/rules/accordion-item-needs-header-and-panel.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | //------------------------------------------------------------------------------
5 | // Requirements
6 | //------------------------------------------------------------------------------
7 |
8 | import { Rule } from "eslint";
9 | import ruleTester from "./helper/ruleTester";
10 | import rule from "../../../lib/rules/accordion-item-needs-header-and-panel";
11 |
12 | //------------------------------------------------------------------------------
13 | // Tests
14 | //------------------------------------------------------------------------------
15 |
16 | ruleTester.run("accordion-item-needs-header-and-panel", rule as unknown as Rule.RuleModule, {
17 | valid: [
18 | `Accordion Header 1 Accordion Panel 1
`,
19 | ` `
20 | ],
21 |
22 | invalid: [
23 | {
24 | code: `Accordion Header 2 `,
25 | errors: [{ messageId: "accordionItemOneHeaderOnePanel" }]
26 | },
27 | {
28 | code: `Accordion Panel 3
`,
29 | errors: [{ messageId: "accordionItemOneHeaderOnePanel" }]
30 | },
31 | {
32 | code: `Accordion Header 1 Accordion Header 2 Accordion Panel 3
`,
33 | errors: [{ messageId: "accordionItemOneHeaderOnePanel" }]
34 | },
35 | {
36 | code: `Accordion Header 1 Accordion Panel 1
Accordion Panel 2
`,
37 | errors: [{ messageId: "accordionItemOneHeaderOnePanel" }]
38 | }
39 | ]
40 | });
41 |
--------------------------------------------------------------------------------
/tests/lib/rules/visual-label-better-than-aria-suggestion.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { Rule } from "eslint";
5 | import ruleTester from "./helper/ruleTester";
6 | import rule from "../../../lib/rules/visual-label-better-than-aria-suggestion";
7 |
8 | import { applicableComponents } from "../../../lib/applicableComponents/inputBasedComponents";
9 |
10 | //------------------------------------------------------------------------------
11 | // Requirements
12 | //------------------------------------------------------------------------------
13 |
14 | //------------------------------------------------------------------------------
15 | // Helper function to generate test cases
16 | //------------------------------------------------------------------------------
17 | const generateTestCases = (componentName: string) => {
18 | return {
19 | valid: [
20 | `<>Some Label <${componentName} id="some-id"/>>`,
21 | `<>Some Label <${componentName} id="some-id" aria-labelledby="test-span"/>>`,
22 | `test<${componentName} /> `,
23 | `<>This is the visual label <${componentName} aria-labelledby="id1" />>`
24 | ],
25 | invalid: [
26 | {
27 | code: `<${componentName} aria-label="This is a component with aria-label" />`,
28 | errors: [{ messageId: "visualLabelSuggestion" }]
29 | }
30 | ]
31 | };
32 | };
33 |
34 | // Collect all test cases for all applicable components
35 | const allTestCases = applicableComponents.flatMap(component => generateTestCases(component));
36 |
37 | //------------------------------------------------------------------------------
38 | // Tests
39 | //------------------------------------------------------------------------------
40 |
41 | ruleTester.run("visual-label-better-than-aria-suggestion", rule as unknown as Rule.RuleModule, {
42 | valid: allTestCases.flatMap(test => test.valid),
43 | invalid: allTestCases.flatMap(test => test.invalid)
44 | });
45 |
--------------------------------------------------------------------------------
/tests/lib/rules/toolbar-missing-aria.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | //------------------------------------------------------------------------------
5 | // Requirements
6 | //------------------------------------------------------------------------------
7 |
8 | import { Rule } from "eslint";
9 | import ruleTester from "./helper/ruleTester";
10 | import rule from "../../../lib/rules/toolbar-missing-aria";
11 |
12 | //------------------------------------------------------------------------------
13 | // Tests
14 | //------------------------------------------------------------------------------
15 |
16 | ruleTester.run("toolbar-missing-aria", rule as unknown as Rule.RuleModule, {
17 | valid: [
18 | ' ',
19 | ' ',
20 | '<>Some Label >'
21 | ],
22 |
23 | invalid: [
24 | {
25 | code: " ",
26 | errors: [{ messageId: "missingLabelOnToolbar" }]
27 | },
28 | {
29 | code: " ",
30 | errors: [{ messageId: "missingLabelOnToolbar" }]
31 | },
32 | {
33 | code: "<> >",
34 | errors: [{ messageId: "missingLabelOnToolbar" }]
35 | },
36 | {
37 | code: '<> >',
38 | errors: [{ messageId: "missingLabelOnToolbar" }]
39 | },
40 | {
41 | code: ' ',
42 | errors: [{ messageId: "missingLabelOnToolbar" }]
43 | },
44 | {
45 | code: '<>Some Label >',
46 | errors: [{ messageId: "missingLabelOnToolbar" }]
47 | },
48 | {
49 | code: '<>Some Label >',
50 | errors: [{ messageId: "missingLabelOnToolbar" }]
51 | }
52 | ]
53 | });
54 |
--------------------------------------------------------------------------------
/tests/lib/rules/tablist-and-tabs-need-labelling.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { Rule } from "eslint";
5 | import ruleTester from "./helper/ruleTester";
6 | import rule from "../../../lib/rules/tablist-and-tabs-need-labelling";
7 |
8 | //------------------------------------------------------------------------------
9 | // Requirements
10 | //------------------------------------------------------------------------------
11 |
12 | //------------------------------------------------------------------------------
13 | // Tests
14 | //------------------------------------------------------------------------------
15 | ruleTester.run("tablist-and-tabs-need-labelling", rule as unknown as Rule.RuleModule, {
16 | valid: [
17 | // Valid cases for Tablist
18 | 'Settings Tab ',
19 | 'Settings Label Settings Tab ',
20 |
21 | // Valid cases
22 | ' } aria-label="Settings" />',
23 | " }>Settings",
24 | "Settings "
25 | ],
26 |
27 | invalid: [
28 | // Invalid cases for Tablist
29 | {
30 | code: "Settings Tab ",
31 | errors: [{ messageId: "missingTablistLabel" }]
32 | },
33 | {
34 | code: 'Settings Label Settings Tab ',
35 | errors: [{ messageId: "missingTablistLabel" }]
36 | },
37 | {
38 | code: "Settings Label Settings Tab ",
39 | errors: [{ messageId: "missingTablistLabel" }]
40 | },
41 |
42 | // Invalid cases for Tab
43 | {
44 | code: " } />",
45 | errors: [{ messageId: "missingTabLabel" }]
46 | },
47 | {
48 | code: " }>",
49 | errors: [{ messageId: "missingTabLabel" }]
50 | }
51 | ]
52 | });
53 |
--------------------------------------------------------------------------------
/tests/lib/rules/prefer-aria-over-title-attribute.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | //------------------------------------------------------------------------------
5 | // Requirements
6 | //------------------------------------------------------------------------------
7 |
8 | import { Rule } from "eslint";
9 | import ruleTester from "./helper/ruleTester";
10 | import rule from "../../../lib/rules/prefer-aria-over-title-attribute";
11 |
12 | //------------------------------------------------------------------------------
13 | // Tests
14 | //------------------------------------------------------------------------------
15 |
16 | ruleTester.run("prefer-aria-over-title-attribute", rule as unknown as Rule.RuleModule, {
17 | valid: [
18 | // give me some code that won't trigger a warning
19 | ' } aria-label="Close" />',
20 | ' } aria-label="Close">',
21 | 'Example ',
22 | " }>Close",
23 | " ",
24 | ' ',
25 | ' } /> ',
26 | ' } /> ',
27 | ' } /> ',
28 | '<>Close } aria-labelledby="label-id-4">>',
29 | '<>Close } aria-labelledby="label-id-4" />>'
30 | ],
31 |
32 | invalid: [
33 | {
34 | code: ' } title="hello">',
35 | errors: [{ messageId: "preferAria" }],
36 | output: ' } title="hello" aria-label="hello">'
37 | }
38 | ]
39 | });
40 |
--------------------------------------------------------------------------------
/lib/rules/field-needs-labelling.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | "use strict";
5 |
6 | import { hasNonEmptyProp } from "../util/hasNonEmptyProp";
7 | const elementType = require("jsx-ast-utils").elementType;
8 | import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
9 |
10 | //------------------------------------------------------------------------------
11 | // Rule Definition
12 | //------------------------------------------------------------------------------
13 |
14 | const rule = ESLintUtils.RuleCreator.withoutDocs({
15 | defaultOptions: [],
16 | meta: {
17 | // possible error messages for the rule
18 | messages: {
19 | noUnlabelledField: "Accessibility: Field must have label"
20 | },
21 | // "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
22 | type: "problem",
23 | // docs for the rule
24 | docs: {
25 | description: "Accessibility: Field must have label",
26 | recommended: "strict",
27 | url: "https://www.w3.org/TR/html-aria/" // URL to the documentation page for this rule
28 | },
29 | schema: []
30 | },
31 |
32 | // create (function) returns an object with methods that ESLint calls to “visit” nodes while traversing the abstract syntax tree
33 | create(context) {
34 | return {
35 | // visitor functions for different types of nodes
36 | JSXOpeningElement(node: TSESTree.JSXOpeningElement) {
37 | // if it is not a Spinner, return
38 | if (elementType(node) !== "Field") {
39 | return;
40 | }
41 |
42 | if (hasNonEmptyProp(node.attributes, "label")) {
43 | return;
44 | }
45 |
46 | // if it has no visual labelling, report error
47 | context.report({
48 | node,
49 | messageId: `noUnlabelledField`
50 | });
51 | }
52 | };
53 | }
54 | });
55 |
56 | export default rule;
57 |
--------------------------------------------------------------------------------
/lib/rules/no-empty-components.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
5 | import { JSXOpeningElement } from "estree-jsx";
6 | import { elementType } from "jsx-ast-utils";
7 |
8 | //------------------------------------------------------------------------------
9 | // Rule Definition
10 | //------------------------------------------------------------------------------
11 | // Define an array of allowed component names
12 | const allowedComponents = ["Text", "Label", "Combobox", "Breadcrumb", "Dropdown", "Accordion", "AccordionItem", "AccordionPanel"];
13 |
14 | const rule = ESLintUtils.RuleCreator.withoutDocs({
15 | defaultOptions: [],
16 | meta: {
17 | // possible error messages for the lint rule
18 | messages: {
19 | noEmptyComponents: `Accessibility: no empty ${allowedComponents.join(", ")} components`
20 | },
21 | type: "problem", // `problem`, `suggestion`, or `layout`
22 | docs: {
23 | description: "FluentUI components should not be empty",
24 | recommended: "strict"
25 | },
26 | schema: [] // Add a schema if the rule has options
27 | },
28 | // create (function) returns an object with methods that ESLint calls to “visit” nodes while traversing the abstract syntax tree
29 | create(context) {
30 | return {
31 | // visitor functions for different types of nodes
32 | JSXElement(node: TSESTree.JSXElement) {
33 | const openingElement = node.openingElement;
34 |
35 | // if it is not a listed component, return
36 | if (!allowedComponents.includes(elementType(openingElement as JSXOpeningElement))) {
37 | return;
38 | }
39 |
40 | const hasChildren = node.children.length > 0;
41 |
42 | // if there are no children, report error
43 | if (!hasChildren) {
44 | context.report({
45 | node,
46 | messageId: `noEmptyComponents`
47 | });
48 | }
49 | }
50 | };
51 | }
52 | });
53 |
54 | export default rule;
55 |
--------------------------------------------------------------------------------
/tests/lib/rules/spinner-needs-labelling.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | //------------------------------------------------------------------------------
5 | // Requirements
6 | //------------------------------------------------------------------------------
7 |
8 | import { Rule } from "eslint";
9 | import ruleTester from "./helper/ruleTester";
10 | import rule from "../../../lib/rules/spinner-needs-labelling";
11 |
12 | //------------------------------------------------------------------------------
13 | // Tests
14 | //------------------------------------------------------------------------------
15 |
16 | ruleTester.run("spinner-needs-labelling", rule as unknown as Rule.RuleModule, {
17 | valid: [
18 | ` `,
19 | ` `,
20 | ` `,
21 | ` `
22 | ],
23 | invalid: [
24 | {
25 | code: ` `,
26 | errors: [{ messageId: "noUnlabelledSpinner" }]
27 | },
28 | {
29 | code: ` `,
30 | errors: [{ messageId: "noUnlabelledSpinner" }]
31 | },
32 | {
33 | code: ` `,
34 | errors: [{ messageId: "noUnlabelledSpinner" }]
35 | },
36 | {
37 | code: ` `,
38 | errors: [{ messageId: "noUnlabelledSpinner" }]
39 | },
40 | {
41 | code: ` `,
42 | errors: [{ messageId: "noUnlabelledSpinner" }]
43 | },
44 | {
45 | code: ` `,
46 | errors: [{ messageId: "noUnlabelledSpinner" }]
47 | }
48 | ]
49 | });
50 |
--------------------------------------------------------------------------------
/scripts/addRuleToIndex.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { kebabToCamelCase } from "./utils/kebabToKamelCase";
5 |
6 | // Sort function to keep rules and config sorted alphabetically
7 | const nameSort = (a, b) => {
8 | const aName = a.key.type === "Literal" ? a.key.value : a.key.name;
9 | const bName = a.key.type === "Literal" ? b.key.value : b.key.name;
10 | if (aName < bName) return -1;
11 | if (aName > bName) return 1;
12 | return 0;
13 | };
14 |
15 | const transformer = (file, api, options) => {
16 | const j = api.jscodeshift;
17 | const root = j(file.source);
18 | const { ruleName } = options; // No need for rulePath in this case
19 |
20 | let changesMade = 0;
21 |
22 | // Step 1: Add rule to the `rules` object (without parentheses)
23 | root.find(j.Property, { key: { name: "rules" } })
24 | .at(0)
25 | .forEach(path => {
26 | const properties = path.value.value.properties;
27 | properties.unshift(
28 | j.property("init", j.literal(ruleName), j.memberExpression(j.identifier("rules"), j.identifier(kebabToCamelCase(ruleName))))
29 | );
30 | properties.sort(nameSort);
31 | changesMade += 1;
32 | });
33 |
34 | // Step 2: Find and modify `configs.recommended.rules`
35 | root.find(j.Property, { key: { name: "configs" } }).forEach(configPath => {
36 | const recommendedConfig = configPath.value.value.properties.find(prop => prop.key.name === "recommended");
37 |
38 | if (recommendedConfig) {
39 | const recommendedRules = recommendedConfig.value.properties.find(prop => prop.key.name === "rules");
40 |
41 | if (recommendedRules) {
42 | const rulesProps = recommendedRules.value.properties;
43 | rulesProps.unshift(j.property("init", j.literal(`@microsoft/fluentui-jsx-a11y/${ruleName}`), j.literal("error")));
44 | rulesProps.sort(nameSort);
45 | changesMade += 1;
46 | }
47 | }
48 | });
49 |
50 | if (changesMade === 0) {
51 | return null;
52 | }
53 |
54 | return root.toSource({ quote: "double", trailingComma: false });
55 | };
56 | module.exports = transformer;
57 |
--------------------------------------------------------------------------------
/lib/rules/tooltip-not-recommended.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
5 | import { elementType } from "jsx-ast-utils";
6 | import { hasToolTipParent } from "../util/hasTooltipParent";
7 | import { JSXOpeningElement } from "estree-jsx";
8 |
9 | //------------------------------------------------------------------------------
10 | // Rule Definition
11 | //------------------------------------------------------------------------------
12 | // Define an array of allowed component names
13 | const allowedComponents = ["MenuItem", "SpinButton"];
14 |
15 | const rule = ESLintUtils.RuleCreator.withoutDocs({
16 | defaultOptions: [],
17 | meta: {
18 | // possible error messages for the lint rule
19 | messages: {
20 | tooltipNotRecommended: `Accessibility: Tooltop not recommended for these components ${allowedComponents.join(", ")}`
21 | },
22 | type: "suggestion", // `problem`, `suggestion`, or `layout`
23 | docs: {
24 | description: `Accessibility: Prefer text content or aria over a tooltip for these components ${allowedComponents.join(", ")}`,
25 | recommended: "strict"
26 | },
27 | schema: [] // Add a schema if the rule has options
28 | },
29 | // create (function) returns an object with methods that ESLint calls to “visit” nodes while traversing the abstract syntax tree
30 | create(context) {
31 | return {
32 | // visitor functions for different types of nodes
33 | JSXElement(node: TSESTree.JSXElement) {
34 | const openingElement = node.openingElement;
35 |
36 | // if it is not a listed component, return
37 | if (!allowedComponents.includes(elementType(openingElement as JSXOpeningElement))) {
38 | return;
39 | }
40 |
41 | // if there are is tooltip, report
42 | if (hasToolTipParent(context)) {
43 | context.report({
44 | node,
45 | messageId: `tooltipNotRecommended`
46 | });
47 | }
48 | }
49 | };
50 | }
51 | });
52 |
53 | export default rule;
54 |
--------------------------------------------------------------------------------
/COVERAGE.md:
--------------------------------------------------------------------------------
1 | # Coverage
2 |
3 | We currently cover the following components:
4 |
5 | - [x] Accordion
6 | - [x] Avatar
7 | - [x] AvatarGroup
8 | - [] Badge
9 | - [x] Badge
10 | - [x] CounterBadge
11 | - [N/A] PresenceBadge
12 | - [x] Breadcrumb
13 | - [x] Button
14 | - [x] Button
15 | - [X] CompoundButton
16 | - [x] MenuButton
17 | - [X] MenuItem
18 | - [] SplitButton
19 | - [x] ToggleButton
20 | - [] ToolbarToggleButton
21 | - [x] Card
22 | - [x] Card
23 | - [] CardFooter
24 | - [] CardHeader
25 | - [] CardPreview
26 | - [] Carousel
27 | - [] Carousel
28 | - [] CarouselNav
29 | - [x] Checkbox
30 | - [] ColorPicker
31 | - [x] Combobox
32 | - [x] DataGrid
33 | - [x] Dialog
34 | - [N/A] Divider
35 | - [] Drawer
36 | - [X] Dropdown
37 | - [x] Field
38 | - [N/A] FluentProvider
39 | - [] Image
40 | - [x] InfoLabel
41 | - [x] Input
42 | - [x] Label
43 | - [x] Link
44 | - [] List
45 | - [x] Menu
46 | - [x] Menu
47 | - [x] MenuList
48 | - [x] MessageBar
49 | - [] Nav
50 | - [N/A] Overflow
51 | - [] Persona
52 | - [] Popover
53 | - [N/A] Portal
54 | - [x] ProgressBar
55 | - [x] Rating
56 | - [] RatingDisplay
57 | - [x] Radio
58 | - [x] RadioGroup
59 | - [x] SearchBox
60 | - [x] Select
61 | - [x] Slider
62 | - [N/A] Skeleton
63 | - [N/A] SkeletonItem
64 | - [x] SpinButton
65 | - [x] Spinner
66 | - [x] SwatchPicker
67 | - [x] ColorSwatch
68 | - [x] ImageSwatch
69 | - [x] EmptySwatch
70 | - [N/A] SwatchPickerRow
71 | - [x] Switch
72 | - [] SearchBox
73 | - [x] Table
74 | - [x] TabList
75 | - [] Tag
76 | - [] InteractionTag
77 | - [] Tag
78 | - [] TagGroup
79 | - [] TagPicker
80 | - [] TeachingPopover
81 | - [x] Text
82 | - [x] TextArea
83 | - [] Toast
84 | - [x] Toolbar
85 | - [x] Tooltip
86 | - [x] Tree
87 | - [x] Datepicker
88 | - [N/A] Calendar
89 | - [x] Timepicker
90 |
--------------------------------------------------------------------------------
/lib/rules/toolbar-missing-aria.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
5 | import { elementType } from "jsx-ast-utils";
6 | import { hasAssociatedLabelViaAriaLabelledBy } from "../util/labelUtils";
7 | import { hasNonEmptyProp } from "../util/hasNonEmptyProp";
8 | import { JSXOpeningElement } from "estree-jsx";
9 |
10 | //------------------------------------------------------------------------------
11 | // Rule Definition
12 | //------------------------------------------------------------------------------
13 |
14 | const rule = ESLintUtils.RuleCreator.withoutDocs({
15 | defaultOptions: [],
16 | meta: {
17 | // possible error messages for the rule
18 | messages: {
19 | missingLabelOnToolbar: "Toolbars need accessible labelling: aria-label or aria-labelledby"
20 | },
21 | // "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
22 | type: "problem",
23 | // docs for the rule
24 | docs: {
25 | description: "Accessibility: Toolbars need accessible labelling: aria-label or aria-labelledby",
26 | recommended: "strict",
27 | url: "https://www.w3.org/WAI/tutorials/forms/labels/" // URL to the documentation page for this rule
28 | },
29 | schema: []
30 | },
31 |
32 | create(context) {
33 | return {
34 | // visitor functions for different types of nodes
35 | JSXOpeningElement(node: TSESTree.JSXOpeningElement) {
36 | // if it is not a Toolbar, return
37 | if (elementType(node as JSXOpeningElement) !== "Toolbar") {
38 | return;
39 | }
40 |
41 | // if the Toolbar has aria labelling, return
42 | if (hasNonEmptyProp(node.attributes, "aria-label") || hasAssociatedLabelViaAriaLabelledBy(node, context)) {
43 | return;
44 | }
45 |
46 | context.report({
47 | node,
48 | messageId: `missingLabelOnToolbar`
49 | });
50 | }
51 | };
52 | }
53 | });
54 |
55 | export default rule;
56 |
--------------------------------------------------------------------------------
/tests/lib/rules/dialogbody-needs-title-content-and-actions.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | //------------------------------------------------------------------------------
5 | // Requirements
6 | //------------------------------------------------------------------------------
7 |
8 | import { Rule } from "eslint";
9 | import ruleTester from "./helper/ruleTester";
10 | import rule from "../../../lib/rules/dialogbody-needs-title-content-and-actions";
11 |
12 | //------------------------------------------------------------------------------
13 | // Tests
14 | //------------------------------------------------------------------------------
15 |
16 | ruleTester.run("dialogbody-needs-title-content-and-actions", rule as any as Rule.RuleModule, {
17 | valid: [
18 | `
19 | Dialog title
20 | Test
21 |
22 | Close
23 | Do Something
24 |
25 | `,
26 | ` `
27 | ],
28 |
29 | invalid: [
30 | {
31 | code: `
32 | Test
33 |
34 | Close
35 | Do Something
36 |
37 | `,
38 | errors: [{ messageId: "dialogBodyOneTitleOneContentOneFooter" }]
39 | },
40 | {
41 | code: `
42 | Dialog title
43 |
44 | Close
45 | Do Something
46 |
47 | `,
48 | errors: [{ messageId: "dialogBodyOneTitleOneContentOneFooter" }]
49 | },
50 | {
51 | code: `
52 | Dialog title
53 | Test
54 | `,
55 | errors: [{ messageId: "dialogBodyOneTitleOneContentOneFooter" }]
56 | }
57 | ]
58 | });
59 |
--------------------------------------------------------------------------------
/tests/lib/rules/infolabel-needs-labelling.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | //------------------------------------------------------------------------------
5 | // Requirements
6 | //------------------------------------------------------------------------------
7 |
8 | import { Rule } from "eslint";
9 | import ruleTester from "./helper/ruleTester";
10 | import rule from "../../../lib/rules/infolabel-needs-labelling";
11 |
12 | //------------------------------------------------------------------------------
13 | // Tests
14 | //------------------------------------------------------------------------------
15 |
16 | ruleTester.run("infolabel-needs-labelling", rule as unknown as Rule.RuleModule, {
17 | valid: [
18 | ` `,
19 | `Help text `,
20 | `? `,
21 | `ℹ️ `,
22 | `<>Information >`,
23 | ` `,
24 | ` `,
25 | ` `
26 | ],
27 | invalid: [
28 | {
29 | code: ` `,
30 | errors: [{ messageId: "infoLabelNeedsLabelling" }]
31 | },
32 | {
33 | code: ` `,
34 | errors: [{ messageId: "infoLabelNeedsLabelling" }]
35 | },
36 | {
37 | code: ` `,
38 | errors: [{ messageId: "infoLabelNeedsLabelling" }]
39 | },
40 | {
41 | code: ` `,
42 | errors: [{ messageId: "infoLabelNeedsLabelling" }]
43 | },
44 | {
45 | code: `<>Information >`,
46 | errors: [{ messageId: "infoLabelNeedsLabelling" }]
47 | },
48 | {
49 | code: ` `,
50 | errors: [{ messageId: "infoLabelNeedsLabelling" }]
51 | },
52 | {
53 | code: ` `,
54 | errors: [{ messageId: "infoLabelNeedsLabelling" }]
55 | }
56 | ]
57 | });
58 |
--------------------------------------------------------------------------------
/tests/lib/rules/buttons/compound-button-needs-labelling.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | //------------------------------------------------------------------------------
5 | // Requirements
6 | //------------------------------------------------------------------------------
7 | import { Rule } from "eslint";
8 | import ruleTester from "../helper/ruleTester";
9 | import rule from "../../../../lib/rules/buttons/compound-button-needs-labelling";
10 |
11 | //------------------------------------------------------------------------------
12 | // Tests
13 | //------------------------------------------------------------------------------
14 |
15 | ruleTester.run("compound-button-needs-labelling", rule as unknown as Rule.RuleModule, {
16 | valid: [
17 | ' }>',
18 | "Example ",
19 | ' ',
20 | ' ',
21 | ' }>',
22 | ' } /> ',
23 | '<>Compound Example }>>'
24 | ],
25 | invalid: [
26 | {
27 | code: " }>",
28 | errors: [{ messageId: "missingAriaLabel" }]
29 | },
30 | {
31 | code: " ",
32 | errors: [{ messageId: "missingAriaLabel" }]
33 | },
34 | {
35 | code: ' ',
36 | errors: [{ messageId: "missingAriaLabel" }]
37 | },
38 | {
39 | code: '<>Compound Example >',
40 | errors: [{ messageId: "missingAriaLabel" }]
41 | },
42 | {
43 | code: "<>Compound Example }/>>",
44 | errors: [{ messageId: "missingAriaLabel" }]
45 | }
46 | ]
47 | });
48 |
--------------------------------------------------------------------------------
/docs/rules/accordion-item-needs-header-and-panel.md:
--------------------------------------------------------------------------------
1 | # An AccordionItem needs exactly one header and one panel (`@microsoft/fluentui-jsx-a11y/accordion-item-needs-header-and-panel`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 | The ESLint rule is designed to enforce accessibility standards in React components, specifically ensuring an accordion component has one button (as a header) which controls one panel region.
8 |
9 | ## Rule Details
10 |
11 | Accordions are common UI components that allow users to expand and collapse sections of content.
12 |
13 | For an accordion to be accessible:
14 |
15 | Each accordion header should be implemented as a button. This is because buttons are inherently accessible, providing keyboard and screen reader support. Making the accordion header a button ensures that users can interact with it using keyboard commands or assistive technologies.
16 |
17 | Each accordion panel should be marked as a region that is controlled by the accordion header. This can be achieved using attributes like aria-controls to link the header to the panel. The panel should be appropriately labeled to ensure it can be identified and understood by users of assistive technologies.
18 |
19 | Examples of **incorrect** code for this rule:
20 |
21 | ```jsx
22 |
23 |
24 | Accordion Header 1
25 | Accordion Header 2
26 |
27 | Accordion Panel 1
28 |
29 |
30 |
31 | ```
32 |
33 | ```jsx
34 |
35 |
36 | Accordion Header 1
37 |
38 | Accordion Panel 1
39 |
40 |
41 | Accordion Panel 2
42 |
43 |
44 |
45 | ```
46 |
47 | Examples of **correct** code for this rule:
48 |
49 | ```jsx
50 |
51 |
52 | Accordion Header 1
53 |
54 | Accordion Panel 1
55 |
56 |
57 |
58 | ```
59 |
60 | ## Further Reading
61 |
62 | [ARIA Patterns](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/)
63 |
--------------------------------------------------------------------------------
/lib/rules/visual-label-better-than-aria-suggestion.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { hasNonEmptyProp } from "../util/hasNonEmptyProp";
5 | import { applicableComponents } from "../applicableComponents/inputBasedComponents";
6 | import { dropdownBasedComponents } from "../applicableComponents/dropdownBasedComponents";
7 | import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
8 | import { elementType } from "jsx-ast-utils";
9 | import { JSXOpeningElement } from "estree-jsx";
10 |
11 | //------------------------------------------------------------------------------
12 | // Rule Definition
13 | //------------------------------------------------------------------------------
14 |
15 | const rule = ESLintUtils.RuleCreator.withoutDocs({
16 | defaultOptions: [],
17 | meta: {
18 | // possible warning messages for the lint rule
19 | messages: {
20 | visualLabelSuggestion: `Visual label is better than an aria-label`
21 | },
22 | type: "suggestion", // `problem`, `suggestion`, or `layout`
23 | docs: {
24 | description: "Visual label is better than an aria-label because sighted users can't read the aria-label text.",
25 | recommended: "strict",
26 | url: undefined // URL to the documentation page for this rule
27 | },
28 | fixable: undefined, // Or `code` or `whitespace`
29 | schema: [] // Add a schema if the rule has options
30 | },
31 |
32 | // create (function) returns an object with methods that ESLint calls to “visit” nodes while traversing the abstract syntax tree
33 | create(context) {
34 | return {
35 | // visitor functions for different types of nodes
36 | JSXOpeningElement(node: TSESTree.JSXOpeningElement) {
37 | // if it is not a listed component, return
38 | if (![dropdownBasedComponents, ...applicableComponents].includes(elementType(node as unknown as JSXOpeningElement))) {
39 | return;
40 | }
41 |
42 | // if the element contains aria-label, show the warning message
43 | if (hasNonEmptyProp(node.attributes, "aria-label")) {
44 | context.report({
45 | node,
46 | messageId: `visualLabelSuggestion`
47 | });
48 | }
49 | }
50 | };
51 | }
52 | });
53 |
54 | export default rule;
55 |
--------------------------------------------------------------------------------
/lib/rules/rating-needs-name.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | "use strict";
5 |
6 | import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
7 | import { hasNonEmptyProp } from "../util/hasNonEmptyProp";
8 | import { elementType } from "jsx-ast-utils";
9 | import { hasAssociatedLabelViaAriaLabelledBy } from "../util/labelUtils";
10 | import { JSXOpeningElement } from "estree-jsx";
11 |
12 | const rule = ESLintUtils.RuleCreator.withoutDocs({
13 | defaultOptions: [],
14 | meta: {
15 | // possible error messages for the rule
16 | messages: {
17 | missingAriaLabel: "Accessibility - ratings must have an accessible name or an itemLabel that generates an aria label"
18 | },
19 | // "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
20 | type: "problem",
21 | // docs for the rule
22 | docs: {
23 | description:
24 | "Accessibility: Ratings must have accessible labelling: name, aria-label, aria-labelledby or itemLabel which generates aria-label",
25 | recommended: "strict",
26 | url: "https://www.w3.org/TR/html-aria/" // URL to the documentation page for this rule
27 | },
28 | schema: []
29 | },
30 |
31 | create(context) {
32 | return {
33 | // visitor functions for different types of nodes
34 | JSXOpeningElement(node: TSESTree.JSXOpeningElement) {
35 | // if it is not a listed component, return
36 | if (elementType(node as JSXOpeningElement) !== "Rating") {
37 | return;
38 | }
39 |
40 | // wrapped in Label tag, labelled with htmlFor, labelled with aria-labelledby
41 | if (
42 | hasNonEmptyProp(node.attributes, "itemLabel") ||
43 | hasNonEmptyProp(node.attributes, "name") ||
44 | hasNonEmptyProp(node.attributes, "aria-label") ||
45 | hasAssociatedLabelViaAriaLabelledBy(node, context)
46 | ) {
47 | return;
48 | }
49 |
50 | context.report({
51 | node,
52 | messageId: `missingAriaLabel`
53 | });
54 | }
55 | };
56 | }
57 | });
58 |
59 | export default rule;
60 |
--------------------------------------------------------------------------------
/lib/rules/spin-button-unrecommended-labelling.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
5 | import { elementType } from "jsx-ast-utils";
6 | import { hasNonEmptyProp } from "../util/hasNonEmptyProp";
7 | import { hasToolTipParent } from "../util/hasTooltipParent";
8 | import { JSXOpeningElement } from "estree-jsx";
9 |
10 | //------------------------------------------------------------------------------
11 | // Rule Definition
12 | //------------------------------------------------------------------------------
13 |
14 | const rule = ESLintUtils.RuleCreator.withoutDocs({
15 | defaultOptions: [],
16 | meta: {
17 | // possible suggestion messages for the rule
18 | messages: {
19 | unRecommendedlabellingSpinButton: "Accessibility: Unrecommended accessibility labelling - SpinButton"
20 | },
21 | // "problem" means the rule is identifying something that could be done in a better way but no errors will occur if the code isn’t changed: https://eslint.org/docs/latest/developer-guide/working-with-rules
22 | type: "suggestion",
23 | // docs for the rule
24 | docs: {
25 | description: "Accessibility: Unrecommended accessibility labelling - SpinButton",
26 | recommended: "strict",
27 | url: "https://www.w3.org/TR/html-aria/" // URL to the documentation page for this rule
28 | },
29 | schema: []
30 | },
31 | // create (function) returns an object with methods that ESLint calls to “visit” nodes while traversing the abstract syntax tree
32 | create(context) {
33 | return {
34 | // visitor functions for different types of nodes
35 | JSXOpeningElement(node: TSESTree.JSXOpeningElement) {
36 | // if it is not a SpinButton, return
37 | if (elementType(node as JSXOpeningElement) !== "SpinButton") {
38 | return;
39 | }
40 |
41 | // if the SpinButton has an aria-label or is wrapped in a Tooltip, show warning
42 | if (hasNonEmptyProp(node.attributes, "aria-label") || hasToolTipParent(context)) {
43 | context.report({
44 | node,
45 | messageId: `unRecommendedlabellingSpinButton`
46 | });
47 | }
48 | }
49 | };
50 | }
51 | });
52 |
53 | export default rule;
54 |
--------------------------------------------------------------------------------
/lib/rules/breadcrumb-needs-labelling.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
5 | import { elementType } from "jsx-ast-utils";
6 | import { hasNonEmptyProp } from "../util/hasNonEmptyProp";
7 | import { hasAssociatedLabelViaAriaLabelledBy } from "../util/labelUtils";
8 | import { JSXOpeningElement } from "estree-jsx";
9 |
10 | //------------------------------------------------------------------------------
11 | // Rule Definition
12 | //------------------------------------------------------------------------------
13 |
14 | const rule = ESLintUtils.RuleCreator.withoutDocs({
15 | defaultOptions: [],
16 | meta: {
17 | // possible error messages for the rule
18 | messages: {
19 | noUnlabelledBreadcrumb: "Accessibility: Breadcrumb must have an accessible label"
20 | },
21 | // "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
22 | type: "problem",
23 | docs: {
24 | description: "All interactive elements must have an accessible name",
25 | recommended: false,
26 | url: "https://www.w3.org/TR/html-aria/" // URL to the documentation page for this rule
27 | },
28 | schema: [] // Add a schema if the rule has options
29 | },
30 |
31 | create(context) {
32 | return {
33 | // visitor functions for different types of nodes
34 | JSXOpeningElement(node: TSESTree.JSXOpeningElement) {
35 | // if it is not a Breadcrumb, return
36 | if (elementType(node as JSXOpeningElement) !== "Breadcrumb") {
37 | return;
38 | }
39 |
40 | // if the Breadcrumb has a label, if the Breadcrumb has an associated label, return
41 | if (
42 | hasNonEmptyProp(node.attributes, "aria-label") || //aria-label
43 | hasAssociatedLabelViaAriaLabelledBy(node, context) // aria-labelledby
44 | ) {
45 | return;
46 | }
47 |
48 | // if it has no visual labelling, report error
49 | context.report({
50 | node,
51 | messageId: `noUnlabelledBreadcrumb`
52 | });
53 | }
54 | };
55 | }
56 | });
57 |
58 | export default rule;
59 |
--------------------------------------------------------------------------------
/tests/lib/rules/utils/hasDefinedProp.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { TSESTree } from "@typescript-eslint/utils";
5 | import { getProp, getPropValue, hasProp } from "jsx-ast-utils";
6 | import { hasDefinedProp } from "../../../../lib/util/hasDefinedProp";
7 |
8 | // Mocking getProp, getPropValue, and hasProp
9 | jest.mock("jsx-ast-utils", () => ({
10 | hasProp: jest.fn(),
11 | getProp: jest.fn(),
12 | getPropValue: jest.fn()
13 | }));
14 |
15 | describe("hasDefinedProp", () => {
16 | const attributes: TSESTree.JSXOpeningElement["attributes"] = [];
17 | const propName = "testProp";
18 |
19 | beforeEach(() => {
20 | jest.clearAllMocks();
21 | });
22 |
23 | it("should return false if the property does not exist", () => {
24 | (hasProp as jest.Mock).mockReturnValue(false);
25 | const result = hasDefinedProp(attributes, propName);
26 | expect(result).toBe(false);
27 | });
28 |
29 | it("should return false if the property is falsy", () => {
30 | (hasProp as jest.Mock).mockReturnValue(true);
31 | (getProp as jest.Mock).mockReturnValue(null);
32 | const result = hasDefinedProp(attributes, propName);
33 | expect(result).toBe(false);
34 | });
35 |
36 | it("should return false if the property value is undefined", () => {
37 | (hasProp as jest.Mock).mockReturnValue(true);
38 | (getProp as jest.Mock).mockReturnValue({});
39 | (getPropValue as jest.Mock).mockReturnValue(undefined);
40 | const result = hasDefinedProp(attributes, propName);
41 | expect(result).toBe(false);
42 | });
43 |
44 | it("should return false if the property value is null", () => {
45 | (hasProp as jest.Mock).mockReturnValue(true);
46 | (getProp as jest.Mock).mockReturnValue({});
47 | (getPropValue as jest.Mock).mockReturnValue(null);
48 | const result = hasDefinedProp(attributes, propName);
49 | expect(result).toBe(false);
50 | });
51 |
52 | ["non-empty string", "", 1, 0, true, false, [], {}].forEach(value => {
53 | it(`should return true if the property value is: ${JSON.stringify(value)}`, () => {
54 | (hasProp as jest.Mock).mockReturnValue(true);
55 | (getProp as jest.Mock).mockReturnValue({});
56 | (getPropValue as jest.Mock).mockReturnValue(value);
57 | const result = hasDefinedProp(attributes, propName);
58 | expect(result).toBe(true);
59 | });
60 | });
61 | });
62 |
--------------------------------------------------------------------------------
/tests/lib/rules/switch-needs-labelling.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | //------------------------------------------------------------------------------
5 | // Requirements
6 | //------------------------------------------------------------------------------
7 |
8 | import { Rule } from "eslint";
9 | import ruleTester from "./helper/ruleTester";
10 | import rule from "../../../lib/rules/switch-needs-labelling";
11 |
12 | //------------------------------------------------------------------------------
13 | // Tests
14 | //------------------------------------------------------------------------------
15 |
16 | ruleTester.run("switch-needs-labelling", rule as unknown as Rule.RuleModule, {
17 | valid: [
18 | "<>This is a Switch >",
19 | ` `,
20 | ` `,
21 | `<>This is a switch >`,
22 | `<>This is a switch >`,
23 | `<>This is a switch >`,
24 | `<>This is a switch This is a switch >`,
25 | `<>This is a switch >`
26 | ],
27 | invalid: [
28 | {
29 | code: ` `,
30 | errors: [{ messageId: "noUnlabelledSwitch" }]
31 | },
32 | {
33 | code: `<>This is a switch >`,
34 | errors: [{ messageId: "noUnlabelledSwitch" }]
35 | },
36 | {
37 | code: `<>This is a switch >`,
38 | errors: [{ messageId: "noUnlabelledSwitch" }]
39 | },
40 | {
41 | code: `<>This is a switch This is a switch >`,
42 | errors: [{ messageId: "noUnlabelledSwitch" }]
43 | }
44 | ]
45 | });
46 |
--------------------------------------------------------------------------------
/tests/lib/rules/radio-button-missing-label.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | //------------------------------------------------------------------------------
5 | // Requirements
6 | //------------------------------------------------------------------------------
7 |
8 | import { Rule } from "eslint";
9 | import ruleTester from "./helper/ruleTester";
10 | import rule from "../../../lib/rules/radio-button-missing-label";
11 |
12 | //------------------------------------------------------------------------------
13 | // Tests
14 | //------------------------------------------------------------------------------
15 |
16 | ruleTester.run("radio-button-missing-label", rule as unknown as Rule.RuleModule, {
17 | valid: [
18 | "<>This is a Radio >",
19 | `<>This is a Radio >`,
20 | ` `,
21 | ` `,
22 | ` `,
23 | ` `,
24 | `<>This is a Radio >`,
25 | `<>This is a Radio >`,
26 | `<>This is a Radio This is a Radio >`
27 | ],
28 | invalid: [
29 | {
30 | code: ` `,
31 | errors: [{ messageId: "noUnlabeledRadioButton" }]
32 | },
33 | {
34 | code: `<>This is a Radio >`,
35 | errors: [{ messageId: "noUnlabeledRadioButton" }]
36 | },
37 | {
38 | code: `<>This is a Radio >`,
39 | errors: [{ messageId: "noUnlabeledRadioButton" }]
40 | },
41 | {
42 | code: `<>This is a Radio This is a Radio >`,
43 | errors: [{ messageId: "noUnlabeledRadioButton" }]
44 | }
45 | ]
46 | });
47 |
--------------------------------------------------------------------------------
/tests/lib/rules/utils/flattenChildren.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { flattenChildren } from "../../../../lib/util/flattenChildren";
5 | import { TSESTree } from "@typescript-eslint/types";
6 |
7 | describe("flattenChildren", () => {
8 | it("should return an empty array when there are no children", () => {
9 | const node: TSESTree.JSXElement = {
10 | children: []
11 | } as any;
12 | expect(flattenChildren(node)).toEqual([]);
13 | });
14 |
15 | it("should return direct children when there are no nested children", () => {
16 | const child1: TSESTree.JSXElement = { children: [], type: "JSXElement" } as any;
17 | const child2: TSESTree.JSXElement = { children: [], type: "JSXElement" } as any;
18 | const node: TSESTree.JSXElement = {
19 | children: [child1, child2]
20 | } as any;
21 |
22 | expect(flattenChildren(node)).toEqual([child1, child2]);
23 | });
24 |
25 | it("should return a flattened array of children with nested JSXElements", () => {
26 | const nestedChild: TSESTree.JSXElement = { children: [], type: "JSXElement" } as any;
27 | const child: TSESTree.JSXElement = { children: [nestedChild], type: "JSXElement" } as any;
28 | const root: TSESTree.JSXElement = { children: [child], type: "JSXElement" } as any;
29 |
30 | expect(flattenChildren(root)).toEqual([child, nestedChild]);
31 | });
32 |
33 | it("should ignore non-JSXElement children", () => {
34 | const child: TSESTree.JSXElement = { children: [], type: "JSXElement" } as any;
35 | const nonJSXChild = { type: "JSXText", value: "Hello" } as any;
36 | const root: TSESTree.JSXElement = { children: [child, nonJSXChild], type: "JSXElement" } as any;
37 |
38 | expect(flattenChildren(root)).toEqual([child]);
39 | });
40 |
41 | it("should handle complex nesting of JSXElements", () => {
42 | const grandchild1: TSESTree.JSXElement = { children: [], type: "JSXElement" } as any;
43 | const grandchild2: TSESTree.JSXElement = { children: [], type: "JSXElement" } as any;
44 | const child1: TSESTree.JSXElement = { children: [grandchild1], type: "JSXElement" } as any;
45 | const child2: TSESTree.JSXElement = { children: [grandchild2], type: "JSXElement" } as any;
46 | const root: TSESTree.JSXElement = { children: [child1, child2], type: "JSXElement" } as any;
47 |
48 | expect(flattenChildren(root)).toEqual([child1, grandchild1, child2, grandchild2]);
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/lib/rules/spinner-needs-labelling.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
5 | import { elementType } from "jsx-ast-utils";
6 | import { hasNonEmptyProp } from "../util/hasNonEmptyProp";
7 | import { JSXOpeningElement } from "estree-jsx";
8 |
9 | //------------------------------------------------------------------------------
10 | // Rule Definition
11 | //------------------------------------------------------------------------------
12 |
13 | const rule = ESLintUtils.RuleCreator.withoutDocs({
14 | defaultOptions: [],
15 | meta: {
16 | // possible error messages for the rule
17 | messages: {
18 | noUnlabelledSpinner: "Accessibility: Spinner must have either aria-label or label, aria-live and aria-busy attributes"
19 | },
20 | // "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
21 | type: "problem",
22 | // docs for the rule
23 | docs: {
24 | description: "Accessibility: Spinner must have either aria-label or label, aria-live and aria-busy attributes",
25 | recommended: "strict",
26 | url: "https://www.w3.org/TR/html-aria/" // URL to the documentation page for this rule
27 | },
28 | schema: []
29 | },
30 | // create (function) returns an object with methods that ESLint calls to “visit” nodes while traversing the abstract syntax tree
31 | create(context) {
32 | return {
33 | // visitor functions for different types of nodes
34 | JSXOpeningElement(node: TSESTree.JSXOpeningElement) {
35 | // if it is not a Spinner, return
36 | if (elementType(node as JSXOpeningElement) !== "Spinner") {
37 | return;
38 | }
39 |
40 | if (
41 | hasNonEmptyProp(node.attributes, "aria-busy") &&
42 | hasNonEmptyProp(node.attributes, "aria-live") &&
43 | (hasNonEmptyProp(node.attributes, "label") || hasNonEmptyProp(node.attributes, "aria-label"))
44 | ) {
45 | return;
46 | }
47 |
48 | // if it has no visual labelling, report error
49 | context.report({
50 | node,
51 | messageId: `noUnlabelledSpinner`
52 | });
53 | }
54 | };
55 | }
56 | });
57 |
58 | export default rule;
59 |
--------------------------------------------------------------------------------
/tests/lib/rules/utils/hasTooltipParent.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { hasToolTipParent } from "../../../../lib/util/hasTooltipParent";
5 | import { TSESLint } from "@typescript-eslint/utils";
6 |
7 | // Mocking the elementType utility
8 | jest.mock("jsx-ast-utils", () => ({
9 | elementType: (openingElement: any) => openingElement.name.name
10 | }));
11 |
12 | describe("hasToolTipParent", () => {
13 | let mockContext: TSESLint.RuleContext;
14 |
15 | beforeEach(() => {
16 | mockContext = {
17 | getAncestors: jest.fn()
18 | } as unknown as TSESLint.RuleContext;
19 | });
20 |
21 | test("should return false when there are no ancestors", () => {
22 | (mockContext.getAncestors as jest.Mock).mockReturnValue([]);
23 |
24 | const result = hasToolTipParent(mockContext);
25 | expect(result).toBe(false);
26 | });
27 |
28 | test("should return false when no Tooltip ancestor exists", () => {
29 | const mockAncestors = [
30 | {
31 | type: "JSXElement",
32 | openingElement: {
33 | type: "JSXOpeningElement",
34 | name: { name: "Button" } // Not a Tooltip
35 | }
36 | },
37 | {
38 | type: "JSXElement",
39 | openingElement: {
40 | type: "JSXOpeningElement",
41 | name: { name: "Div" } // Not a Tooltip
42 | }
43 | }
44 | ];
45 | (mockContext.getAncestors as jest.Mock).mockReturnValue(mockAncestors);
46 |
47 | const result = hasToolTipParent(mockContext);
48 | expect(result).toBe(false);
49 | });
50 |
51 | test("should return true when a Tooltip ancestor exists", () => {
52 | const mockAncestors = [
53 | {
54 | type: "JSXElement",
55 | openingElement: {
56 | type: "JSXOpeningElement",
57 | name: { name: "Div" } // Not a Tooltip
58 | }
59 | },
60 | {
61 | type: "JSXElement",
62 | openingElement: {
63 | type: "JSXOpeningElement",
64 | name: { name: "Tooltip" } // This is a Tooltip
65 | }
66 | }
67 | ];
68 | (mockContext.getAncestors as jest.Mock).mockReturnValue(mockAncestors);
69 |
70 | const result = hasToolTipParent(mockContext);
71 | expect(result).toBe(true);
72 | });
73 | });
74 |
--------------------------------------------------------------------------------
/lib/rules/avatar-needs-name.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
5 | import { JSXOpeningElement } from "estree-jsx";
6 | import { elementType } from "jsx-ast-utils";
7 | import { hasNonEmptyProp } from "../util/hasNonEmptyProp";
8 | import { hasAssociatedLabelViaAriaLabelledBy } from "../util/labelUtils";
9 |
10 | //------------------------------------------------------------------------------
11 | // Rule Definition
12 | //------------------------------------------------------------------------------
13 |
14 | const rule = ESLintUtils.RuleCreator.withoutDocs({
15 | defaultOptions: [],
16 | meta: {
17 | // possible error messages for the rule
18 | messages: {
19 | missingAriaLabel: "Accessibility: Avatar must have an accessible name"
20 | },
21 | // "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
22 | type: "problem",
23 | // docs for the rule
24 | docs: {
25 | // DONE
26 | description: "Accessibility: Avatar must have an accessible labelling: name, aria-label, aria-labelledby",
27 | recommended: "strict",
28 | url: "https://www.w3.org/TR/html-aria/" // URL to the documentation page for this rule
29 | },
30 | schema: []
31 | },
32 | // create (function) returns an object with methods that ESLint calls to “visit” nodes while traversing the abstract syntax tree
33 | create(context) {
34 | return {
35 | // visitor functions for different types of nodes
36 | JSXOpeningElement(node: TSESTree.JSXOpeningElement) {
37 | // if it is not an Avatar, return
38 | if (elementType(node as JSXOpeningElement) !== "Avatar") {
39 | return;
40 | }
41 |
42 | // if the Avatar has a name, aria-label or aria-labelledby, return
43 | if (
44 | hasNonEmptyProp(node.attributes, "name") ||
45 | hasNonEmptyProp(node.attributes, "aria-label") ||
46 | hasAssociatedLabelViaAriaLabelledBy(node, context)
47 | ) {
48 | return;
49 | }
50 |
51 | // no aria
52 | context.report({
53 | node,
54 | messageId: `missingAriaLabel`
55 | });
56 | }
57 | };
58 | }
59 | });
60 |
61 | export default rule;
62 |
--------------------------------------------------------------------------------
/lib/rules/accordion-item-needs-header-and-panel.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { AST_NODE_TYPES, ESLintUtils, TSESTree } from "@typescript-eslint/utils";
5 |
6 | //------------------------------------------------------------------------------
7 | // Rule Definition
8 | //------------------------------------------------------------------------------
9 |
10 | const rule = ESLintUtils.RuleCreator.withoutDocs({
11 | defaultOptions: [],
12 | meta: {
13 | messages: {
14 | accordionItemOneHeaderOnePanel: "ensure AccordionItem has exactly one header and one panel"
15 | },
16 | type: "problem", // `problem`, `suggestion`, or `layout`
17 | docs: {
18 | description: "An AccordionItem needs exactly one header and one panel",
19 | recommended: "strict",
20 | url: "https://www.w3.org/WAI/ARIA/apg/patterns/accordion/" // URL to the documentation page for this rule
21 | },
22 | schema: [] // Add a schema if the rule has options
23 | },
24 |
25 | create(context) {
26 | return {
27 | JSXOpeningElement(node: TSESTree.JSXOpeningElement) {
28 | const isAccordionItem = node.name.type === AST_NODE_TYPES.JSXIdentifier && node.name.name === "AccordionItem";
29 |
30 | if (!isAccordionItem) return;
31 |
32 | if (!(node.parent && node.parent.type === AST_NODE_TYPES.JSXElement)) {
33 | return;
34 | }
35 |
36 | const children = node.parent.children.filter(child => child.type === "JSXElement");
37 |
38 | const hasOneHeader =
39 | children.filter(
40 | child =>
41 | child.openingElement.name.type === AST_NODE_TYPES.JSXIdentifier &&
42 | child.openingElement.name.name === "AccordionHeader"
43 | ).length === 1;
44 |
45 | const hasOnePanel =
46 | children.filter(
47 | child =>
48 | child.openingElement.name.type === AST_NODE_TYPES.JSXIdentifier &&
49 | child.openingElement.name.name === "AccordionPanel"
50 | ).length === 1;
51 |
52 | if (!hasOneHeader || !hasOnePanel || children.length !== 2) {
53 | context.report({
54 | node,
55 | messageId: "accordionItemOneHeaderOnePanel"
56 | });
57 | }
58 | }
59 | };
60 | }
61 | });
62 |
63 | export default rule;
64 |
--------------------------------------------------------------------------------
/docs/rules/avoid-using-aria-describedby-for-primary-labelling.md:
--------------------------------------------------------------------------------
1 | # Aria-describedby provides additional context and is not meant for primary labeling (`@microsoft/fluentui-jsx-a11y/avoid-using-aria-describedby-for-primary-labelling`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 |
6 |
7 | You should avoid using `aria-describedby` as a primary labeling mechanism because it is intended to provide supplementary or additional information, not to act as the main label for an element.
8 |
9 | **Purpose:** `aria-describedby` is designed to describe an element in more detail beyond the primary label, such as offering extended help text, usage instructions, or explanations. It’s meant to be used alongside a label, not to replace it.
10 |
11 | **Accessibility and User Experience:** Some screen readers may not announce content associated with `aria-describedby`, or users might disable this feature for various reasons, such as reducing verbosity or simplifying their experience. This makes relying on `aria-describedby` as the primary labeling mechanism risky because it can lead to critical information being missed by users who need it. Screen readers treat `aria-describedby` differently than `aria-labelledby`. The `aria-labelledby` attribute is explicitly used for primary labeling, and it ensures that the name of an element is read in the expected order and with the right emphasis. On the other hand, `aria-describedby` provides additional context that may be read after the main label, so it may confuse users if used as the primary label.
12 |
13 | ## Rule Details
14 |
15 | Examples of **incorrect** code for this rule:
16 |
17 | ```jsx
18 | Click to submit your form. This will save your data.
20 | ```
21 |
22 | ```jsx
23 |
24 | Name
25 | ```
26 |
27 | Examples of **correct** code for this rule:
28 |
29 | ```jsx
30 | Click to submit your form. This will save your data.
32 | ```
33 |
34 | ```jsx
35 | Name
36 |
37 | This field is for your full legal name.
38 | ```
39 |
40 | ## Further Reading
41 |
42 | - [ARIA Describedby: Definition & Examples for Screen Readers](https://accessiblyapp.com/blog/aria-describedby/)
43 | - [aria-describedby](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-describedby)
44 |
45 |
--------------------------------------------------------------------------------
/tests/lib/rules/radioGroup-missing-label.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | //------------------------------------------------------------------------------
5 | // Requirements
6 | //------------------------------------------------------------------------------
7 |
8 | import { Rule } from "eslint";
9 | import ruleTester from "./helper/ruleTester";
10 | import rule from "../../../lib/rules/radiogroup-missing-label";
11 |
12 | //------------------------------------------------------------------------------
13 | // Tests
14 | //------------------------------------------------------------------------------
15 |
16 | ruleTester.run("radioGroup-missing-label", rule as unknown as Rule.RuleModule, {
17 | valid: [
18 | "<>This is a RadioGroup >",
19 | `<>This is a RadioGroup >`,
20 | ` `,
21 | ` `,
22 | ` `,
23 | ` `,
24 | `<>This is a RadioGroup >`,
25 | `<>This is a RadioGroup >`,
26 | `<>This is a RadioGroup This is a RadioGroup >`
27 | ],
28 | invalid: [
29 | {
30 | code: ` `,
31 | errors: [{ messageId: "noUnlabeledRadioGroup" }]
32 | },
33 | {
34 | code: `<>This is a RadioGroup >`,
35 | errors: [{ messageId: "noUnlabeledRadioGroup" }]
36 | },
37 | {
38 | code: `<>This is a RadioGroup >`,
39 | errors: [{ messageId: "noUnlabeledRadioGroup" }]
40 | },
41 | {
42 | code: `<>This is a RadioGroup This is a RadioGroup >`,
43 | errors: [{ messageId: "noUnlabeledRadioGroup" }]
44 | }
45 | ]
46 | });
47 |
--------------------------------------------------------------------------------
/tests/lib/rules/badge-needs-accessible-name.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | //------------------------------------------------------------------------------
5 | // Requirements
6 | //------------------------------------------------------------------------------
7 |
8 | import { Rule } from "eslint";
9 | import ruleTester from "./helper/ruleTester";
10 | import rule from "../../../lib/rules/badge-needs-accessible-name";
11 |
12 | //------------------------------------------------------------------------------
13 | // Tests
14 | //------------------------------------------------------------------------------
15 |
16 | ruleTester.run("badge-needs-accessible-name", rule as unknown as Rule.RuleModule, {
17 | valid: [
18 | ` `,
19 | ` } />`,
20 | `999+
`
21 | ],
22 |
23 | invalid: [
24 | {
25 | code: ` `,
26 | errors: [{ messageId: "colourOnlyBadgesNeedAttributes" }],
27 | output: ` `
28 | },
29 | {
30 | code: ` `,
31 | errors: [{ messageId: "colourOnlyBadgesNeedAttributes" }],
32 | output: ` `
33 | },
34 | {
35 | code: ` `,
36 | errors: [{ messageId: "colourOnlyBadgesNeedAttributes" }],
37 | output: ` `
38 | },
39 | {
40 | code: ` } />`,
41 | errors: [{ messageId: "badgeIconNeedsLabelling" }],
42 | output: ` } />`
43 | },
44 | {
45 | code: ` `,
46 | errors: [{ messageId: "colourOnlyBadgesNeedAttributes" }],
47 | output: ` `
48 | },
49 | {
50 | code: ` `,
51 | errors: [{ messageId: "badgeNeedsAccessibleName" }],
52 | output: null
53 | },
54 | {
55 | code: ` `,
56 | errors: [{ messageId: "badgeNeedsAccessibleName" }],
57 | output: null
58 | }
59 | ]
60 | });
61 |
--------------------------------------------------------------------------------
/lib/rules/spin-button-needs-labelling.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
5 | import { elementType } from "jsx-ast-utils";
6 | import { hasAssociatedLabelViaAriaLabelledBy, isInsideLabelTag, hasAssociatedLabelViaHtmlFor } from "../util/labelUtils";
7 | import { hasFieldParent } from "../util/hasFieldParent";
8 | import { JSXOpeningElement } from "estree-jsx";
9 |
10 | //------------------------------------------------------------------------------
11 | // Rule Definition
12 | //------------------------------------------------------------------------------
13 |
14 | const rule = ESLintUtils.RuleCreator.withoutDocs({
15 | defaultOptions: [],
16 | meta: {
17 | // possible error messages for the rule
18 | messages: {
19 | noUnlabelledSpinButton: "Accessibility: SpinButtons must have an accessible label"
20 | },
21 | // "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
22 | type: "problem",
23 | // docs for the rule
24 | docs: {
25 | description: "Accessibility: SpinButtons must have an accessible label",
26 | recommended: "strict",
27 | url: "https://www.w3.org/TR/html-aria/" // URL to the documentation page for this rule
28 | },
29 | schema: []
30 | },
31 | // create (function) returns an object with methods that ESLint calls to “visit” nodes while traversing the abstract syntax tree
32 | create(context) {
33 | return {
34 | // visitor functions for different types of nodes
35 | JSXOpeningElement(node: TSESTree.JSXOpeningElement) {
36 | // if it is not a SpinButton, return
37 | if (elementType(node as JSXOpeningElement) !== "SpinButton") {
38 | return;
39 | }
40 |
41 | // if the SpinButton has an associated label, return
42 | if (
43 | hasFieldParent(context) ||
44 | isInsideLabelTag(context) ||
45 | hasAssociatedLabelViaHtmlFor(node, context) ||
46 | hasAssociatedLabelViaAriaLabelledBy(node, context)
47 | ) {
48 | return;
49 | }
50 |
51 | // if it has no visual labelling, report error
52 | context.report({
53 | node,
54 | messageId: `noUnlabelledSpinButton`
55 | });
56 | }
57 | };
58 | }
59 | });
60 |
61 | export default rule;
62 |
--------------------------------------------------------------------------------
/tests/lib/rules/colorswatch-needs-labelling.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { Rule } from "eslint";
5 | import ruleTester from "./helper/ruleTester";
6 | import rule from "../../../lib/rules/colorswatch-needs-labelling";
7 |
8 | // -----------------------------------------------------------------------------
9 | // Tests
10 | // -----------------------------------------------------------------------------
11 |
12 | ruleTester.run("colorswatch-needs-labelling", rule as unknown as Rule.RuleModule, {
13 | valid: [
14 | // aria-label
15 | `
16 |
17 |
18 | `,
19 |
20 | // Field wrapper with label prop
21 | `
22 |
23 |
24 |
25 |
26 |
27 |
28 | `,
29 |
30 | // tooltip parent
31 | `
32 |
33 |
34 |
35 |
36 |
37 |
38 | `,
39 |
40 | // htmlFor
41 | `
42 | <>
43 | red
44 |
45 | >
46 | `,
47 |
48 | // wrapped in label
49 | `red `,
50 |
51 | // text content child
52 | `red `
53 | ],
54 |
55 | invalid: [
56 | {
57 | // no labels
58 | code: `
59 |
60 |
61 |
62 | `,
63 | errors: [{ messageId: "noUnlabeledColorSwatch" }]
64 | },
65 | {
66 | // aria-labelledby
67 | code: `
68 | <>
69 | red
70 |
71 |
72 |
73 | >
74 | `,
75 | errors: [{ messageId: "noUnlabeledColorSwatch" }]
76 | }
77 | ]
78 | });
79 |
--------------------------------------------------------------------------------
/tests/lib/rules/spin-button-needs-labelling.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | //------------------------------------------------------------------------------
5 | // Requirements
6 | //------------------------------------------------------------------------------
7 |
8 | import { Rule } from "eslint";
9 | import ruleTester from "./helper/ruleTester";
10 | import rule from "../../../lib/rules/spin-button-needs-labelling";
11 |
12 | //------------------------------------------------------------------------------
13 | // Tests
14 | //------------------------------------------------------------------------------
15 |
16 | ruleTester.run("spin-button-needs-labelling", rule as unknown as Rule.RuleModule, {
17 | valid: [
18 | "<>This is a SpinButton >",
19 | `<>This is a spin button >`,
20 | `<>This is a spin button >`,
21 | `<>This is a spin button >`,
22 | `<>This is a spin button This is a spin button >`,
23 | `<>This is a spin button >`
24 | ],
25 | invalid: [
26 | {
27 | code: ` `,
28 | errors: [{ messageId: "noUnlabelledSpinButton" }]
29 | },
30 | {
31 | code: `<>This is a spin button >`,
32 | errors: [{ messageId: "noUnlabelledSpinButton" }]
33 | },
34 | {
35 | code: `<>This is a spin button >`,
36 | errors: [{ messageId: "noUnlabelledSpinButton" }]
37 | },
38 | {
39 | code: `<>This is a spin button This is a spin button >`,
40 | errors: [{ messageId: "noUnlabelledSpinButton" }]
41 | }
42 | ]
43 | });
44 |
--------------------------------------------------------------------------------
/tests/lib/rules/checkbox-needs-labelling.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | //------------------------------------------------------------------------------
5 | // Requirements
6 | //------------------------------------------------------------------------------
7 |
8 | import { Rule } from "eslint";
9 | import ruleTester from "./helper/ruleTester";
10 | import rule from "../../../lib/rules/checkbox-needs-labelling";
11 |
12 | //------------------------------------------------------------------------------
13 | // Tests
14 | //------------------------------------------------------------------------------
15 |
16 | ruleTester.run("checkbox-needs-labelling", rule as unknown as Rule.RuleModule, {
17 | valid: [
18 | "<>This is a Checkbox >",
19 | `<>This is a Checkbox >`,
20 | ` `,
21 | ` `,
22 | `<>This is a Checkbox >`,
23 | `<>This is a Checkbox >`,
24 | `<>This is a Checkbox This is a Checkbox >`,
25 | `<>This is a Checkbox >`,
26 | ' '
27 | ],
28 | invalid: [
29 | {
30 | code: ` `,
31 | errors: [{ messageId: "noUnlabelledCheckbox" }]
32 | },
33 | {
34 | code: `<>This is a Checkbox >`,
35 | errors: [{ messageId: "noUnlabelledCheckbox" }]
36 | },
37 | {
38 | code: `<>This is a Checkbox >`,
39 | errors: [{ messageId: "noUnlabelledCheckbox" }]
40 | },
41 | {
42 | code: `<>This is a Checkbox This is a Checkbox >`,
43 | errors: [{ messageId: "noUnlabelledCheckbox" }]
44 | },
45 | {
46 | code: "<> >",
47 | errors: [{ messageId: "noUnlabelledCheckbox" }]
48 | }
49 | ]
50 | });
51 |
--------------------------------------------------------------------------------
/docs/rules/prefer-aria-over-title-attribute.md:
--------------------------------------------------------------------------------
1 | # The title attribute is not consistently read by screen readers, and its behavior can vary depending on the screen reader and the user's settings (`@microsoft/fluentui-jsx-a11y/prefer-aria-over-title-attribute`)
2 |
3 | ⚠️ This rule _warns_ in the ✅ `recommended` config.
4 |
5 | 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6 |
7 |
8 |
9 | ⚠️ This rule _warns_ in the ✅ `recommended` config.
10 |
11 | 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
12 |
13 |
14 |
15 | 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
16 |
17 |
18 |
19 | ## Rule Details
20 |
21 | Using aria-label over the title attribute for accessibility labeling is generally recommended for several reasons:
22 |
23 | **Screen Reader Support:**
24 |
25 | aria-label is specifically designed for accessibility and is better supported by screen readers. Screen readers will consistently read out the aria-label content, making it clear to users what the purpose of the element is.
26 | The title attribute is not consistently read by screen readers, and its behavior can vary depending on the screen reader and the user's settings.
27 |
28 | **Usability:**
29 |
30 | The title attribute typically displays as a tooltip when the user hovers over the element with a mouse. However, this is not helpful for users who navigate via keyboard or touch devices, where tooltips are not displayed.
31 | aria-label provides a reliable method for labeling elements that works across different interaction methods (keyboard, touch, mouse).
32 |
33 | **Visual Focus:**
34 |
35 | The title attribute is not visible on focus, which means users navigating with a keyboard won't see the tooltip and may miss important information.
36 | aria-label ensures that the label information is available to assistive technologies regardless of how the user interacts with the page.
37 |
38 | **Consistency:**
39 |
40 | Using aria-label ensures that labeling is consistently available and presented to users of assistive technologies, promoting a more predictable and accessible user experience.
41 |
42 | Examples of **incorrect** code for this rule:
43 |
44 | ```jsx
45 | } title="hello">
46 | ```
47 |
48 | Examples of **correct** code for this rule:
49 |
50 | The aria-label will override the title attribute but the title will still display when a mouse is hovered over it.
51 |
52 | ```jsx
53 | } aria-label="hello world" title="hello world">
54 | ```
55 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Security
4 |
5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
6 |
7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below.
8 |
9 | ## Reporting Security Issues
10 |
11 | **Please do not report security vulnerabilities through public GitHub issues.**
12 |
13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report).
14 |
15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey).
16 |
17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc).
18 |
19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
20 |
21 | - Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
22 | - Full paths of source file(s) related to the manifestation of the issue
23 | - The location of the affected source code (tag/branch/commit or direct URL)
24 | - Any special configuration required to reproduce the issue
25 | - Step-by-step instructions to reproduce the issue
26 | - Proof-of-concept or exploit code (if possible)
27 | - Impact of the issue, including how an attacker might exploit the issue
28 |
29 | This information will help us triage your report more quickly.
30 |
31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs.
32 |
33 | ## Preferred Languages
34 |
35 | We prefer all communications to be in English.
36 |
37 | ## Policy
38 |
39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd).
40 |
41 |
42 |
--------------------------------------------------------------------------------
/lib/rules/switch-needs-labelling.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
5 | import { elementType } from "jsx-ast-utils";
6 | import { hasNonEmptyProp } from "../util/hasNonEmptyProp";
7 | import { hasAssociatedLabelViaAriaLabelledBy, isInsideLabelTag, hasAssociatedLabelViaHtmlFor } from "../util/labelUtils";
8 | import { hasFieldParent } from "../util/hasFieldParent";
9 | import { JSXOpeningElement } from "estree-jsx";
10 |
11 | //------------------------------------------------------------------------------
12 | // Rule Definition
13 | //------------------------------------------------------------------------------
14 |
15 | const rule = ESLintUtils.RuleCreator.withoutDocs({
16 | defaultOptions: [],
17 | meta: {
18 | // possible error messages for the rule
19 | messages: {
20 | noUnlabelledSwitch: "Accessibility: Switch must have an accessible label"
21 | },
22 | // "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
23 | type: "problem",
24 | // docs for the rule
25 | docs: {
26 | description: "Accessibility: Switch must have an accessible label",
27 | recommended: "strict",
28 | url: "https://www.w3.org/TR/html-aria/" // URL to the documentation page for this rule
29 | },
30 | schema: []
31 | },
32 | // create (function) returns an object with methods that ESLint calls to “visit” nodes while traversing the abstract syntax tree
33 | create(context) {
34 | return {
35 | // visitor functions for different types of nodes
36 | JSXOpeningElement(node: TSESTree.JSXOpeningElement) {
37 | // if it is not a Switch, return
38 | if (elementType(node as JSXOpeningElement) !== "Switch") {
39 | return;
40 | }
41 |
42 | // if the Switch has a label, if the Switch has an associated label, return
43 | if (
44 | hasNonEmptyProp(node.attributes, "label") ||
45 | hasFieldParent(context) ||
46 | isInsideLabelTag(context) ||
47 | hasAssociatedLabelViaHtmlFor(node, context) ||
48 | hasAssociatedLabelViaAriaLabelledBy(node, context)
49 | ) {
50 | return;
51 | }
52 |
53 | // if it has no visual labelling, report error
54 | context.report({
55 | node,
56 | messageId: `noUnlabelledSwitch`
57 | });
58 | }
59 | };
60 | }
61 | });
62 |
63 | export default rule;
64 |
--------------------------------------------------------------------------------
/tests/lib/rules/tag-dismissible-needs-labelling.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | import { Rule } from "eslint";
5 | import ruleTester from "./helper/ruleTester";
6 | import rule from "../../../lib/rules/tag-dismissible-needs-labelling";
7 |
8 | //------------------------------------------------------------------------------
9 | // Requirements
10 | //------------------------------------------------------------------------------
11 |
12 | //------------------------------------------------------------------------------
13 | // Tests
14 | //------------------------------------------------------------------------------
15 | ruleTester.run("tag-dismissible-needs-labelling", rule as unknown as Rule.RuleModule, {
16 | valid: [
17 | // Valid cases for dismissible Tag component
18 | // Non-dismissible tags should be ignored
19 | "Regular tag ",
20 | " }>Tag with icon",
21 | // Option 1: dismissIcon with aria-label
22 | 'Dismissible tag ',
23 | ' }>Tag with icon',
24 | // Option 2: Tag with aria-label and dismissIcon with role
25 | 'Dismissible tag '
26 | ],
27 |
28 | invalid: [
29 | // Invalid cases for dismissible Tag component
30 | {
31 | code: "Dismissible tag ",
32 | errors: [{ messageId: "missingDismissLabel" }]
33 | },
34 | {
35 | code: "Dismissible tag ",
36 | errors: [{ messageId: "missingDismissLabel" }]
37 | },
38 | {
39 | code: 'Dismissible tag ',
40 | errors: [{ messageId: "missingDismissLabel" }]
41 | },
42 | // Missing aria-label on Tag when dismissIcon has role
43 | {
44 | code: 'Dismissible tag ',
45 | errors: [{ messageId: "missingDismissLabel" }]
46 | },
47 | // Empty aria-label on Tag with dismissIcon role
48 | {
49 | code: 'Dismissible tag ',
50 | errors: [{ messageId: "missingDismissLabel" }]
51 | },
52 | // Tag has aria-label but dismissIcon has empty role
53 | {
54 | code: 'Dismissible tag ',
55 | errors: [{ messageId: "missingDismissLabel" }]
56 | }
57 | ]
58 | });
59 |
--------------------------------------------------------------------------------