Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[label-has-associated-control] regression - rule errors when a label does not directly have text, even if it has htmlFor #966

Closed
bradzacher opened this issue Nov 14, 2023 · 22 comments · Fixed by #1004

Comments

@bradzacher
Copy link
Contributor

bradzacher commented Nov 14, 2023

function Foo() {
  return (
    <>
      <label className={styles.linkLabel} htmlFor="1">
        <CustomText />
      </label>
      <input id="1" />
    </>
  );
}

In 6.7.1 this code was fine and did not error.
In 6.8.0 this code results in an error.

@ljharb
Copy link
Member

ljharb commented Nov 14, 2023

what’s your eslint config?

@bradzacher
Copy link
Contributor Author

Sorry forgot to mention - using the default config for the rule with no settings.

@ljharb
Copy link
Member

ljharb commented Nov 14, 2023

then in that case, I'd assume CustomInput isn't recognized as an input, which means the label does not have an associated control, and the new error is a bug fix.

If you change your settings so that CustomInput is treated as an input, I'd expect the rule to no longer warn.

@bradzacher bradzacher changed the title jsx-a11y/label-has-associated-control error with a variable htmlFor jsx-a11y/label-has-associated-control regression - rule errors when a label has a custom child Nov 14, 2023
@bradzacher
Copy link
Contributor Author

Sorry, I just revisited to do an isolated repro outside our repo and have updated the example in the OP to match the correct repro case.

screenshot of example in OP within VSCode

The change in behaviour occurs when you specifically have a custom component as the child for the label. In that case the rule (as of 6.8.0) ignores the htmlFor.

@bradzacher bradzacher changed the title jsx-a11y/label-has-associated-control regression - rule errors when a label has a custom child [label-has-associated-control] regression - rule errors when a label has a custom child, even if it has htmlFor Nov 14, 2023
@ljharb
Copy link
Member

ljharb commented Nov 14, 2023

In that case, you have two conflicting mechanisms - nesting and for/ID. What do browsers do in that case?

@bradzacher
Copy link
Contributor Author

bradzacher commented Nov 15, 2023

The code will only produce invalid HTML if you assume CustomText renders an input element. In all other cases it's technically valid.
EG the rendered HTML that browser sees could easily be something like <label htmlFor="1"><span>text</span></label><input id="1" /> - which is valid.

The rule also works fine if the CustomText element accepts a string child, eg this code does not error.

function Foo() {
  return (
    <>
      <label className={styles.linkLabel} htmlFor="1">
        <CustomText>Text</CustomText>
      </label>
      <input id="1" />
    </>
  );
}

