diff --git a/CHANGELOG.md b/CHANGELOG.md index add90503f59..27f82dada9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - Deleting selected items now resets the select all checkbox to an unchecked state - The select all checkbox only becomes checked when all selectable rows are checked, not just some of them +**Breaking changes** + +- Changed `` to be responsible for instantiating toasts, tracking their lifetimes, and dismissing them. It now acepts `toasts`, `dismissToast`, and `toastLifeTimeMs` props. It no longer accepts `children`. ([]()) + # [`0.0.18`](https://github.com/elastic/eui/tree/v0.0.18) **Bug fixes** @@ -21,7 +25,7 @@ # [`0.0.16`](https://github.com/elastic/eui/tree/v0.0.16) - `EuiRadio` now supports the `input` tag's `name` attribute. `EuiRadioGroup` accepts a `name` prop that will propagate to its `EuiRadio`s. ([#348](https://github.com/elastic/eui/pull/348)) -- Machine Learning create jobs icon set. ([#338](https://github.com/elastic/eui/pull/338)) +- Added Machine Learning create jobs icon set. ([#338](https://github.com/elastic/eui/pull/338)) - Added `EuiTableOfRecords`, a higher level table component to take away all your table listings frustrations. ([#250](https://github.com/elastic/eui/pull/250)) **Bug fixes** diff --git a/src-docs/src/views/toast/toast_list.js b/src-docs/src/views/toast/toast_list.js index 8693e1d492b..fe38428e6f4 100644 --- a/src-docs/src/views/toast/toast_list.js +++ b/src-docs/src/views/toast/toast_list.js @@ -1,22 +1,16 @@ import React, { - cloneElement, Component, + Fragment, } from 'react'; import { EuiGlobalToastList, - EuiGlobalToastListItem, EuiLink, - EuiToast, } from '../../../../src/components'; -const TOAST_LIFE_TIME_MS = 4000; -const TOAST_FADE_OUT_MS = 250; -let toastIdCounter = 0; -const timeoutIds = []; - let addToastHandler; let removeAllToastsHandler; +let toastId = 0; export function addToast() { addToastHandler(); @@ -39,45 +33,14 @@ export default class extends Component { } addToast = () => { - const { - toast, - toastId, - } = this.renderRandomToast(); + const toast = this.getRandomToast(); this.setState({ toasts: this.state.toasts.concat(toast), }); - - this.scheduleToastForDismissal(toastId); - }; - - scheduleToastForDismissal = (toastId, isImmediate = false) => { - const lifeTime = isImmediate ? TOAST_FADE_OUT_MS : TOAST_LIFE_TIME_MS; - - timeoutIds.push(setTimeout(() => { - this.dismissToast(toastId); - }, lifeTime)); - - timeoutIds.push(setTimeout(() => { - this.startDismissingToast(toastId); - }, lifeTime - TOAST_FADE_OUT_MS)); }; - startDismissingToast(toastId) { - this.setState({ - toasts: this.state.toasts.map(toast => { - if (toast.key === toastId) { - return cloneElement(toast, { - isDismissed: true, - }); - } - - return toast; - }), - }); - } - - dismissToast(toastId) { + removeToast(toastId) { this.setState({ toasts: this.state.toasts.filter(toast => toast.key !== toastId), }); @@ -89,20 +52,11 @@ export default class extends Component { }); }; - componentWillUnmount() { - timeoutIds.forEach(timeoutId => clearTimeout(timeoutId)); - } - - renderRandomToast = () => { - const toastId = (toastIdCounter++).toString(); - const dismissToast = () => this.scheduleToastForDismissal(toastId, true); - - const toasts = [ - ( - + getRandomToast = () => { + const toasts = [{ + title: `Check it out, here's a really long title that will wrap within a narrower browser`, + text: ( +

