diff --git a/package.json b/package.json
index a0ae80509a70..cb8cad93ed35 100644
--- a/package.json
+++ b/package.json
@@ -76,7 +76,7 @@
},
"dependencies": {
"@elastic/datemath": "4.0.2",
- "@elastic/eui": "0.0.19",
+ "@elastic/eui": "0.0.18",
"@elastic/filesaver": "1.1.2",
"@elastic/numeral": "2.3.0",
"@elastic/test-subj-selector": "0.2.1",
diff --git a/src/ui/public/notify/toasts/__snapshots__/global_toast_list.test.js.snap b/src/ui/public/notify/toasts/__snapshots__/global_toast_list.test.js.snap
new file mode 100644
index 000000000000..111a9dbf5b16
--- /dev/null
+++ b/src/ui/public/notify/toasts/__snapshots__/global_toast_list.test.js.snap
@@ -0,0 +1,146 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`GlobalToastList is rendered 1`] = `
+
+`;
+
+exports[`GlobalToastList props toasts is rendered 1`] = `
+
+
+
+
+
+
+
+
+ a
+
+
+
+
+
+
+
+
+
+ b
+
+
+
+`;
diff --git a/src/ui/public/notify/toasts/global_toast_list.js b/src/ui/public/notify/toasts/global_toast_list.js
index b65f1fa6b553..d08f07a3f903 100644
--- a/src/ui/public/notify/toasts/global_toast_list.js
+++ b/src/ui/public/notify/toasts/global_toast_list.js
@@ -5,37 +5,126 @@ import PropTypes from 'prop-types';
import {
EuiGlobalToastList,
+ EuiGlobalToastListItem,
+ EuiToast,
} from '@elastic/eui';
+export const TOAST_FADE_OUT_MS = 250;
+
export class GlobalToastList extends Component {
constructor(props) {
super(props);
+ this.state = {
+ toastIdToDismissedMap: {}
+ };
+
+ this.timeoutIds = [];
+ this.toastIdToScheduledForDismissalMap = {};
+
if (this.props.subscribe) {
this.props.subscribe(() => this.forceUpdate());
}
}
static propTypes = {
- subscribe: PropTypes.func,
toasts: PropTypes.array,
+ subscribe: PropTypes.func,
dismissToast: PropTypes.func.isRequired,
toastLifeTimeMs: PropTypes.number.isRequired,
};
+ static defaultProps = {
+ toasts: [],
+ };
+
+ scheduleAllToastsForDismissal = () => {
+ this.props.toasts.forEach(toast => {
+ if (!this.toastIdToScheduledForDismissalMap[toast.id]) {
+ this.scheduleToastForDismissal(toast);
+ }
+ });
+ };
+
+ scheduleToastForDismissal = (toast, isImmediate = false) => {
+ this.toastIdToScheduledForDismissalMap[toast.id] = true;
+ const toastLifeTimeMs = isImmediate ? 0 : this.props.toastLifeTimeMs;
+
+ // Start fading the toast out once its lifetime elapses.
+ this.timeoutIds.push(setTimeout(() => {
+ this.startDismissingToast(toast);
+ }, toastLifeTimeMs));
+
+ // Remove the toast after it's done fading out.
+ this.timeoutIds.push(setTimeout(() => {
+ this.props.dismissToast(toast);
+ this.setState(prevState => {
+ const toastIdToDismissedMap = { ...prevState.toastIdToDismissedMap };
+ delete toastIdToDismissedMap[toast.id];
+ delete this.toastIdToScheduledForDismissalMap[toast.id];
+
+ return {
+ toastIdToDismissedMap,
+ };
+ });
+ }, toastLifeTimeMs + TOAST_FADE_OUT_MS));
+ };
+
+ startDismissingToast(toast) {
+ this.setState(prevState => {
+ const toastIdToDismissedMap = {
+ ...prevState.toastIdToDismissedMap,
+ [toast.id]: true,
+ };
+
+ return {
+ toastIdToDismissedMap,
+ };
+ });
+ }
+
+ componentDidMount() {
+ this.scheduleAllToastsForDismissal();
+ }
+
+ componentWillUnmount() {
+ this.timeoutIds.forEach(clearTimeout);
+ }
+
+ componentDidUpdate() {
+ this.scheduleAllToastsForDismissal();
+ }
+
render() {
const {
toasts,
- dismissToast,
- toastLifeTimeMs,
} = this.props;
+ const renderedToasts = toasts.map(toast => {
+ const {
+ text,
+ ...rest
+ } = toast;
+
+ return (
+
+
+ {text}
+
+
+ );
+ });
+
return (
-
+
+ {renderedToasts}
+
);
}
}
diff --git a/src/ui/public/notify/toasts/global_toast_list.test.js b/src/ui/public/notify/toasts/global_toast_list.test.js
new file mode 100644
index 000000000000..4bdd6cfd15f3
--- /dev/null
+++ b/src/ui/public/notify/toasts/global_toast_list.test.js
@@ -0,0 +1,103 @@
+import React from 'react';
+import { render, mount } from 'enzyme';
+import sinon from 'sinon';
+import { findTestSubject } from '@elastic/eui/lib/test';
+
+import {
+ GlobalToastList,
+ TOAST_FADE_OUT_MS,
+} from './global_toast_list';
+
+describe('GlobalToastList', () => {
+ test('is rendered', () => {
+ const component = render(
+ {}}
+ toastLifeTimeMs={5}
+ />
+ );
+
+ expect(component)
+ .toMatchSnapshot();
+ });
+
+ describe('props', () => {
+ describe('toasts', () => {
+ test('is rendered', () => {
+ const toasts = [{
+ title: 'A',
+ text: 'a',
+ color: 'success',
+ iconType: 'check',
+ 'data-test-subj': 'a',
+ id: 'a',
+ }, {
+ title: 'B',
+ text: 'b',
+ color: 'danger',
+ iconType: 'alert',
+ 'data-test-subj': 'b',
+ id: 'b',
+ }];
+
+ const component = render(
+ {}}
+ toastLifeTimeMs={5}
+ />
+ );
+
+ expect(component)
+ .toMatchSnapshot();
+ });
+ });
+
+ describe('dismissToast', () => {
+ test('is called when a toast is clicked', done => {
+ const dismissToastSpy = sinon.spy();
+ const component = mount(
+
+ );
+
+ const toastB = findTestSubject(component, 'b');
+ const closeButton = findTestSubject(toastB, 'toastCloseButton');
+ closeButton.simulate('click');
+
+ // The callback is invoked once the toast fades from view.
+ setTimeout(() => {
+ expect(dismissToastSpy.called).toBe(true);
+ done();
+ }, TOAST_FADE_OUT_MS + 1);
+ });
+
+ test('is called when the toast lifetime elapses', done => {
+ const TOAST_LIFE_TIME_MS = 5;
+ const dismissToastSpy = sinon.spy();
+ mount(
+
+ );
+
+ // The callback is invoked once the toast fades from view.
+ setTimeout(() => {
+ expect(dismissToastSpy.called).toBe(true);
+ done();
+ }, TOAST_LIFE_TIME_MS + TOAST_FADE_OUT_MS);
+ });
+ });
+ });
+});
diff --git a/yarn.lock b/yarn.lock
index 9c016484a1a1..5493a1b5b0e7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -87,9 +87,9 @@
version "0.0.0"
uid ""
-"@elastic/eui@0.0.19":
- version "0.0.19"
- resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-0.0.19.tgz#f5e57151c20eb9ef0544155b09a1b9c5d8bfa6ef"
+"@elastic/eui@0.0.18":
+ version "0.0.18"
+ resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-0.0.18.tgz#eeae6e93ead3bd014f401f577f06af75fff60898"
dependencies:
brace "^0.10.0"
classnames "^2.2.5"