Skip to content

Commit

Permalink
[SvgIcon] allow svg as a child (#37231)
Browse files Browse the repository at this point in the history
  • Loading branch information
siriwatknp committed Jun 20, 2023
1 parent 7e7b32f commit 9246969
Show file tree
Hide file tree
Showing 12 changed files with 221 additions and 15 deletions.
16 changes: 16 additions & 0 deletions docs/data/material/components/icons/CreateSvgIcon.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ const HomeIcon = createSvgIcon(
'Home',
);

const PlusIcon = createSvgIcon(
// credit: plus icon from https://heroicons.com/
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>,
'Plus',
);

export default function CreateSvgIcon() {
return (
<Box
Expand All @@ -18,6 +32,8 @@ export default function CreateSvgIcon() {
>
<HomeIcon />
<HomeIcon color="primary" />
<PlusIcon />
<PlusIcon color="secondary" />
</Box>
);
}
16 changes: 16 additions & 0 deletions docs/data/material/components/icons/CreateSvgIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ const HomeIcon = createSvgIcon(
'Home',
);

const PlusIcon = createSvgIcon(
// credit: plus icon from https://heroicons.com/
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>,
'Plus',
);

export default function CreateSvgIcon() {
return (
<Box
Expand All @@ -18,6 +32,8 @@ export default function CreateSvgIcon() {
>
<HomeIcon />
<HomeIcon color="primary" />
<PlusIcon />
<PlusIcon color="secondary" />
</Box>
);
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
<HomeIcon />
<HomeIcon color="primary" />
<HomeIcon color="primary" />
<PlusIcon />
<PlusIcon color="secondary" />
23 changes: 23 additions & 0 deletions docs/data/material/components/icons/SvgIconChildren.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import * as React from 'react';
import SvgIcon from '@mui/material/SvgIcon';

export default function SvgIconChildren() {
return (
<SvgIcon>
{/* credit: plus icon from https://heroicons.com/ */}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 12a7.5 7.5 0 0015 0m-15 0a7.5 7.5 0 1115 0m-15 0H3m16.5 0H21m-1.5 0H12m-8.457 3.077l1.41-.513m14.095-5.13l1.41-.513M5.106 17.785l1.15-.964m11.49-9.642l1.149-.964M7.501 19.795l.75-1.3m7.5-12.99l.75-1.3m-6.063 16.658l.26-1.477m2.605-14.772l.26-1.477m0 17.726l-.26-1.477M10.698 4.614l-.26-1.477M16.5 19.794l-.75-1.299M7.5 4.205L12 12m6.894 5.785l-1.149-.964M6.256 7.178l-1.15-.964m15.352 8.864l-1.41-.513M4.954 9.435l-1.41-.514M12.002 12l-3.75 6.495"
/>
</svg>
</SvgIcon>
);
}
23 changes: 23 additions & 0 deletions docs/data/material/components/icons/SvgIconChildren.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import * as React from 'react';
import SvgIcon from '@mui/material/SvgIcon';

export default function SvgIconChildren() {
return (
<SvgIcon>
{/* credit: plus icon from https://heroicons.com/ */}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 12a7.5 7.5 0 0015 0m-15 0a7.5 7.5 0 1115 0m-15 0H3m16.5 0H21m-1.5 0H12m-8.457 3.077l1.41-.513m14.095-5.13l1.41-.513M5.106 17.785l1.15-.964m11.49-9.642l1.149-.964M7.501 19.795l.75-1.3m7.5-12.99l.75-1.3m-6.063 16.658l.26-1.477m2.605-14.772l.26-1.477m0 17.726l-.26-1.477M10.698 4.614l-.26-1.477M16.5 19.794l-.75-1.299M7.5 4.205L12 12m6.894 5.785l-1.149-.964M6.256 7.178l-1.15-.964m15.352 8.864l-1.41-.513M4.954 9.435l-1.41-.514M12.002 12l-3.75 6.495"
/>
</svg>
</SvgIcon>
);
}
16 changes: 16 additions & 0 deletions docs/data/material/components/icons/SvgIconChildren.tsx.preview
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<SvgIcon>
{/* credit: plus icon from https://heroicons.com/ */}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 12a7.5 7.5 0 0015 0m-15 0a7.5 7.5 0 1115 0m-15 0H3m16.5 0H21m-1.5 0H12m-8.457 3.077l1.41-.513m14.095-5.13l1.41-.513M5.106 17.785l1.15-.964m11.49-9.642l1.149-.964M7.501 19.795l.75-1.3m7.5-12.99l.75-1.3m-6.063 16.658l.26-1.477m2.605-14.772l.26-1.477m0 17.726l-.26-1.477M10.698 4.614l-.26-1.477M16.5 19.794l-.75-1.299M7.5 4.205L12 12m6.894 5.785l-1.149-.964M6.256 7.178l-1.15-.964m15.352 8.864l-1.41-.513M4.954 9.435l-1.41-.514M12.002 12l-3.75 6.495"
/>
</svg>
</SvgIcon>
28 changes: 18 additions & 10 deletions docs/data/material/components/icons/icons.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,16 +98,9 @@ This component extends the native `<svg>` element:
This can be customized with the `viewBox` attribute.
To inherit the `viewBox` value from the original image, the `inheritViewBox` prop can be used.
- By default, the component inherits the current color. Optionally, you can apply one of the theme colors using the `color` prop.
- It supports `<svg>` element as a child so you can copy and paste your SVG directly to `SvgIcon` component.

```jsx
function HomeIcon(props) {
return (
<SvgIcon {...props}>
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z" />
</SvgIcon>
);
}
```
{{"demo": "SvgIconChildren.js"}}

### Color

Expand Down Expand Up @@ -152,13 +145,28 @@ import { ReactComponent as StarIcon } from './star.svg';
### createSvgIcon
The `createSvgIcon` utility component is used to create the [Material Icons](#material-icons). It can be used to wrap an SVG path with an SvgIcon component.
The `createSvgIcon` utility component is used to create the [Material Icons](#material-icons). It can be used to wrap an `<svg>` element or an SVG path which is passed as a child to the [`SvgIcon`](#svgicon) component.
```jsx
const HomeIcon = createSvgIcon(
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z" />,
'Home',
);
// or with custom SVG
const PlusIcon = createSvgIcon(
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>,
'Plus',
);
```
{{"demo": "CreateSvgIcon.js"}}
Expand Down
43 changes: 43 additions & 0 deletions packages/mui-joy/src/SvgIcon/SvgIcon.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,21 @@ describe('<SvgIcon />', () => {
expect(container.firstChild).to.have.attribute('aria-hidden', 'true');
});

it('renders children of provided svg and merge the props', () => {
const { container } = render(
<SvgIcon>
<svg viewBox="0 0 48 48" strokeWidth="1.5">
{path}
</svg>
</SvgIcon>,
);

expect(container.firstChild).to.have.tagName('svg');
expect(container.firstChild?.firstChild).to.have.tagName('path');
expect(container.firstChild).to.have.attribute('viewBox', '0 0 48 48');
expect(container.firstChild).to.have.attribute('stroke-width', '1.5');
});

describe('prop: titleAccess', () => {
it('should be able to make an icon accessible', () => {
const { container, queryByText } = render(<SvgIcon titleAccess="Network">{path}</SvgIcon>);
Expand Down Expand Up @@ -158,4 +173,32 @@ describe('<SvgIcon />', () => {
);
expect(container.firstChild).toHaveComputedStyle({ fontSize: '20px' }); // fontSize: xl -> 1.25rem = 20px
});

it('should have `fill="currentColor"`', function test() {
if (!/jsdom/.test(window.navigator.userAgent)) {
this.skip();
}
const { container } = render(
<SvgIcon>
<path />
</SvgIcon>,
);

expect(container.firstChild).toHaveComputedStyle({ fill: 'currentColor' });
});

it('should not add `fill` if svg is a direct child', function test() {
if (!/jsdom/.test(window.navigator.userAgent)) {
this.skip();
}
const { container } = render(
<SvgIcon>
<svg>
<path />
</svg>
</SvgIcon>,
);

expect(container.firstChild).not.toHaveComputedStyle({ fill: 'currentColor' });
});
});
10 changes: 8 additions & 2 deletions packages/mui-joy/src/SvgIcon/SvgIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ const SvgIconRoot = styled('svg', {
width: '1em',
height: '1em',
display: 'inline-block',
fill: 'currentColor',
// the <svg> will define the property that has `currentColor`
// e.g. heroicons uses fill="none" and stroke="currentColor"
fill: ownerState.hasSvgAsChild ? undefined : 'currentColor',
flexShrink: 0,
...(ownerState.fontSize &&
ownerState.fontSize !== 'inherit' && {
Expand Down Expand Up @@ -85,6 +87,8 @@ const SvgIcon = React.forwardRef(function SvgIcon(inProps, ref) {
...other
} = props;

const hasSvgAsChild = React.isValidElement(children) && children.type === 'svg';

const ownerState = {
...props,
color,
Expand All @@ -93,6 +97,7 @@ const SvgIcon = React.forwardRef(function SvgIcon(inProps, ref) {
instanceFontSize: inProps.fontSize,
inheritViewBox,
viewBox,
hasSvgAsChild,
};

const classes = useUtilityClasses(ownerState);
Expand All @@ -110,12 +115,13 @@ const SvgIcon = React.forwardRef(function SvgIcon(inProps, ref) {
...(titleAccess && { role: 'img' }),
...(!titleAccess && { 'aria-hidden': true }),
...(!inheritViewBox && { viewBox }),
...(hasSvgAsChild && (children.props as Omit<React.SVGProps<SVGSVGElement>, 'ref'>)),
},
});

return (
<SlotRoot {...rootProps}>
{children}
{hasSvgAsChild ? (children.props as React.SVGProps<SVGSVGElement>).children : children}
{titleAccess ? <title>{titleAccess}</title> : null}
</SlotRoot>
);
Expand Down
4 changes: 4 additions & 0 deletions packages/mui-joy/src/SvgIcon/SvgIconProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,8 @@ export interface SvgIconOwnerState extends ApplyColorInversion<SvgIconProps> {
* The `size` specified explicitly on the instance.
*/
instanceFontSize: SvgIconProps['fontSize'];
/**
* The `children` has a `svg` element as a child.
*/
hasSvgAsChild: boolean;
}
10 changes: 8 additions & 2 deletions packages/mui-material/src/SvgIcon/SvgIcon.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ const SvgIconRoot = styled('svg', {
width: '1em',
height: '1em',
display: 'inline-block',
fill: 'currentColor',
// the <svg> will define the property that has `currentColor`
// e.g. heroicons uses fill="none" and stroke="currentColor"
fill: ownerState.hasSvgAsChild ? undefined : 'currentColor',
flexShrink: 0,
transition: theme.transitions?.create?.('fill', {
duration: theme.transitions?.duration?.shorter,
Expand Down Expand Up @@ -74,6 +76,8 @@ const SvgIcon = React.forwardRef(function SvgIcon(inProps, ref) {
...other
} = props;

const hasSvgAsChild = React.isValidElement(children) && children.type === 'svg';

const ownerState = {
...props,
color,
Expand All @@ -82,6 +86,7 @@ const SvgIcon = React.forwardRef(function SvgIcon(inProps, ref) {
instanceFontSize: inProps.fontSize,
inheritViewBox,
viewBox,
hasSvgAsChild,
};

const more = {};
Expand All @@ -103,9 +108,10 @@ const SvgIcon = React.forwardRef(function SvgIcon(inProps, ref) {
ref={ref}
{...more}
{...other}
{...(hasSvgAsChild && children.props)}
ownerState={ownerState}
>
{children}
{hasSvgAsChild ? children.props.children : children}
{titleAccess ? <title>{titleAccess}</title> : null}
</SvgIconRoot>
);
Expand Down
43 changes: 43 additions & 0 deletions packages/mui-material/src/SvgIcon/SvgIcon.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,21 @@ describe('<SvgIcon />', () => {
expect(container.firstChild).to.have.attribute('aria-hidden', 'true');
});

it('renders children of provided svg and merge the props', () => {
const { container } = render(
<SvgIcon>
<svg viewBox="0 0 48 48" strokeWidth="1.5">
{path}
</svg>
</SvgIcon>,
);

expect(container.firstChild).to.have.tagName('svg');
expect(container.firstChild.firstChild).to.have.tagName('path');
expect(container.firstChild).to.have.attribute('viewBox', '0 0 48 48');
expect(container.firstChild).to.have.attribute('stroke-width', '1.5');
});

describe('prop: titleAccess', () => {
it('should be able to make an icon accessible', () => {
const { container, queryByText } = render(
Expand Down Expand Up @@ -130,4 +145,32 @@ describe('<SvgIcon />', () => {
const { container } = render(<SvgIcon ownerState={{ fontSize: 'large' }}>{path}</SvgIcon>);
expect(container.firstChild).toHaveComputedStyle({ fontSize: '24px' }); // fontSize: medium -> 1.5rem = 24px
});

it('should have `fill="currentColor"`', function test() {
if (!/jsdom/.test(window.navigator.userAgent)) {
this.skip();
}
const { container } = render(
<SvgIcon>
<path />
</SvgIcon>,
);

expect(container.firstChild).toHaveComputedStyle({ fill: 'currentColor' });
});

it('should not add `fill` if svg is a direct child', function test() {
if (!/jsdom/.test(window.navigator.userAgent)) {
this.skip();
}
const { container } = render(
<SvgIcon>
<svg>
<path />
</svg>
</SvgIcon>,
);

expect(container.firstChild).not.toHaveComputedStyle({ fill: 'currentColor' });
});
});

0 comments on commit 9246969

Please sign in to comment.