Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Accessibility Rule for dropdown #55

Merged
merged 12 commits into from
Mar 13, 2024
2 changes: 1 addition & 1 deletion COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ We currently cover the following components:
- [] DataGrid
- [] Dialog
- [N/A] Divider
- [] Dropdown
- [X] Dropdown
- [] FluentProvider
- [] Image
- [x] Input
Expand Down
44 changes: 44 additions & 0 deletions docs/rules/dropdown-needs-labelling-v9.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Accessibility: Dropdown mising label or missing aria-labelledby (`@microsoft/fluentui-jsx-a11y/dropdown-needs-labelling-v9`)

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

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
Dropdown having label linked via htmlFor in Label is recommended

<https://www.w3.org/WAI/WCAG22/Techniques/aria/ARIA16>

## Ways to fix

- Add a label with htmlFor, add the id having same value as htmlFor to dropdown.
- Add a label with id, add the aria-labelledby having same value as id to dropdown.
- Add a aria-label to dropdown

## Rule Details

This rule aims to make dropdown accessible

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

```jsx
<Dropdown />
<Dropdown aria-labelledby="dropdown-id"></Dropdown>
<>
<Label />
<Dropdown aria-labelledby="dropdown-id"></Dropdown>
</>
```

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

```jsx
<>
<Label htmlFor="dropdown-id" />
<Dropdown id="dropdown-id"></Dropdown>
</>
<>
<Label id="dropdown-id" />
<Dropdown aria-labelledby="dropdown-id"></Dropdown>
</>
<Dropdown aria-label="dropdown-label"></Dropdown>
</>
```
60 changes: 60 additions & 0 deletions lib/rules/dropdown-needs-labelling-v9.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

"use strict";

var elementType = require("jsx-ast-utils").elementType;
const { hasAssociatedLabelViaAriaLabelledBy, hasAssociatedLabelViaHtmlFor } = require("../util/labelUtils");
const { hasNonEmptyProp } = require("../util/hasNonEmptyProp");

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

module.exports = {
meta: {
// possible error messages for the rule
messages: {
missingLabelOrAriaLabeledByInDropdown: "Accessibility: Dropdown mising label or missing aria-labelledby"
},
// "problem" means the rule is identifying code that either will cause an error or may cause a confusing behavior: https://eslint.org/docs/latest/developer-guide/working-with-rules
type: "problem",
// docs for the rule
docs: {
description: "Accessibility: Dropdown menu must have an id and it needs to be linked via htmlFor of a Label",
recommended: true,
url: null
},
schema: []
},

// create (function) returns an object with methods that ESLint calls to “visit” nodes while traversing the abstract syntax tree
create(context) {
return {
// visitor functions for different types of nodes
JSXOpeningElement(node) {
// if it is not a Dropdown, return
if (elementType(node) !== "Dropdown") {
return;
}

// if the dropdown has a aria-LabeledBy with same value present in id of Label, return (Most recommended)
// if the dropdown has an id and a label with htmlFor with sanme value as id, return
// if the dropdown has an associated label, return
if (
hasAssociatedLabelViaHtmlFor(node, context) ||
hasAssociatedLabelViaAriaLabelledBy(node, context) ||
hasNonEmptyProp(node.attributes, "aria-label")
) {
return;
}

// if it has no visual labelling, report error
context.report({
node,
messageId: `missingLabelOrAriaLabeledByInDropdown`
});
}
};
}
};
3 changes: 2 additions & 1 deletion lib/rules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ module.exports = {
"image-button-missing-aria-v9": require("./image-button-missing-aria-v9"),
"toolbar-missing-aria-v9": require("./toolbar-missing-aria-v9"),
"combobox-needs-labelling-v9": require("./combobox-needs-labelling-v9"),
"no-empty-components-v9": require("./no-empty-components-v9")
"no-empty-components-v9": require("./no-empty-components-v9"),
"dropdown-needs-labelling-v9": require("./dropdown-needs-labelling-v9")
};
57 changes: 57 additions & 0 deletions tests/lib/rules/dropdown-needs-labelling-v9.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

"use strict";

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

const RuleTester = require("eslint").RuleTester;

const rule = require("../../../lib/rules/dropdown-needs-labelling-v9");

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

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

const ruleTester = new RuleTester();
ruleTester.run("dropdown-needs-labelling-v9", rule, {
valid: [
`<><Label htmlFor={comboId}>Best pet</Label> <Dropdown id={comboId} multiselect={true} placeholder="Select an animal" {...props} > {options.map((option) => ( <Option key={option} disabled={option === "Ferret"}> {option} </Option> ))}</Dropdown></>`,
`<><Label id="my-dropdownid" /><Dropdown aria-labelledby="my-dropdownid" /></>`,
`<><Label id={comboId}>Best pet</Label> <Dropdown aria-labelledby={comboId} multiselect={true} placeholder="Select an animal" {...props} > {options.map((option) => ( <Option key={option} disabled={option === "Ferret"}> {option} </Option> ))}</Dropdown></>`,
`<><Label id={comboId2}>This is a Dropdown</Label><Dropdown aria-labelledby={comboId2} /></>`
],
invalid: [
{
code: `<Dropdown multiselect={true} placeholder="Select an animal" {...props} > {options.map((option) => ( <Option key={option} disabled={option === "Ferret"}> {option} </Option> ))}</Dropdown>`,
errors: [{ messageId: "missingLabelOrAriaLabeledByInDropdown" }]
},
{
code: `<Dropdown aria-labelledby={comboId} multiselect={true} placeholder="Select an animal" {...props} > {options.map((option) => ( <Option key={option} disabled={option === "Ferret"}> {option} </Option> ))}</Dropdown>`,
errors: [{ messageId: "missingLabelOrAriaLabeledByInDropdown" }]
},
{
code: `<><Label>This is a Dropdown</Label><Dropdown aria-labelledby={comboId} multiselect={true} placeholder="Select an animal" {...props} > {options.map((option) => ( <Option key={option} disabled={option === "Ferret"}> {option} </Option> ))}</Dropdown></>`,
errors: [{ messageId: "missingLabelOrAriaLabeledByInDropdown" }]
},
{
code: `<><Label id="another-id">This is a Dropdown</Label><Dropdown aria-labelledby={comboId} multiselect={true} placeholder="Select an animal" {...props} > {options.map((option) => ( <Option key={option} disabled={option === "Ferret"}> {option} </Option> ))}</Dropdown></>`,
errors: [{ messageId: "missingLabelOrAriaLabeledByInDropdown" }]
},
{
code: `<><Label htmlFor="id1">This is a Dropdown</Label><Dropdown aria-labelledby={id1} multiselect={true} placeholder="Select an animal" {...props} > {options.map((option) => ( <Option key={option} disabled={option === "Ferret"}> {option} </Option> ))}</Dropdown></>`,
errors: [{ messageId: "missingLabelOrAriaLabeledByInDropdown" }]
}
]
});
Loading