diff --git a/README.md b/README.md index 283f0d39..e5119223 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,8 @@ For example, if you set the `polymorphicPropName` setting to `as` then this elem will be evaluated as an `h3`. If no `polymorphicPropName` is set, then the component will be evaluated as `Box`. +To restrict polymorphic linting to specified components, additionally set `polymorphicAllowList` to an array of component names. + ⚠️ Polymorphic components can make code harder to maintain; please use this feature with caution. ## Supported Rules diff --git a/__tests__/src/util/getElementType-test.js b/__tests__/src/util/getElementType-test.js index 97127687..81b6ff42 100644 --- a/__tests__/src/util/getElementType-test.js +++ b/__tests__/src/util/getElementType-test.js @@ -106,5 +106,49 @@ test('getElementType', (t) => { st.end(); }); + t.test('polymorphicPropName settings and explicitly defined polymorphicAllowList in context', (st) => { + const elementType = getElementType({ + settings: { + 'jsx-a11y': { + polymorphicPropName: 'asChild', + polymorphicAllowList: [ + 'Box', + 'Icon', + ], + components: { + Box: 'div', + Icon: 'svg', + }, + }, + }, + }); + + st.equal( + elementType(JSXElementMock('Spinner', [JSXAttributeMock('asChild', 'img')]).openingElement), + 'Spinner', + 'does not use the polymorphic prop if polymorphicAllowList is defined, but element is not part of polymorphicAllowList', + ); + + st.equal( + elementType(JSXElementMock('Icon', [JSXAttributeMock('asChild', 'img')]).openingElement), + 'img', + 'uses the polymorphic prop if it is in explicitly defined polymorphicAllowList', + ); + + st.equal( + elementType(JSXElementMock('Box', [JSXAttributeMock('asChild', 'span')]).openingElement), + 'span', + 'returns the tag name provided by the polymorphic prop, "asChild", defined in the settings instead of the component mapping tag', + ); + + st.equal( + elementType(JSXElementMock('Box', [JSXAttributeMock('as', 'a')]).openingElement), + 'div', + 'returns the tag name provided by the component mapping if the polymorphic prop, "asChild", defined in the settings is not set', + ); + + st.end(); + }); + t.end(); }); diff --git a/flow/eslint.js b/flow/eslint.js index 670d1ab1..af299b6b 100644 --- a/flow/eslint.js +++ b/flow/eslint.js @@ -9,9 +9,10 @@ export type ESLintReport = { export type ESLintSettings = { [string]: mixed, 'jsx-a11y'?: { - polymorphicPropName?: string, components?: { [string]: string }, attributes?: { for?: string[] }, + polymorphicPropName?: string, + polymorphicAllowList?: Array, }, } diff --git a/src/util/getElementType.js b/src/util/getElementType.js index c735a805..1fdab721 100644 --- a/src/util/getElementType.js +++ b/src/util/getElementType.js @@ -4,6 +4,7 @@ import type { JSXOpeningElement } from 'ast-types-flow'; import hasOwn from 'hasown'; +import includes from 'array-includes'; import { elementType, getProp, getLiteralPropValue } from 'jsx-ast-utils'; import type { ESLintContext } from '../../flow/eslint'; @@ -11,11 +12,20 @@ import type { ESLintContext } from '../../flow/eslint'; const getElementType = (context: ESLintContext): ((node: JSXOpeningElement) => string) => { const { settings } = context; const polymorphicPropName = settings['jsx-a11y']?.polymorphicPropName; + const polymorphicAllowList = settings['jsx-a11y']?.polymorphicAllowList; + const componentMap = settings['jsx-a11y']?.components; return (node: JSXOpeningElement): string => { const polymorphicProp = polymorphicPropName ? getLiteralPropValue(getProp(node.attributes, polymorphicPropName)) : undefined; - const rawType = polymorphicProp ?? elementType(node); + + let rawType = elementType(node); + if ( + polymorphicProp + && (!polymorphicAllowList || includes(polymorphicAllowList, rawType)) + ) { + rawType = polymorphicProp; + } if (!componentMap) { return rawType;