Skip to content

Commit

Permalink
Add Timer service. Don't dismiss toasts while the user is interacting…
Browse files Browse the repository at this point in the history
… with them.
  • Loading branch information
cjcenizal committed Feb 7, 2018
1 parent 04a6ede commit 2315632
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 25 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# [`master`](https://github.com/elastic/eui/tree/master)

- `EuiGlobalToastList` now prevents toasts from disappearing while the user's mouse is over the list. Added `timer/Timer` service ([#370](https://github.com/elastic/eui/pull/370))

**Bug fixes**

- `EuiTableOfRecords` selection bugs ([#365](https://github.com/elastic/eui/pull/365))
Expand All @@ -8,7 +10,7 @@

**Breaking changes**

- Changed `<EuiGlobalToastList>` 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`. ([#370](https://github.com/elastic/eui/pull/370))
- Changed `EuiGlobalToastList` 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`. ([#370](https://github.com/elastic/eui/pull/370))

# [`0.0.18`](https://github.com/elastic/eui/tree/v0.0.18)

Expand Down
10 changes: 5 additions & 5 deletions src-docs/src/views/toast/toast_list.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ export default class extends Component {
});
};

removeToast(toastId) {
this.setState({
toasts: this.state.toasts.filter(toast => toast.key !== toastId),
});
}
removeToast = (removedToast) => {
this.setState(prevState => ({
toasts: prevState.toasts.filter(toast => toast.id !== removedToast.id),
}));
};

removeAllToasts = () => {
this.setState({
Expand Down
55 changes: 37 additions & 18 deletions src/components/toast/global_toast_list.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React, {
import PropTypes from 'prop-types';
import classNames from 'classnames';

import { Timer } from '../../services/time';
import { EuiGlobalToastListItem } from './global_toast_list_item';
import { EuiToast } from './toast';

Expand All @@ -17,8 +18,8 @@ export class EuiGlobalToastList extends Component {
toastIdToDismissedMap: {},
};

this.timeoutIds = [];
this.toastIdToScheduledForDismissalMap = {};
this.dismissTimeoutIds = [];
this.toastIdToTimerMap = {};

this.isScrollingToBottom = false;
this.isScrolledToBottom = true;
Expand Down Expand Up @@ -65,10 +66,24 @@ export class EuiGlobalToastList extends Component {
// the list.
this.isScrollingToBottom = false;
this.isUserInteracting = true;

// Don't let toasts dismiss themselves while the user is interacting with them.
for (const toastId in this.toastIdToTimerMap) {
if (this.toastIdToTimerMap.hasOwnProperty(toastId)) {
const timer = this.toastIdToTimerMap[toastId];
timer.pause();
}
}
};

onMouseLeave = () => {
this.isUserInteracting = false;
for (const toastId in this.toastIdToTimerMap) {
if (this.toastIdToTimerMap.hasOwnProperty(toastId)) {
const timer = this.toastIdToTimerMap[toastId];
timer.resume();
}
}
};

onScroll = () => {
Expand All @@ -78,37 +93,35 @@ export class EuiGlobalToastList extends Component {

scheduleAllToastsForDismissal = () => {
this.props.toasts.forEach(toast => {
if (!this.toastIdToScheduledForDismissalMap[toast.id]) {
if (!this.toastIdToTimerMap[toast.id]) {
this.scheduleToastForDismissal(toast);
}
});
};

scheduleToastForDismissal = (toast, isImmediate = false) => {
this.toastIdToScheduledForDismissalMap[toast.id] = true;
const toastLifeTimeMs = isImmediate ? 0 : this.props.toastLifeTimeMs;

scheduleToastForDismissal = (toast) => {
// Start fading the toast out once its lifetime elapses.
this.timeoutIds.push(setTimeout(() => {
this.startDismissingToast(toast);
}, toastLifeTimeMs));
this.toastIdToTimerMap[toast.id] =
new Timer(this.dismissToast.bind(this, toast), this.props.toastLifeTimeMs);
};

dismissToast = (toast) => {
// Remove the toast after it's done fading out.
this.timeoutIds.push(setTimeout(() => {
this.dismissTimeoutIds.push(setTimeout(() => {
this.props.dismissToast(toast);
this.toastIdToTimerMap[toast.id].clear();
delete this.toastIdToTimerMap[toast.id];

this.setState(prevState => {
const toastIdToDismissedMap = { ...prevState.toastIdToDismissedMap };
delete toastIdToDismissedMap[toast.id];
delete this.toastIdToScheduledForDismissalMap[toast.id];

return {
toastIdToDismissedMap,
};
});
}, toastLifeTimeMs + TOAST_FADE_OUT_MS));
};
}, TOAST_FADE_OUT_MS));

startDismissingToast(toast) {
this.setState(prevState => {
const toastIdToDismissedMap = {
...prevState.toastIdToDismissedMap,
Expand All @@ -119,7 +132,7 @@ export class EuiGlobalToastList extends Component {
toastIdToDismissedMap,
};
});
}
};

componentDidMount() {
this.listElement.addEventListener('scroll', this.onScroll);
Expand All @@ -146,7 +159,13 @@ 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);
this.dismissTimeoutIds.forEach(clearTimeout);
for (const toastId in this.toastIdToTimerMap) {
if (this.toastIdToTimerMap.hasOwnProperty(toastId)) {
const timer = this.toastIdToTimerMap[toastId];
timer.clear();
}
}
}

render() {
Expand All @@ -170,7 +189,7 @@ export class EuiGlobalToastList extends Component {
isDismissed={this.state.toastIdToDismissedMap[toast.id]}
>
<EuiToast
onClose={this.scheduleToastForDismissal.bind(toast, true)}
onClose={this.dismissToast.bind(this, toast)}
{...rest}
>
{text}
Expand Down
2 changes: 1 addition & 1 deletion src/components/toast/global_toast_list.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ describe('EuiGlobalToastList', () => {
setTimeout(() => {
expect(dismissToastSpy.called).toBe(true);
done();
}, TOAST_LIFE_TIME_MS + TOAST_FADE_OUT_MS);
}, TOAST_LIFE_TIME_MS + TOAST_FADE_OUT_MS + 10);
});
});
});
Expand Down
1 change: 1 addition & 0 deletions src/services/time/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Timer } from './timer';
32 changes: 32 additions & 0 deletions src/services/time/timer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export class Timer {
constructor(callback, timeMs) {
this.id = setTimeout(this.finish, timeMs);
this.callback = callback;
this.finishTime = Date.now() + timeMs;
this.timeRemaining = undefined;
}

pause = () => {
clearTimeout(this.id);
this.id = undefined;
this.timeRemaining = this.finishTime - Date.now();
};

resume = () => {
this.id = setTimeout(this.finish, this.timeRemaining);
this.timeRemaining = undefined;
};

clear = () => {
clearTimeout(this.id);
this.id = undefined;
this.callback = undefined;
this.finishTime = undefined;
this.timeRemaining = undefined;
};

finish = () => {
this.callback();
this.clear();
};
}
68 changes: 68 additions & 0 deletions src/services/time/timer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import sinon from 'sinon';

import {
Timer,
} from './timer';

describe('Timer', () => {
describe('constructor', () => {
test('counts down until time elapses and calls callback', done => {
const callbackSpy = sinon.spy();
const timer = new Timer(callbackSpy, 5); // eslint-disable-line no-unused-vars

setTimeout(() => {
expect(callbackSpy.called).toBe(true);
done();
}, 8);
});
});

describe('pause', () => {
test('stops timer', done => {
const callbackSpy = sinon.spy();
const timer = new Timer(callbackSpy, 5);
timer.pause(0);

setTimeout(() => {
expect(callbackSpy.called).toBe(false);
done();
}, 8);
});
});

describe('resume', () => {
test('starts timer again', done => {
const callbackSpy = sinon.spy();
const timer = new Timer(callbackSpy, 5);
timer.pause(0);
timer.resume();

setTimeout(() => {
expect(callbackSpy.called).toBe(true);
done();
}, 8);
});
});

describe('clear', () => {
test('prevents timer from being called', done => {
const callbackSpy = sinon.spy();
const timer = new Timer(callbackSpy, 5);
timer.clear(0);

setTimeout(() => {
expect(callbackSpy.called).toBe(false);
done();
}, 8);
});
});

describe('finish', () => {
test('calls callback immediately', () => {
const callbackSpy = sinon.spy();
const timer = new Timer(callbackSpy, 5);
timer.finish();
expect(callbackSpy.called).toBe(true);
});
});
});

0 comments on commit 2315632

Please sign in to comment.