Repository: mui/material-ui (TypeScript)
Date: 2018-09-16 15:32:18+00:00
Task type: Feature

Issue:
[Chip] Support non-avatared icon on Chip
The Material Design spec allows us to put a simple icon which is not wrapped by an avatar component on Chips.
See https://material.io/design/components/chips.html#
But Chip of material-ui presupposes an Avatar as icon.

<!-- Checked checkbox should look like this: [x] -->
- [x] This is a v1.x and v3.0 issue.
- [x] I have searched the [issues](https://github.com/mui-org/material-ui/issues) of this repository and believe that this is not a duplicate.

## Expected Behavior
Support `<Chip avatar={<SvgIcon />} />` or add a new props like `<Chip icon={<SvgIcon />}`.

## Current Behavior
`<Chip avatar={<SvgIcon />} />` renders the icon too big.

## Examples
https://codesandbox.io/s/m5mkwyy19y?module=%2Fsrc%2Fpages%2Findex.js



Issue discussion:
@oliviertassinari @hidai  I'd like to tackle this issue. I'm thinking it'd be better to use an "icon" attribute as opposed to extending the avatar attribute. That's more explicit and I imagine a little easier to code, easier to deal with from a testing standpoint and also a little more modular. What do you think?
@aretheregods I had the same thought. I think that adding an `icon` property has potential.

On a side note, here is a quick workaround:

![capture d ecran 2018-09-11 a 23 10 52](https://user-images.githubusercontent.com/3165635/45387984-f4101d80-b617-11e8-927a-0ec5d4c83ae5.png)
```jsx
      <Chip
        label={
          <React.Fragment>
            <FaceIcon style={{ marginRight: 8, marginLeft: -8 }} />
            Basic Chip
          </React.Fragment>
        }
      />
```
@fzaninotto Regarding your icon positioning issue. I just had a look at vuetify, they are doing something interesting https://vuetifyjs.com/en/components/chips#introduction.
```jsx
    <v-chip color="orange" text-color="white">
      Premium
      <v-icon right>star</v-icon>
    </v-chip>

    <v-chip color="primary" text-color="white">
      <v-icon left>cake</v-icon>
      1 Year
    </v-chip>
```

What do you think of adding a property like this to the Icon & SvgIcon component?:
```ts
placement?: 'start' | 'end';
```
Alternatively, I really like what [styled-system](https://github.com/jxnblk/) is doing. It's a great source of inspiration for more Material-UI utilities component, like a Spacing or something that can be composed into a Box.

Solution patch:
diff --git a/docs/src/pages/demos/chips/Chips.js b/docs/src/pages/demos/chips/Chips.js
--- a/docs/src/pages/demos/chips/Chips.js
+++ b/docs/src/pages/demos/chips/Chips.js
@@ -53,6 +53,13 @@ function Chips(props) {
         onDelete={handleDelete}
         className={classes.chip}
       />
+      <Chip
+        icon={<FaceIcon />}
+        label="Clickable Deletable Chip"
+        onClick={handleClick}
+        onDelete={handleDelete}
+        className={classes.chip}
+      />
       <Chip
         label="Custom delete icon Chip"
         onClick={handleClick}
@@ -76,6 +83,15 @@ function Chips(props) {
         onDelete={handleDelete}
         deleteIcon={<DoneIcon />}
       />
+      <Chip
+        icon={<FaceIcon />}
+        label="Primary Clickable Chip"
+        clickable
+        className={classes.chip}
+        color="primary"
+        onDelete={handleDelete}
+        deleteIcon={<DoneIcon />}
+      />
       <Chip
         label="Deletable Primary Chip"
         onDelete={handleDelete}
@@ -93,6 +109,13 @@ function Chips(props) {
         className={classes.chip}
         color="secondary"
       />
+      <Chip
+        icon={<FaceIcon />}
+        label="Deletable Secondary Chip"
+        onDelete={handleDelete}
+        className={classes.chip}
+        color="secondary"
+      />
     </div>
   );
 }
diff --git a/docs/src/pages/demos/chips/ChipsArray.js b/docs/src/pages/demos/chips/ChipsArray.js
--- a/docs/src/pages/demos/chips/ChipsArray.js
+++ b/docs/src/pages/demos/chips/ChipsArray.js
@@ -1,7 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import { withStyles } from '@material-ui/core/styles';
-import Avatar from '@material-ui/core/Avatar';
 import Chip from '@material-ui/core/Chip';
 import Paper from '@material-ui/core/Paper';
 import TagFacesIcon from '@material-ui/icons/TagFaces';
@@ -49,20 +48,16 @@ class ChipsArray extends React.Component {
     return (
       <Paper className={classes.root}>
         {this.state.chipData.map(data => {
-          let avatar = null;
+          let icon = null;
 
           if (data.label === 'React') {
-            avatar = (
-              <Avatar>
-                <TagFacesIcon className={classes.svgIcon} />
-              </Avatar>
-            );
+            icon = <TagFacesIcon />;
           }
 
           return (
             <Chip
               key={data.key}
-              avatar={avatar}
+              icon={icon}
               label={data.label}
               onDelete={this.handleDelete(data)}
               className={classes.chip}
diff --git a/docs/src/pages/demos/chips/ChipsPlayground.js b/docs/src/pages/demos/chips/ChipsPlayground.js
--- a/docs/src/pages/demos/chips/ChipsPlayground.js
+++ b/docs/src/pages/demos/chips/ChipsPlayground.js
@@ -31,6 +31,7 @@ class ChipsPlayground extends React.Component {
     color: 'default',
     onDelete: 'none',
     avatar: 'none',
+    icon: 'none',
     variant: 'default',
   };
 
@@ -46,7 +47,7 @@ class ChipsPlayground extends React.Component {
 
   render() {
     const { classes } = this.props;
-    const { color, onDelete, avatar, variant } = this.state;
+    const { color, onDelete, avatar, icon, variant } = this.state;
 
     const colorToCode = color !== 'default' ? `color="${color}" ` : '';
     const variantToCode = variant !== 'default' ? `variant="${variant}" ` : '';
@@ -64,6 +65,18 @@ class ChipsPlayground extends React.Component {
         break;
     }
 
+    let iconToCode;
+    let iconToPlayground;
+    switch (icon) {
+      case 'none':
+        iconToCode = '';
+        break;
+      default:
+        iconToCode = 'icon={<FaceIcon />}';
+        iconToPlayground = <FaceIcon />;
+        break;
+    }
+
     let avatarToCode;
     let avatarToPlayground;
     switch (avatar) {
@@ -88,9 +101,14 @@ class ChipsPlayground extends React.Component {
         break;
     }
 
+    if (avatar !== 'none') {
+      iconToCode = '';
+      iconToPlayground = null;
+    }
+
     const code = `
 \`\`\`jsx
-<Chip ${colorToCode}${onDeleteToCode}${avatarToCode}${variantToCode}/>
+<Chip ${colorToCode}${onDeleteToCode}${avatarToCode}${iconToCode}${variantToCode}/>
 \`\`\`
 `;
 
@@ -105,6 +123,7 @@ class ChipsPlayground extends React.Component {
                 deleteIcon={onDelete === 'custom' ? <DoneIcon /> : undefined}
                 onDelete={onDelete !== 'none' ? this.handleDeleteExample : undefined}
                 avatar={avatarToPlayground}
+                icon={iconToPlayground}
                 variant={variant}
               />
             </Grid>
@@ -145,6 +164,21 @@ class ChipsPlayground extends React.Component {
                   </RadioGroup>
                 </FormControl>
               </Grid>
+              <Grid item xs={12}>
+                <FormControl component="fieldset">
+                  <FormLabel>icon</FormLabel>
+                  <RadioGroup
+                    row
+                    name="icon"
+                    aria-label="icon"
+                    value={icon}
+                    onChange={this.handleChange('icon')}
+                  >
+                    <FormControlLabel value="none" control={<Radio />} label="none" />
+                    <FormControlLabel value="icon" control={<Radio />} label="icon" />
+                  </RadioGroup>
+                </FormControl>
+              </Grid>
               <Grid item xs={12}>
                 <FormControl component="fieldset">
                   <FormLabel>avatar</FormLabel>
diff --git a/docs/src/pages/demos/chips/OutlinedChips.js b/docs/src/pages/demos/chips/OutlinedChips.js
--- a/docs/src/pages/demos/chips/OutlinedChips.js
+++ b/docs/src/pages/demos/chips/OutlinedChips.js
@@ -56,6 +56,14 @@ function OutlinedChips(props) {
         className={classes.chip}
         variant="outlined"
       />
+      <Chip
+        icon={<FaceIcon />}
+        label="Clickable Deletable Chip"
+        onClick={handleClick}
+        onDelete={handleDelete}
+        className={classes.chip}
+        variant="outlined"
+      />
       <Chip
         label="Custom delete icon Chip"
         onClick={handleClick}
@@ -82,6 +90,16 @@ function OutlinedChips(props) {
         deleteIcon={<DoneIcon />}
         variant="outlined"
       />
+      <Chip
+        icon={<FaceIcon />}
+        label="Primary Clickable Chip"
+        clickable
+        className={classes.chip}
+        color="primary"
+        onDelete={handleDelete}
+        deleteIcon={<DoneIcon />}
+        variant="outlined"
+      />
       <Chip
         label="Deletable Primary Chip"
         onDelete={handleDelete}
@@ -101,6 +119,14 @@ function OutlinedChips(props) {
         color="secondary"
         variant="outlined"
       />
+      <Chip
+        icon={<FaceIcon />}
+        label="Deletable Secondary Chip"
+        onDelete={handleDelete}
+        className={classes.chip}
+        color="secondary"
+        variant="outlined"
+      />
     </div>
   );
 }
diff --git a/packages/material-ui/src/Chip/Chip.d.ts b/packages/material-ui/src/Chip/Chip.d.ts
--- a/packages/material-ui/src/Chip/Chip.d.ts
+++ b/packages/material-ui/src/Chip/Chip.d.ts
@@ -8,6 +8,7 @@ export interface ChipProps
   color?: PropTypes.Color;
   component?: React.ReactType<ChipProps>;
   deleteIcon?: React.ReactElement<any>;
+  icon?: React.ReactElement<any>;
   label?: React.ReactNode;
   onDelete?: React.EventHandler<any>;
   variant?: 'default' | 'outlined';
diff --git a/packages/material-ui/src/Chip/Chip.js b/packages/material-ui/src/Chip/Chip.js
--- a/packages/material-ui/src/Chip/Chip.js
+++ b/packages/material-ui/src/Chip/Chip.js
@@ -2,9 +2,10 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import classNames from 'classnames';
 import keycode from 'keycode';
+import warning from 'warning';
 import CancelIcon from '../internal/svg-icons/Cancel';
 import withStyles from '../styles/withStyles';
-import { emphasize, fade, darken } from '../styles/colorManipulator';
+import { emphasize, fade } from '../styles/colorManipulator';
 import unsupportedProp from '../utils/unsupportedProp';
 import { capitalize } from '../utils/helpers';
 import '../Avatar/Avatar'; // So we don't have any override priority issue.
@@ -115,19 +116,17 @@ export const styles = theme => {
     /* Styles applied to the root element if `variant="outlined"` and `color="primary"`. */
     outlinedPrimary: {
       color: theme.palette.primary.main,
-      border: `1px solid ${fade(theme.palette.primary.main, 0.5)}`,
+      border: `1px solid ${theme.palette.primary.main}`,
       '$clickable&:hover, $clickable&:focus, $deletable&:focus': {
         backgroundColor: fade(theme.palette.primary.main, theme.palette.action.hoverOpacity),
-        border: `1px solid ${theme.palette.primary.main}`,
       },
     },
     /* Styles applied to the root element if `variant="outlined"` and `color="secondary"`. */
     outlinedSecondary: {
       color: theme.palette.secondary.main,
-      border: `1px solid ${fade(theme.palette.secondary.main, 0.5)}`,
+      border: `1px solid ${theme.palette.secondary.main}`,
       '$clickable&:hover, $clickable&:focus, $deletable&:focus': {
         backgroundColor: fade(theme.palette.secondary.main, theme.palette.action.hoverOpacity),
-        border: `1px solid ${theme.palette.secondary.main}`,
       },
     },
     /* Styles applied to the `avatar` element. */
@@ -138,14 +137,14 @@ export const styles = theme => {
       color: theme.palette.type === 'light' ? theme.palette.grey[700] : theme.palette.grey[300],
       fontSize: theme.typography.pxToRem(16),
     },
-    /* Styles applied to the `avatar` element if `checked={true}` and `color="primary"` */
+    /* Styles applied to the `avatar` element if `color="primary"` */
     avatarColorPrimary: {
-      color: darken(theme.palette.primary.contrastText, 0.1),
+      color: theme.palette.primary.contrastText,
       backgroundColor: theme.palette.primary.dark,
     },
-    /* Styles applied to the `avatar` element if `checked={true}` and `color="secondary"` */
+    /* Styles applied to the `avatar` element if `color="secondary"` */
     avatarColorSecondary: {
-      color: darken(theme.palette.secondary.contrastText, 0.1),
+      color: theme.palette.secondary.contrastText,
       backgroundColor: theme.palette.secondary.dark,
     },
     /* Styles applied to the `avatar` elements children. */
@@ -153,6 +152,20 @@ export const styles = theme => {
       width: 19,
       height: 19,
     },
+    /* Styles applied to the `icon` element. */
+    icon: {
+      color: theme.palette.type === 'light' ? theme.palette.grey[700] : theme.palette.grey[300],
+      marginLeft: 4,
+      marginRight: -8,
+    },
+    /* Styles applied to the `icon` element if `color="primary"` */
+    iconColorPrimary: {
+      color: 'inherit',
+    },
+    /* Styles applied to the `icon` element if `color="secondary"` */
+    iconColorSecondary: {
+      color: 'inherit',
+    },
     /* Styles applied to the label `span` element`. */
     label: {
       display: 'flex',
@@ -177,28 +190,28 @@ export const styles = theme => {
     },
     /* Styles applied to the deleteIcon element if `color="primary"` and `variant="default"`. */
     deleteIconColorPrimary: {
-      color: fade(theme.palette.primary.contrastText, 0.65),
+      color: fade(theme.palette.primary.contrastText, 0.7),
       '&:hover, &:active': {
         color: theme.palette.primary.contrastText,
       },
     },
     /* Styles applied to the deleteIcon element if `color="secondary"` and `variant="default"`. */
     deleteIconColorSecondary: {
-      color: fade(theme.palette.primary.contrastText, 0.65),
+      color: fade(theme.palette.primary.contrastText, 0.7),
       '&:hover, &:active': {
         color: theme.palette.primary.contrastText,
       },
     },
     /* Styles applied to the deleteIcon element if `color="primary"` and `variant="outlined"`. */
     deleteIconOutlinedColorPrimary: {
-      color: fade(theme.palette.primary.main, 0.65),
+      color: fade(theme.palette.primary.main, 0.7),
       '&:hover, &:active': {
         color: theme.palette.primary.main,
       },
     },
     /* Styles applied to the deleteIcon element if `color="secondary"` and `variant="outlined"`. */
     deleteIconOutlinedColorSecondary: {
-      color: fade(theme.palette.secondary.main, 0.65),
+      color: fade(theme.palette.secondary.main, 0.7),
       '&:hover, &:active': {
         color: theme.palette.secondary.main,
       },
@@ -270,6 +283,7 @@ class Chip extends React.Component {
       color,
       component: Component,
       deleteIcon: deleteIconProp,
+      icon: iconProp,
       label,
       onClick,
       onDelete,
@@ -333,12 +347,27 @@ class Chip extends React.Component {
       });
     }
 
+    let icon = null;
+    if (iconProp && React.isValidElement(iconProp)) {
+      icon = React.cloneElement(iconProp, {
+        className: classNames(classes.icon, iconProp.props.className, {
+          [classes[`iconColor${capitalize(color)}`]]: color !== 'default',
+        }),
+      });
+    }
+
     let tabIndex = tabIndexProp;
 
     if (!tabIndex) {
       tabIndex = onClick || onDelete || clickable ? 0 : -1;
     }
 
+    warning(
+      !avatar || !icon,
+      'Material-UI: the Chip component can not handle the avatar ' +
+        'and the icon property at the same time. Pick one.',
+    );
+
     return (
       <Component
         role="button"
@@ -352,7 +381,7 @@ class Chip extends React.Component {
         }}
         {...other}
       >
-        {avatar}
+        {avatar || icon}
         <span className={classes.label}>{label}</span>
         {deleteIcon}
       </Component>
@@ -398,6 +427,10 @@ Chip.propTypes = {
    * Override the default delete icon element. Shown only if `onDelete` is set.
    */
   deleteIcon: PropTypes.element,
+  /**
+   * Icon element.
+   */
+  icon: PropTypes.element,
   /**
    * The content of the label.
    */
diff --git a/pages/api/chip.md b/pages/api/chip.md
--- a/pages/api/chip.md
+++ b/pages/api/chip.md
@@ -26,6 +26,7 @@ Chips represent complex entities in small blocks, such as a contact.
 | <span class="prop-name">color</span> | <span class="prop-type">enum:&nbsp;'default'&nbsp;&#124;<br>&nbsp;'primary'&nbsp;&#124;<br>&nbsp;'secondary'<br> | <span class="prop-default">'default'</span> | The color of the component. It supports those theme colors that make sense for this component. |
 | <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">'div'</span> | The component used for the root node. Either a string to use a DOM element or a component. |
 | <span class="prop-name">deleteIcon</span> | <span class="prop-type">element |   | Override the default delete icon element. Shown only if `onDelete` is set. |
+| <span class="prop-name">icon</span> | <span class="prop-type">element |   | Icon element. |
 | <span class="prop-name">label</span> | <span class="prop-type">node |   | The content of the label. |
 | <span class="prop-name">onDelete</span> | <span class="prop-type">func |   | Callback function fired when the delete icon is clicked. If set, the delete icon will be shown. |
 | <span class="prop-name">variant</span> | <span class="prop-type">enum:&nbsp;'default'&nbsp;&#124;<br>&nbsp;'outlined'<br> | <span class="prop-default">'default'</span> | The variant to use. |
@@ -53,9 +54,12 @@ This property accepts the following keys:
 | <span class="prop-name">outlinedPrimary</span> | Styles applied to the root element if `variant="outlined"` and `color="primary"`.
 | <span class="prop-name">outlinedSecondary</span> | Styles applied to the root element if `variant="outlined"` and `color="secondary"`.
 | <span class="prop-name">avatar</span> | Styles applied to the `avatar` element.
-| <span class="prop-name">avatarColorPrimary</span> | Styles applied to the `avatar` element if `checked={true}` and `color="primary"`
-| <span class="prop-name">avatarColorSecondary</span> | Styles applied to the `avatar` element if `checked={true}` and `color="secondary"`
+| <span class="prop-name">avatarColorPrimary</span> | Styles applied to the `avatar` element if `color="primary"`
+| <span class="prop-name">avatarColorSecondary</span> | Styles applied to the `avatar` element if `color="secondary"`
 | <span class="prop-name">avatarChildren</span> | Styles applied to the `avatar` elements children.
+| <span class="prop-name">icon</span> | Styles applied to the `icon` element.
+| <span class="prop-name">iconColorPrimary</span> | Styles applied to the `icon` element if `color="primary"`
+| <span class="prop-name">iconColorSecondary</span> | Styles applied to the `icon` element if `color="secondary"`
 | <span class="prop-name">label</span> | Styles applied to the label `span` element`.
 | <span class="prop-name">deleteIcon</span> | Styles applied to the `deleteIcon` element.
 | <span class="prop-name">deleteIconColorPrimary</span> | Styles applied to the deleteIcon element if `color="primary"` and `variant="default"`.


Test patch:
diff --git a/packages/material-ui/src/Chip/Chip.test.js b/packages/material-ui/src/Chip/Chip.test.js
--- a/packages/material-ui/src/Chip/Chip.test.js
+++ b/packages/material-ui/src/Chip/Chip.test.js
@@ -501,4 +501,17 @@ describe('<Chip />', () => {
       });
     });
   });
+
+  describe('prop: icon', () => {
+    it('should render the icon', () => {
+      const wrapper = shallow(<Chip icon={<span />} />);
+      assert.strictEqual(
+        wrapper
+          .find('span')
+          .first()
+          .hasClass(classes.icon),
+        true,
+      );
+    });
+  });
 });