Repository: mui/material-ui (TypeScript)
Date: 2018-06-26 21:45:58+00:00
Task type: Feature

Issue:
SVG Icons: Unable to Supply <defs> Element
Although the SVG font icons support a `children` prop, I am unable to pass in a `defs` element to define a custom linear gradient. I.e.:

```jsx
         import Stars from 'material-ui/svg-icons/action/stars';

          <Stars
            children={
              <defs>
                <linearGradient id="MyGradient">
                  <stop offset="5%" stopColor="#F60" />
                  <stop offset="95%" stopColor="#FF6" />
                </linearGradient>
              </defs>
            }
            className="star--half"
          />
```

The icon renders without the `defs` element.



Issue discussion:
@joncursi The pre-built icons don't currently support children, as the path for the icon itself is the `SvgIcon` children.

```jsx
import React from 'react';
import pure from 'recompose/pure';
import SvgIcon from '../../SvgIcon';

let ActionStars = (props) => (
  <SvgIcon {...props}>
    <path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zm4.24 16L12 15.45 7.77 18l1.12-4.81-3.73-3.23 4.92-.42L12 5l1.92 4.53 4.92.42-3.73 3.23L16.23 18z"/>
  </SvgIcon>
);
ActionStars = pure(ActionStars);
ActionStars.displayName = 'ActionStars';
ActionStars.muiName = 'SvgIcon';

export default ActionStars;
```

@joncursi If the prebuilt SvgIcons supported defs as children, how would you apply them to the built-in SVG paths?

@mbrookes I was looking to apply a linear gradient over the SVG path rather than a solid color. CSS can't do this alone, it requires DOM elements be defined like this: http://www.w3schools.com/svg/svg_grad_linear.asp

The fill name can then either be passed in as an additional prop, or specified to be used in CSS.

@joncursi The problem is that some icons have multiple paths (or other elements `circle` etc.), so which to apply what id to?

Requiring that SvgIcons embed the raw source of an SVG instead of some way to link or load from an external source is rather limiting. Creating these in TypeScript, you can't embed an icon-specific `<style>` tag if there's a class to be reused by some/all of the contained paths. it fails to compile because it interprets the css. I know the styles could potentially be applied by the caller creating the icon, or at a global level, but this seems really awkward if the goal is to encapsulate an 'icon' with this pattern.
We have 3 paths going forward:
1. We tell users to copy and paste the icon path. So we don't change anything.
2. We make all our icons exporting the path.
3. We add an extra property to the `SvgIcon` component. `extraChildren` or something like that.

Option 1 or 3 sounds like the best ones to me.
@dustingraves People can already do that with Material-UI. I don't follow.
I didn't read the original problem fully... I will remove my comment to keep the issue clean
Actually, I think that we have an even better solution to the problem. We can make the `SvgIcon` supports a `component` property. It's a pattern we use everywhere.

