diff --git a/COVERAGE.md b/COVERAGE.md index 3a8ebc0..70e187c 100644 --- a/COVERAGE.md +++ b/COVERAGE.md @@ -30,7 +30,7 @@ We currently cover the following components: - [N/A] Divider - [] Drawer - [X] Dropdown - - [] Field + - [x] Field - [N/A] FluentProvider - [] Image - [] InfoLabel diff --git a/README.md b/README.md index e71727f..d269b58 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,7 @@ Any use of third-party trademarks or logos are subject to those third-party's po | [dialogbody-needs-title-content-and-actions](docs/rules/dialogbody-needs-title-content-and-actions.md) | A DialogBody should have a header(DialogTitle), content(DialogContent), and footer(DialogActions) | ✅ | | | | [dialogsurface-needs-aria](docs/rules/dialogsurface-needs-aria.md) | DialogueSurface need accessible labelling: aria-describedby on DialogueSurface and aria-label or aria-labelledby(if DialogueTitle is missing) | ✅ | | | | [dropdown-needs-labelling](docs/rules/dropdown-needs-labelling.md) | Accessibility: Dropdown menu must have an id and it needs to be linked via htmlFor of a Label | ✅ | | | +| [field-needs-labelling](docs/rules/field-needs-labelling.md) | Accessibility: Field must have either label, validationMessage and hint attributes | ✅ | | | | [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 | ✅ | | | | [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 | ✅ | | | | [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. | ✅ | | 🔧 | diff --git a/dist/lib/index.js b/dist/lib/index.js index 1d25d3d..e1784d5 100644 --- a/dist/lib/index.js +++ b/dist/lib/index.js @@ -33,6 +33,7 @@ const dialogsurface_needs_aria_1 = __importDefault(require("./rules/dialogsurfac const spinner_needs_labelling_1 = __importDefault(require("./rules/spinner-needs-labelling")); const badge_needs_accessible_name_1 = __importDefault(require("./rules/badge-needs-accessible-name")); const progressbar_needs_labelling_1 = __importDefault(require("./rules/progressbar-needs-labelling")); +const field_needs_labelling_1 = __importDefault(require("./rules/field-needs-labelling")); //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ @@ -68,7 +69,8 @@ module.exports = { "dialogsurface-needs-aria": dialogsurface_needs_aria_1.default, "spinner-needs-labelling": spinner_needs_labelling_1.default, "badge-needs-accessible-name": badge_needs_accessible_name_1.default, - "progressbar-needs-labelling": progressbar_needs_labelling_1.default + "progressbar-needs-labelling": progressbar_needs_labelling_1.default, + "field-needs-labelling": field_needs_labelling_1.default }, configs: { recommended: { @@ -98,7 +100,8 @@ module.exports = { "@microsoft/fluentui-jsx-a11y/dialogbody-needs-title-content-and-actions": "error", "@microsoft/fluentui-jsx-a11y/dialogsurface-needs-aria": "error", "@microsoft/fluentui-jsx-a11y/spinner-needs-labelling": "error", - "@microsoft/fluentui-jsx-a11y/progressbar-needs-labelling": "error" + "@microsoft/fluentui-jsx-a11y/progressbar-needs-labelling": "error", + "@microsoft/fluentui-jsx-a11y/field-needs-labelling": "error" } } } diff --git a/dist/lib/rules/field-needs-labelling.d.ts b/dist/lib/rules/field-needs-labelling.d.ts new file mode 100644 index 0000000..01fa01d --- /dev/null +++ b/dist/lib/rules/field-needs-labelling.d.ts @@ -0,0 +1,15 @@ +export namespace meta { + namespace messages { + let noUnlabelledField: string; + } + let type: string; + namespace docs { + let description: string; + let recommended: boolean; + let url: string; + } + let schema: never[]; +} +export function create(context: any): { + JSXOpeningElement(node: any): void; +}; diff --git a/dist/lib/rules/field-needs-labelling.js b/dist/lib/rules/field-needs-labelling.js new file mode 100644 index 0000000..eb3af8e --- /dev/null +++ b/dist/lib/rules/field-needs-labelling.js @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +"use strict"; +const { hasNonEmptyProp } = require("../util/hasNonEmptyProp"); +const elementType = require("jsx-ast-utils").elementType; +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ +module.exports = { + meta: { + // possible error messages for the rule + messages: { + noUnlabelledField: "Accessibility: Field must have either label, validationMessage and hint attributes" + }, + // "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: Field must have either label, validationMessage and hint attributes", + recommended: true, + url: "https://www.w3.org/TR/html-aria/" // URL to the documentation page for this rule + }, + 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 Spinner, return + if (elementType(node) !== "Field") { + return; + } + if (hasNonEmptyProp(node.attributes, "label", true) && + (hasNonEmptyProp(node.attributes, "validationMessage", true) || hasNonEmptyProp(node.attributes, "hint", true))) { + return; + } + // if it has no visual labelling, report error + context.report({ + node, + messageId: `noUnlabelledField` + }); + } + }; + } +}; diff --git a/docs/rules/field-needs-labelling.md b/docs/rules/field-needs-labelling.md new file mode 100644 index 0000000..37fb792 --- /dev/null +++ b/docs/rules/field-needs-labelling.md @@ -0,0 +1,61 @@ +# Accessibility: Field must have either label, validationMessage and hint attributes (`@microsoft/fluentui-jsx-a11y/field-needs-labelling`) + +💼 This rule is enabled in the ✅ `recommended` config. + + + +Field must have `label` prop and either `validationMessage` or `hint` prop. + + + +## Ways to fix + +- Make sure that Field component has following props: + - `label` + - `validationMessage` or `hint` + +## Rule Details + +This rule aims to make Field component accessible. + +Examples of **incorrect** code for this rule: + +```jsx + + + +``` + +```jsx + + + +``` + +Examples of **correct** code for this rule: + +```jsx + + + +``` + +```jsx + + + +``` diff --git a/lib/index.ts b/lib/index.ts index d7ee438..3fcff30 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -29,7 +29,7 @@ import dialogsurfaceNeedsAria from "./rules/dialogsurface-needs-aria"; import spinnerNeedsLabelling from "./rules/spinner-needs-labelling"; import badgeNeedsAccessibleName from "./rules/badge-needs-accessible-name"; import progressbarNeedsLabelling from "./rules/progressbar-needs-labelling"; - +import fieldNeedsLabelling from "./rules/field-needs-labelling"; //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ @@ -67,7 +67,8 @@ module.exports = { "dialogsurface-needs-aria": dialogsurfaceNeedsAria, "spinner-needs-labelling": spinnerNeedsLabelling, "badge-needs-accessible-name": badgeNeedsAccessibleName, - "progressbar-needs-labelling": progressbarNeedsLabelling + "progressbar-needs-labelling": progressbarNeedsLabelling, + "field-needs-labelling": fieldNeedsLabelling }, configs: { recommended: { @@ -97,7 +98,8 @@ module.exports = { "@microsoft/fluentui-jsx-a11y/dialogbody-needs-title-content-and-actions": "error", "@microsoft/fluentui-jsx-a11y/dialogsurface-needs-aria": "error", "@microsoft/fluentui-jsx-a11y/spinner-needs-labelling": "error", - "@microsoft/fluentui-jsx-a11y/progressbar-needs-labelling": "error" + "@microsoft/fluentui-jsx-a11y/progressbar-needs-labelling": "error", + "@microsoft/fluentui-jsx-a11y/field-needs-labelling": "error" } } } @@ -107,3 +109,4 @@ module.exports = { module.exports.processors = { // add your processors here }; + diff --git a/lib/rules/field-needs-labelling.js b/lib/rules/field-needs-labelling.js new file mode 100644 index 0000000..4db33e4 --- /dev/null +++ b/lib/rules/field-needs-labelling.js @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +"use strict"; + +const { hasNonEmptyProp } = require("../util/hasNonEmptyProp"); +const elementType = require("jsx-ast-utils").elementType; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + // possible error messages for the rule + messages: { + noUnlabelledField: "Accessibility: Field must have either label, validationMessage and hint attributes" + }, + // "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: Field must have either label, validationMessage and hint attributes", + recommended: true, + url: "https://www.w3.org/TR/html-aria/" // URL to the documentation page for this rule + }, + 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 Spinner, return + if (elementType(node) !== "Field") { + return; + } + + if ( + hasNonEmptyProp(node.attributes, "label", true) && + (hasNonEmptyProp(node.attributes, "validationMessage", true) || hasNonEmptyProp(node.attributes, "hint", true)) + ) { + return; + } + + // if it has no visual labelling, report error + context.report({ + node, + messageId: `noUnlabelledField` + }); + } + }; + } +}; + diff --git a/tests/lib/rules/field-needs-labelling.js b/tests/lib/rules/field-needs-labelling.js new file mode 100644 index 0000000..56fb68e --- /dev/null +++ b/tests/lib/rules/field-needs-labelling.js @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const rule = require("../../../lib/rules/field-needs-labelling"), + RuleTester = require("eslint").RuleTester; + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester(); +ruleTester.run("field-needs-labelling", rule, { + valid: [ + ` + + `, + ` + + ` + ], + invalid: [ + { + code: ` + + `, + errors: [{ messageId: "noUnlabelledField" }] + }, + { + code: ` + + `, + errors: [{ messageId: "noUnlabelledField" }] + } + ] +}); +