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.22 kB
import { getProp, getPropValue } from '../util/module/jsx-ast-utils.js'; import { generateObjSchema, arraySchema } from '../util/schemas.js'; import getElementType from '../util/getElementType.js'; import mayContainChildComponent from '../util/mayContainChildComponent.js'; import mayHaveAccessibleLabel from '../util/mayHaveAccessibleLabel.js'; const errorMessage = "A form label must be associated with a control."; const schema = generateObjSchema({ labelComponents: arraySchema, labelAttributes: arraySchema, controlComponents: 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 = getProp(node.attributes, "htmlFor"); const htmlForValue = 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 }; } }; export { ruleOfLabelHasAssociatedControl as default };