label-has-associated-control.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports["default"] = void 0;
  6. var _jsxAstUtils = require("jsx-ast-utils");
  7. var _schemas = require("../util/schemas");
  8. var _getElementType = _interopRequireDefault(require("../util/getElementType"));
  9. var _mayContainChildComponent = _interopRequireDefault(require("../util/mayContainChildComponent"));
  10. var _mayHaveAccessibleLabel = _interopRequireDefault(require("../util/mayHaveAccessibleLabel"));
  11. function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; }
  12. /**
  13. * @fileoverview Enforce label tags have an associated control.
  14. * @author Jesse Beach
  15. *
  16. *
  17. */
  18. // ----------------------------------------------------------------------------
  19. // Rule Definition
  20. // ----------------------------------------------------------------------------
  21. var errorMessage = 'A form label must be associated with a control.';
  22. var errorMessageNoLabel = 'A form label must have accessible text.';
  23. var schema = (0, _schemas.generateObjSchema)({
  24. labelComponents: _schemas.arraySchema,
  25. labelAttributes: _schemas.arraySchema,
  26. controlComponents: _schemas.arraySchema,
  27. assert: {
  28. description: 'Assert that the label has htmlFor, a nested label, both or either',
  29. type: 'string',
  30. "enum": ['htmlFor', 'nesting', 'both', 'either']
  31. },
  32. depth: {
  33. description: 'JSX tree depth limit to check for accessible label',
  34. type: 'integer',
  35. minimum: 0
  36. }
  37. });
  38. function validateID(node, context) {
  39. var _settings$jsxA11y$at, _settings$jsxA11y, _settings$jsxA11y$att;
  40. var settings = context.settings;
  41. var htmlForAttributes = (_settings$jsxA11y$at = (_settings$jsxA11y = settings['jsx-a11y']) === null || _settings$jsxA11y === void 0 ? void 0 : (_settings$jsxA11y$att = _settings$jsxA11y.attributes) === null || _settings$jsxA11y$att === void 0 ? void 0 : _settings$jsxA11y$att["for"]) !== null && _settings$jsxA11y$at !== void 0 ? _settings$jsxA11y$at : ['htmlFor'];
  42. for (var i = 0; i < htmlForAttributes.length; i += 1) {
  43. var attribute = htmlForAttributes[i];
  44. if ((0, _jsxAstUtils.hasProp)(node.attributes, attribute)) {
  45. var htmlForAttr = (0, _jsxAstUtils.getProp)(node.attributes, attribute);
  46. var htmlForValue = (0, _jsxAstUtils.getPropValue)(htmlForAttr);
  47. return htmlForAttr !== false && !!htmlForValue;
  48. }
  49. }
  50. return false;
  51. }
  52. var _default = exports["default"] = {
  53. meta: {
  54. docs: {
  55. description: 'Enforce that a `label` tag has a text label and an associated control.',
  56. url: 'https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/label-has-associated-control.md'
  57. },
  58. schema: [schema]
  59. },
  60. create: function create(context) {
  61. var options = context.options[0] || {};
  62. var labelComponents = options.labelComponents || [];
  63. var assertType = options.assert || 'either';
  64. var componentNames = ['label'].concat(labelComponents);
  65. var elementType = (0, _getElementType["default"])(context);
  66. var rule = function rule(node) {
  67. if (componentNames.indexOf(elementType(node.openingElement)) === -1) {
  68. return;
  69. }
  70. var controlComponents = ['input', 'meter', 'output', 'progress', 'select', 'textarea'].concat(options.controlComponents || []);
  71. // Prevent crazy recursion.
  72. var recursionDepth = Math.min(options.depth === undefined ? 2 : options.depth, 25);
  73. var hasLabelId = validateID(node.openingElement, context);
  74. // Check for multiple control components.
  75. var hasNestedControl = controlComponents.some(function (name) {
  76. return (0, _mayContainChildComponent["default"])(node, name, recursionDepth, elementType);
  77. });
  78. var hasAccessibleLabel = (0, _mayHaveAccessibleLabel["default"])(node, recursionDepth, options.labelAttributes, elementType, controlComponents);
  79. if (!hasAccessibleLabel) {
  80. context.report({
  81. node: node.openingElement,
  82. message: errorMessageNoLabel
  83. });
  84. return;
  85. }
  86. switch (assertType) {
  87. case 'htmlFor':
  88. if (hasLabelId) {
  89. return;
  90. }
  91. break;
  92. case 'nesting':
  93. if (hasNestedControl) {
  94. return;
  95. }
  96. break;
  97. case 'both':
  98. if (hasLabelId && hasNestedControl) {
  99. return;
  100. }
  101. break;
  102. case 'either':
  103. if (hasLabelId || hasNestedControl) {
  104. return;
  105. }
  106. break;
  107. default:
  108. break;
  109. }
  110. // htmlFor case
  111. context.report({
  112. node: node.openingElement,
  113. message: errorMessage
  114. });
  115. };
  116. // Create visitor selectors.
  117. return {
  118. JSXElement: rule
  119. };
  120. }
  121. };
  122. module.exports = exports.default;