Skip to content

Commit

Permalink
[feature] Add labelComponents glob checks in `label-has-associated-…
Browse files Browse the repository at this point in the history
…control`

Add ability for `labelComponents` within the `label-has-associated-control` to use the same glob checking mechanism as `controlComponents`.

- Ensure existing tests pass and update unit tests for new behaviour
- Add extra tests for documented `???Foo` syntax for component glob matching
- Update documentation to have appropriate examples for label/control glob usage

Closes #972
  • Loading branch information
lb- committed Oct 23, 2024
1 parent 032c2e0 commit 16e355b
Show file tree
Hide file tree
Showing 3 changed files with 12 additions and 6 deletions.
5 changes: 5 additions & 0 deletions __tests__/src/rules/label-has-associated-control-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ const htmlForValid = [
{ code: '<CustomLabel htmlFor="js_id" aria-label="A label" />', options: [{ labelComponents: ['CustomLabel'] }] },
{ code: '<CustomLabel htmlFor="js_id" label="A label" />', options: [{ labelAttributes: ['label'], labelComponents: ['CustomLabel'] }] },
{ code: '<CustomLabel htmlFor="js_id" aria-label="A label" />', settings: componentsSettings },
{ code: '<MUILabel htmlFor="js_id" aria-label="A label" />', options: [{ labelComponents: ['*Label'] }] },
{ code: '<LabelCustom htmlFor="js_id" label="A label" />', options: [{ labelAttributes: ['label'], labelComponents: ['Label*'] }] },
// Custom label attributes.
{ code: '<label htmlFor="js_id" label="A label" />', options: [{ labelAttributes: ['label'] }] },
// Glob support for controlComponents option.
Expand Down Expand Up @@ -93,6 +95,7 @@ const nestingValid = [
// Glob support for controlComponents option.
{ code: '<label><span>A label<CustomInput /></span></label>', options: [{ controlComponents: ['Custom*'] }] },
{ code: '<label><span>A label<CustomInput /></span></label>', options: [{ controlComponents: ['*Input'] }] },
{ code: '<label><span>A label<TextInput /></span></label>', options: [{ controlComponents: ['????Input'] }] },
// Rule does not error if presence of accessible label cannot be determined
{ code: '<label><CustomText /><input /></label>' },
];
Expand All @@ -105,6 +108,7 @@ const bothValid = [
// Custom label component.
{ code: '<CustomLabel htmlFor="js_id" aria-label="A label"><input /></CustomLabel>', options: [{ labelComponents: ['CustomLabel'] }] },
{ code: '<CustomLabel htmlFor="js_id" label="A label"><input /></CustomLabel>', options: [{ labelAttributes: ['label'], labelComponents: ['CustomLabel'] }] },
{ code: '<CustomLabel htmlFor="js_id" label="A label"><input /></CustomLabel>', options: [{ labelAttributes: ['label'], labelComponents: ['*Label'] }] },
{ code: '<CustomLabel htmlFor="js_id" aria-label="A label"><input /></CustomLabel>', settings: componentsSettings },
{ code: '<CustomLabel htmlFor="js_id" aria-label="A label"><CustomInput /></CustomLabel>', settings: componentsSettings },
// Custom label attributes.
Expand Down Expand Up @@ -158,6 +162,7 @@ const neverValid = [
{ code: '<div><label>A label</label><input /></div>', errors: [expectedError] },
// Custom label component.
{ code: '<CustomLabel aria-label="A label" />', options: [{ labelComponents: ['CustomLabel'] }], errors: [expectedError] },
{ code: '<MUILabel aria-label="A label" />', options: [{ labelComponents: ['???Label'] }], errors: [expectedError] },
{ code: '<CustomLabel label="A label" />', options: [{ labelAttributes: ['label'], labelComponents: ['CustomLabel'] }], errors: [expectedError] },
{ code: '<CustomLabel aria-label="A label" />', settings: componentsSettings, errors: [expectedError] },
// Custom label attributes.
Expand Down
4 changes: 2 additions & 2 deletions docs/rules/label-has-associated-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,11 @@ This rule takes one optional object argument of type object:
}
```

`labelComponents` is a list of custom React Component names that should be checked for an associated control.
`labelComponents` is a list of custom React Component names that should be checked for an associated control.[Glob format](https://linuxhint.com/bash_globbing_tutorial/) is also supported for specifying names (e.g., `Label*` matches `LabelComponent` but not `CustomLabel`, `????Label` matches `LinkLabel` but not `CustomLabel`).

`labelAttributes` is a list of attributes to check on the label component and its children for a label. Use this if you have a custom component that uses a string passed on a prop to render an HTML `label`, for example.

`controlComponents` is a list of custom React Components names that will output an input element. [Glob format](https://linuxhint.com/bash_globbing_tutorial/) is also supported for specifying names (e.g., `Label*` matches `LabelComponent` but not `CustomLabel`, `????Label` matches `LinkLabel` but not `CustomLabel`).
`controlComponents` is a list of custom React Components names that will output an input element. [Glob format](https://linuxhint.com/bash_globbing_tutorial/) is also supported for specifying names (e.g., `Input*` matches `InputCustom` but not `CustomInput`, `????Input` matches `TextInput` but not `CustomInput`).

`assert` asserts that the label has htmlFor, a nested label, both or either. Available options: `'htmlFor', 'nesting', 'both', 'either'`.

Expand Down
9 changes: 5 additions & 4 deletions src/rules/label-has-associated-control.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import { hasProp, getProp, getPropValue } from 'jsx-ast-utils';
import type { JSXElement } from 'ast-types-flow';
import minimatch from 'minimatch';
import { generateObjSchema, arraySchema } from '../util/schemas';
import type { ESLintConfig, ESLintContext, ESLintVisitorSelectorConfig } from '../../flow/eslint';
import getElementType from '../util/getElementType';
Expand Down Expand Up @@ -66,13 +67,13 @@ export default ({
const options = context.options[0] || {};
const labelComponents = options.labelComponents || [];
const assertType = options.assert || 'either';
const componentNames = ['label'].concat(labelComponents);
const labelComponentNames = ['label'].concat(labelComponents);
const elementType = getElementType(context);

const rule = (node: JSXElement) => {
if (componentNames.indexOf(elementType(node.openingElement)) === -1) {
return;
}
const isLabelComponent = labelComponentNames.some((name) => minimatch(elementType(node.openingElement), name));
if (!isLabelComponent) return;

const controlComponents = [
'input',
'meter',
Expand Down

0 comments on commit 16e355b

Please sign in to comment.