eslint-plugin-jsx-a11y icon indicating copy to clipboard operation
eslint-plugin-jsx-a11y copied to clipboard

label-has-associated-control fails for identical string values of id and htmlFor

Open kevindice opened this issue 6 years ago • 15 comments

The following code fails on jsx-a11y/label-has-associated-control. Changing id and htmlFor to string literals results in a pass.

const DotfilesCheckBox = (props: Props) => {
  const { id, showDotfiles, toggleDotfiles } = props;

  return (
    <div
      className="checkbox"
      style={{
        marginLeft: '1.0em',
      }}
    >
      <input
        checked={showDotfiles}
        id={`DotfilesCheckbox${id}`}
        type="checkbox"
        onChange={toggleDotfiles}
      />

      <label htmlFor={`DotfilesCheckbox${id}`}>Show Dotfiles</label>
    </div>
  );
};

I could see this failing by design for mutable identifiers, but since these are defined as const, it seems safe to conclude that they will evaluate to identical strings if they are in the same block scope. I also realize there are limitations to what can be done statically.

kevindice avatar Dec 14 '18 06:12 kevindice

It's not really feasible for the linter rule to try to follow every variable in scope and try to figure out if it's the same or not.

What happens if you put the ID in a variable, and use the same variable in both places?

ljharb avatar Dec 14 '18 06:12 ljharb

Thought that might fix it, but it still fails:

const DotfilesCheckBox = (props: Props) => {
  const { id, showDotfiles, toggleDotfiles } = props;
  const inputId = `DotfilesCheckbox${id}`;

  return (
    <div
      className="checkbox"
      style={{
        marginLeft: '1.0em',
      }}
    >
      <input
        checked={showDotfiles}
        id={inputId}
        type="checkbox"
        onChange={toggleDotfiles}
      />

      <label htmlFor={inputId}>Show Dotfiles</label>
    </div>
  );
};

kevindice avatar Dec 14 '18 08:12 kevindice

I have the same issue with the code below:

const Foo = () => {
  const domId = 'foo';

  return (
    <div>
      <label htmlFor={domId}>Foo</label>
      <input type="text" id={domId} />
    </div>
  );
};

pjchender avatar Jan 02 '19 03:01 pjchender

Yep, same issue. Contrary to @ljharb's suggestion, I actually don't think it's that unfeasible for the linter to be able to handle this, since it's a somewhat common use-case. I will start on a PR.

himynameisdave avatar Jan 29 '19 05:01 himynameisdave

@himynameisdave note i said “every” :-) a PR that can catch more cases would be very appreciated!

ljharb avatar Jan 29 '19 06:01 ljharb

And I have an even simpler case where linting fails:

     <PopupContent>
        <label htmlFor="popup" style={{ margintop : '0px', marginbottom : '0px' }}>log out</label>
        <button type="button" id="popup" onClick={this.logUserOut} />
      </PopupContent>

Errors:

84:9  error  A form label must be associated with a control  jsx-a11y/label-has-associated-control
 85:9  error  A control must be associated with a text label  jsx-a11y/control-has-associated-label

JESii avatar Aug 22 '19 21:08 JESii

Nest the input in the label.

ljharb avatar Aug 22 '19 21:08 ljharb

Nope; doesn't solve the problem.

JESii avatar Aug 22 '19 22:08 JESii

ah, maybe because it's a button. buttons don't typically have labels because they contain their own CTA text, but maybe they should be considered? cc @jessebeach

ljharb avatar Aug 23 '19 04:08 ljharb

I have a very similar issue. With version 6.2.3 of the plugin the following code reports an error;

   <form>
      <label htmlFor="id1">Postalcode</label>
      <input
        id="id1"
        value={zipcode}
      />
    </form>

40:7 error A form label must be associated with a control jsx-a11y/label-has-associated-control

Please don't respond that I need to wrap the input in the label. I don't want this. In fact, my actual code is more complex, but even this simple example doesn't work

jeroenpeeters avatar Sep 24 '19 07:09 jeroenpeeters

@jeroenpeeters is your rule configured to allow the more lax arrangement of not properly wrapping the input in the label? By default i believe it requires both wrapping and linking.

ljharb avatar Sep 24 '19 07:09 ljharb

@ljharb That would be strange, because the documentation (https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/label-has-associated-control.md) clearly states that;

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

The rule is not explicitly configured, so it must be defaults then.

jeroenpeeters avatar Sep 24 '19 07:09 jeroenpeeters

Are you extending any shared configs, like airbnb, that might be setting it?

ljharb avatar Sep 24 '19 16:09 ljharb

Having the same issue with the following code. I didn't think it was an HTML requirement to wrap inputs with labels -- more like an option? So maybe it should be an option in this linter rule as well

<label htmlFor="name">Name</label>
<input
  className="form-control"
  id="name"
  type="text"
  onChange={(e) => setName(e.target.value)}
  value={name}
 />

nth-chile avatar Nov 14 '19 01:11 nth-chile

@nth-chile it's a requirement if you want to support 100% of assistive devices.

It is an option for this linter rule, but the default is to do both. The second-best option is to nest, since the majority of devices support that.

ljharb avatar Nov 14 '19 05:11 ljharb