So the rule does make some assumptions (i.e. there's no requirement that a component renders children).

From what i can tell - there's also no way around this error either. There's no config option to tell the rule "hey this component renders text it's okay" - the only way to satisfy the rule is to satisfy this utility function's checks - i.e. nested no more than depth layers deep you must have either:

  • a JSXExpressionContainer as a child,
  • JSXText as a child, or
  • a configured labelling prop on a child.

To summarise the problem:

  1. as of 6.8.0 the rule has started enforcing that the above constraint strictly where it didn't before
  2. the error message when this constraint is violated reads as A form label must be associated with a control. - which is very misleading.
  3. there is no way to opt-out of this new behaviour at all to configure an allowlist of "components that render valid label children" or similar.

I would propose that the whole "has accessible label" check be moved to its own rule entirely - it seems very weird that a rule entitled "label has associated control" also enforces that the label has valid text too.

@ljharb
Copy link
Member

ljharb commented Nov 15, 2023

Hmm, interesting problem.

I agree that since a custom component may not render its children, that a custom element with a text child can't be statically assumed to render text - that's a bug in that utility, and it should be changed to not have that false assumption.

I also agree that "label has associated control" should only be concerned with a label and a control, not whether either of those things is accessible.

I'd appreciate PRs for either or both of those issues.

@bradzacher bradzacher changed the title [label-has-associated-control] regression - rule errors when a label has a custom child, even if it has htmlFor [label-has-associated-control] regression - rule errors when a label does not directly have text, even if it has htmlFor Nov 15, 2023
@Parkreiner
Copy link

Sorry for pinging this issue! We're just encountering issues with our CI/CD, and I wanted to link a similar issue for documentation. Still ruling out whether the issue is on our end

lipemat added a commit to lipemat/eslint-config that referenced this issue Apr 10, 2024
Version 6.8.0 has false positives

jsx-eslint/eslint-plugin-jsx-a11y#966

A form label must be associated w ith a control jsx-a11y/label-has-associated-cont rol
@tswaters
Copy link

I've hit this problem with the following:

<label>
  <div>
    <input />
  </div>
</label>

Seems like any non-text node children of labels can cause some issues here.

@ljharb
Copy link
Member

ljharb commented Jun 5, 2024

@tswaters are you sure you're not configured to use both, ie, to require an id/for combo also?

@tswaters
Copy link

tswaters commented Jun 6, 2024

As far as I'm aware we're using recommended and the rule started giving false positives after an update.

In the code where we hit false positives, id/for are defined for everything. Without the div wrapping the input in the example, it works fine.... think of this as reduced test case 😁

@tswaters
Copy link

More details now that I'm back at work. Here's some sample code that raises the lint rule:

<input
	id={`${this.props.id}-time-picker`}
	name={`${this.props.id}-time-picker`}
	type="time"
	placeholder={this.props.placeholder}
	disabled={this.props.disabled || this.props.readOnly}
	className={`form-control time-input ${this.props.disabled ? 'disabled' : ''}`}
	onChange={this.handleTimeChangeFromInput}
	value={this.state.value ? moment(this.state.value).format('HH:mm') : ''}
	onBlur={this.handleBlurEventFromInput}
/>
<div className={`input-group-addon ${this.props.disabled ? 'disabled' : ''}`}>
	<label htmlFor={`${this.props.id}-time-picker`}>
		<span aria-hidden="true" className="rw-i rw-i-clock-o" />
	</label>
</div>

As a test, if I apply this diff, it doesn't raise the lint rule:

diff --git a/eos-web/components/js/common/components/DateTimePicker.jsx b/eos-web/components/js/common/components/DateTimePicker.jsx
index 51f17c8f16..89bbe216c2 100644
--- a/DateTimePicker.jsx
+++ b/DateTimePicker.jsx
@@ -340,7 +340,7 @@ class DateTimePicker extends PureComponent {
                                />
                                <div className={`input-group-addon ${this.props.disabled ? 'disabled' : ''}`}>
                                        <label htmlFor={`${this.props.id}-time-picker`}>
-                                               <span aria-hidden="true" className="rw-i rw-i-clock-o" />
+                                               This is just a text node, surely it won't work?
                                        </label>
                                </div>
                        </div>

So, this component was written in 2017 and it's been like that basically since it was written. We started linting it with a11y in ~2022-09-29 -- and between then and now, both are using v6.6.1 of this module.

I did run an npm update to cause this, maybe something has changed upstream? I looked at the diff of the npm update commit and the only thing that stood out to me was babel modules from 7.22 => 7.24.

eslint 8.24.0
@babel/core 7.24.7
babel-eslint 7.24.7
eslint-plugin-jsx-a11y: 6.6.1

@ljharb
Copy link
Member

ljharb commented Jun 10, 2024

@tswaters ok so as I try to turn this into a test case, I see that it's erroring, with or without the div around the label.

Indeed, visible text content inside the label is required, otherwise it's not an accessible label. I don't think this is a regression; I think this is a bug fix that unfortunately introduced a lint warning for you.

@bradzacher
Copy link
Contributor Author

I think what I mentioned here applies to their case as well:

I would propose that the whole "has accessible label" check be moved to its own rule entirely - it seems very weird that a rule entitled "label has associated control" also enforces that the label has valid text too.

@ljharb
Copy link
Member

ljharb commented Jun 10, 2024

True, thanks for the reminder.

The entire point of this rule is "has accessible label", though - what's the point of a label element if there's no label text in it?

@bradzacher
Copy link
Contributor Author

I see two separate concerns here:

  1. a label is associated with an input
  2. a label has accessible content

The two concerns are related to one another in the context of a11y - I.e. "an input must have a label with accessible content" - but ultimately the two concerns are separate and isolated.
One can be true without the other and they can be validated independently.

IMO (1) matches with the name of the rule but (2) doesn't.
The rule used to only check (1), but as of 6.8.0 it also checks (2).
IMO (2) deserves a second rule to enforce such a constraint.

@ljharb
Copy link
Member

ljharb commented Jun 10, 2024

Separate from organizational purity, what's the benefit of checking only the first? like, why would anyone want a label without accessible content to be present? (and why would the a11y linting plugin have that rule)

@tswaters
Copy link

IMO, the rule should be catching "oops! I forgot to add htmlFor to this label" and maybe some very basic structural checks like "this input has a label in parentNodes somewhere"

Correct me if I'm wrong, but when looking at just the AST, it wouldn't be possible to check what the underlying component actually renders?

@ljharb
Copy link
Member

ljharb commented Jun 10, 2024

@tswaters you're totally correct, and if the children of the label have any custom (non-HTML) components, then we can't know for sure either way what they render (and thus shouldn't warn on them). In the case above, though, it looks like you only have the label for an icon, which is not accessible (because it relies on non-text affordances) - can you add a text label for the icon?

@bradzacher
Copy link
Contributor Author

bradzacher commented Jun 10, 2024

Because it is not possible to validate (2) statically. To do so requires cross-file analysis.

For example imagine you have the following:

// mod1.js
export function Custom() {
  return <>Accessible label</>;
}

// mod2.js
import {Custom} from './mod1';

export function Component() {
  return (
    <>
      <label htmlFor="1">
        <Custom />
      </label>
      <input id="1" />
    </>
  );
}

This code is 100% valid and accessible - but the rule errors on it because it cannot determine that <Custom /> renders something accessible.

Such a check may be desirable in some cases - I.e. To enforce that a11y constraints are declared in some statically analysable way - but many codebases would not want this check also and would be okay with assumptions.

It would be possible to add options to configure this behaviour - including turning it off. But at that point it does seem to be better as a separate rule entirely that can be opted-into.

@tswaters
Copy link

tswaters commented Jun 10, 2024

** looks closely ** Oh shoot, that component isn't accessible at all, is it.... I'll need to double check how it behaves tomorrow when I'm back at work. I think I'm going to need to add "aria-label" to the span element. I'm pretty sure I had legit false positives though, that was just the first one I copied out 😂

@ljharb
Copy link
Member

ljharb commented Jun 11, 2024

@bradzacher in your example, I agree the linter rule shouldn't be warning on it. There are plenty of cases it can statically know nothing is rendered, but there a great many like your example where it can't know, and thus shouldn't complain.

BillyLevin added a commit to BillyLevin/eslint-plugin-jsx-a11y that referenced this issue Aug 22, 2024
Fixes jsx-eslint#966

The rule no longer errors if the existence of label text cannot be
determined
@ljharb ljharb closed this as completed in a08fbcc Aug 22, 2024
renovate bot added a commit to mmkal/eslint-plugin-mmkal that referenced this issue Sep 4, 2024
##### [v6.10.0](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/HEAD/CHANGELOG.md#v6100---2024-09-03)

##### Fixed

-   \[New] `label-has-associated-control`: add additional error message [`#1005`](jsx-eslint/eslint-plugin-jsx-a11y#1005)
-   \[Fix] `label-has-associated-control`: ignore undetermined label text [`#966`](jsx-eslint/eslint-plugin-jsx-a11y#966)

##### Commits

-   \[Tests] switch from jest to tape [`a284cbf`](jsx-eslint/eslint-plugin-jsx-a11y@a284cbf)
-   \[New] add eslint 9 support [`deac4fd`](jsx-eslint/eslint-plugin-jsx-a11y@deac4fd)
-   \[New] add `attributes` setting [`a1ee7f8`](jsx-eslint/eslint-plugin-jsx-a11y@a1ee7f8)
-   \[New] allow polymorphic linting to be restricted [`6cd1a70`](jsx-eslint/eslint-plugin-jsx-a11y@6cd1a70)
-   \[Tests] remove duplicate tests [`74d5dec`](jsx-eslint/eslint-plugin-jsx-a11y@74d5dec)
-   \[Dev Deps] update `@babel/cli`, `@babel/core`, `@babel/eslint-parser`, `@babel/plugin-transform-flow-strip-types` [`6eca235`](jsx-eslint/eslint-plugin-jsx-a11y@6eca235)
-   \[readme] remove deprecated travis ci badge; add github actions badge [`0be7ea9`](jsx-eslint/eslint-plugin-jsx-a11y@0be7ea9)
-   \[Tests] use `npm audit` instead of `aud` [`05a5e49`](jsx-eslint/eslint-plugin-jsx-a11y@05a5e49)
-   \[Deps] update `axobject-query` [`912e98c`](jsx-eslint/eslint-plugin-jsx-a11y@912e98c)
-   \[Deps] unpin `axobject-query` [`75147aa`](jsx-eslint/eslint-plugin-jsx-a11y@75147aa)
-   \[Deps] update `axe-core` [`27ff7cb`](jsx-eslint/eslint-plugin-jsx-a11y@27ff7cb)
-   \[readme] fix jsxA11y import name [`ce846e0`](jsx-eslint/eslint-plugin-jsx-a11y@ce846e0)
-   \[readme] fix typo in shareable config section in readme [`cca288b`](jsx-eslint/eslint-plugin-jsx-a11y@cca288b)
renovate bot added a commit to andrei-picus-tink/auto-renovate that referenced this issue Sep 5, 2024
| datasource | package                | from  | to     |
| ---------- | ---------------------- | ----- | ------ |
| npm        | eslint-plugin-jsx-a11y | 6.9.0 | 6.10.0 |


## [v6.10.0](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/HEAD/CHANGELOG.md#v6100---2024-09-03)

##### Fixed

-   \[New] `label-has-associated-control`: add additional error message [`#1005`](jsx-eslint/eslint-plugin-jsx-a11y#1005)
-   \[Fix] `label-has-associated-control`: ignore undetermined label text [`#966`](jsx-eslint/eslint-plugin-jsx-a11y#966)

##### Commits

-   \[Tests] switch from jest to tape [`a284cbf`](jsx-eslint/eslint-plugin-jsx-a11y@a284cbf)
-   \[New] add eslint 9 support [`deac4fd`](jsx-eslint/eslint-plugin-jsx-a11y@deac4fd)
-   \[New] add `attributes` setting [`a1ee7f8`](jsx-eslint/eslint-plugin-jsx-a11y@a1ee7f8)
-   \[New] allow polymorphic linting to be restricted [`6cd1a70`](jsx-eslint/eslint-plugin-jsx-a11y@6cd1a70)
-   \[Tests] remove duplicate tests [`74d5dec`](jsx-eslint/eslint-plugin-jsx-a11y@74d5dec)
-   \[Dev Deps] update `@babel/cli`, `@babel/core`, `@babel/eslint-parser`, `@babel/plugin-transform-flow-strip-types` [`6eca235`](jsx-eslint/eslint-plugin-jsx-a11y@6eca235)
-   \[readme] remove deprecated travis ci badge; add github actions badge [`0be7ea9`](jsx-eslint/eslint-plugin-jsx-a11y@0be7ea9)
-   \[Tests] use `npm audit` instead of `aud` [`05a5e49`](jsx-eslint/eslint-plugin-jsx-a11y@05a5e49)
-   \[Deps] update `axobject-query` [`912e98c`](jsx-eslint/eslint-plugin-jsx-a11y@912e98c)
-   \[Deps] unpin `axobject-query` [`75147aa`](jsx-eslint/eslint-plugin-jsx-a11y@75147aa)
-   \[Deps] update `axe-core` [`27ff7cb`](jsx-eslint/eslint-plugin-jsx-a11y@27ff7cb)
-   \[readme] fix jsxA11y import name [`ce846e0`](jsx-eslint/eslint-plugin-jsx-a11y@ce846e0)
-   \[readme] fix typo in shareable config section in readme [`cca288b`](jsx-eslint/eslint-plugin-jsx-a11y@cca288b)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
4 participants