Skip to content

Commit 40089c3

Browse files
committed
Merge branch 'main' into lw/adds-svg-rule
2 parents e77c35e + 3844902 commit 40089c3

7 files changed

+46
-102
lines changed

README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,16 @@ _Note: This is experimental and subject to change._
4747

4848
The `react` config includes rules which target specific HTML elements. You may provide a mapping of custom components to an HTML element in your `eslintrc` configuration to increase linter coverage.
4949

50-
For each component, you may specify a `default` and/or `props`. `default` may make sense if there's a 1:1 mapping between a component and an HTML element. However, if the HTML output of a component is dependent on a prop value, you can provide a mapping using the `props` key. To minimize conflicts and complexity, this currently only supports the mapping of a single prop type.
50+
By default, these eslint rules will check the "as" prop for underlying element changes. If your repo uses a different prop name for polymorphic components provide the prop name in your `eslintrc` configuration under `polymorphicPropName`.
5151

5252
```json
5353
{
5454
"settings": {
5555
"github": {
56+
"polymorphicPropName": "asChild",
5657
"components": {
57-
"Box": {"default": "p"},
58-
"Link": {"props": {"as": {"undefined": "a", "a": "a", "button": "button"}}}
58+
"Box": "p",
59+
"Link": "a"
5960
}
6061
}
6162
}
@@ -66,9 +67,7 @@ This config will be interpreted in the following way:
6667

6768
- All `<Box>` elements will be treated as a `p` element type.
6869
- `<Link>` without a defined `as` prop will be treated as a `a`.
69-
- `<Link as='a'>` will treated as an `a` element type.
7070
- `<Link as='button'>` will be treated as a `button` element type.
71-
- `<Link as='summary'>` will be treated as the raw `Link` type because there is no configuration set for `as='summary'`.
7271

7372
### Rules
7473

lib/rules/a11y-no-visually-hidden-interactive-element.js

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ const {generateObjSchema} = require('eslint-plugin-jsx-a11y/lib/util/schemas')
44

55
const defaultClassName = 'sr-only'
66
const defaultcomponentName = 'VisuallyHidden'
7-
const defaultHtmlPropName = 'as'
87

98
const schema = generateObjSchema({
109
className: {type: 'string'},
@@ -18,12 +17,11 @@ const schema = generateObjSchema({
1817
*/
1918
const INTERACTIVELEMENTS = ['a', 'button', 'summary', 'select', 'option', 'textarea']
2019

21-
const checkIfInteractiveElement = (context, htmlPropName, node) => {
20+
const checkIfInteractiveElement = (context, node) => {
2221
const elementType = getElementType(context, node.openingElement)
23-
const asProp = getPropValue(getProp(node.openingElement.attributes, htmlPropName))
2422

2523
for (const interactiveElement of INTERACTIVELEMENTS) {
26-
if ((asProp ?? elementType) === interactiveElement) {
24+
if (elementType === interactiveElement) {
2725
return true
2826
}
2927
}
@@ -32,14 +30,14 @@ const checkIfInteractiveElement = (context, htmlPropName, node) => {
3230

3331
// if the node is visually hidden recursively check if it has interactive children
3432
const checkIfVisuallyHiddenAndInteractive = (context, options, node, isParentVisuallyHidden) => {
35-
const {className, componentName, htmlPropName} = options
33+
const {className, componentName} = options
3634
if (node.type === 'JSXElement') {
3735
const classes = getPropValue(getProp(node.openingElement.attributes, 'className'))
3836
const isVisuallyHiddenElement = node.openingElement.name.name === componentName
3937
const hasSROnlyClass = typeof classes !== 'undefined' && classes.includes(className)
4038
let isHidden = false
4139
if (hasSROnlyClass || isVisuallyHiddenElement || !!isParentVisuallyHidden) {
42-
if (checkIfInteractiveElement(context, htmlPropName, node)) {
40+
if (checkIfInteractiveElement(context, node)) {
4341
return true
4442
}
4543
isHidden = true
@@ -69,11 +67,10 @@ module.exports = {
6967
const config = options[0] || {}
7068
const className = config.className || defaultClassName
7169
const componentName = config.componentName || defaultcomponentName
72-
const htmlPropName = config.htmlPropName || defaultHtmlPropName
7370

7471
return {
7572
JSXElement: node => {
76-
if (checkIfVisuallyHiddenAndInteractive(context, {className, componentName, htmlPropName}, node, false)) {
73+
if (checkIfVisuallyHiddenAndInteractive(context, {className, componentName}, node, false)) {
7774
context.report({
7875
node,
7976
message:

lib/utils/get-element-type.js

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,18 @@ For now, we only support the mapping of one prop type to an element type, rather
99
*/
1010
function getElementType(context, node) {
1111
const {settings} = context
12-
const rawElement = elementType(node)
13-
if (!settings) return rawElement
1412

15-
const componentMap = settings.github && settings.github.components
16-
if (!componentMap) return rawElement
17-
const component = componentMap[rawElement]
18-
if (!component) return rawElement
19-
let element = component.default ? component.default : rawElement
13+
// check if the node contains a polymorphic prop
14+
const polymorphicPropName = settings?.github?.polymorphicPropName ?? 'as'
15+
const rawElement = getPropValue(getProp(node.attributes, polymorphicPropName)) ?? elementType(node)
2016

21-
if (component.props) {
22-
const props = Object.entries(component.props)
23-
for (const [key, value] of props) {
24-
const propMap = value
25-
const propValue = getPropValue(getProp(node.attributes, key))
26-
const mapValue = propMap[propValue]
17+
// if a component configuration does not exists, return the raw element
18+
if (!settings?.github?.components?.[rawElement]) return rawElement
2719

28-
if (mapValue) {
29-
element = mapValue
30-
}
31-
}
32-
}
33-
return element
20+
const defaultComponent = settings.github.components[rawElement]
21+
22+
// check if the default component is also defined in the configuration
23+
return defaultComponent ? defaultComponent : defaultComponent
3424
}
3525

3626
module.exports = {getElementType}

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"lint:eslint-docs": "npm run update:eslint-docs -- --check",
1616
"lint:js": "eslint .",
1717
"pretest": "mkdir -p node_modules/ && ln -fs $(pwd) node_modules/",
18-
"test": "npm run eslint-check && npm run lint && mocha tests/**/*.js tests/",
18+
"test": "mocha tests/**/*.js tests/",
1919
"update:eslint-docs": "eslint-doc-generator"
2020
},
2121
"repository": {
@@ -65,4 +65,4 @@
6565
"mocha": "^10.0.0",
6666
"npm-run-all": "^4.1.5"
6767
}
68-
}
68+
}

tests/a11y-no-generic-link-text.js

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,7 @@ ruleTester.run('a11y-no-generic-link-text', rule, {
2626
settings: {
2727
github: {
2828
components: {
29-
Link: {
30-
props: {as: {undefined: 'a'}},
31-
},
29+
Link: 'a',
3230
},
3331
},
3432
},
@@ -41,9 +39,7 @@ ruleTester.run('a11y-no-generic-link-text', rule, {
4139
settings: {
4240
github: {
4341
components: {
44-
ButtonLink: {
45-
default: 'a',
46-
},
42+
ButtonLink: 'a',
4743
},
4844
},
4945
},
@@ -54,25 +50,14 @@ ruleTester.run('a11y-no-generic-link-text', rule, {
5450
settings: {
5551
github: {
5652
components: {
57-
Link: {
58-
props: {as: {undefined: 'a'}},
59-
},
53+
Link: 'a',
6054
},
6155
},
6256
},
6357
},
6458
{
6559
code: '<Test as="a" href="#">Read more</Test>',
6660
errors: [{message: errorMessage}],
67-
settings: {
68-
github: {
69-
components: {
70-
Test: {
71-
props: {as: {a: 'a'}},
72-
},
73-
},
74-
},
75-
},
7661
},
7762
{
7863
code: "<Box><a href='#'>Click here</a></Box>;",

tests/a11y-no-visually-hidden-interactive-element.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ ruleTester.run('a11y-no-visually-hidden-interactive-element', rule, {
4444
},
4545
{
4646
code: "<VisuallyHidden as='button'>Submit</VisuallyHidden>",
47-
options: [
48-
{
49-
htmlPropName: 'html',
47+
settings: {
48+
github: {
49+
polymorphicPropName: 'html',
5050
},
51-
],
51+
},
5252
},
5353
],
5454
invalid: [
@@ -86,12 +86,12 @@ ruleTester.run('a11y-no-visually-hidden-interactive-element', rule, {
8686
},
8787
{
8888
code: "<VisuallyHidden html='button'>Submit</VisuallyHidden>",
89-
options: [
90-
{
91-
htmlPropName: 'html',
92-
},
93-
],
9489
errors: [{message: errorMessage}],
90+
settings: {
91+
github: {
92+
polymorphicPropName: 'html',
93+
},
94+
},
9595
},
9696
],
9797
})

tests/utils/get-element-type.js

Lines changed: 14 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ function mockSetting(componentSetting = {}) {
3434
settings: {
3535
github: {
3636
components: componentSetting,
37+
polymorphicPropName: 'as',
3738
},
3839
},
3940
}
@@ -45,64 +46,36 @@ describe('getElementType', function () {
4546
expect(getElementType({}, node)).to.equal('a')
4647
})
4748

48-
it('returns element type from default if set', function () {
49-
const node = mockJSXOpeningElement('Link', [mockJSXAttribute('as', 'summary')])
49+
it('returns polymorphic element type', function () {
50+
const node = mockJSXOpeningElement('Link', [mockJSXAttribute('as', 'button')])
5051
const setting = mockSetting({
51-
Link: {
52-
default: 'button',
53-
},
52+
Link: 'a',
5453
})
5554
expect(getElementType(setting, node)).to.equal('button')
5655
})
5756

58-
it('returns element type from matching props setting if set', function () {
59-
const setting = mockSetting({
60-
Link: {
61-
default: 'a',
62-
props: {
63-
as: {summary: 'summary'},
64-
},
65-
},
66-
})
67-
68-
const node = mockJSXOpeningElement('Link', [mockJSXAttribute('as', 'summary')])
69-
expect(getElementType(setting, node)).to.equal('summary')
70-
})
71-
7257
it('returns raw type if no default or matching prop setting', function () {
73-
const setting = mockSetting({
74-
Link: {
75-
props: {
76-
as: {summary: 'summary'},
77-
},
78-
},
79-
})
80-
const node = mockJSXOpeningElement('Link', [mockJSXAttribute('as', 'p')])
58+
const setting = mockSetting({})
59+
60+
const node = mockJSXOpeningElement('Link')
8161
expect(getElementType(setting, node)).to.equal('Link')
8262
})
8363

84-
it('allows undefined prop to be mapped to a type', function () {
64+
it('returns default type if no polymorphic prop is passed in', function () {
8565
const setting = mockSetting({
86-
Link: {
87-
props: {
88-
as: {undefined: 'a'},
89-
},
90-
},
66+
Link: 'a',
9167
})
9268
const node = mockJSXOpeningElement('Link')
9369
expect(getElementType(setting, node)).to.equal('a')
9470
})
9571

96-
it('returns raw type if prop does not match props setting and no default type', function () {
72+
it('if rendered as another component check its default type', function () {
9773
const setting = mockSetting({
98-
Link: {
99-
props: {
100-
as: {undefined: 'a'},
101-
},
102-
},
74+
Link: 'a',
75+
Button: 'button',
10376
})
10477

105-
const node = mockJSXOpeningElement('Link', [mockJSXAttribute('as', 'p')])
106-
expect(getElementType(setting, node)).to.equal('Link')
78+
const node = mockJSXOpeningElement('Link', [mockJSXAttribute('as', 'Button')])
79+
expect(getElementType(setting, node)).to.equal('button')
10780
})
10881
})

0 commit comments

Comments
 (0)