Solution patch:
diff --git a/.size-limit.js b/.size-limit.js
--- a/.size-limit.js
+++ b/.size-limit.js
@@ -27,7 +27,7 @@ module.exports = [
     name: 'The size of all the modules of material-ui.',
     webpack: true,
     path: 'packages/material-ui/build/index.js',
-    limit: '94.8 KB',
+    limit: '94.9 KB',
   },
   {
     name: 'The main bundle of the docs',
diff --git a/docs/src/pages/style/icons/SvgIcons.js b/docs/src/pages/style/icons/SvgIcons.js
--- a/docs/src/pages/style/icons/SvgIcons.js
+++ b/docs/src/pages/style/icons/SvgIcons.js
@@ -2,6 +2,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { withStyles } from '@material-ui/core/styles';
 import red from '@material-ui/core/colors/red';
+import blue from '@material-ui/core/colors/blue';
 import SvgIcon from '@material-ui/core/SvgIcon';
 
 const styles = theme => ({
@@ -39,6 +40,22 @@ function SvgIcons(props) {
       <HomeIcon className={classes.icon} color="action" />
       <HomeIcon className={classes.iconHover} color="error" style={{ fontSize: 30 }} />
       <HomeIcon color="disabled" className={classes.icon} style={{ fontSize: 36 }} />
+      <HomeIcon
+        className={classes.icon}
+        color="primary"
+        style={{ fontSize: 36 }}
+        component={svgProps => (
+          <svg {...svgProps}>
+            <defs>
+              <linearGradient id="gradient1">
+                <stop offset="30%" stopColor={blue[400]} />
+                <stop offset="70%" stopColor={red[400]} />
+              </linearGradient>
+            </defs>
+            {React.cloneElement(svgProps.children[0], { fill: 'url(#gradient1)' })}
+          </svg>
+        )}
+      />
     </div>
   );
 }
diff --git a/packages/material-ui/src/SvgIcon/SvgIcon.d.ts b/packages/material-ui/src/SvgIcon/SvgIcon.d.ts
--- a/packages/material-ui/src/SvgIcon/SvgIcon.d.ts
+++ b/packages/material-ui/src/SvgIcon/SvgIcon.d.ts
@@ -4,6 +4,7 @@ import { StandardProps, PropTypes } from '..';
 export interface SvgIconProps
   extends StandardProps<React.SVGProps<SVGSVGElement>, SvgIconClassKey> {
   color?: PropTypes.Color | 'action' | 'disabled' | 'error';
+  component?: React.ReactType<SvgIconProps>;
   fontSize?: 'inherit' | 'default';
   nativeColor?: string;
   titleAccess?: string;
diff --git a/packages/material-ui/src/SvgIcon/SvgIcon.js b/packages/material-ui/src/SvgIcon/SvgIcon.js
--- a/packages/material-ui/src/SvgIcon/SvgIcon.js
+++ b/packages/material-ui/src/SvgIcon/SvgIcon.js
@@ -43,6 +43,7 @@ function SvgIcon(props) {
     classes,
     className: classNameProp,
     color,
+    component: Component,
     fontSize,
     nativeColor,
     titleAccess,
@@ -60,7 +61,7 @@ function SvgIcon(props) {
   );
 
   return (
-    <svg
+    <Component
       className={className}
       focusable="false"
       viewBox={viewBox}
@@ -68,9 +69,9 @@ function SvgIcon(props) {
       aria-hidden={titleAccess ? 'false' : 'true'}
       {...other}
     >
-      {titleAccess ? <title>{titleAccess}</title> : null}
       {children}
-    </svg>
+      {titleAccess ? <title>{titleAccess}</title> : null}
+    </Component>
   );
 }
 
@@ -93,6 +94,11 @@ SvgIcon.propTypes = {
    * You can use the `nativeColor` property to apply a color attribute to the SVG element.
    */
   color: PropTypes.oneOf(['inherit', 'primary', 'secondary', 'action', 'error', 'disabled']),
+  /**
+   * The component used for the root node.
+   * Either a string to use a DOM element or a component.
+   */
+  component: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.object]),
   /**
    * The fontSize applied to the icon. Defaults to 24px, but can be configure to inherit font size.
    */
@@ -118,8 +124,9 @@ SvgIcon.propTypes = {
 
 SvgIcon.defaultProps = {
   color: 'inherit',
-  viewBox: '0 0 24 24',
+  component: 'svg',
   fontSize: 'default',
+  viewBox: '0 0 24 24',
 };
 
 SvgIcon.muiName = 'SvgIcon';
diff --git a/pages/api/svg-icon.md b/pages/api/svg-icon.md
--- a/pages/api/svg-icon.md
+++ b/pages/api/svg-icon.md
@@ -18,6 +18,7 @@ title: SvgIcon API
 | <span class="prop-name required">children *</span> | <span class="prop-type">node |   | Node passed into the SVG element. |
 | <span class="prop-name">classes</span> | <span class="prop-type">object |   | Override or extend the styles applied to the component. See [CSS API](#css-api) below for more details. |
 | <span class="prop-name">color</span> | <span class="prop-type">enum:&nbsp;'inherit', 'primary', 'secondary', 'action', 'error', 'disabled'<br> | <span class="prop-default">'inherit'</span> | The color of the component. It supports those theme colors that make sense for this component. You can use the `nativeColor` property to apply a color attribute to the SVG element. |
+| <span class="prop-name">component</span> | <span class="prop-type">union:&nbsp;string&nbsp;&#124;<br>&nbsp;func&nbsp;&#124;<br>&nbsp;object<br> | <span class="prop-default">'svg'</span> | The component used for the root node. Either a string to use a DOM element or a component. |
 | <span class="prop-name">fontSize</span> | <span class="prop-type">enum:&nbsp;'inherit'&nbsp;&#124;<br>&nbsp;'default'<br> | <span class="prop-default">'default'</span> | The fontSize applied to the icon. Defaults to 24px, but can be configure to inherit font size. |
 | <span class="prop-name">nativeColor</span> | <span class="prop-type">string |   | Applies a color attribute to the SVG element. |
 | <span class="prop-name">titleAccess</span> | <span class="prop-type">string |   | Provides a human-readable title for the element that contains it. https://www.w3.org/TR/SVG-access/#Equivalent |


Test patch:
diff --git a/packages/material-ui/src/SvgIcon/SvgIcon.test.js b/packages/material-ui/src/SvgIcon/SvgIcon.test.js
--- a/packages/material-ui/src/SvgIcon/SvgIcon.test.js
+++ b/packages/material-ui/src/SvgIcon/SvgIcon.test.js
@@ -2,20 +2,26 @@
 
 import React from 'react';
 import { assert } from 'chai';
-import { createShallow, getClasses } from '../test-utils';
+import { createShallow, createMount, getClasses } from '../test-utils';
 import SvgIcon from './SvgIcon';
 
 describe('<SvgIcon />', () => {
   let shallow;
+  let mount;
   let classes;
   let path;
 
   before(() => {
     shallow = createShallow({ dive: true });
+    mount = createMount();
     classes = getClasses(<SvgIcon>foo</SvgIcon>);
     path = <path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z" />;
   });
 
+  after(() => {
+    mount.cleanUp();
+  });
+
   it('renders children by default', () => {
     const wrapper = shallow(<SvgIcon>{path}</SvgIcon>);
     assert.strictEqual(wrapper.contains(path), true, 'should contain the children');
@@ -99,4 +105,27 @@ describe('<SvgIcon />', () => {
       );
     });
   });
+
+  describe('prop: component', () => {
+    it('should render component before path', () => {
+      const wrapper = mount(
+        <SvgIcon
+          component={props => (
+            <svg {...props}>
+              <defs>
+                <linearGradient id="gradient1">
+                  <stop offset="20%" stopColor="#39F" />
+                  <stop offset="90%" stopColor="#F3F" />
+                </linearGradient>
+              </defs>
+              {props.children}
+            </svg>
+          )}
+        >
+          {path}
+        </SvgIcon>,
+      );
+      assert.strictEqual(wrapper.find('defs').length, 1);
+    });
+  });
 });