Repository: uber-web/baseui
Pull request #225 (2018-09-12 00:28:32)

feat: extract Tag into separate component
#### Major:  extract Tag into separate component


baseui-probot-app-workflow[bot]: Found TODOs without GitHub issues:
https://github.com/uber-web/baseui/blob/0619e5928c58552b7244988d727275b0314850e3/src/select/styled-components.js#L121
baseui-probot-app-workflow[bot]: Found TODOs without GitHub issues:
https://github.com/uber-web/baseui/blob/a411bbaf8e32720cad5c6bf88a3e2fe0489743b7/src/select/styled-components.js#L121
baseui-probot-app-workflow[bot]: Found TODOs without GitHub issues:
https://github.com/uber-web/baseui/blob/db8b467881ace2c1a9ea4a3af242f3bcef95b12b/src/select/styled-components.js#L121
baseui-probot-app-workflow[bot]: Found TODOs without GitHub issues:
https://github.com/uber-web/baseui/blob/28e90c851724cadb3af1f1c3ea4a475bad797038/src/select/styled-components.js#L121

Patch:
diff --git a/rfcs/tag-component.md b/rfcs/tag-component.md
index ef8ea50bb..f30de7228 100644
--- a/rfcs/tag-component.md
+++ b/rfcs/tag-component.md
@@ -18,7 +18,7 @@ export default () => {
   return (
     <div>
       <Tag
-        onActionClick={event => {}}
+        onActionClick={(event, children) => {}}
         disabled={false}
         overrides={{
           Root: CustomRoot,
@@ -40,8 +40,8 @@ export default () => {
 
 ## `Tag` API
 
-* `color: STYLE.blue(theme.colors.primary470) | STYLE.orange(theme.colors.warning400) | STYLE.green(theme.colors.positive400) | STYLE.red(theme.colors.negative400)`:
-  the color theme to be applied to a Tag. Default is `STYLE.blue`.
+* `color: STYLE.primary(theme.colors.primary470) | STYLE.warning(theme.colors.warning400) | STYLE.positive(theme.colors.positive400) | STYLE.negative(theme.colors.negative400)`:
+  the color theme to be applied to a Tag. Default is `STYLE.primary`.
 * `children: React$Node`:
   Component or String value for label of tag. Default is empty string
 * `disabled: boolean`:
@@ -49,5 +49,5 @@ export default () => {
 * `overrides: {Root: (props: {[string]: any}) => React$Node, Action: (props: {[string]: any}) => React$Node}`
   * Custom `Action` button.
   * `Root` wrapper element for the whole tag control to apply styles
-* `onActionClick: func(evt => void)`:
-  handler for events on Action button element (default is Cross).
+* `onActionClick: func((evt, children) => void)`:
+  handler for events on Action button element (default is Cross). `children` provides which actual Tag was clicked
diff --git a/src/select/constants.js b/src/select/constants.js
index 95a65e246..0866bb1a4 100644
--- a/src/select/constants.js
+++ b/src/select/constants.js
@@ -18,7 +18,6 @@ export const TYPE = {
 
 export const ICON = {
   clearAll: 'clear_all',
-  clearTag: 'clear_tag',
   select: 'select',
   selected: 'selected',
   loop: 'loop',
diff --git a/src/select/index.js b/src/select/index.js
index f5a168e80..bc3b8ca9c 100644
--- a/src/select/index.js
+++ b/src/select/index.js
@@ -13,7 +13,7 @@ export {
   Root as StyledRoot,
   Input as StyledInput,
   InputContainer as StyledInputContainer,
-  Tag as StyledTag,
+  SingleSelection as StyledSingleSelection,
   SearchIcon as StyledSearchIcon,
   DropDown as StyledDropDown,
   Option as StyledOption,
diff --git a/src/select/select.js b/src/select/select.js
index 5449c3b94..d18bb003b 100644
--- a/src/select/select.js
+++ b/src/select/select.js
@@ -12,11 +12,12 @@ import {
   Root as StyledRoot,
   Input as StyledInput,
   InputContainer as StyledInputContainer,
-  Tag as StyledTag,
+  SingleSelection as StyledSingleSelection,
   SearchIcon as StyledSearchIcon,
 } from './styled-components';
 
 import {Input as InputComponent} from '../input';
+import {Tag as StyledTag} from '../tag';
 import {ICON, TYPE, STATE_CHANGE_TYPE} from './constants';
 import SelectDropDown from './dropdown';
 import type {
@@ -290,12 +291,12 @@ class Select extends React.Component<PropsT, StatelessStateT> {
             }
           />
         )}
-        {selectedOptions.map(option => (
-          <Tag key={option.id} $multiple={multiple}>
-            {this.getSelectedOptionLabel(option)}
-            {multiple && (
-              <SearchIcon
-                onClick={e => {
+        {selectedOptions.map(
+          option =>
+            multiple ? (
+              <Tag
+                key={option.id}
+                onActionClick={e => {
                   this.setState({
                     selectedOptions: this.state.selectedOptions.filter(
                       selectedOption => selectedOption.id !== option.id,
@@ -303,14 +304,15 @@ class Select extends React.Component<PropsT, StatelessStateT> {
                   });
                   e.stopPropagation();
                 }}
-                $type={ICON.clearTag}
-                src={
-                  'data:image/svg+xml;utf8,<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M0.195262 0.195262C0.455612 -0.0650874 0.877722 -0.0650874 1.13807 0.195262L3.33333 2.39052L5.5286 0.195262C5.78895 -0.0650874 6.21106 -0.0650874 6.4714 0.195262C6.73175 0.455612 6.73175 0.877722 6.4714 1.13807L4.27614 3.33333L6.4714 5.5286C6.73175 5.78895 6.73175 6.21106 6.4714 6.4714C6.21106 6.73175 5.78895 6.73175 5.5286 6.4714L3.33333 4.27614L1.13807 6.4714C0.877722 6.73175 0.455612 6.73175 0.195262 6.4714C-0.0650874 6.21106 -0.0650874 5.78895 0.195262 5.5286L2.39052 3.33333L0.195262 1.13807C-0.0650874 0.877722 -0.0650874 0.455612 0.195262 0.195262Z" transform="translate(4.66675 4.6665)" fill="#276EF1"/></svg>'
-                }
-              />
-            )}
-          </Tag>
-        ))}
+              >
+                {this.getSelectedOptionLabel(option)}
+              </Tag>
+            ) : (
+              <StyledSingleSelection>
+                {this.getSelectedOptionLabel(option)}
+              </StyledSingleSelection>
+            ),
+        )}
       </React.Fragment>
     );
   }
diff --git a/src/select/styled-components.js b/src/select/styled-components.js
index 45e300325..5ded3a1c3 100644
--- a/src/select/styled-components.js
+++ b/src/select/styled-components.js
@@ -60,41 +60,18 @@ export const InputContainer = styled('div', props => {
   };
 });
 
-export const Tag = styled('span', props => {
-  const {$theme, $multiple} = props;
+export const SingleSelection = styled('span', props => {
+  const {$theme} = props;
   const {
-    colors: {primary400, mono400, mono1000},
-    sizing: {scale800, scale0},
-    typography: {font200},
+    colors: {mono1000},
   } = $theme;
-  return $multiple
-    ? {
-        ...font200,
-        height: scale800,
-        fontWeight: 'bold',
-        display: 'flex',
-        alignItems: 'center',
-        paddingTop: scale0,
-        paddingBottom: scale0,
-        paddingRight: '5px',
-        paddingLeft: '5px',
-        margin: '5px',
-        borderWidth: '1px',
-        borderColor: primary400,
-        color: primary400,
-        borderRadius: '7px',
-        backgroundColor: mono400,
-      }
-    : {
-        ...getInputStyles({...props, $size: SIZE.default, $disabled: true}),
-        color: mono1000,
-        cursor: 'pointer',
-        width: 'auto',
-        flexGrow: '1',
-        ':hover': {
-          cursor: 'pointer',
-        },
-      };
+  return {
+    ...getInputStyles({...props, $size: SIZE.default, $disabled: true}),
+    cursor: 'pointer',
+    width: 'auto',
+    flexGrow: '1',
+    color: mono1000,
+  };
 });
 
 export const SearchIcon = styled('img', props => {
@@ -112,6 +89,8 @@ export const SearchIcon = styled('img', props => {
       };
     case ICON.select:
       return {
+        position: 'absolute',
+        right: scale600,
         marginRight: scale500,
       };
     case ICON.selected:
@@ -122,7 +101,6 @@ export const SearchIcon = styled('img', props => {
       return {
         paddingLeft: scale300,
       };
-    case ICON.clearTag:
     default:
       return {};
   }
diff --git a/src/styles/index.js b/src/styles/index.js
index fde62a5e5..a90ceb699 100644
--- a/src/styles/index.js
+++ b/src/styles/index.js
@@ -6,5 +6,6 @@ LICENSE file in the root directory of this source tree.
 */
 // @flow
 export {default as styled} from './styled';
+export {hexToRgb} from './util';
 export {default as ThemeProvider} from './theme-provider';
 export type {ThemeT} from './types';
diff --git a/src/styles/util.js b/src/styles/util.js
new file mode 100644
index 000000000..a6df89ddf
--- /dev/null
+++ b/src/styles/util.js
@@ -0,0 +1,20 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+export function hexToRgb(hex: string = '', alpha: string = '1') {
+  const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
+  hex = hex.replace(shorthandRegex, function(m, r, g, b) {
+    return r + r + g + g + b + b;
+  });
+  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+  return result
+    ? `rgba(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(
+        result[3],
+        16,
+      )}, ${alpha})`
+    : null;
+}
diff --git a/src/tag/constants.js b/src/tag/constants.js
new file mode 100644
index 000000000..4021c021a
--- /dev/null
+++ b/src/tag/constants.js
@@ -0,0 +1,18 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+/* eslint-disable import/prefer-default-export */
+import DEFAULT_THEME from '../../src/themes/light-theme';
+
+export const STYLE = {
+  primary: DEFAULT_THEME.colors.primary470,
+  warning: DEFAULT_THEME.colors.warning400,
+  positive: DEFAULT_THEME.colors.positive400,
+  negative: DEFAULT_THEME.colors.negative400,
+};
+
+/* eslint-enable import/prefer-default-export */
diff --git a/src/tag/examples.js b/src/tag/examples.js
new file mode 100644
index 000000000..71d04db46
--- /dev/null
+++ b/src/tag/examples.js
@@ -0,0 +1,128 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+/* eslint-disable react/display-name*/
+
+import * as React from 'react';
+// Styled elements
+import {Tag, STYLE, StyledRoot} from './index';
+import {withStyle} from 'styletron-react';
+
+export const suite = 'Tag Test Suite';
+export const tests = {
+  ALL_BASIC_COLORS: 'All basic colors',
+  WITH_CUSTOM_COLORS: 'With custom colors',
+  WITH_CUSTOM_COMPONENTS: 'With custom components',
+  DISABLED: 'Disabled',
+};
+
+const colorIcons = {
+  '#05dc12': `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAC8ElEQVRYhb2TS08aYRhGPzdd9A+4bFfua4ziBauhGgxRUcBxwCJFjHhlLKK1ar2ARI0ViLFGjcUoIqhIWwsxJsa0SfsD2j/CvtWni6rfFKZMZhSf5KyYk/fABEKuZhqOc+xwLMW6Ysgpw7GUaTjOEf7YoRjHOA9gGjmC3XuCgaUzOALnd8rA0hns3hOYRo7AOA/ADsVoRItzP2V0HWIwcI7e+VN0TCXwYuL4TumYSqB3/hSDgXMYXYdoce6nbgL0XAQ2dxLWySTax45zinUyCZs7CT0XwU2AzhGGzXOC568/3Qs2zwl0jjANaOoPwTx+jLbRj7dCNxBBnS2Iksa3GfCfM48fo6k/RAO0fdswvvpwK9Qd79E9FsXG3ncILf15bd82DWjo2ULrSFw2jf1hrO99Ezx8vXSnoWeLBtTbg2BcR7JxzSeyHgeQ4dTbgzRA07UJw9CRbNbD2b89gAxH07VJA+o6N6BzxmRzcXn5z7Ffvy+w+/kH3rz7+l+nrnODBqhta2h+eSgboU2sfMnqqG1rNKDWugotdyAboYk5tdZVGlBjWUGDY18ShZqFG4TG/7xQs5Dh11hWaICqfRn1A1FJSF26r2pfpgHV5gA0/RFJSF26X20O0ICqNj/qeiOSkLp0v6rNTwMqjUtQ9+xJItvS/5ahxM8Mv9K4RAOU7CJqu8OyEZqYo2QXaUA5s4Bn9l3ZCE3MKWcWaECpYQ6qrpBshCbmlBrmaIBC70V1545shCbmKPReGlDc7MFT245shCbmFDd7aEBRkxsl7JpshCbmFDW5eX9D1gvzTEI2QhNzKlneK1Ays2BG47IRmpijZGZpQEWLG1oudG8BWi6EihbeK6honb1QWfz3FqCy+FHeOntxfT9PafIFywzTUFl80HI7OQvQcjtQWXwoM0xDafIFCSF5hBDygBCSr9DPREv17stS3SRyit59qdDNRAkh+Ve3yUNCSAEh5Mk9U3B1++8vQAh5xONxjuDfyCeEPPgDoeJfSvwdT7oAAAAASUVORK5CYII=`,
+  '#ef0335': `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAEZElEQVRYhb2UXUhbZxzGX3ppylo9S6L5PJrEr0ST5sukSUwKlXghwWWFMtjF9FYYAy/sVegOtA22MZ3rRdttQlPpREKZtdBtQluEYG0C6ya6YCuhVWiDUpQ28/LZjedLT+zojH94eP/vc97/+/w4Bw4hgir4/ZdW/P7VFb8fFdJqwe+/RKTquc+3+LK3F5tDQ/jn/PmKaHNoCC97e/Hc51sUhec9nviLSATvBwaw3d2N7VOnKqPubrwfGMCLSAR5jyfOASx0dKwV+/tRCgWxFezEnNOBX23WA9Wc04GtYCdKoSCK/f1Y6OhY4wCeuV1Y7zqNd8FOzIeC2MhmcdC1kc1iPhTEu2An1rtO45nbBQ4g53RgM+DHZsBfkXAhBJuTczp4gHmHHW/9Prz1+yQHtVotGIYBADAMA4qiPhqCzZl32HmAjN2GdZ8X6z6v5BDDMNBqtQAAl8uFvr6+jwZgczJ2Gw8wa7Wi6PWg6PVIDhWLRVAUhXQ6DZlMhmKxiHg8DoqiQFEU4vE4AEAmk3Ezwl50107OrNXKAzxst+C1x43XHndZ8mg0CpPJhGg0CgAwmUyIxWKIxWLc25HL5dx5YS8sNudhu4UHmLGYseZ2Yc3tKguQz+dB0zQymQwAgKZp7hnbS3m7i82ZsZh5gAfmFrxyOfDK5SgLAABms3nfvtxzYbE5D8wtPMD91mYUnHYUnPZ9Adxu/hN1dXUhmUwimUwiEAhwzxmGQU9Pj+issNic+63NPMBUcyNW7Das2G37AoRCIa5PpVIIh8MIh8NIpVIAgEgkgnA4jMHBQdFZYbE5U82NPMDdJhOWT1ixfMKKUi63L8T/qVIux+XcbTLxAJONBuRtbcjb2rAcOIlS7uD/hqVcFsuBk1zOZKOBB5gwGrDUbjlUTRgFAOOGeiy0tR6qxg31PMCtBhp/Wlr26Kd6/R5JnZGa/ZBuNdA8wBitwx+tTXskVVJnpGY/pDFaxwPc1GuQazGVFQDkWky4ITjH9gCQqtfihl7DefeMNLe/Z6Ql77yp1/AA17VqPG0ylhUA0brb21pa4t7Q0yYjsskEt88mE5J3XteqeYBrmjrMNTaUFQDRuttb/f47LH75hchLed1Ied2iGaGuaep4gFF1LTKm+rICIFr/iycsqTtH1bU8wIhKiVmDvqwAcOs0rcW4TiXyftap8QutEXmPzp7Bo7NnkP78M8k7R1RKHuBKnQLTtAaPG3SSAsCt0/1f4e/JCZH3148/4PdvvhZ5TxKX8SRxmfOEmqY1uFKn4AGGFfI3t9W1eEhrJAWAWwFge21V5LF71itcHeH2hasje+67ra7FsEL+hgNIKKnRYZUSU1oVZvTqimpKq8KwSomEkhpl848QQqouKKpXLipqMKaSI61R4jdd3YEqrVFiTCXHRUUNLiiqVwghVTvZpIoQYiCEWM7VHLvDUMc3vv20GpUQQx3fOFdz7A4hxLKTWSUCIIS0H5JEAEd2mqOEkE8OSUfZT/AvpLZs7lVi3lIAAAAASUVORK5CYII=`,
+};
+
+//$FlowFixMe
+const tagStyles: Array<string> = Object.values(STYLE);
+
+export default {
+  [tests.ALL_BASIC_COLORS]: () => {
+    return (
+      <React.Fragment>
+        <div style={{width: '200px'}}>
+          {tagStyles.map(color => (
+            <Tag
+              key={color}
+              color={color}
+              onActionClick={(e, tag) => {
+                if (typeof tag === 'string') {
+                  // eslint-disable-next-line no-console
+                  console.log('Tag is clicked:' + tag);
+                }
+              }}
+            >
+              Color {color}
+            </Tag>
+          ))}
+        </div>
+      </React.Fragment>
+    );
+  },
+  [tests.WITH_CUSTOM_COLORS]: () => {
+    return (
+      <React.Fragment>
+        <div style={{width: '200px'}}>
+          {['#000000', '#00ff35'].map((color: string) => (
+            <Tag
+              key={color}
+              color={color}
+              onActionClick={(e, tag) => {
+                if (typeof tag === 'string') {
+                  // eslint-disable-next-line no-console
+                  console.log('Tag is clicked:' + tag);
+                }
+              }}
+            >
+              Color {color}
+            </Tag>
+          ))}
+        </div>
+      </React.Fragment>
+    );
+  },
+  [tests.WITH_CUSTOM_COMPONENTS]: () => {
+    return (
+      <React.Fragment>
+        <div style={{width: '200px'}}>
+          {//$FlowFixMe
+          Object.entries(colorIcons).map(([color, icon]: Array<string>) => (
+            <Tag
+              key={color}
+              color={color}
+              onActionClick={(e, tag) => {
+                if (typeof tag === 'string') {
+                  // eslint-disable-next-line no-console
+                  console.log('Tag is clicked:' + tag);
+                }
+              }}
+              overrides={{
+                Root: withStyle(StyledRoot, props => ({
+                  color: props.$color,
+                  backgroundColor: props.$color + '0f',
+                })),
+              }}
+            >
+              Color {color} <img src={icon} />
+            </Tag>
+          ))}
+        </div>
+      </React.Fragment>
+    );
+  },
+  [tests.DISABLED]: () => {
+    return (
+      <React.Fragment>
+        <div style={{width: '200px'}}>
+          {tagStyles.map(color => (
+            <Tag
+              disabled={true}
+              key={color}
+              color={color}
+              onActionClick={(e, tag = '') => {
+                if (typeof tag === 'string') {
+                  // eslint-disable-next-line no-console
+                  console.log('Tag is clicked:' + tag);
+                }
+              }}
+            >
+              Color {color}
+            </Tag>
+          ))}
+        </div>
+      </React.Fragment>
+    );
+  },
+};
diff --git a/src/tag/index.js b/src/tag/index.js
new file mode 100644
index 000000000..7f1b90f4b
--- /dev/null
+++ b/src/tag/index.js
@@ -0,0 +1,15 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+export {default as Tag} from './tag';
+// Styled elements
+export {
+  Root as StyledRoot,
+  Action as StyledAction,
+  ActionIcon as StyledActionIcon,
+} from './styled-components';
+export {STYLE} from './constants';
diff --git a/src/tag/stories.js b/src/tag/stories.js
new file mode 100644
index 000000000..ac0c7a687
--- /dev/null
+++ b/src/tag/stories.js
@@ -0,0 +1,22 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+/* global module */
+import {storiesOf} from '@storybook/react';
+
+// Styled elements
+import examples from './examples';
+import {withReadme} from 'storybook-readme';
+//$FlowFixMe
+import SelectReadme from '../../rfcs/tag-component.md';
+
+Object.entries(examples).forEach(([description, example]) =>
+  storiesOf('Tag', module)
+    .addDecorator(withReadme(SelectReadme))
+    // $FlowFixMe
+    .add(description, example),
+);
diff --git a/src/tag/styled-components.js b/src/tag/styled-components.js
new file mode 100644
index 000000000..6f27e1df2
--- /dev/null
+++ b/src/tag/styled-components.js
@@ -0,0 +1,60 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+import {styled, hexToRgb} from '../styles/index';
+
+export const Action = styled('div', props => {
+  const {$color, $disabled} = props;
+  return {
+    display: 'flex',
+    alignItems: 'center',
+    padding: '12px',
+    borderTopRightRadius: '2px',
+    borderBottomRightRadius: '2px',
+    marginLeft: '8px',
+    cursor: $disabled ? 'not-allowed' : 'pointer',
+    ':hover': {
+      backgroundColor: $disabled ? null : hexToRgb($color, '0.2'),
+    },
+  };
+});
+
+export const ActionIcon = styled('svg', () => {
+  return {};
+});
+
+export const Root = styled('span', props => {
+  const {$theme, $color, $disabled, $isFocused, $isActive} = props;
+  const {
+    sizing: {scale800, scale100, scale300},
+    typography: {font200},
+  } = $theme;
+  return {
+    ...font200,
+    display: 'inline-flex',
+    height: scale800,
+    fontWeight: 'bold',
+    margin: '5px',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    paddingTop: scale100,
+    paddingBottom: scale100,
+    paddingLeft: scale300,
+    borderWidth: '1px',
+    borderColor: $color,
+    color: $color,
+    opacity: $disabled ? '.56' : null,
+    borderRadius: $theme.borders.useRoundedCorners
+      ? $theme.borders.radius100
+      : '0px',
+    cursor: $disabled ? 'not-allowed' : 'auto',
+    backgroundColor: hexToRgb($color, $isFocused || $isActive ? '0.2' : '0.06'),
+    ':hover': {
+      backgroundColor: $disabled ? null : hexToRgb($color, '0.2'),
+    },
+  };
+});
diff --git a/src/tag/tag.js b/src/tag/tag.js
new file mode 100644
index 000000000..ec4853800
--- /dev/null
+++ b/src/tag/tag.js
@@ -0,0 +1,85 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+import React from 'react';
+import {getOverrideObject} from '../helpers/overrides';
+import type {PropsT} from './types';
+import {
+  Action as StyledAction,
+  Root as StyledRoot,
+  ActionIcon as StyledActionIcon,
+} from './styled-components';
+import {STYLE} from './constants';
+
+class Tag extends React.Component<PropsT, {}> {
+  static defaultProps = {
+    overrides: {},
+    onActionClick: () => {},
+    color: STYLE.primary,
+    disabled: false,
+    isFocused: false,
+    isHovered: false,
+  };
+
+  render() {
+    const {
+      overrides = {},
+      disabled,
+      isFocused,
+      isHovered,
+      children,
+      onActionClick,
+      color,
+    } = this.props;
+    const {component: Root, props: rootProps} = getOverrideObject(
+      overrides.Root,
+      StyledRoot,
+    );
+    const {component: Action, props: actionProps} = getOverrideObject(
+      overrides.Action,
+      StyledAction,
+    );
+    const {component: ActionIcon, props: actionIconProps} = getOverrideObject(
+      overrides.ActionIcon,
+      StyledActionIcon,
+    );
+    const sharedProps = {
+      $isFocused: isFocused,
+      $isHovered: isHovered,
+      $disabled: disabled,
+      $color: color,
+    };
+    return (
+      <Root {...sharedProps} {...rootProps}>
+        {children}
+        <Action
+          {...sharedProps}
+          {...actionProps}
+          onClick={e => onActionClick(e, children)}
+        >
+          <ActionIcon
+            width={'8'}
+            height={'8'}
+            viewBox={'0 0 8 8'}
+            fill={'none'}
+            xmlns={'http://www.w3.org/2000/svg'}
+            {...actionIconProps}
+          >
+            <path
+              fillRule="evenodd"
+              clipRule="evenodd"
+              d="M0.861278 0.862254C1.12163 0.601905 1.54374 0.601905 1.80409 0.862254L3.99935 3.05752L6.19461 0.862254C6.45496 0.601905 6.87707 0.601905 7.13742 0.862254C7.39777 1.1226 7.39777 1.54471 7.13742 1.80506L4.94216 4.00033L7.13742 6.19559C7.39777 6.45594 7.39777 6.87805 7.13742 7.1384C6.87707 7.39875 6.45496 7.39875 6.19461 7.1384L3.99935 4.94313L1.80409 7.1384C1.54374 7.39875 1.12163 7.39875 0.861278 7.1384C0.600928 6.87805 0.600928 6.45594 0.861278 6.19559L3.05654 4.00033L0.861278 1.80506C0.600928 1.54471 0.600928 1.1226 0.861278 0.862254Z"
+              fill={color}
+            />
+          </ActionIcon>
+        </Action>
+      </Root>
+    );
+  }
+}
+
+export default Tag;
diff --git a/src/tag/types.js b/src/tag/types.js
new file mode 100644
index 000000000..0838f3835
--- /dev/null
+++ b/src/tag/types.js
@@ -0,0 +1,29 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+
+import type {OverrideT} from '../helpers/overrides';
+
+export type OverridesT = {
+  Root?: OverrideT<*>,
+  Action?: OverrideT<*>,
+  ActionIcon?: OverrideT<*>,
+};
+
+export type PropsT = {
+  overrides?: OverridesT,
+  disabled?: boolean,
+  isFocused?: boolean,
+  isHovered?: boolean,
+  children?: React$Node,
+  color: string,
+  $theme?: *,
+  onActionClick: (
+    e: SyntheticEvent<HTMLInputElement>,
+    children: React$Node,
+  ) => void,
+};


Test patch:
diff --git a/src/select/__tests__/__snapshots__/styled-components.test.js.snap b/src/select/__tests__/__snapshots__/styled-components.test.js.snap
index 7e2937427..852ca6ca6 100644
--- a/src/select/__tests__/__snapshots__/styled-components.test.js.snap
+++ b/src/select/__tests__/__snapshots__/styled-components.test.js.snap
@@ -164,8 +164,6 @@ Object {
 }
 `;
 
-exports[`Select styled components StyledSearchIcon : StyledSearchIcon has correct styles when type is clear_tag 1`] = `Object {}`;
-
 exports[`Select styled components StyledSearchIcon : StyledSearchIcon has correct styles when type is loop 1`] = `
 Object {
   "paddingLeft": "$theme.sizing.scale300",
@@ -175,6 +173,8 @@ Object {
 exports[`Select styled components StyledSearchIcon : StyledSearchIcon has correct styles when type is select 1`] = `
 Object {
   "marginRight": "$theme.sizing.scale500",
+  "position": "absolute",
+  "right": "$theme.sizing.scale600",
 }
 `;
 
@@ -183,55 +183,3 @@ Object {
   "paddingRight": "$theme.sizing.scale300",
 }
 `;
-
-exports[`Select styled components StyledTag : StyledTag has correct styles when  1`] = `
-Object {
-  "::placeholder": Object {
-    "color": "$theme.colors.mono600",
-  },
-  ":hover": Object {
-    "cursor": "pointer",
-  },
-  "backgroundColor": "transparent",
-  "borderStyle": "none",
-  "borderWidth": "0",
-  "boxSizing": "border-box",
-  "caretColor": "$theme.colors.primary",
-  "color": "$theme.colors.mono1000",
-  "cursor": "pointer",
-  "flexGrow": "1",
-  "fontFamily": "$theme.typography.font300.fontFamily",
-  "fontSize": "$theme.typography.font300.fontSize",
-  "fontWeight": "$theme.typography.font300.fontWeight",
-  "lineHeight": "$theme.typography.font300.lineHeight",
-  "maxWidth": "100%",
-  "outline": "none",
-  "paddingBottom": "$theme.sizing.scale400",
-  "paddingLeft": "$theme.sizing.scale500",
-  "paddingRight": "$theme.sizing.scale500",
-  "paddingTop": "$theme.sizing.scale400",
-  "width": "auto",
-}
-`;
-
-exports[`Select styled components StyledTag : StyledTag has correct styles when $multiple 1`] = `
-Object {
-  "alignItems": "center",
-  "backgroundColor": "$theme.colors.mono400",
-  "borderColor": "$theme.colors.primary400",
-  "borderRadius": "7px",
-  "borderWidth": "1px",
-  "color": "$theme.colors.primary400",
-  "display": "flex",
-  "fontFamily": "$theme.typography.font200.fontFamily",
-  "fontSize": "$theme.typography.font200.fontSize",
-  "fontWeight": "bold",
-  "height": "$theme.sizing.scale800",
-  "lineHeight": "$theme.typography.font200.lineHeight",
-  "margin": "5px",
-  "paddingBottom": "$theme.sizing.scale0",
-  "paddingLeft": "5px",
-  "paddingRight": "5px",
-  "paddingTop": "$theme.sizing.scale0",
-}
-`;
diff --git a/src/select/__tests__/select.test.js b/src/select/__tests__/select.test.js
index 4016e979d..2e2aa0fb3 100644
--- a/src/select/__tests__/select.test.js
+++ b/src/select/__tests__/select.test.js
@@ -9,6 +9,7 @@ import React from 'react';
 import {mount, shallow} from 'enzyme';
 import {Select, StyledSearchIcon, StyledInput, StyledOption} from '../index';
 import {STATE_CHANGE_TYPE, TYPE, ICON} from '../constants';
+import {StyledAction} from '../../tag';
 
 describe('Stateless select', function() {
   let wrapper,
@@ -207,10 +208,7 @@ describe('Stateless select', function() {
     });
 
     test('should remove selected tag from selected options if X button is clicked', function() {
-      clearIcon = wrapper.find(StyledSearchIcon);
-      clearIcon = clearIcon.filterWhere(
-        comp => comp.props().$type === ICON.clearTag,
-      );
+      clearIcon = wrapper.find(StyledAction);
       expect(wrapper.instance().state.selectedOptions).toEqual(selectedOptions);
       clearIcon.first().simulate('click');
       expect(wrapper.instance().state.selectedOptions).toEqual([]);
diff --git a/src/select/__tests__/stateful-select.test.js b/src/select/__tests__/stateful-select.test.js
index ef2c2eb1b..31e79acf9 100644
--- a/src/select/__tests__/stateful-select.test.js
+++ b/src/select/__tests__/stateful-select.test.js
@@ -26,18 +26,19 @@ describe('Stateful checkbox', function() {
       StyledRoot,
       StyledInput,
       StyledInputContainer,
-      StyledTag,
       StyledSearchIcon,
       StyledDropDown,
       StyledOption,
       StyledDropDownItem,
       StatefulSelect,
     } = require('../index');
+
+    const {Tag} = require('../../tag');
     allProps.overrides = {
       Root: StyledRoot,
       Input: StyledInput,
       InputContainer: StyledInputContainer,
-      Tag: StyledTag,
+      Tag: Tag,
       SearchIcon: StyledSearchIcon,
       DropDown: StyledDropDown,
       Option: StyledOption,
@@ -50,7 +51,7 @@ describe('Stateful checkbox', function() {
       Root: StyledRoot,
       Input: StyledInput,
       InputContainer: StyledInputContainer,
-      Tag: StyledTag,
+      Tag: Tag,
       SearchIcon: StyledSearchIcon,
       DropDown: StyledDropDown,
       Option: StyledOption,
diff --git a/src/select/__tests__/styled-components.test.js b/src/select/__tests__/styled-components.test.js
index 02ce53c69..417bf1240 100644
--- a/src/select/__tests__/styled-components.test.js
+++ b/src/select/__tests__/styled-components.test.js
@@ -10,7 +10,6 @@ import {shallow} from 'enzyme';
 import {
   StyledInput,
   StyledInputContainer,
-  StyledTag,
   StyledSearchIcon,
   StyledDropDown,
   StyledDropDownItem,
@@ -44,39 +43,21 @@ describe('Select styled components', () => {
     });
   });
 
-  describe('StyledTag', () => {
-    test.each([[''], ['$multiple']])('', prop => {
-      const props = {};
-      props[prop] = true;
-      const component = shallow(
-        <StyledTag {...props}>
-          <div />
-        </StyledTag>,
-      );
-      expect(component.instance().getStyles()).toMatchSnapshot(
-        'StyledTag has correct styles when ' + prop,
-      );
-    });
-  });
-
   describe('StyledSearchIcon', () => {
-    test.each([
-      [ICON.clearAll],
-      [ICON.select],
-      [ICON.loop],
-      [ICON.clearTag],
-      [ICON.selected],
-    ])('', $type => {
-      const props = {$type};
-      const component = shallow(
-        <StyledSearchIcon {...props}>
-          <div />
-        </StyledSearchIcon>,
-      );
-      expect(component.instance().getStyles()).toMatchSnapshot(
-        'StyledSearchIcon has correct styles when type is ' + $type,
-      );
-    });
+    test.each([[ICON.clearAll], [ICON.select], [ICON.loop], [ICON.selected]])(
+      '',
+      $type => {
+        const props = {$type};
+        const component = shallow(
+          <StyledSearchIcon {...props}>
+            <div />
+          </StyledSearchIcon>,
+        );
+        expect(component.instance().getStyles()).toMatchSnapshot(
+          'StyledSearchIcon has correct styles when type is ' + $type,
+        );
+      },
+    );
   });
 
   describe('StyledDropDown', () => {
diff --git a/src/tag/__tests__/__snapshots__/styled-components.test.js.snap b/src/tag/__tests__/__snapshots__/styled-components.test.js.snap
new file mode 100644
index 000000000..f77b3c5a5
--- /dev/null
+++ b/src/tag/__tests__/__snapshots__/styled-components.test.js.snap
@@ -0,0 +1,139 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Tag styled components StyledAction : StyledAction has correct styles when  1`] = `
+Object {
+  ":hover": Object {
+    "backgroundColor": null,
+  },
+  "alignItems": "center",
+  "borderBottomRightRadius": "2px",
+  "borderTopRightRadius": "2px",
+  "cursor": "pointer",
+  "display": "flex",
+  "marginLeft": "8px",
+  "padding": "12px",
+}
+`;
+
+exports[`Tag styled components StyledAction : StyledAction has correct styles when $disabled 1`] = `
+Object {
+  ":hover": Object {
+    "backgroundColor": null,
+  },
+  "alignItems": "center",
+  "borderBottomRightRadius": "2px",
+  "borderTopRightRadius": "2px",
+  "cursor": "not-allowed",
+  "display": "flex",
+  "marginLeft": "8px",
+  "padding": "12px",
+}
+`;
+
+exports[`Tag styled components StyledRoot : StyledRoot has correct styles when  1`] = `
+Object {
+  ":hover": Object {
+    "backgroundColor": null,
+  },
+  "alignItems": "center",
+  "backgroundColor": null,
+  "borderColor": undefined,
+  "borderRadius": "$theme.borders.radius100",
+  "borderWidth": "1px",
+  "color": undefined,
+  "cursor": "auto",
+  "display": "inline-flex",
+  "fontFamily": "$theme.typography.font200.fontFamily",
+  "fontSize": "$theme.typography.font200.fontSize",
+  "fontWeight": "bold",
+  "height": "$theme.sizing.scale800",
+  "justifyContent": "space-between",
+  "lineHeight": "$theme.typography.font200.lineHeight",
+  "margin": "5px",
+  "opacity": null,
+  "paddingBottom": "$theme.sizing.scale100",
+  "paddingLeft": "$theme.sizing.scale300",
+  "paddingTop": "$theme.sizing.scale100",
+}
+`;
+
+exports[`Tag styled components StyledRoot : StyledRoot has correct styles when $disabled 1`] = `
+Object {
+  ":hover": Object {
+    "backgroundColor": null,
+  },
+  "alignItems": "center",
+  "backgroundColor": null,
+  "borderColor": undefined,
+  "borderRadius": "$theme.borders.radius100",
+  "borderWidth": "1px",
+  "color": undefined,
+  "cursor": "not-allowed",
+  "display": "inline-flex",
+  "fontFamily": "$theme.typography.font200.fontFamily",
+  "fontSize": "$theme.typography.font200.fontSize",
+  "fontWeight": "bold",
+  "height": "$theme.sizing.scale800",
+  "justifyContent": "space-between",
+  "lineHeight": "$theme.typography.font200.lineHeight",
+  "margin": "5px",
+  "opacity": ".56",
+  "paddingBottom": "$theme.sizing.scale100",
+  "paddingLeft": "$theme.sizing.scale300",
+  "paddingTop": "$theme.sizing.scale100",
+}
+`;
+
+exports[`Tag styled components StyledRoot : StyledRoot has correct styles when $isActive 1`] = `
+Object {
+  ":hover": Object {
+    "backgroundColor": null,
+  },
+  "alignItems": "center",
+  "backgroundColor": null,
+  "borderColor": undefined,
+  "borderRadius": "$theme.borders.radius100",
+  "borderWidth": "1px",
+  "color": undefined,
+  "cursor": "auto",
+  "display": "inline-flex",
+  "fontFamily": "$theme.typography.font200.fontFamily",
+  "fontSize": "$theme.typography.font200.fontSize",
+  "fontWeight": "bold",
+  "height": "$theme.sizing.scale800",
+  "justifyContent": "space-between",
+  "lineHeight": "$theme.typography.font200.lineHeight",
+  "margin": "5px",
+  "opacity": null,
+  "paddingBottom": "$theme.sizing.scale100",
+  "paddingLeft": "$theme.sizing.scale300",
+  "paddingTop": "$theme.sizing.scale100",
+}
+`;
+
+exports[`Tag styled components StyledRoot : StyledRoot has correct styles when $isFocused 1`] = `
+Object {
+  ":hover": Object {
+    "backgroundColor": null,
+  },
+  "alignItems": "center",
+  "backgroundColor": null,
+  "borderColor": undefined,
+  "borderRadius": "$theme.borders.radius100",
+  "borderWidth": "1px",
+  "color": undefined,
+  "cursor": "auto",
+  "display": "inline-flex",
+  "fontFamily": "$theme.typography.font200.fontFamily",
+  "fontSize": "$theme.typography.font200.fontSize",
+  "fontWeight": "bold",
+  "height": "$theme.sizing.scale800",
+  "justifyContent": "space-between",
+  "lineHeight": "$theme.typography.font200.lineHeight",
+  "margin": "5px",
+  "opacity": null,
+  "paddingBottom": "$theme.sizing.scale100",
+  "paddingLeft": "$theme.sizing.scale300",
+  "paddingTop": "$theme.sizing.scale100",
+}
+`;
diff --git a/src/tag/__tests__/__snapshots__/tag.test.js.snap b/src/tag/__tests__/__snapshots__/tag.test.js.snap
new file mode 100644
index 000000000..a2983295e
--- /dev/null
+++ b/src/tag/__tests__/__snapshots__/tag.test.js.snap
@@ -0,0 +1,61 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Stateless tag should render component: Component has correct render 1`] = `
+<Tag
+  color="#276EF1"
+  disabled={false}
+  isFocused={false}
+  isHovered={false}
+  onActionClick={[MockFunction]}
+  overrides={Object {}}
+>
+  <MockStyledComponent
+    $color="#276EF1"
+    $disabled={false}
+    $isFocused={false}
+    $isHovered={false}
+  >
+    <span
+      styled-component="true"
+    >
+      Some tag
+      <MockStyledComponent
+        $color="#276EF1"
+        $disabled={false}
+        $isFocused={false}
+        $isHovered={false}
+        onClick={[Function]}
+      >
+        <div
+          onClick={[Function]}
+          styled-component="true"
+        >
+          <MockStyledComponent
+            fill="none"
+            height="8"
+            viewBox="0 0 8 8"
+            width="8"
+            xmlns="http://www.w3.org/2000/svg"
+          >
+            <svg
+              fill="none"
+              height="8"
+              styled-component="true"
+              viewBox="0 0 8 8"
+              width="8"
+              xmlns="http://www.w3.org/2000/svg"
+            >
+              <path
+                clipRule="evenodd"
+                d="M0.861278 0.862254C1.12163 0.601905 1.54374 0.601905 1.80409 0.862254L3.99935 3.05752L6.19461 0.862254C6.45496 0.601905 6.87707 0.601905 7.13742 0.862254C7.39777 1.1226 7.39777 1.54471 7.13742 1.80506L4.94216 4.00033L7.13742 6.19559C7.39777 6.45594 7.39777 6.87805 7.13742 7.1384C6.87707 7.39875 6.45496 7.39875 6.19461 7.1384L3.99935 4.94313L1.80409 7.1384C1.54374 7.39875 1.12163 7.39875 0.861278 7.1384C0.600928 6.87805 0.600928 6.45594 0.861278 6.19559L3.05654 4.00033L0.861278 1.80506C0.600928 1.54471 0.600928 1.1226 0.861278 0.862254Z"
+                fill="#276EF1"
+                fillRule="evenodd"
+              />
+            </svg>
+          </MockStyledComponent>
+        </div>
+      </MockStyledComponent>
+    </span>
+  </MockStyledComponent>
+</Tag>
+`;
diff --git a/src/tag/__tests__/styled-components.test.js b/src/tag/__tests__/styled-components.test.js
new file mode 100644
index 000000000..3dd487288
--- /dev/null
+++ b/src/tag/__tests__/styled-components.test.js
@@ -0,0 +1,45 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+import React from 'react';
+import {shallow} from 'enzyme';
+import {StyledRoot, StyledAction} from '../index';
+
+describe('Tag styled components', () => {
+  describe('StyledRoot', () => {
+    test.each([[''], ['$disabled'], ['$isFocused'], ['$isActive']])(
+      '',
+      prop => {
+        const props = {};
+        props[prop] = true;
+        const component = shallow(
+          <StyledRoot {...props}>
+            <div />
+          </StyledRoot>,
+        );
+        expect(component.instance().getStyles()).toMatchSnapshot(
+          'StyledRoot has correct styles when ' + prop,
+        );
+      },
+    );
+  });
+
+  describe('StyledAction', () => {
+    test.each([['$disabled'], ['']])('', prop => {
+      const props = {};
+      props[prop] = true;
+      const component = shallow(
+        <StyledAction {...props}>
+          <div />
+        </StyledAction>,
+      );
+      expect(component.instance().getStyles()).toMatchSnapshot(
+        'StyledAction has correct styles when ' + prop,
+      );
+    });
+  });
+});
diff --git a/src/tag/__tests__/tag.test.js b/src/tag/__tests__/tag.test.js
new file mode 100644
index 000000000..779c66346
--- /dev/null
+++ b/src/tag/__tests__/tag.test.js
@@ -0,0 +1,55 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+import React from 'react';
+import {mount} from 'enzyme';
+import {Tag, StyledAction} from '../index';
+
+describe('Stateless tag', function() {
+  let wrapper,
+    events = {},
+    children;
+  let allProps: any = {},
+    mockFn;
+
+  beforeEach(function() {
+    mockFn = jest.fn();
+    events = {
+      onActionClick: mockFn,
+    };
+    allProps = {
+      ...events,
+    };
+    children = 'Some tag';
+  });
+
+  afterEach(function() {
+    jest.restoreAllMocks();
+    wrapper && wrapper.unmount();
+  });
+
+  test('should render component', function() {
+    wrapper = mount(<Tag {...allProps}>{children}</Tag>);
+    expect(wrapper).toMatchSnapshot('Component has correct render');
+  });
+
+  describe('On action', function() {
+    beforeEach(function() {
+      allProps.onActionClick = jest.fn();
+      wrapper = mount(<Tag {...allProps}>{children}</Tag>);
+    });
+
+    test('should call callback if action button is clicked', function() {
+      const actionIcon = wrapper.find(StyledAction);
+      actionIcon.first().simulate('click');
+      expect(allProps.onActionClick).toHaveBeenCalledWith(
+        allProps.onActionClick.mock.calls[0][0],
+        children,
+      );
+    });
+  });
+});