Skip to content

Commit d557441

Browse files
authored
Toast (#69)
* Toast WIP * Add tests for Toast * Fix missing test on FadeIn
1 parent 0bec28a commit d557441

12 files changed

Lines changed: 531 additions & 5 deletions

File tree

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react'
2-
import { mount } from 'enzyme'
2+
import { shallow, mount } from 'enzyme'
33

44
import { NAMESPACE } from '../../../constants'
55
import { FadeIn } from '../'
@@ -9,15 +9,23 @@ describe('<FadeIn />', () => {
99
jest.useFakeTimers()
1010
const $ = mount(
1111
<FadeIn>
12-
<div className="test">_</div>
12+
<div />
1313
</FadeIn>
1414
)
1515

16-
expect($.hasClass('test')).toBe(true)
1716
expect($.hasClass(`${NAMESPACE}c-fade-in`)).toBe(true)
1817
expect($.hasClass(`${NAMESPACE}c-fade-in--has-mounted`)).toBe(false)
1918

2019
jest.runAllTimers()
2120
expect($.hasClass(`${NAMESPACE}c-fade-in--has-mounted`)).toBe(true)
2221
})
22+
23+
it('adds the child class once mounted', () => {
24+
const $ = shallow(
25+
<FadeIn>
26+
<div className="test">_</div>
27+
</FadeIn>
28+
)
29+
expect($.hasClass('test')).toBe(true)
30+
})
2331
})

src/components/StatusCard/Body.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import T from 'prop-types'
44
import { classNames } from '../../utils/'
55
import { NAMESPACE, ALL_TAGS } from '../../constants'
66

