Stacked toast view
From the previous commit, we are now showing toasts in the reverse order (latest to oldest). Next, extend the "hide snack" timer to handle multiple snacks. It will dismiss them one by one, restarting itself until no more toasts. Show a stacked GUI when there are multiple toasts. Users can still manually dismiss the toasts.
This commit is contained in:
parent
55916f0763
commit
dae78c488f
4 changed files with 50 additions and 15 deletions
|
@ -1,14 +1,15 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doDismissToast } from 'redux/actions/notifications';
|
import { doDismissToast } from 'redux/actions/notifications';
|
||||||
import { selectToast } from 'redux/selectors/notifications';
|
import { selectToast, selectToastCount } from 'redux/selectors/notifications';
|
||||||
import SnackBar from './view';
|
import SnackBar from './view';
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = (dispatch) => ({
|
||||||
removeSnack: () => dispatch(doDismissToast()),
|
removeSnack: () => dispatch(doDismissToast()),
|
||||||
});
|
});
|
||||||
|
|
||||||
const select = state => ({
|
const select = (state) => ({
|
||||||
snack: selectToast(state),
|
snack: selectToast(state),
|
||||||
|
snackCount: selectToastCount(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(SnackBar);
|
export default connect(select, perform)(SnackBar);
|
||||||
|
|
|
@ -9,40 +9,46 @@ import LbcMessage from 'component/common/lbc-message';
|
||||||
type Props = {
|
type Props = {
|
||||||
removeSnack: (any) => void,
|
removeSnack: (any) => void,
|
||||||
snack: ?ToastParams,
|
snack: ?ToastParams,
|
||||||
|
snackCount: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
class SnackBar extends React.PureComponent<Props> {
|
class SnackBar extends React.PureComponent<Props> {
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.hideTimeout = null;
|
this.intervalId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
hideTimeout: ?TimeoutID;
|
intervalId: ?IntervalID;
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { snack, removeSnack } = this.props;
|
const { snack, snackCount, removeSnack } = this.props;
|
||||||
|
|
||||||
if (!snack) {
|
if (!snack) {
|
||||||
this.hideTimeout = null; // should be unmounting anyway, but be safe?
|
clearInterval(this.intervalId);
|
||||||
|
this.intervalId = null; // should be unmounting anyway, but be safe?
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { message, subMessage, duration, linkText, linkTarget, isError } = snack;
|
const { message, subMessage, duration, linkText, linkTarget, isError } = snack;
|
||||||
|
|
||||||
if (this.hideTimeout === null) {
|
if (this.intervalId) {
|
||||||
this.hideTimeout = setTimeout(
|
// TODO: render should be pure
|
||||||
|
clearInterval(this.intervalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.intervalId = setInterval(
|
||||||
() => {
|
() => {
|
||||||
this.hideTimeout = null;
|
|
||||||
removeSnack();
|
removeSnack();
|
||||||
},
|
},
|
||||||
duration === 'long' ? 15000 : 5000
|
duration === 'long' ? 10000 : 5000
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classnames('snack-bar', {
|
className={classnames('snack-bar', {
|
||||||
'snack-bar--error': isError,
|
'snack-bar--error': isError,
|
||||||
|
'snack-bar--stacked-error': snackCount > 1 && isError,
|
||||||
|
'snack-bar--stacked-non-error': snackCount > 1 && !isError,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className="snack-bar__message">
|
<div className="snack-bar__message">
|
||||||
|
|
|
@ -41,6 +41,8 @@ export const selectToast = createSelector(selectState, (state) => {
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const selectToastCount = (state) => selectState(state).toasts.length;
|
||||||
|
|
||||||
export const selectError = createSelector(selectState, (state) => {
|
export const selectError = createSelector(selectState, (state) => {
|
||||||
if (state.errors.length) {
|
if (state.errors.length) {
|
||||||
const { error } = state.errors[0];
|
const { error } = state.errors[0];
|
||||||
|
|
|
@ -37,6 +37,32 @@
|
||||||
background-color: var(--color-snack-bg-error);
|
background-color: var(--color-snack-bg-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.snack-bar--stacked-non-error {
|
||||||
|
box-shadow: // The top layer shadow
|
||||||
|
0 1px 1px var(--color-border),
|
||||||
|
// The second layer
|
||||||
|
0 10px 0 -5px var(--color-primary),
|
||||||
|
// The second layer shadow
|
||||||
|
0 10px 1px -4px var(--color-border);
|
||||||
|
//// The third layer
|
||||||
|
//0 20px 0 -10px var(--color-primary),
|
||||||
|
//// The third layer shadow
|
||||||
|
//0 20px 1px -9px var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.snack-bar--stacked-error {
|
||||||
|
box-shadow: // The top layer shadow
|
||||||
|
0 1px 1px var(--color-border),
|
||||||
|
// The second layer
|
||||||
|
0 10px 0 -5px var(--color-snack-bg-error),
|
||||||
|
// The second layer shadow
|
||||||
|
0 10px 1px -4px var(--color-border);
|
||||||
|
//// The third layer
|
||||||
|
//0 20px 0 -10px var(--color-snack-bg-error),
|
||||||
|
//// The third layer shadow
|
||||||
|
//0 20px 1px -9px var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
.snack-bar--notification {
|
.snack-bar--notification {
|
||||||
@extend .card;
|
@extend .card;
|
||||||
background-color: var(--color-card-background);
|
background-color: var(--color-card-background);
|
||||||
|
|
Loading…
Reference in a new issue