Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to set max number of active toasts #3770

Merged
merged 16 commits into from
Nov 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/core/src/common/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,6 @@ export const DIALOG_WARN_NO_HEADER_CLOSE_BUTTON =
export const DRAWER_VERTICAL_IS_IGNORED = ns + ` <Drawer> vertical is ignored if position is defined`;
export const DRAWER_ANGLE_POSITIONS_ARE_CASTED =
ns + ` <Drawer> all angle positions are casted into pure position (TOP, BOTTOM, LEFT or RIGHT)`;

export const TOASTER_MAX_TOASTS_INVALID = ns + ` <Toaster> maxToasts is set to an invalid number, must be greater than 0`;

28 changes: 27 additions & 1 deletion packages/core/src/components/toast/toaster.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 undefined
*/
maxToasts?: number;
}

export interface IToasterState {
Expand Down Expand Up @@ -133,6 +141,10 @@ export class Toaster extends AbstractPureComponent2<IToasterProps, IToasterState
private toastId = 0;

public show(props: IToastProps, key?: string) {
if (this.props.maxToasts) {
// check if active number of toasts are at the maxToasts limit
this.dismissIfAtLimit();
}
const options = this.createToastOptions(props, key);
if (key === undefined || this.isNewToastKey(key)) {
this.setState(prevState => ({
Expand Down Expand Up @@ -190,10 +202,24 @@ export class Toaster extends AbstractPureComponent2<IToasterProps, IToasterState
);
}

protected validateProps(props: IToasterProps) {
// maximum number of toasts should not be a number less than 1
if (props.maxToasts < 1) {
throw new Error(TOASTER_MAX_TOASTS_INVALID);
}
}

private isNewToastKey(key: string) {
return this.state.toasts.every(toast => toast.key !== key);
}

private dismissIfAtLimit() {
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);
}
}

private renderToast(toast: IToastOptions) {
return <Toast {...toast} onDismiss={this.getDismissHandler(toast)} />;
}
Expand Down
16 changes: 15 additions & 1 deletion packages/core/test/toast/toasterTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ 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 } from "../../src/common/errors";
import { TOASTER_CREATE_NULL, TOASTER_MAX_TOASTS_INVALID } from "../../src/common/errors";
import { IToaster, Toaster } from "../../src/index";

describe("Toaster", () => {
Expand Down Expand Up @@ -134,6 +135,19 @@ 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("throws an error when max toast is set to a number less than 1", () => {
expectPropValidationError(Toaster, { maxToasts: 0 }, TOASTER_MAX_TOASTS_INVALID);
});

describe("with autoFocus set to true", () => {
before(() => {
testsContainerElement = document.createElement("div");
Expand Down
21 changes: 20 additions & 1 deletion packages/docs-app/src/examples/core-examples/toastExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
IToasterProps,
IToastProps,
Label,
NumericInput,
Position,
ProgressBar,
Switch,
Expand Down Expand Up @@ -128,14 +129,24 @@ export class ToastExample extends React.PureComponent<IExampleProps<IBlueprintEx
}

protected renderOptions() {
const { autoFocus, canEscapeKeyClear, position } = this.state;
const { autoFocus, canEscapeKeyClear, position, maxToasts } = this.state;
return (
<>
<H5>Props</H5>
<Label>
Position
<HTMLSelect value={position} onChange={this.handlePositionChange} options={POSITIONS} />
</Label>
<Label>
Maximum active toasts
<NumericInput
allowNumericCharactersOnly={true}
placeholder="No maximum!"
min={1}
value={maxToasts}
onValueChange={this.handleValueChange}
/>
</Label>
<Switch label="Auto focus" checked={autoFocus} onChange={this.toggleAutoFocus} />
<Switch label="Can escape key clear" checked={canEscapeKeyClear} onChange={this.toggleEscapeKey} />
</>
Expand Down Expand Up @@ -186,4 +197,12 @@ export class ToastExample extends React.PureComponent<IExampleProps<IBlueprintEx
}
}, 1000);
};

private handleValueChange = (value: number) => {
if (value) {
this.setState({ maxToasts: Math.max(1, value) });
} else {
this.setState({ maxToasts: undefined });
}
};
}