Skip to content

Commit 00bd24a

Browse files
authored
Merge pull request #108 from Harsh-Modi278/user/harshmodi/FieldLintRule
feat: Add lint rule for Field component
2 parents 351fd16 + f4cfee3 commit 00bd24a

9 files changed

+246
-6
lines changed

COVERAGE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ We currently cover the following components:
3030
- [N/A] Divider
3131
- [] Drawer
3232
- [X] Dropdown
33-
- [] Field
33+
- [x] Field
3434
- [N/A] FluentProvider
3535
- [] Image
3636
- [] InfoLabel

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ Any use of third-party trademarks or logos are subject to those third-party's po
159159
| [dialogbody-needs-title-content-and-actions](docs/rules/dialogbody-needs-title-content-and-actions.md) | A DialogBody should have a header(DialogTitle), content(DialogContent), and footer(DialogActions) | ✅ | | |
160160
| [dialogsurface-needs-aria](docs/rules/dialogsurface-needs-aria.md) | DialogueSurface need accessible labelling: aria-describedby on DialogueSurface and aria-label or aria-labelledby(if DialogueTitle is missing) | ✅ | | |
161161
| [dropdown-needs-labelling](docs/rules/dropdown-needs-labelling.md) | Accessibility: Dropdown menu must have an id and it needs to be linked via htmlFor of a Label | ✅ | | |
162+
| [field-needs-labelling](docs/rules/field-needs-labelling.md) | Accessibility: Field must have either label, validationMessage and hint attributes | ✅ | | |
162163
| [image-button-missing-aria](docs/rules/image-button-missing-aria.md) | Accessibility: Image buttons must have accessible labelling: title, aria-label, aria-labelledby, aria-describedby | ✅ | | |
163164
| [input-components-require-accessible-name](docs/rules/input-components-require-accessible-name.md) | Accessibility: Input fields must have accessible labelling: aria-label, aria-labelledby or an associated label | ✅ | | |
164165
| [link-missing-labelling](docs/rules/link-missing-labelling.md) | Accessibility: Image links must have an accessible name. Add either text content, labelling to the image or labelling to the link itself. | ✅ | | 🔧 |

