From c0acacabc7885cf2f982812deab00ccb13cdd17c Mon Sep 17 00:00:00 2001 From: james Date: Mon, 7 Oct 2019 00:19:52 +1100 Subject: [PATCH 01/11] Add maxToasts prop + tests --- .../core/src/components/toast/toaster.tsx | 21 +++++++++++++++++++ packages/core/test/toast/toasterTests.ts | 9 ++++++++ 2 files changed, 30 insertions(+) diff --git a/packages/core/src/components/toast/toaster.tsx b/packages/core/src/components/toast/toaster.tsx index d5fd7aaeae..b7e7c6bc3e 100644 --- a/packages/core/src/components/toast/toaster.tsx +++ b/packages/core/src/components/toast/toaster.tsx @@ -88,6 +88,14 @@ export interface IToasterProps extends IProps { * @default Position.TOP */ position?: ToasterPosition; + + /** + * The maximum number of active toasts that can be displayed at once. + * + * When the limit is about to be exceeded, the oldest active toast is removed. + * @default 5 + */ + maxToasts?: number; } export interface IToasterState { @@ -101,6 +109,7 @@ export class Toaster extends AbstractPureComponent2 ({ @@ -143,6 +156,7 @@ export class Toaster extends AbstractPureComponent2 (t.key === key ? options : t)), })); } + this.dismissIfAtLimit(); return options.key; } @@ -194,6 +208,13 @@ export class Toaster extends AbstractPureComponent2 toast.key !== key); } + private dismissIfAtLimit() { + if (this.state.toasts.length > this.props.maxToasts) { + // dismiss the oldest toast to stay below the maxToasts limit + this.dismiss(this.state.toasts[this.state.toasts.length - 1].key); + } + } + private renderToast(toast: IToastOptions) { return ; } diff --git a/packages/core/test/toast/toasterTests.ts b/packages/core/test/toast/toasterTests.ts index 5bfabd871f..a0c0e8841e 100644 --- a/packages/core/test/toast/toasterTests.ts +++ b/packages/core/test/toast/toasterTests.ts @@ -83,6 +83,15 @@ describe("Toaster", () => { assert.lengthOf(toaster.getToasts(), 0, "expected 0 toasts"); }); + it("does not exceed the maximum toast limit set", () => { + toaster = Toaster.create({ maxToasts: 3 }); + toaster.show({ message: "one" }); + toaster.show({ message: "two" }); + toaster.show({ message: "three" }); + toaster.show({ message: "oh no" }); + assert.lengthOf(toaster.getToasts(), 3, "expected 3 toasts"); + }); + it("action onClick callback invoked when action clicked", () => { const onClick = spy(); toaster.show({ From 27142159e1c3d7eeb9cb62518c63b44e305da649 Mon Sep 17 00:00:00 2001 From: james Date: Mon, 7 Oct 2019 00:55:58 +1100 Subject: [PATCH 02/11] Move check to earlier in function, change conditional slightly --- packages/core/src/components/toast/toaster.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/core/src/components/toast/toaster.tsx b/packages/core/src/components/toast/toaster.tsx index b7e7c6bc3e..c9c3131505 100644 --- a/packages/core/src/components/toast/toaster.tsx +++ b/packages/core/src/components/toast/toaster.tsx @@ -146,6 +146,8 @@ export class Toaster extends AbstractPureComponent2 ({ @@ -156,7 +158,6 @@ export class Toaster extends AbstractPureComponent2 (t.key === key ? options : t)), })); } - this.dismissIfAtLimit(); return options.key; } @@ -209,8 +210,8 @@ export class Toaster extends AbstractPureComponent2 this.props.maxToasts) { - // dismiss the oldest toast to stay below the maxToasts limit + if (this.state.toasts.length === this.props.maxToasts) { + // dismiss the oldest toast to stay within the maxToasts limit this.dismiss(this.state.toasts[this.state.toasts.length - 1].key); } } From 42fceec65dd46eb213c3f873856559f3def4fbb3 Mon Sep 17 00:00:00 2001 From: james Date: Mon, 7 Oct 2019 07:54:05 +1100 Subject: [PATCH 03/11] New test for maxProps < 1 logic --- packages/core/test/toast/toasterTests.ts | 25 +++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/core/test/toast/toasterTests.ts b/packages/core/test/toast/toasterTests.ts index a0c0e8841e..0b56df03b6 100644 --- a/packages/core/test/toast/toasterTests.ts +++ b/packages/core/test/toast/toasterTests.ts @@ -83,15 +83,6 @@ describe("Toaster", () => { assert.lengthOf(toaster.getToasts(), 0, "expected 0 toasts"); }); - it("does not exceed the maximum toast limit set", () => { - toaster = Toaster.create({ maxToasts: 3 }); - toaster.show({ message: "one" }); - toaster.show({ message: "two" }); - toaster.show({ message: "three" }); - toaster.show({ message: "oh no" }); - assert.lengthOf(toaster.getToasts(), 3, "expected 3 toasts"); - }); - it("action onClick callback invoked when action clicked", () => { const onClick = spy(); toaster.show({ @@ -143,6 +134,22 @@ describe("Toaster", () => { assert.isFalse(errorSpy.calledWithMatch("two children with the same key"), "mutation side effect!"); }); + it("does not exceed the maximum toast limit set", () => { + toaster = Toaster.create({ maxToasts: 3 }); + toaster.show({ message: "one" }); + toaster.show({ message: "two" }); + toaster.show({ message: "three" }); + toaster.show({ message: "oh no" }); + assert.lengthOf(toaster.getToasts(), 3, "expected 3 toasts"); + }); + + it("shows no toasts when max toast is set to a number less than 1", () => { + toaster = Toaster.create({ maxToasts: 0 }); + toaster.show({ message: "one" }); + toaster.show({ message: "two" }); + assert.lengthOf(toaster.getToasts(), 0, "expected 0 toasts"); + }); + describe("with autoFocus set to true", () => { before(() => { testsContainerElement = document.createElement("div"); From 49d199aa1815015a9778c87e02f13dd3ac627aed Mon Sep 17 00:00:00 2001 From: james Date: Tue, 5 Nov 2019 01:36:49 +1100 Subject: [PATCH 04/11] Setting default maxToasts to undefined, change maxToasts<1 case to fail loudly and updated tests --- packages/core/src/components/toast/toaster.tsx | 16 +++++++++------- packages/core/test/toast/toasterTests.ts | 13 +++++++------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/core/src/components/toast/toaster.tsx b/packages/core/src/components/toast/toaster.tsx index c9c3131505..af8ba78d13 100644 --- a/packages/core/src/components/toast/toaster.tsx +++ b/packages/core/src/components/toast/toaster.tsx @@ -19,7 +19,7 @@ import * as React from "react"; import * as ReactDOM from "react-dom"; import { polyfill } from "react-lifecycles-compat"; import { AbstractPureComponent2, Classes, Position } from "../../common"; -import { TOASTER_CREATE_NULL, TOASTER_WARN_INLINE } from "../../common/errors"; +import { TOASTER_CREATE_NULL, TOASTER_MAX_TOASTS_INVALID, TOASTER_WARN_INLINE } from "../../common/errors"; import { ESCAPE } from "../../common/keys"; import { DISPLAYNAME_PREFIX, IProps } from "../../common/props"; import { isNodeEnv, safeInvoke } from "../../common/utils"; @@ -93,7 +93,7 @@ export interface IToasterProps extends IProps { * The maximum number of active toasts that can be displayed at once. * * When the limit is about to be exceeded, the oldest active toast is removed. - * @default 5 + * @default undefined */ maxToasts?: number; } @@ -109,7 +109,6 @@ export class Toaster extends AbstractPureComponent2 toast.key !== key); } diff --git a/packages/core/test/toast/toasterTests.ts b/packages/core/test/toast/toasterTests.ts index 0b56df03b6..4c67ccaf13 100644 --- a/packages/core/test/toast/toasterTests.ts +++ b/packages/core/test/toast/toasterTests.ts @@ -21,7 +21,7 @@ import { spy } from "sinon"; import { mount } from "enzyme"; import * as Classes from "../../src/common/classes"; -import { TOASTER_CREATE_NULL } from "../../src/common/errors"; +import { TOASTER_CREATE_NULL, TOASTER_MAX_TOASTS_INVALID } from "../../src/common/errors"; import { IToaster, Toaster } from "../../src/index"; describe("Toaster", () => { @@ -143,11 +143,12 @@ describe("Toaster", () => { assert.lengthOf(toaster.getToasts(), 3, "expected 3 toasts"); }); - it("shows no toasts when max toast is set to a number less than 1", () => { - toaster = Toaster.create({ maxToasts: 0 }); - toaster.show({ message: "one" }); - toaster.show({ message: "two" }); - assert.lengthOf(toaster.getToasts(), 0, "expected 0 toasts"); + it("throws an error when max toast is set to a number less than 1", () => { + try { + Toaster.create({ maxToasts: 0 }); + } catch (err) { + assert.equal(err.message, TOASTER_MAX_TOASTS_INVALID); + } }); describe("with autoFocus set to true", () => { From 9748a9d2c1f2b2615de38764513eb3c91f76083a Mon Sep 17 00:00:00 2001 From: james Date: Tue, 5 Nov 2019 01:41:18 +1100 Subject: [PATCH 05/11] Infinite toasts for undefined maxToasts --- packages/core/src/components/toast/toaster.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/core/src/components/toast/toaster.tsx b/packages/core/src/components/toast/toaster.tsx index af8ba78d13..b63fec5a87 100644 --- a/packages/core/src/components/toast/toaster.tsx +++ b/packages/core/src/components/toast/toaster.tsx @@ -141,8 +141,10 @@ export class Toaster extends AbstractPureComponent2 ({ From 49880ddb38ee8b920881041de114dc9166c8a92f Mon Sep 17 00:00:00 2001 From: jamesgiu Date: Mon, 4 Nov 2019 09:46:36 -0500 Subject: [PATCH 06/11] Adding error message --- packages/core/src/common/errors.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core/src/common/errors.ts b/packages/core/src/common/errors.ts index c858c8535e..38b67ff347 100644 --- a/packages/core/src/common/errors.ts +++ b/packages/core/src/common/errors.ts @@ -98,3 +98,5 @@ export const DIALOG_WARN_NO_HEADER_CLOSE_BUTTON = export const DRAWER_VERTICAL_IS_IGNORED = ns + ` vertical is ignored if position is defined`; export const DRAWER_ANGLE_POSITIONS_ARE_CASTED = ns + ` all angle positions are casted into pure position (TOP, BOTTOM, LEFT or RIGHT)`; + +export const TOASTER_MAX_TOASTS_INVALID = ns + ` maxToasts is set to an invalid number, must be greater than 0` From fb06a7d805b2804aa7ac60a33bc7cb0cb009a9b7 Mon Sep 17 00:00:00 2001 From: jamesgiu Date: Mon, 4 Nov 2019 10:29:43 -0500 Subject: [PATCH 07/11] Expand example for Toaster to include new maxToasts prop --- .../examples/core-examples/toastExample.tsx | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/docs-app/src/examples/core-examples/toastExample.tsx b/packages/docs-app/src/examples/core-examples/toastExample.tsx index 49aca9d9ee..18a5894ef2 100644 --- a/packages/docs-app/src/examples/core-examples/toastExample.tsx +++ b/packages/docs-app/src/examples/core-examples/toastExample.tsx @@ -26,6 +26,7 @@ import { IToasterProps, IToastProps, Label, + NumericInput, Position, ProgressBar, Switch, @@ -50,6 +51,7 @@ export class ToastExample extends React.PureComponent
Props
@@ -136,6 +138,16 @@ export class ToastExample extends React.PureComponent + @@ -186,4 +198,12 @@ export class ToastExample extends React.PureComponent { + if (value) { + this.setState({ maxToasts: Math.max(1, value) }); + } else { + this.setState({ maxToasts: undefined }); + } + }; } From 0c7387b86bb41ead76cfe26aebb863abeaa212e2 Mon Sep 17 00:00:00 2001 From: james Date: Tue, 5 Nov 2019 02:55:27 +1100 Subject: [PATCH 08/11] Refactor test --- packages/core/test/toast/toasterTests.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/core/test/toast/toasterTests.ts b/packages/core/test/toast/toasterTests.ts index 4c67ccaf13..b91b494b5e 100644 --- a/packages/core/test/toast/toasterTests.ts +++ b/packages/core/test/toast/toasterTests.ts @@ -144,11 +144,16 @@ describe("Toaster", () => { }); it("throws an error when max toast is set to a number less than 1", () => { - try { - Toaster.create({ maxToasts: 0 }); - } catch (err) { - assert.equal(err.message, TOASTER_MAX_TOASTS_INVALID); + class InvalidMaxToastsToaster extends React.Component { + public render() { + return Toaster.create({ maxToasts: 0 }); + } + + public componentDidCatch(err: Error): void { + assert.equal(err.message, TOASTER_MAX_TOASTS_INVALID); + } } + mount(React.createElement(InvalidMaxToastsToaster)); }); describe("with autoFocus set to true", () => { From 97d2f6f41e55bcc0f6cc213a39d5f529a723e2b4 Mon Sep 17 00:00:00 2001 From: james Date: Tue, 5 Nov 2019 02:57:29 +1100 Subject: [PATCH 09/11] lint fix --- packages/core/test/toast/toasterTests.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/test/toast/toasterTests.ts b/packages/core/test/toast/toasterTests.ts index b91b494b5e..209248e18d 100644 --- a/packages/core/test/toast/toasterTests.ts +++ b/packages/core/test/toast/toasterTests.ts @@ -174,6 +174,7 @@ describe("Toaster", () => { }); it("throws an error if used within a React lifecycle method", () => { + // tslint:disable-next-line:max-classes-per-file class LifecycleToaster extends React.Component { public render() { return React.createElement("div"); From 1731e13b3149811e7efc266bcca6316fb08a869f Mon Sep 17 00:00:00 2001 From: james Date: Tue, 5 Nov 2019 11:55:52 +1100 Subject: [PATCH 10/11] Fix test for react-16 --- packages/core/test/toast/toasterTests.ts | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/packages/core/test/toast/toasterTests.ts b/packages/core/test/toast/toasterTests.ts index 209248e18d..713ffa3326 100644 --- a/packages/core/test/toast/toasterTests.ts +++ b/packages/core/test/toast/toasterTests.ts @@ -19,6 +19,7 @@ import * as React from "react"; import * as ReactDOM from "react-dom"; import { spy } from "sinon"; +import { expectPropValidationError } from "@blueprintjs/test-commons"; import { mount } from "enzyme"; import * as Classes from "../../src/common/classes"; import { TOASTER_CREATE_NULL, TOASTER_MAX_TOASTS_INVALID } from "../../src/common/errors"; @@ -144,16 +145,7 @@ describe("Toaster", () => { }); it("throws an error when max toast is set to a number less than 1", () => { - class InvalidMaxToastsToaster extends React.Component { - public render() { - return Toaster.create({ maxToasts: 0 }); - } - - public componentDidCatch(err: Error): void { - assert.equal(err.message, TOASTER_MAX_TOASTS_INVALID); - } - } - mount(React.createElement(InvalidMaxToastsToaster)); + expectPropValidationError(Toaster, { maxToasts: 0 }, TOASTER_MAX_TOASTS_INVALID); }); describe("with autoFocus set to true", () => { @@ -174,7 +166,6 @@ describe("Toaster", () => { }); it("throws an error if used within a React lifecycle method", () => { - // tslint:disable-next-line:max-classes-per-file class LifecycleToaster extends React.Component { public render() { return React.createElement("div"); From 9adefbaca84423b61d5e49030a666866c1a9b79d Mon Sep 17 00:00:00 2001 From: james Date: Tue, 5 Nov 2019 12:01:54 +1100 Subject: [PATCH 11/11] Added missing semi-colon, reword label in example for clarity --- packages/core/src/common/errors.ts | 3 ++- packages/docs-app/src/examples/core-examples/toastExample.tsx | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/common/errors.ts b/packages/core/src/common/errors.ts index 38b67ff347..5f71c69a68 100644 --- a/packages/core/src/common/errors.ts +++ b/packages/core/src/common/errors.ts @@ -99,4 +99,5 @@ export const DRAWER_VERTICAL_IS_IGNORED = ns + ` vertical is ignored if export const DRAWER_ANGLE_POSITIONS_ARE_CASTED = ns + ` all angle positions are casted into pure position (TOP, BOTTOM, LEFT or RIGHT)`; -export const TOASTER_MAX_TOASTS_INVALID = ns + ` maxToasts is set to an invalid number, must be greater than 0` +export const TOASTER_MAX_TOASTS_INVALID = ns + ` maxToasts is set to an invalid number, must be greater than 0`; + diff --git a/packages/docs-app/src/examples/core-examples/toastExample.tsx b/packages/docs-app/src/examples/core-examples/toastExample.tsx index 18a5894ef2..40090feb7c 100644 --- a/packages/docs-app/src/examples/core-examples/toastExample.tsx +++ b/packages/docs-app/src/examples/core-examples/toastExample.tsx @@ -51,7 +51,6 @@ export class ToastExample extends React.PureComponent