UNPKG

@jsxtools/eslint-plugin-jsx-a11y

Version:

Static AST checker for accessibility rules on JSX elements for flat ESLint Config.

106 lines (103 loc) 3.26 kB
const jsxAstUtils = require('../util/module/jsx-ast-utils.cjs'); const schemas = require('../util/schemas.cjs'); const getElementType = require('../util/getElementType.cjs'); const mayContainChildComponent = require('../util/mayContainChildComponent.cjs'); const mayHaveAccessibleLabel = require('../util/mayHaveAccessibleLabel.cjs'); const errorMessage = "A form label must be associated with a control."; const schema = schemas.generateObjSchema({ labelComponents: schemas.arraySchema, labelAttributes: schemas.arraySchema, controlComponents: schemas.arraySchema, assert: { description: "Assert that the label has htmlFor, a nested label, both or either", type: "string", enum: ["htmlFor", "nesting", "both", "either"] }, depth: { description: "JSX tree depth limit to check for accessible label", type: "integer", minimum: 0 } }); const validateId = (node) => { const htmlForAttr = jsxAstUtils.getProp(node.attributes, "htmlFor"); const htmlForValue = jsxAstUtils.getPropValue(htmlForAttr); return htmlForAttr !== false && !!htmlForValue; }; const ruleOfLabelHasAssociatedControl = { meta: { docs: { description: "Enforce that a `label` tag has a text label and an associated control.", url: "https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/label-has-associated-control.md" }, schema: [schema] }, create: (context) => { const options = context.options[0] || {}; const labelComponents = options.labelComponents || []; const assertType = options.assert || "either"; const componentNames = ["label"].concat(labelComponents); const elementType = getElementType(context); const rule = (node) => { if (componentNames.indexOf(elementType(node.openingElement)) === -1) { return; } const controlComponents = [ "input", "meter", "output", "progress", "select", "textarea" ].concat(options.controlComponents || []); const recursionDepth = Math.min( options.depth === void 0 ? 2 : options.depth, 25 ); const hasLabelId = validateId(node.openingElement); const hasNestedControl = controlComponents.some((name) => mayContainChildComponent( node, name, recursionDepth, elementType )); const hasAccessibleLabel = mayHaveAccessibleLabel( node, recursionDepth, options.labelAttributes ); if (hasAccessibleLabel) { switch (assertType) { case "htmlFor": if (hasLabelId) { return; } break; case "nesting": if (hasNestedControl) { return; } break; case "both": if (hasLabelId && hasNestedControl) { return; } break; case "either": if (hasLabelId || hasNestedControl) { return; } break; } } context.report({ node: node.openingElement, message: errorMessage }); }; return { JSXElement: rule }; } }; module.exports = ruleOfLabelHasAssociatedControl;