7-
const StatusCardBody = ({ status, tag, className, children, ...rest }) =>
7+
const StatusCardBody = ({ tag, className, children, ...rest }) =>
88
E(
99
tag || 'div',
1010
{

src/components/StatusCard/Component.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import T from 'prop-types'
44
import { classNames } from '../../utils/'
55
import { NAMESPACE, ALL_TAGS } from '../../constants'
66

7-
const StatusCardComponent = ({ status, tag, className, children, ...rest }) =>
7+
const StatusCardComponent = ({ tag, className, children, ...rest }) =>
88
E(
99
tag || 'div',
1010
{

src/components/Toast/Dismiss.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { createElement as E } from 'react'
2+
import T from 'prop-types'
3+
4+
import { classNames } from '../../utils/'
5+
import { NAMESPACE, ALL_TAGS } from '../../constants'
6+
7+
const ToastDismiss = ({ children, className, tag, ...rest }, { dismissToastItem, toastItemId }) => {
8+
const handleClick = () => dismissToastItem(toastItemId)()
9+
return E(
10+
tag || 'button',
11+
{
12+
className: classNames(
13+
`${NAMESPACE}c-toast__dismiss`,
14+
{ [`${NAMESPACE}c-toast__dismiss--styled`]: !children },
15+
className
16+
),
17+
onClick: handleClick,
18+
...rest
19+
},
20+
children
21+
)
22+
}
23+
24+
ToastDismiss.contextTypes = {
25+
dismissToastItem: T.func.isRequired,
26+
toastItemId: T.string.isRequired
27+
}
28+
29+
ToastDismiss.propTypes = {
30+
tag: T.oneOf(ALL_TAGS),
31+
className: T.string,
32+
children: T.node
33+
}
34+
35+
export default ToastDismiss

src/components/Toast/Item.jsx

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { Component, createElement as E } from 'react'
2+
import T from 'prop-types'
3+
4+
import { classNames } from '../../utils/'
5+
import { NAMESPACE, ALL_TAGS } from '../../constants'
6+
7+
class ToastItem extends Component {
8+
state = { hasMounted: false }
9+
10+
getChildContext = () => ({
11+
toastItemId: this.props.id
12+
})
13+
14+
componentWillMount() {
15+
this.props.addItemId(this.props.id)
16+
}
17+
18+
componentDidMount() {
19+
this.node.focus()
20+
21+
setTimeout(() => {
22+
this.setState({ hasMounted: true })
23+
}, 0)
24+
25+
if (this.props.timeout) {
26+
this.handleTimeout()
27+
}
28+
}
29+
30+
handleTimeout = () => {
31+
setTimeout(this.props.dismiss, this.props.timeout)
32+
}
33+
34+
handleKeyUp = ({ keyCode }) => {
35+
if (keyCode === 27) {
36+
this.props.dismiss()
37+
}
38+
}
39+
40+
render() {
41+
const {
42+
addItemId, dismiss, timeout,
43+
tag, className, children, ...rest
44+
} = this.props
45+
return E(
46+
tag || 'li',
47+
{
48+
className: classNames(
49+
`${NAMESPACE}c-toast__item`,
50+
{ [`${NAMESPACE}c-toast__item--has-mounted`]: this.state.hasMounted },
51+
className
52+
),
53+
onKeyUp: this.handleKeyUp,
54+
tabIndex: '0',
55+
ref: (n) => { this.node = n },
56+
role: 'alert',
57+
...rest
58+
},
59+
children
60+
)
61+
}
62+
}
63+
64+
ToastItem.childContextTypes = {
65+
toastItemId: T.string.isRequired
66+
}
67+
68+
ToastItem.propTypes = {
69+
id: T.string.isRequired,
70+
tag: T.oneOf(ALL_TAGS),
71+
timeout: T.number,
72+
className: T.string,
73+
children: T.node.isRequired,
74+
dismiss: T.func,
75+
addItemId: T.func
76+
}
77+
78+
export default ToastItem

src/components/Toast/Wrapper.js

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import React, { Component, cloneElement, createElement as E } from 'react'
2+
import T from 'prop-types'
3+
4+
import { classNames } from '../../utils/'
5+
import { NAMESPACE, ALL_TAGS, TOAST_POSITIONS } from '../../constants'
6+
import Item from './Item'
7+
8+
const addToastItem = id => state => ({
9+
toastItems: {
10+
...state.toastItems,
11+
[id]: { dismissed: false }
12+
}
13+
})
14+
15+
const removeToastItem = id => state => ({
16+
toastItems: {
17+
...state.toastItems,
18+
[id]: { dismissed: true }
19+
}
20+
})
21+
22+
class ToastWrapper extends Component {
23+
state = { toastItems: {} }
24+
25+
getChildContext = () => ({
26+
dismissToastItem: this.dismissItem
27+
})
28+
29+
addItemId = id =>
30+
this.setState(addToastItem(id))
31+
32+
dismissItem = id => () => {
33+
if (this.props.onItemDismiss) {
34+
this.props.onItemDismiss(id)
35+
}
36+
37+
return this.setState(removeToastItem(id))
38+
}
39+
40+
render() {
41+
const {
42+
onItemDismiss,
43+
tag, position, className, children, ...rest
44+
} = this.props
45+
const { toastItems } = this.state
46+
const enhancedChildren = children && React.Children.map(children, (child) => {
47+
if (child.type !== Item) {
48+
return console.warn(`${child} is not a valid child of <Toast.Wrapper />`)
49+
}
50+
51+
const { id } = child && child.props
52+
53+
if (toastItems[id] && toastItems[id].dismissed) {
54+
return null
55+
}
56+
57+
return cloneElement(child, {
58+
addItemId: this.addItemId,
59+
dismiss: this.dismissItem(id)
60+
})
61+
})
62+
63+
return E(
64+
tag || 'ul',
65+
{
66+
className: classNames(
67+
`${NAMESPACE}c-toast`,
68+
{ [`${NAMESPACE}c-toast--${position}`]: position },
69+
className
70+
),
71+
...rest
72+
},
73+
enhancedChildren
74+
)
75+
}
76+
}
77+
78+
ToastWrapper.childContextTypes = {
79+
dismissToastItem: T.func.isRequired
80+
}
81+
82+
ToastWrapper.propTypes = {
83+
tag: T.oneOf(ALL_TAGS),
84+
className: T.string,
85+
onItemDismiss: T.func,
86+
position: T.oneOf(TOAST_POSITIONS),
87+
children: T.node
88+
}
89+
90+
export default ToastWrapper
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import React from 'react'
2+
import { shallow } from 'enzyme'
3+
4+
import { NAMESPACE } from '../../../constants'
5+
import { Toast } from '../'
6+
7+
const defaultContext = {
8+
dismissToastItem: jest.fn(),
9+
toastItemId: '_'
10+
}
11+
12+
describe('<Toast.Dismiss />', () => {
13+
it('takes a className that gets appended', () => {
14+
const $ = shallow(<Toast.Dismiss className="test">_</Toast.Dismiss>
15+
, { context: defaultContext })
16+
expect($.hasClass(`${NAMESPACE}c-toast__dismiss`)).toBe(true)
17+
expect($.hasClass('test')).toBe(true)
18+
})
19+
20+
it('renders a defined tag type', () => {
21+
const $ = shallow(<Toast.Dismiss tag="article">_</Toast.Dismiss>
22+
, { context: defaultContext })
23+
expect($.type()).toBe('article')
24+
})
25+
26+
it('renders a button by default', () => {
27+
const $ = shallow(<Toast.Dismiss>_</Toast.Dismiss>
28+
, { context: defaultContext })
29+
expect($.type()).toBe('button')
30+
})
31+
32+
it('renders with attributes', () => {
33+
const $ = shallow(
34+
<Toast.Dismiss style={{ position: 'relative' }} ariaHidden>
35+
_
36+
</Toast.Dismiss>
37+
, { context: defaultContext })
38+
expect($.prop('style')).toEqual({
39+
position: 'relative'
40+
})
41+
expect($.prop('ariaHidden')).toBe(true)
42+
})
43+
44+
it('renders children', () => {
45+
const $ = shallow(<Toast.Dismiss>test child</Toast.Dismiss>
46+
, { context: defaultContext })
47+
expect($.contains('test child')).toBe(true)
48+
})
49+
50+
it('renders styled if no children', () => {
51+
const $ = shallow(<Toast.Dismiss />
52+
, { context: defaultContext })
53+
expect($.hasClass(`${NAMESPACE}c-toast__dismiss`)).toBe(true)
54+
expect($.hasClass(`${NAMESPACE}c-toast__dismiss--styled`)).toBe(true)
55+
})
56+
57+
it('renders unstyled if children', () => {
58+
const $ = shallow(<Toast.Dismiss>_</Toast.Dismiss>
59+
, { context: defaultContext })
60+
expect($.hasClass(`${NAMESPACE}c-toast__dismiss`)).toBe(true)
61+
expect($.hasClass(`${NAMESPACE}c-toast__dismiss--styled`)).toBe(false)
62+
})
63+
64+
it('handles click events', () => {
65+
const mockDismissToastItem = jest.fn(() => () => {})
66+
const context = {
67+
...defaultContext,
68+
dismissToastItem: mockDismissToastItem,
69+
toastItemId: 'test'
70+
}
71+
const $ = shallow(<Toast.Dismiss>_</Toast.Dismiss>, { context })
72+
73+
expect(mockDismissToastItem).not.toHaveBeenCalled()
74+
75+
$.simulate('click')
76+
expect(mockDismissToastItem).toHaveBeenCalledWith('test')
77+
})
78+
})

0 commit comments

Comments
 (0)