label-has-associated-control

Try in Playground
jsx-a11y

oscar143

Best PracticeWarning

0

No tags

No CWE or CVE

jsx-a11y/label-has-associated-control

Enforce that a label tag has a text label and associated control.

There are two supported ways to associate a label with a control:

  • Wrapping a control in a label tag.
  • Adding htmlFor to a label and assigning it a DOM ID string that indicates an input on the page. (not covered)

This rule checks that any label tag wraps an input element, has an htmlFor attribute and that the label tag has text content.

Accessibility guidelines

  • WCAG 1.3.1
  • WCAG 3.3.2
  • WCAG 4.1.2

Ast Rule: html element


label-has-associated-control

How to write a rule
const TYPES = {
  string: getPropStringValue,
  sequence: getPropSequenceValue,
  array: getPropArrayValue,
  object: getPropObjectValue,
  object_element: getPropObjectValue,
  functiondefinition: () => {},
};

const CONTROLS = [
  'input',
  'meter',
  'output',
  'progress',
  'select',
  'textarea',
];

function getTag(node) {
  if (node && node.tag) {
    return node.tag.value;
  }
}

function getProp(attributes = [], prop = "") {
  if (!prop) return;

  return attributes.find((attribute) => {
    if (attribute && attribute.name && attribute.name.value) {
      return attribute.name.value === prop;
    }
  });
}

function getPropStringValue(value) {
  if (value.value === "true") {
    return true;
  }

  if (value.value === "false") {
    return false;
  }

  return value.value.replace(/^\"/g, "").replace(/\"$/g, "");
}

function getPropArrayValue(value) {
  const array = [];
  const arrayElements = value.elements;

  if (arrayElements.length) {
    for (const element of arrayElements) {
      array.push(TYPES[element.astType](element));
    }
  }

  return array;
}

function getPropObjectValue(value) {
  const object = {};
  const objectElements = value.elements;

  if (objectElements.length) {
    for (const element of objectElements) {
      object[element.name.value] = TYPES[element.value.astType](element.value);
    }
  }

  return object;
}

function getPropSequenceValue(value) {
  if (value.elements.length) {
    const element = value.elements[0];

    if (element) {
      return TYPES[element.astType](element);
    }
  }
}

function extractValue(attribute, extractor) {
  if (attribute) {
    // Null valued attributes imply truthiness.
    // For example: <div aria-hidden />
    if (attribute.value === null) return true;

    return extractor(attribute.value);
  }
}

function getValue(value) {
  return TYPES[value.astType](value);
}

function getPropValue(attribute) {
  return extractValue(attribute, getValue);
}

function propHasValue(attribute) {
  const value = getPropValue(attribute);

  if (typeof value !== "boolean") {
    return !!value;
  }

  return false;
}

function mayContainChildComponent(
  root,
  tag,
  maxDepth = 2,
) {
  function traverseChildren(
    node,
    depth,
  ) {
    // Bail when maxDepth is exceeded.
    if (depth > maxDepth) {
      return false;
    }

    if (node.htmlChildren) {
      for (let i = 0; i < node.htmlChildren.length; i += 1) {
        // TODO: check if expression renders a label
        const childNode = node.htmlChildren[i];

        // Check for comonents with the provided name.
        if (getTag(childNode) === tag) {
          return true;
        }

        if (traverseChildren(childNode, depth + 1)) {
          return true;
        }
      }
    }

    return false;
  }

  return traverseChildren(root, 0);
}

function visit(node, filename, code) {
  // TODO: support options maxDepth, custom controls, custom attributes, custom labels
  if (getTag(node) === "label") {
    const htmlFor = getPropValue(getProp(node.attributes, "htmlFor"));
    const nestedControl = CONTROLS.some((name) => mayContainChildComponent(node, name));
    const textNode = node.htmlChildren.find((child) => child.astType === "htmldata");
    const textContent = textNode && getValue(textNode.value);

    if (nestedControl && htmlFor && textContent) {
      return;
    }

    const error = buildError(
      node.start.line,
      node.start.col,
      node.end.line,
      node.end.col,
      'A form label must be associated with a control.',
      "ERROR",
      "BEST_PRACTICES"
    );

    addError(error);
  }
}

bad.jsx

Expected test result: has error

<label htmlFor="my-control">
  <input type="text" />
</label>

<label htmlFor="my-control">
  Surname
</label>

<label>
  <input type="text" />
  Surname
</label>

good.jsx

Expected test result: no error

<label htmlFor="my-control">
  <input type="text" />
  Surname
</label>
Add comment

Log in to add a comment


    Be the first one to leave a comment!

Codiga Logo
Codiga Hub
  • Rulesets
  • Playground
  • Snippets
  • Cookbooks
soc-2 icon

We are SOC-2 Compliance Certified

G2 high performer medal

Codiga – All rights reserved 2022.