Skip to content

Commit

Permalink
Support <MdiIcon case (#148)
Browse files Browse the repository at this point in the history
  • Loading branch information
umpox authored Jul 12, 2022
1 parent b8c162b commit 155e8cf
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,58 @@ testCodemod('mdiIconToMdiPath', mdiIconToMdiPath, [
`,
],
},
{
label: 'handles direct usage of mdi icons',
initialSource: `
import CloseIcon from 'mdi-react/CloseIcon'
export const Test = <CloseIcon className="hello" />`,
expectedSource: `
import { mdiClose } from '@mdi/js'
import { Icon } from '@sourcegraph/wildcard'
export const Test = <Icon className="hello" svgPath={mdiClose} inline={false} aria-hidden={true} />
`,
expectedManualChangeMessages: [
`
/test.tsx:3:20 - warning: <MdiIcon /> component did not have accessibility attributes and has been hidden from screen readers automatically. Please review manually
>>> <Icon className="hello" svgPath={mdiClose} inline={false} aria-hidden={true} />
`,
],
},
{
label: 'handles direct usage of mdi icons with existing aria attributes',
initialSource: `
import CloseIcon from 'mdi-react/CloseIcon'
export const Test = <CloseIcon className="hello" aria-label="Close" />`,
expectedSource: `
import { mdiClose } from '@mdi/js'
import { Icon } from '@sourcegraph/wildcard'
export const Test = <Icon className="hello" aria-label="Close" svgPath={mdiClose} inline={false} />
`,
},
{
label: 'handles direct usage of mdi icons with the size prop',
initialSource: `
import CloseIcon from 'mdi-react/CloseIcon'
export const Test = <CloseIcon className="hello" aria-label="Close" size="2rem" />
export const Test2 = <CloseIcon className="hello" aria-label="Close" size={16} />`,
expectedSource: `
import { mdiClose } from '@mdi/js'
import { Icon } from '@sourcegraph/wildcard'
export const Test = (
<Icon className="hello" aria-label="Close" svgPath={mdiClose} inline={false} height="2rem" width="2rem" />
)
export const Test2 = (
<Icon className="hello" aria-label="Close" svgPath={mdiClose} inline={false} height={16} width={16} />
)
`,
},
])
89 changes: 86 additions & 3 deletions packages/transforms/src/mdiIconToMdiPath/mdiIconToMdiPath.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { Node } from 'ts-morph'

import { addOrUpdateSourcegraphWildcardImportIfNeeded } from '@sourcegraph/codemod-toolkit-packages'
import {
runTransform,
getParentUntilOrThrow,
Expand All @@ -9,7 +12,7 @@ import {
* Convert `<Icon as={MdiIcon} />` element to `<Icon svgPath={mdiIconPath} />` component.
*/
export const mdiIconToMdiPath = runTransform(context => {
const { throwManualChangeError } = context
const { throwManualChangeError, addManualChangeLog } = context

const mdiIconPathsToImport = new Set<string>()

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

return {
/**
* Handles converting <MdiIcon /> to <Icon svgPath={mdiIcon} />
*/
JsxSelfClosingElement(jsxElement) {
const tagElementName = jsxElement.getTagNameNode().getText()
const iconRegex = /(\w*.)Icon/

if (!tagElementName.match(iconRegex) || !isMdiReactToken(tagElementName)) {
// Not <MdiIcon component, so we exit
return
}

const updatedValue = `mdi${tagElementName.replace(iconRegex, '$1')}`

// e.g. update <CloseIcon /> to <Icon /> (we handle correct import later)
jsxElement.set({
name: 'Icon',
})

// Add updated svgPath attribute
jsxElement.addAttribute({
name: 'svgPath',
initializer: `{${updatedValue}}`,
})

// Ensure `inline` is set to false to guarantee that we aren't introducing any new CSS with this change.
jsxElement.addAttribute({
name: 'inline',
initializer: '{false}',
})

// We need to set accessibility attributes on all icons
// If these aren't already set, we default to `aria-hidden={true}` and leave a message to review.
if (!jsxElement.getAttribute('aria-label') && !jsxElement.getAttribute('aria-hidden')) {
jsxElement.addAttribute({
name: 'aria-hidden',
initializer: '{true}',
})

addManualChangeLog({
node: jsxElement,
message:
'<MdiIcon /> component did not have accessibility attributes and has been hidden from screen readers automatically. Please review manually',
})
}

// Our previous icon library supported a `size` prop, which set height and width.
// We convert this to height and width to be explicit.
const sizeAttribute = jsxElement.getAttribute('size')
if (sizeAttribute && Node.isJsxAttribute(sizeAttribute)) {
jsxElement.addAttribute({
name: 'height',
initializer: sizeAttribute.getInitializer()?.getText(),
})

jsxElement.addAttribute({
name: 'width',
initializer: sizeAttribute.getInitializer()?.getText(),
})

// Remove the old attribute
jsxElement.getAttribute('size')?.remove()
}

// Store this value so we can import it once finished with this file.
mdiIconPathsToImport.add(updatedValue)
},
/**
* Handles converting <Icon as={MdiIcon} /> to <Icon svgPath={mdiIcon} />
*/
JsxAttribute(jsxAttribute) {
const jsxTagElement = getParentUntilOrThrow(jsxAttribute, isJsxTagElement)
if (jsxTagElement.getTagNameNode().getText() !== 'Icon') {
Expand Down Expand Up @@ -75,9 +148,19 @@ export const mdiIconToMdiPath = runTransform(context => {
namedImports: [...mdiIconPathsToImport],
moduleSpecifier: '@mdi/js',
})
}

sourceFile.fixUnusedIdentifiers()
// If we're using the <Icon /> component for the first time,
// we need to add the import
addOrUpdateSourcegraphWildcardImportIfNeeded({
sourceFile,
importStructure: {
namedImports: ['Icon'],
},
})

// Clean up
sourceFile.fixUnusedIdentifiers()
}
},
}
})

0 comments on commit 155e8cf

Please sign in to comment.