Skip to content

Commit 155e8cf

Browse files
authored
Support <MdiIcon case (#148)
1 parent b8c162b commit 155e8cf

File tree

2 files changed

+140
-3
lines changed

2 files changed

+140
-3
lines changed

packages/transforms/src/mdiIconToMdiPath/__tests__/mdiIconToMdiPath.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,58 @@ testCodemod('mdiIconToMdiPath', mdiIconToMdiPath, [
6262
`,
6363
],
6464
},
65+
{
66+
label: 'handles direct usage of mdi icons',
67+
initialSource: `
68+
import CloseIcon from 'mdi-react/CloseIcon'
69+
70+
export const Test = <CloseIcon className="hello" />`,
71+
expectedSource: `
72+
import { mdiClose } from '@mdi/js'
73+
74+
import { Icon } from '@sourcegraph/wildcard'
75+
76+
export const Test = <Icon className="hello" svgPath={mdiClose} inline={false} aria-hidden={true} />
77+
`,
78+
expectedManualChangeMessages: [
79+
`
80+
/test.tsx:3:20 - warning: <MdiIcon /> component did not have accessibility attributes and has been hidden from screen readers automatically. Please review manually
81+
>>> <Icon className="hello" svgPath={mdiClose} inline={false} aria-hidden={true} />
82+
`,
83+
],
84+
},
85+
{
86+
label: 'handles direct usage of mdi icons with existing aria attributes',
87+
initialSource: `
88+
import CloseIcon from 'mdi-react/CloseIcon'
89+
90+
export const Test = <CloseIcon className="hello" aria-label="Close" />`,
91+
expectedSource: `
92+
import { mdiClose } from '@mdi/js'
93+
94+
import { Icon } from '@sourcegraph/wildcard'
95+
96+
export const Test = <Icon className="hello" aria-label="Close" svgPath={mdiClose} inline={false} />
97+
`,
98+
},
99+
{
100+
label: 'handles direct usage of mdi icons with the size prop',
101+
initialSource: `
102+
import CloseIcon from 'mdi-react/CloseIcon'
103+
104+
export const Test = <CloseIcon className="hello" aria-label="Close" size="2rem" />
105+
export const Test2 = <CloseIcon className="hello" aria-label="Close" size={16} />`,
106+
expectedSource: `
107+
import { mdiClose } from '@mdi/js'
108+
109+
import { Icon } from '@sourcegraph/wildcard'
110+
111+
export const Test = (
112+
<Icon className="hello" aria-label="Close" svgPath={mdiClose} inline={false} height="2rem" width="2rem" />
113+
)
114+
export const Test2 = (
115+
<Icon className="hello" aria-label="Close" svgPath={mdiClose} inline={false} height={16} width={16} />
116+
)
117+
`,
118+
},
65119
])

packages/transforms/src/mdiIconToMdiPath/mdiIconToMdiPath.ts

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { Node } from 'ts-morph'
2+
3+
import { addOrUpdateSourcegraphWildcardImportIfNeeded } from '@sourcegraph/codemod-toolkit-packages'
14
import {
25
runTransform,
36
getParentUntilOrThrow,
@@ -9,7 +12,7 @@ import {
912
* Convert `<Icon as={MdiIcon} />` element to `<Icon svgPath={mdiIconPath} />` component.
1013
*/
1114
export const mdiIconToMdiPath = runTransform(context => {
12-
const { throwManualChangeError } = context
15+
const { throwManualChangeError, addManualChangeLog } = context
1316

1417
const mdiIconPathsToImport = new Set<string>()
1518

@@ -19,6 +22,76 @@ export const mdiIconToMdiPath = runTransform(context => {
1922
}
2023

2124
return {
25+
/**
26+
* Handles converting <MdiIcon /> to <Icon svgPath={mdiIcon} />
27+
*/
28+
JsxSelfClosingElement(jsxElement) {
29+
const tagElementName = jsxElement.getTagNameNode().getText()
30+
const iconRegex = /(\w*.)Icon/
31+
32+
if (!tagElementName.match(iconRegex) || !isMdiReactToken(tagElementName)) {
33+
// Not <MdiIcon component, so we exit
34+
return
35+
}
36+
37+
const updatedValue = `mdi${tagElementName.replace(iconRegex, '$1')}`
38+
39+
// e.g. update <CloseIcon /> to <Icon /> (we handle correct import later)
40+
jsxElement.set({
41+
name: 'Icon',
42+
})
43+
44+
// Add updated svgPath attribute
45+
jsxElement.addAttribute({
46+
name: 'svgPath',
47+
initializer: `{${updatedValue}}`,
48+
})
49+
50+
// Ensure `inline` is set to false to guarantee that we aren't introducing any new CSS with this change.
51+
jsxElement.addAttribute({
52+
name: 'inline',
53+
initializer: '{false}',
54+
})
55+
56+
// We need to set accessibility attributes on all icons
57+
// If these aren't already set, we default to `aria-hidden={true}` and leave a message to review.
58+
if (!jsxElement.getAttribute('aria-label') && !jsxElement.getAttribute('aria-hidden')) {
59+
jsxElement.addAttribute({
60+
name: 'aria-hidden',
61+
initializer: '{true}',
62+
})
63+
64+
addManualChangeLog({
65+
node: jsxElement,
66+
message:
67+
'<MdiIcon /> component did not have accessibility attributes and has been hidden from screen readers automatically. Please review manually',
68+
})
69+
}
70+
71+
// Our previous icon library supported a `size` prop, which set height and width.
72+
// We convert this to height and width to be explicit.
73+
const sizeAttribute = jsxElement.getAttribute('size')
74+
if (sizeAttribute && Node.isJsxAttribute(sizeAttribute)) {
75+
jsxElement.addAttribute({
76+
name: 'height',
77+
initializer: sizeAttribute.getInitializer()?.getText(),
78+
})
79+
80+
jsxElement.addAttribute({
81+
name: 'width',
82+
initializer: sizeAttribute.getInitializer()?.getText(),
83+
})
84+
85+
// Remove the old attribute
86+
jsxElement.getAttribute('size')?.remove()
87+
}
88+
89+
// Store this value so we can import it once finished with this file.
90+
mdiIconPathsToImport.add(updatedValue)
91+
},
92+
/**
93+
* Handles converting <Icon as={MdiIcon} /> to <Icon svgPath={mdiIcon} />
94+
*/
2295
JsxAttribute(jsxAttribute) {
2396
const jsxTagElement = getParentUntilOrThrow(jsxAttribute, isJsxTagElement)
2497
if (jsxTagElement.getTagNameNode().getText() !== 'Icon') {
@@ -75,9 +148,19 @@ export const mdiIconToMdiPath = runTransform(context => {
75148
namedImports: [...mdiIconPathsToImport],
76149
moduleSpecifier: '@mdi/js',
77150
})
78-
}
79151

80-
sourceFile.fixUnusedIdentifiers()
152+
// If we're using the <Icon /> component for the first time,
153+
// we need to add the import
154+
addOrUpdateSourcegraphWildcardImportIfNeeded({
155+
sourceFile,
156+
importStructure: {
157+
namedImports: ['Icon'],
158+
},
159+
})
160+
161+
// Clean up
162+
sourceFile.fixUnusedIdentifiers()
163+
}
81164
},
82165
}
83166
})

0 commit comments

Comments
 (0)