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`] = ` +