Skip to content

Commit

Permalink
Add ToastMessage component
Browse files Browse the repository at this point in the history
  • Loading branch information
guilhermebruzzi committed Nov 8, 2018
1 parent 24b042b commit 3116e4b
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 46 deletions.
120 changes: 75 additions & 45 deletions react/components/Toast/README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
#### Toasts give users instant feedback about the tasks they just did. Its main objective is to ensure tasks confirmation and success.
### Overview

Toasts give users instant feedback about the tasks they just did. Its main objective is to ensure tasks confirmation and success.

### 👍 Dos
- Toasts are always self-dismissing, but users should be allowed to dismiss by themselves as well.
- Keep messages in a low to mild priority spectrum. Toasts are intended to be either neutral or positive.

- Toasts are always self-dismissing, but users should be allowed to dismiss by themselves as well.
- Keep messages in a low to mild priority spectrum. Toasts are intended to be either neutral or positive.

### 👎 Don'ts

- Do not present critical or high priority actions on a Toast. If that's the case, you might consider using [Alerts](#alert) instead.
- Due to its low to mild priority usage and dismissability, warning and error semantic styles do not apply to Toasts.


```
```js
const ToastProvider = require('./index').ToastProvider
const ToastConsumer = require('./index').ToastConsumer

const App = () => (
// Wrap the entire application on a toast provider
<ToastProvider
positioning="window"
>
<Content/>
<ToastProvider positioning="window">
<Content />
</ToastProvider>
)