Here’s some stuff that you need to know. We can make this text really long so that, when viewed within a browser that’s fairly narrow, it will wrap, too. @@ -110,59 +64,54 @@ export default class extends Component {

And some other stuff on another line, just for kicks. And here’s a link.

-
- ), ( - -

- Thanks for your patience! -

-
- ), ( - + + ), + }, { + title: 'Download complete!', + color: 'success', + text: ( +

+ Thanks for your patience! +

+ ), + }, { + title: 'Logging you out soon, due to inactivity', + color: 'warning', + iconType: 'user', + text: ( +

This is a security measure.

Please move your mouse to show that you’re still using Kibana.

-
- ), ( - -

- Sorry. We’ll try not to let it happen it again. -

-
+ ), - ]; - - const toast = ( - - {toasts[Math.floor(Math.random() * toasts.length)]} - - ); + }, { + title: 'Oops, there was an error', + color: 'danger', + iconType: 'help', + text: ( +

+ Sorry. We’ll try not to let it happen it again. +

+ ), + }]; - return { toast, toastId }; + return { + id: toastId++, + ...toasts[Math.floor(Math.random() * toasts.length)], + }; }; render() { return ( - - {this.state.toasts} - + ); } } diff --git a/src/components/toast/__snapshots__/global_toast_list.test.js.snap b/src/components/toast/__snapshots__/global_toast_list.test.js.snap index 812fd7b0fc9..3d5e88846c2 100644 --- a/src/components/toast/__snapshots__/global_toast_list.test.js.snap +++ b/src/components/toast/__snapshots__/global_toast_list.test.js.snap @@ -5,9 +5,135 @@ exports[`EuiGlobalToastList is rendered 1`] = ` aria-label="aria-label" class="euiGlobalToastList testClass1 testClass2" data-test-subj="test subject string" +/> +`; + +exports[`EuiGlobalToastList props toasts is rendered 1`] = ` +
-
- hi +
+
+ + A + +
+ +
+
+
+ + B + +
+ +
+
+
+ + C + +
+
`; diff --git a/src/components/toast/global_toast_list.js b/src/components/toast/global_toast_list.js index 0f3c34bdb2d..646dcac8605 100644 --- a/src/components/toast/global_toast_list.js +++ b/src/components/toast/global_toast_list.js @@ -4,17 +4,37 @@ import React, { import PropTypes from 'prop-types'; import classNames from 'classnames'; +import { EuiGlobalToastListItem } from './global_toast_list_item'; +import { EuiToast } from './toast'; + +export const TOAST_FADE_OUT_MS = 250; + export class EuiGlobalToastList extends Component { constructor(props) { super(props); + this.state = { + toastIdToDismissedMap: {}, + }; + + this.timeoutIds = []; + this.toastIdToScheduledForDismissalMap = {}; + this.isScrollingToBottom = false; this.isScrolledToBottom = true; - this.onScroll = this.onScroll.bind(this); - this.onMouseEnter = this.onMouseEnter.bind(this); - this.onMouseLeave = this.onMouseLeave.bind(this); } + static propTypes = { + className: PropTypes.string, + toasts: PropTypes.array, + dismissToast: PropTypes.func.isRequired, + toastLifeTimeMs: PropTypes.number.isRequired, + }; + + static defaultProps = { + toasts: [], + }; + startScrollingToBottom() { this.isScrollingToBottom = true; @@ -40,34 +60,82 @@ export class EuiGlobalToastList extends Component { window.requestAnimationFrame(scrollToBottom); } - onMouseEnter() { + onMouseEnter = () => { // Stop scrolling to bottom if we're in mid-scroll, because the user wants to interact with // the list. this.isScrollingToBottom = false; this.isUserInteracting = true; - } + }; - onMouseLeave() { + onMouseLeave = () => { this.isUserInteracting = false; - } + }; - onScroll() { + onScroll = () => { this.isScrolledToBottom = this.listElement.scrollHeight - this.listElement.scrollTop === this.listElement.clientHeight; + }; + + 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.listElement.addEventListener('scroll', this.onScroll); this.listElement.addEventListener('mouseenter', this.onMouseEnter); this.listElement.addEventListener('mouseleave', this.onMouseLeave); + this.scheduleAllToastsForDismissal(); } componentDidUpdate(prevProps) { + this.scheduleAllToastsForDismissal(); + if (!this.isUserInteracting) { // If the user has scrolled up the toast list then we don't want to annoy them by scrolling // all the way back to the bottom. if (this.isScrolledToBottom) { - if (prevProps.children.length < this.props.children.length) { + if (prevProps.toasts.length < this.props.toasts.length) { this.startScrollingToBottom(); } } @@ -78,15 +146,39 @@ export class EuiGlobalToastList extends Component { this.listElement.removeEventListener('scroll', this.onScroll); this.listElement.removeEventListener('mouseenter', this.onMouseEnter); this.listElement.removeEventListener('mouseleave', this.onMouseLeave); + this.timeoutIds.forEach(clearTimeout); } render() { const { - children, className, + toasts, + dismissToast, // eslint-disable-line no-unused-vars + toastLifeTimeMs, // eslint-disable-line no-unused-vars ...rest } = this.props; + const renderedToasts = toasts.map(toast => { + const { + text, + ...rest + } = toast; + + return ( + + + {text} + + + ); + }); + const classes = classNames('euiGlobalToastList', className); return ( @@ -95,13 +187,8 @@ export class EuiGlobalToastList extends Component { className={classes} {...rest} > - {children} + {renderedToasts}
); } } - -EuiGlobalToastList.propTypes = { - children: PropTypes.node, - className: PropTypes.string, -}; diff --git a/src/components/toast/global_toast_list.test.js b/src/components/toast/global_toast_list.test.js index 0980bb37402..f0935ffc351 100644 --- a/src/components/toast/global_toast_list.test.js +++ b/src/components/toast/global_toast_list.test.js @@ -7,12 +7,35 @@ import { EuiGlobalToastList } from './global_toast_list'; describe('EuiGlobalToastList', () => { test('is rendered', () => { const component = render( - -
hi
-
+ {}} + toastLifeTimeMs={1000} + /> ); - expect(component) - .toMatchSnapshot(); + expect(component).toMatchSnapshot(); + }); + + describe('props', () => { + describe('toasts', () => { + it('is rendered', () => { + const toats = [ + { id: 'A', title: 'A' }, + { id: 'B', title: 'B' }, + { id: 'C', title: 'C' }, + ]; + + const component = render( + {}} + toastLifeTimeMs={1000} + /> + ); + + expect(component).toMatchSnapshot(); + }); + }); }); });