diff --git a/config/.stylelintrc.json b/config/.stylelintrc.json
index 841e374f2d..41b441ba7b 100644
--- a/config/.stylelintrc.json
+++ b/config/.stylelintrc.json
@@ -12,7 +12,8 @@
{
"ignoreAtRules": [
"mixin",
- "include"
+ "include",
+ "each"
]
}
]
diff --git a/src/components/Icon/Icon.jsx b/src/components/Icon/Icon.jsx
new file mode 100644
index 0000000000..981908065a
--- /dev/null
+++ b/src/components/Icon/Icon.jsx
@@ -0,0 +1,111 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+
+import { deprecate } from '../../warn'
+
+import styles from './Icon.modules.scss'
+
+const Icon = ({ glyph, variant, label, fixedWidth, size, className, children, ...rest }) => {
+ if (className) {
+ deprecate('Icon', 'Custom CSS classes are deprecated. This component will soon stop supporting custom styling.')
+ }
+
+ if (rest.style) {
+ deprecate('Icon', 'Inline styles are deprecated. This component will soon stop supporting custom styling.')
+ }
+
+ if (variant === 'disabled') {
+ deprecate('Icon', '\'disabled\' variant is deprecated.')
+ }
+
+ if (fixedWidth) {
+ deprecate('Icon', '\'fixedWidth\' prop is deprecated.')
+ }
+
+ const classes = `${styles.icon} ${styles[`icon-core-${glyph}`]}`
+ + ` ${variant ? styles[`icon--${variant}`] : ''}`
+ + `${fixedWidth ? ` ${styles['icon--fw']}` : ''}`
+ + `${size ? ` ${styles[`icon--${size}`]}` : ''}`
+ + `${className ? ` ${className}` : ''}`
+
+ return (
+
+ {children}
+
+ )
+}
+
+Icon.propTypes = {
+ /**
+ * Name of the icon glyph.
+ */
+ glyph: PropTypes.oneOf([
+ 'caret-down',
+ 'caret-up',
+ 'checkmark',
+ 'chevron',
+ 'left-chevron',
+ 'exclamation-point-circle',
+ 'expander',
+ 'hamburger',
+ 'incomplete',
+ 'location',
+ 'minus',
+ 'plus',
+ 'question-mark-circle',
+ 'spyglass',
+ 'times'
+ ]).isRequired,
+ /**
+ * The appearance of the Icon.
+ */
+ variant: PropTypes.oneOf([
+ 'inherit',
+ 'primary',
+ 'secondary',
+ 'inverted',
+ 'disabled',
+ 'error'
+ ]),
+ /**
+ * Whether or not to give the icon a fixed width.
+ *
+ * @deprecated an alternative will be provided soon.
+ */
+ fixedWidth: PropTypes.bool,
+ /**
+ *
+ */
+ size: PropTypes.oneOf(['small', 'medium', 'large']),
+ /**
+ * One or more CSS class names separated by spaces to append onto the icon.
+ * Don't advertise as we plan on removing this feature soon.
+ *
+ * @ignore
+ */
+ className: PropTypes.string,
+ /**
+ * Creates an `aria-label` attribute with the label you specify.
+ *
+ * If not provided, `aria-hidden` is set to true.
+ */
+ label: PropTypes.string,
+ /**
+ * @ignore
+ */
+ children: PropTypes.node
+}
+Icon.defaultProps = {
+ variant: 'inherit',
+ fixedWidth: false,
+ size: 'medium',
+ className: '',
+ children: null
+}
+
+export default Icon
diff --git a/src/components/Icon/Icon.md b/src/components/Icon/Icon.md
new file mode 100644
index 0000000000..83c721222c
--- /dev/null
+++ b/src/components/Icon/Icon.md
@@ -0,0 +1,81 @@
+### Available Icons
+
+```
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### Modifying color
+
+Use the `variant` prop to alter the icon's color. Each variant has semantic meaning.
+
+```
+
+
+
+
+
+```
+
+#### Primary and secondary
+
+Indicates a primary or secondary action.
+
+```
+
+
Add a user
+
Remove a user
+
+```
+
+#### Error
+
+Indicates a problem.
+
+```
+
+ Your location needs to be updated
+
+```
+
+### Accessibility considerations
+
+Icons can be either decorative or meaningful.
+
+**Decorative icons** do not perform a role beyond visual aesthetics and should be hidden from screen readers using the
+`aria-hidden` attribute. Usually, the information being communicated with the icon is also conveyed in another manner.
+
+This example shows a decorative icon that is hidden from screen readers. The Icon component sets `aria-hidden` to `true` by default; you can inspect the element and see.
+
+```
+
+ You are located in British Columbia.
+
+```
+
+**Meaningful icons** have meaning within the context of the page, which should be communicated to screen readers with the
+`aria-label` attribute. Meaningful icons can also be interactive elements, which should be designated with
+the `role` prop.
+
+This example shows a meaningful icon that needs accessibility attributes.
+
+```
+
+
+
+```
diff --git a/src/components/Icon/Icon.modules.scss b/src/components/Icon/Icon.modules.scss
new file mode 100644
index 0000000000..590999488c
--- /dev/null
+++ b/src/components/Icon/Icon.modules.scss
@@ -0,0 +1,73 @@
+@import '../../scss/settings/icons';
+@import '../../scss/settings/colours';
+@import '../../scss/utility/icons-utility';
+
+@font-face {
+ font-family: "TELUS Core Icons";
+ src: url('#{$icon-font-prefix}/core-icons.eot');
+ src:
+ url('#{$icon-font-prefix}/core-icons.woff2') format('woff2'),
+ url('#{$icon-font-prefix}/core-icons.woff') format('woff'),
+ url('#{$icon-font-prefix}/core-icons.ttf') format('truetype'),
+ url('#{$icon-font-prefix}/core-icons.eot?#iefix') format('eot');
+ font-weight: normal;
+ font-style: normal;
+}
+
+.icon {
+ @include core-icon;
+
+ transition: color 0.1s linear;
+
+ &--primary {
+ color: $color-icon-primary;
+ }
+
+ &--secondary {
+ color: $color-icon-secondary;
+ }
+
+ &--inverted {
+ color: $color-white;
+ }
+
+ &--error {
+ color: $color-cardinal;
+ }
+
+ &--disabled {
+ color: $color-icon-disabled;
+ }
+
+ &--fw {
+ width: 1.09rem;
+ text-align: center;
+ }
+
+ &--small {
+ font-size: 1 rem;
+ }
+
+ &--medium {
+ font-size: 1.5rem;
+ }
+
+ &--large {
+ font-size: 3rem;
+ }
+}
+
+@each $name, $codepoint in $core-icon-codepoints {
+ .icon-core-#{$name}::before {
+ content: $codepoint;
+ }
+}
+
+.icon-core-left-chevron {
+ @include core-icon(chevron);
+
+ &::before {
+ display: inline-block;
+ transform: rotate(-180deg) translateY(1.5px);
+ }
+}
diff --git a/src/components/Icon/__tests__/Icon.spec.jsx b/src/components/Icon/__tests__/Icon.spec.jsx
new file mode 100644
index 0000000000..99806450f5
--- /dev/null
+++ b/src/components/Icon/__tests__/Icon.spec.jsx
@@ -0,0 +1,109 @@
+import React from 'react'
+import { shallow } from 'enzyme'
+import toJson from 'enzyme-to-json'
+import { deprecate } from '../../../warn'
+
+import Icon from '../Icon'
+
+jest.mock('../../../warn', () => (
+ { deprecate: jest.fn() }
+))
+
+describe('', () => {
+ const defaultProps = {
+ glyph: 'checkmark'
+ }
+
+ const doShallow = (overrides = {}) => shallow()
+
+ it('renders', () => {
+ const icon = doShallow()
+
+ expect(toJson(icon)).toMatchSnapshot()
+ })
+
+ it('needs a glyph', () => {
+ const icon = doShallow({ glyph: 'spyglass' })
+
+ expect(icon).toHaveClassName('icon-core-spyglass')
+ })
+
+ it('supports variants', () => {
+ const icon = doShallow({ variant: 'secondary' })
+
+ expect(icon).toHaveClassName('icon--secondary')
+ })
+
+ it('can be fixed width', () => {
+ const icon = doShallow({ fixedWidth: true })
+
+ expect(icon).toHaveClassName('icon--fw')
+ })
+
+ it('can be sized', () => {
+ const icon = doShallow({ size: 'small' })
+
+ expect(icon).toHaveClassName('icon--small')
+ })
+
+ it('supports custom CSS classes', () => {
+ const icon = doShallow({ className: 'custom-class' })
+
+ expect(icon).toHaveClassName('custom-class')
+ })
+
+ it('passes additional attributes to the icon element', () => {
+ const icon = doShallow({ id: 'the-icon', role: 'button' })
+
+ expect(icon).toHaveProp('id', 'the-icon')
+ expect(icon).toHaveProp('role', 'button')
+ })
+
+ describe('deprecated props', () => {
+ it('deprecates className', () => {
+ jest.clearAllMocks()
+ const icon = doShallow({ className: 'my-custom-class' })
+
+ expect(icon).toHaveProp('className')
+ expect(deprecate).toHaveBeenCalled()
+ })
+
+ it('deprecates style', () => {
+ jest.clearAllMocks()
+ const icon = doShallow({ style: 'color: hotpink' })
+
+ expect(icon).toHaveProp('style')
+ expect(deprecate).toHaveBeenCalled()
+ })
+
+ it('deprecates disabled variant', () => {
+ jest.clearAllMocks()
+ const icon = doShallow({ variant: 'disabled' })
+
+ expect(icon).toHaveClassName('icon--disabled')
+ expect(deprecate).toHaveBeenCalled()
+ })
+
+ it('deprecates fixedWidth prop', () => {
+ jest.clearAllMocks()
+ const icon = doShallow({ fixedWidth: true })
+
+ expect(icon).toHaveClassName('icon--fw')
+ expect(deprecate).toHaveBeenCalled()
+ })
+ })
+
+ it('provides a label to specific glyphs', () => {
+ const icon = doShallow({ glyph: 'exclamation-point-circle', label: 'alert' })
+
+ expect(icon).toHaveProp('aria-label', 'alert')
+ expect(icon).not.toHaveProp('aria-hidden', 'undefined')
+ })
+
+ it('sets aria-hidden to true when label is not set', () => {
+ const icon = doShallow({ glyph: 'checkmark' })
+
+ expect(icon).toHaveProp('aria-hidden', 'true')
+ expect(icon).not.toHaveProp('aria-label', 'undefined')
+ })
+})
diff --git a/src/components/Icon/__tests__/__snapshots__/Icon.spec.jsx.snap b/src/components/Icon/__tests__/__snapshots__/Icon.spec.jsx.snap
new file mode 100644
index 0000000000..23636cad3d
--- /dev/null
+++ b/src/components/Icon/__tests__/__snapshots__/Icon.spec.jsx.snap
@@ -0,0 +1,8 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` renders 1`] = `
+
+`;