dist/lib/index.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const dialogsurface_needs_aria_1 = __importDefault(require("./rules/dialogsurfac
3333
const spinner_needs_labelling_1 = __importDefault(require("./rules/spinner-needs-labelling"));
3434
const badge_needs_accessible_name_1 = __importDefault(require("./rules/badge-needs-accessible-name"));
3535
const progressbar_needs_labelling_1 = __importDefault(require("./rules/progressbar-needs-labelling"));
36+
const field_needs_labelling_1 = __importDefault(require("./rules/field-needs-labelling"));
3637
//------------------------------------------------------------------------------
3738
// Requirements
3839
//------------------------------------------------------------------------------
@@ -68,7 +69,8 @@ module.exports = {
6869
"dialogsurface-needs-aria": dialogsurface_needs_aria_1.default,
6970
"spinner-needs-labelling": spinner_needs_labelling_1.default,
7071
"badge-needs-accessible-name": badge_needs_accessible_name_1.default,
71-
"progressbar-needs-labelling": progressbar_needs_labelling_1.default
72+
"progressbar-needs-labelling": progressbar_needs_labelling_1.default,
73+
"field-needs-labelling": field_needs_labelling_1.default
7274
},
7375
configs: {
7476
recommended: {
@@ -98,7 +100,8 @@ module.exports = {
98100
"@microsoft/fluentui-jsx-a11y/dialogbody-needs-title-content-and-actions": "error",
99101
"@microsoft/fluentui-jsx-a11y/dialogsurface-needs-aria": "error",
100102
"@microsoft/fluentui-jsx-a11y/spinner-needs-labelling": "error",
101-
"@microsoft/fluentui-jsx-a11y/progressbar-needs-labelling": "error"
103+
"@microsoft/fluentui-jsx-a11y/progressbar-needs-labelling": "error",
104+
"@microsoft/fluentui-jsx-a11y/field-needs-labelling": "error"
102105
}
103106
}
104107
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export namespace meta {
2+
namespace messages {
3+
let noUnlabelledField: string;
4+
}
5+
let type: string;
6+
namespace docs {
7+
let description: string;
8+
let recommended: boolean;
9+
let url: string;
10+
}
11+
let schema: never[];
12+
}
13+
export function create(context: any): {
14+
JSXOpeningElement(node: any): void;
15+
};
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
"use strict";
4+
const { hasNonEmptyProp } = require("../util/hasNonEmptyProp");
5+
const elementType = require("jsx-ast-utils").elementType;
6+
//------------------------------------------------------------------------------
7+
// Rule Definition
8+
//------------------------------------------------------------------------------
9+
module.exports = {
10+
meta: {
11+
// possible error messages for the rule
12+
messages: {
13+
noUnlabelledField: "Accessibility: Field must have either label, validationMessage and hint attributes"
14+
},
15+
// "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
16+
type: "problem",
17+
// docs for the rule
18+
docs: {
19+
description: "Accessibility: Field must have either label, validationMessage and hint attributes",
20+
recommended: true,
21+
url: "https://www.w3.org/TR/html-aria/" // URL to the documentation page for this rule
22+
},
23+
schema: []
24+
},
25+
// create (function) returns an object with methods that ESLint calls to “visit” nodes while traversing the abstract syntax tree
26+
create(context) {
27+
return {
28+
// visitor functions for different types of nodes
29+
JSXOpeningElement(node) {
30+
// if it is not a Spinner, return
31+
if (elementType(node) !== "Field") {
32+
return;
33+
}
34+
if (hasNonEmptyProp(node.attributes, "label", true) &&
35+
(hasNonEmptyProp(node.attributes, "validationMessage", true) || hasNonEmptyProp(node.attributes, "hint", true))) {
36+
return;
37+
}
38+
// if it has no visual labelling, report error
39+
context.report({
40+
node,
41+
messageId: `noUnlabelledField`
42+
});
43+
}
44+
};
45+
}
46+
};

docs/rules/field-needs-labelling.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Accessibility: Field must have either label, validationMessage and hint attributes (`@microsoft/fluentui-jsx-a11y/field-needs-labelling`)
2+
3+
💼 This rule is enabled in the ✅ `recommended` config.
4+
5+
<!-- end auto-generated rule header -->
6+
7+
Field must have `label` prop and either `validationMessage` or `hint` prop.
8+
9+
<https://www.w3.org/TR/html-aria/>
10+
11+
## Ways to fix
12+
13+
- Make sure that Field component has following props:
14+
- `label`
15+
- `validationMessage` or `hint`
16+
17+
## Rule Details
18+
19+
This rule aims to make Field component accessible.
20+
21+
Examples of **incorrect** code for this rule:
22+
23+
```jsx
24+
<Field
25+
label="Example field"
26+
validationState="success"
27+
>
28+
<ProgressBar value={0.5} max={1} />
29+
</Field>
30+
```
31+
32+
```jsx
33+
<Field
34+
validationState="success"
35+
hint="This is a hint."
36+
>
37+
<ProgressBar value={0.5} max={1} />
38+
</Field>
39+
```
40+
41+
Examples of **correct** code for this rule:
42+
43+
```jsx
44+
<Field
45+
label="Example field"
46+
validationState="success"
47+
validationMessage="This is a success message."
48+
>
49+
<ProgressBar value={0.5} max={1} />
50+
</Field>
51+
```
52+
53+
```jsx
54+
<Field
55+
label="Example field"
56+
validationState="success"
57+
hint="This is a hint."
58+
>
59+
<ProgressBar value={0.5} max={1} />
60+
</Field>
61+
```

lib/index.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import dialogsurfaceNeedsAria from "./rules/dialogsurface-needs-aria";
2929
import spinnerNeedsLabelling from "./rules/spinner-needs-labelling";
3030
import badgeNeedsAccessibleName from "./rules/badge-needs-accessible-name";
3131
import progressbarNeedsLabelling from "./rules/progressbar-needs-labelling";
32-
32+
import fieldNeedsLabelling from "./rules/field-needs-labelling";
3333
//------------------------------------------------------------------------------
3434
// Requirements
3535
//------------------------------------------------------------------------------
@@ -67,7 +67,8 @@ module.exports = {
6767
"dialogsurface-needs-aria": dialogsurfaceNeedsAria,
6868
"spinner-needs-labelling": spinnerNeedsLabelling,
6969
"badge-needs-accessible-name": badgeNeedsAccessibleName,
70-
"progressbar-needs-labelling": progressbarNeedsLabelling
70+
"progressbar-needs-labelling": progressbarNeedsLabelling,
71+
"field-needs-labelling": fieldNeedsLabelling
7172
},
7273
configs: {
7374
recommended: {
@@ -97,7 +98,8 @@ module.exports = {
9798
"@microsoft/fluentui-jsx-a11y/dialogbody-needs-title-content-and-actions": "error",
9899
"@microsoft/fluentui-jsx-a11y/dialogsurface-needs-aria": "error",
99100
"@microsoft/fluentui-jsx-a11y/spinner-needs-labelling": "error",
100-
"@microsoft/fluentui-jsx-a11y/progressbar-needs-labelling": "error"
101+
"@microsoft/fluentui-jsx-a11y/progressbar-needs-labelling": "error",
102+
"@microsoft/fluentui-jsx-a11y/field-needs-labelling": "error"
101103
}
102104
}
103105
}
@@ -107,3 +109,4 @@ module.exports = {
107109
module.exports.processors = {
108110
// add your processors here
109111
};
112+

lib/rules/field-needs-labelling.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
"use strict";
5+
6+
const { hasNonEmptyProp } = require("../util/hasNonEmptyProp");
7+
const elementType = require("jsx-ast-utils").elementType;
8+
9+
//------------------------------------------------------------------------------
10+
// Rule Definition
11+
//------------------------------------------------------------------------------
12+
13+
module.exports = {
14+
meta: {
15+
// possible error messages for the rule
16+
messages: {
17+
noUnlabelledField: "Accessibility: Field must have either label, validationMessage and hint attributes"
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: "Accessibility: Field must have either label, validationMessage and hint attributes",
24+
recommended: true,
25+
url: "https://www.w3.org/TR/html-aria/" // URL to the documentation page for this rule
26+
},
27+
schema: []
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+
JSXOpeningElement(node) {
34+
// if it is not a Spinner, return
35+
if (elementType(node) !== "Field") {
36+
return;
37+
}
38+
39+
if (
40+
hasNonEmptyProp(node.attributes, "label", true) &&
41+
(hasNonEmptyProp(node.attributes, "validationMessage", true) || hasNonEmptyProp(node.attributes, "hint", true))
42+
) {
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+
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
"use strict";
5+
6+
//------------------------------------------------------------------------------
7+
// Requirements
8+
//------------------------------------------------------------------------------
9+
10+
const rule = require("../../../lib/rules/field-needs-labelling"),
11+
RuleTester = require("eslint").RuleTester;
12+
13+
//------------------------------------------------------------------------------
14+
// Tests
15+
//------------------------------------------------------------------------------
16+
17+
const ruleTester = new RuleTester();
18+
ruleTester.run("field-needs-labelling", rule, {
19+
valid: [
20+
`<Field
21+
label="Example field"
22+
validationState="success"
23+
validationMessage="This is a success message."
24+
>
25+
<ProgressBar value={0.5} max={1} />
26+
</Field>`,
27+
`<Field
28+
label="Example field"
29+
validationState="success"
30+
hint="This is a hint."
31+
>
32+
<ProgressBar value={0.5} max={1} />
33+
</Field>`
34+
],
35+
invalid: [
36+
{
37+
code: `<Field
38+
label="Example field"
39+
validationState="success"
40+
>
41+
<ProgressBar value={0.5} max={1} />
42+
</Field>`,
43+
errors: [{ messageId: "noUnlabelledField" }]
44+
},
45+
{
46+
code: `<Field
47+
validationState="success"
48+
hint="This is a hint."
49+
>
50+
<ProgressBar value={0.5} max={1} />
51+
</Field>`,
52+
errors: [{ messageId: "noUnlabelledField" }]
53+
}
54+
]
55+
});
56+

0 commit comments

Comments
 (0)