label-has-associated-control.js 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. "use strict";
  2. var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
  3. var _jsxAstUtils = require("jsx-ast-utils");
  4. var _schemas = require("../util/schemas");
  5. var _mayContainChildComponent = _interopRequireDefault(require("../util/mayContainChildComponent"));
  6. var _mayHaveAccessibleLabel = _interopRequireDefault(require("../util/mayHaveAccessibleLabel"));
  7. /**
  8. * @fileoverview Enforce label tags have an associated control.
  9. * @author Jesse Beach
  10. *
  11. *
  12. */
  13. // ----------------------------------------------------------------------------
  14. // Rule Definition
  15. // ----------------------------------------------------------------------------
  16. var errorMessage = 'A form label must be associated with a control.';
  17. var schema = (0, _schemas.generateObjSchema)({
  18. labelComponents: _schemas.arraySchema,
  19. labelAttributes: _schemas.arraySchema,
  20. controlComponents: _schemas.arraySchema,
  21. assert: {
  22. description: 'Assert that the label has htmlFor, a nested label, both or either',
  23. type: 'string',
  24. "enum": ['htmlFor', 'nesting', 'both', 'either']
  25. },
  26. depth: {
  27. description: 'JSX tree depth limit to check for accessible label',
  28. type: 'integer',
  29. minimum: 0
  30. }
  31. });
  32. var validateId = function validateId(node) {
  33. var htmlForAttr = (0, _jsxAstUtils.getProp)(node.attributes, 'htmlFor');
  34. var htmlForValue = (0, _jsxAstUtils.getPropValue)(htmlForAttr);
  35. return htmlForAttr !== false && !!htmlForValue;
  36. };
  37. module.exports = {
  38. meta: {
  39. docs: {},
  40. schema: [schema]
  41. },
  42. create: function create(context) {
  43. var options = context.options[0] || {};
  44. var labelComponents = options.labelComponents || [];
  45. var assertType = options.assert || 'either';
  46. var componentNames = ['label'].concat(labelComponents);
  47. var rule = function rule(node) {
  48. if (componentNames.indexOf((0, _jsxAstUtils.elementType)(node.openingElement)) === -1) {
  49. return;
  50. }
  51. var controlComponents = ['input', 'meter', 'output', 'progress', 'select', 'textarea'].concat(options.controlComponents || []); // Prevent crazy recursion.
  52. var recursionDepth = Math.min(options.depth === undefined ? 2 : options.depth, 25);
  53. var hasLabelId = validateId(node.openingElement); // Check for multiple control components.
  54. var hasNestedControl = controlComponents.some(function (name) {
  55. return (0, _mayContainChildComponent["default"])(node, name, recursionDepth);
  56. });
  57. var hasAccessibleLabel = (0, _mayHaveAccessibleLabel["default"])(node, recursionDepth, options.labelAttributes);
  58. if (hasAccessibleLabel) {
  59. switch (assertType) {
  60. case 'htmlFor':
  61. if (hasLabelId) {
  62. return;
  63. }
  64. break;
  65. case 'nesting':
  66. if (hasNestedControl) {
  67. return;
  68. }
  69. break;
  70. case 'both':
  71. if (hasLabelId && hasNestedControl) {
  72. return;
  73. }
  74. break;
  75. case 'either':
  76. if (hasLabelId || hasNestedControl) {
  77. return;
  78. }
  79. break;
  80. default:
  81. break;
  82. }
  83. } // htmlFor case
  84. context.report({
  85. node: node.openingElement,
  86. message: errorMessage
  87. });
  88. }; // Create visitor selectors.
  89. return {
  90. JSXElement: rule
  91. };
  92. }
  93. };