Expand All @@ -27,12 +28,10 @@ const Content = () => (
// on a ToastConsumer, with a function as a child, as
// the example below:
<div>
<div className="mb5">
Click on a button to show the corresponding toast
</div>
<div className="mb5">Click on a button to show the corresponding toast</div>
<div className="mb8">
<ToastConsumer>
{ ({ showToast }) => (
{({ showToast }) => (
<div className="flex">
<div className="mr5">
<Button
Expand All @@ -46,34 +45,32 @@ const Content = () => (
<Button
size="small"
variation="secondary"
onClick={
() => showToast({
onClick={() =>
showToast({
message: 'The bread has been inserted into the toaster',
action: {
label:'Undo',
onClick: () =>
alert('Bread removed from toaster'),
label: 'Undo',
onClick: () => alert('Bread removed from toaster'),
},
})
}
>
}>
With action
</Button>
</div>
<div className="mr5">
<Button
size="small"
variation="secondary"
onClick={
() => showToast({
message: 'The toast is done, with a layer of butter on top, along with a nice cup of coffee.',
onClick={() =>
showToast({
message:
'The toast is done, with a layer of butter on top, along with a nice cup of coffee.',
action: {
label: 'Thanks',
onClick: () => {},
}
},
})
}
>
}>
Long text
</Button>
</div>
Expand All @@ -82,56 +79,89 @@ const Content = () => (
</ToastConsumer>
</div>

<div className="mb5">
Toast duration and control
</div>
<div className="mb5">Toast duration and control</div>
<ToastConsumer>
{({showToast, hideToast}) => (
<div className="flex">
{({ showToast, hideToast }) => (
<div className="flex mb5">
<div className="mr5">
<Button
size="small"
variation="secondary"
onClick={
() => showToast({
onClick={() =>
showToast({
message: 'This message lasts 30 seconds',
duration: 30000,
})
}
>
}>
30 seconds
</Button>
</div>
<div className="mr5">
<Button
size="small"
variation="secondary"
onClick={
() => showToast({
onClick={() =>
showToast({
message: 'This message stays here until closed',
duration: Infinity,
})
}
>
}>
Permanent
</Button>
</div>
<div className="mr5">
<Button
size="small"
variation="danger"
onClick={hideToast}
>
<Button size="small" variation="danger" onClick={hideToast}>
Close all toasts
</Button>
</div>
</div>
)}
</ToastConsumer>

<div className="mb5">
Using with ToastMessage component so you can call immediately on render
</div>
<ToastConsumer>
{({ ToastMessage }) => <ShowToastWithReact ToastMessage={ToastMessage} />}
</ToastConsumer>
</div>
)

class ShowToastWithReact extends React.Component {
constructor(props) {
super(props)
this.state = {
active: false,
}
this.handleClick = this.handleClick.bind(this)
this.duration = 3000
}

handleClick() {
const { active } = this.state
this.setState({ active: true })
setTimeout(() => {
this.setState({ active: false })
}, this.duration)
}

;<App/>
render() {
const { active } = this.state
const { ToastMessage } = this.props
return (
<div className="mr5">
<Button size="small" variation="secondary" onClick={this.handleClick}>
Show Toast
</Button>
{active ? (
<ToastMessage key="ToastMessage" duration={this.duration}>
{`This message stays here for ${this.duration / 1000}s`}
</ToastMessage>
) : null}
</div>
)
}
}

;<App />
```
74 changes: 74 additions & 0 deletions react/components/Toast/ToastMessage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Component } from 'react'
import PropTypes from 'prop-types'

class ToastMessage extends Component {
constructor(props) {
super(props)
this.state = {
message: '',
}
}

showToast() {
this.props.showToast({
message: this.props.message,
duration: this.props.duration,
action: this.props.action,
})
}

static getDerivedStateFromProps(props, state) {
return {
message:
state.message && props.message !== state.message
? props.message
: state.message,
}
}

shouldComponentUpdate(nextProps, nextState) {
return (
(nextProps.action &&
this.props.action &&
nextProps.action.label !== this.props.action.label) ||
nextProps.message !== this.props.message ||
nextState.message !== this.state.message
)
}

componentDidUpdate(_, prevState) {
if (prevState.message !== this.state.message && this.state.message) {
this.showToast()
}
}

componentDidMount() {
if (this.props.message) {
this.setState({
message: this.props.message,
})
}
}

render() {
return null
}
}

ToastMessage.defaultProps = {
duration: Infinity,
message: '',
}

ToastMessage.propTypes = {
message: PropTypes.string.isRequired,
action: PropTypes.shape({
label: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
}),
duration: PropTypes.number,
showToast: PropTypes.func.isRequired,
hideToast: PropTypes.func.isRequired,
}

export default ToastMessage
30 changes: 29 additions & 1 deletion react/components/Toast/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Toast from './Toast'
import isString from 'lodash/isString'
import isEqual from 'lodash/isEqual'

import Toast from './Toast'
import ToastMessage from './ToastMessage'

const ToastContext = React.createContext({
showToast: () => {},
Expand All @@ -28,6 +31,14 @@ class ToastProvider extends Component {
const { message = '', action, duration } = args

if (this.state.currentToast) {
// Check if is the same toast
if (
isEqual(this.state.currentToast, { message, action, duration }) ||
isEqual(this.state.nextToast, { message, action, duration })
) {
return
}

// If there is a toast present already, queue up the next toast
// It will be displayed when the current toast is closed, on handleToastClose
this.setState({
Expand Down Expand Up @@ -102,6 +113,15 @@ class ToastProvider extends Component {
}
}

shouldComponentUpdate(nextProps, nextState) {
return (
nextProps.positioning !== this.props.positioning ||
nextState.isToastVisible !== this.state.isToastVisible ||
!isEqual(nextState.currentToast, this.state.currentToast) ||
!isEqual(nextState.nextToast, this.state.nextToast)
)
}

componentDidUpdate() {
this.updateContainerBounds()
}
Expand Down Expand Up @@ -156,6 +176,14 @@ class ToastConsumer extends Component {
children({
showToast: value.showToast,
hideToast: value.hideToast,
ToastMessage: ({ children, ...props }) => (
<ToastMessage
message={children}
showToast={value.showToast}
hideToast={value.hideToast}
{...props}
/>
),
})
}
</ToastContext.Consumer>
Expand Down

0 comments on commit 3116e4b

Please sign in to comment.