Skip to content

Commit

Permalink
feat(icon): Copy to /components folder
Browse files Browse the repository at this point in the history
  • Loading branch information
lzcabrera committed Sep 20, 2017
1 parent bc081eb commit ee8f17a
Show file tree
Hide file tree
Showing 6 changed files with 384 additions and 1 deletion.
3 changes: 2 additions & 1 deletion config/.stylelintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
{
"ignoreAtRules": [
"mixin",
"include"
"include",
"each"
]
}
]
Expand Down
111 changes: 111 additions & 0 deletions src/components/Icon/Icon.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<i
{...rest}
className={classes}
aria-label={label}
aria-hidden={label ? undefined : 'true'}
>
{children}
</i>
)
}

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
81 changes: 81 additions & 0 deletions src/components/Icon/Icon.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
### Available Icons

```
<div className="docs--horizontal-spacing">
<Icon glyph="caret-down" />
<Icon glyph="caret-up" />
<Icon glyph="checkmark" />
<Icon glyph="left-chevron" />
<Icon glyph="chevron" />
<Icon glyph="exclamation-point-circle" />
<Icon glyph="expander" />
<Icon glyph="hamburger" />
<Icon glyph="incomplete" />
<Icon glyph="location" />
<Icon glyph="minus" />
<Icon glyph="plus" />
<Icon glyph="question-mark-circle" />
<Icon glyph="spyglass" />
<Icon glyph="times" />
</div>
```

### Modifying color

Use the `variant` prop to alter the icon's color. Each variant has semantic meaning.

```
<div className="docs--horizontal-spacing">
<Icon glyph="checkmark" variant="primary" />
<Icon glyph="incomplete" variant="secondary" />
<Icon glyph="exclamation-point-circle" variant="error" />
</div>
```

#### Primary and secondary

Indicates a primary or secondary action.

```
<div>
<div><Icon glyph="plus" variant="primary" /> Add a user</div>
<div><Icon glyph="minus" variant="secondary" /> Remove a user</div>
</div>
```

#### Error

Indicates a problem.

```
<div>
<Icon glyph="location" variant="error" /> Your location needs to be updated
</div>
```

### 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.

```
<Paragraph>
<Icon glyph="location" /> You are located in British Columbia.
</Paragraph>
```

**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.

```
<Paragraph>
<button style={{appearance: 'none', background: 'none', border: 0, cursor: 'pointer'}} type="submit"><Icon glyph="spyglass" label="search" /></button>
</Paragraph>
```
73 changes: 73 additions & 0 deletions src/components/Icon/Icon.modules.scss
Original file line number Diff line number Diff line change
@@ -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);
}
}
109 changes: 109 additions & 0 deletions src/components/Icon/__tests__/Icon.spec.jsx
Original file line number Diff line number Diff line change
@@ -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('<Icon />', () => {
const defaultProps = {
glyph: 'checkmark'
}

const doShallow = (overrides = {}) => shallow(<Icon {...defaultProps} {...overrides} />)

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')
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<Icon /> renders 1`] = `
<i
aria-hidden="true"
className="icon icon-core-checkmark icon--inherit icon--medium"
/>
`;

0 comments on commit ee8f17a

Please sign in to comment.