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

[core] deprecate Toaster in favor of OverlayToaster #6146

Merged
merged 1 commit into from
May 11, 2023
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
10 changes: 5 additions & 5 deletions packages/core/src/common/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,16 +105,16 @@ export const SPINNER_WARN_CLASSES_SIZE = ns + ` <Spinner> Classes.SMALL/LARGE ar

export const TOASTER_CREATE_NULL =
ns +
` Toaster.create() is not supported inside React lifecycle methods in React 16.` +
` OverlayToaster.create() is not supported inside React lifecycle methods in React 16.` +
` See usage example on the docs site.`;
export const TOASTER_WARN_INLINE = ns + ` Toaster.create() ignores inline prop as it always creates a new element.`;
export const TOASTER_MAX_TOASTS_INVALID =
ns + ` <OverlayToaster> maxToasts is set to an invalid number, must be greater than 0`;
export const TOASTER_WARN_INLINE =
ns + ` OverlayToaster.create() ignores inline prop as it always creates a new element.`;

export const DIALOG_WARN_NO_HEADER_ICON = ns + ` <Dialog> iconName is ignored if title is omitted.`;
export const DIALOG_WARN_NO_HEADER_CLOSE_BUTTON =
ns + ` <Dialog> isCloseButtonShown prop is ignored if title is omitted.`;

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`;
57 changes: 29 additions & 28 deletions packages/core/src/components/toast/toast.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,18 @@ You can also apply the same visual intent styles to `Toast`s that you can to [`B

@interface ToastProps

@### Toaster
@### OverlayToaster

The `Toaster` React component is a stateful container for a single list of toasts. Internally, it
uses [`Overlay`](#core/components/overlay) to manage children and transitions. It can be vertically
The __OverlayToaster__ component is a stateful container for a single list of toasts. Internally, it
uses the [Overlay](#core/components/overlay) component to manage children and transitions. It can be vertically
aligned along the top or bottom edge of its container (new toasts will slide in from that edge) and
horizontally aligned along the left edge, center, or right edge of its container.

There are three ways to use the `Toaster` component:
There are three ways to use __OverlayToaster__:

1. `Toaster.create(props)` static method returns a new `ToasterInstance` instance. Use the instance method `toaster.show()` to manipulate this instance. __(recommended)__
1. `<Toaster><Toast />...</Toaster>`: Render a `<Toaster>` element with React `children`.
1. `<Toaster ref={ref => ref.show({ ...toast })} />`: Render a `<Toaster>` element and use the `ref` prop to access its instance methods.
1. `OverlayToaster.create(props)` static method returns a new `ToasterInstance` instance. Use the instance method `toaster.show()` to manipulate this instance. __(recommended)__
1. `<OverlayToaster><Toast />...</OverlayToaster>`: Render a `<OverlayToaster>` element with React `children`.
1. `<OverlayToaster ref={(ref: ToasterInstance) => ref.show({ ...toast })} />`: Render a `<OverlayToaster>` element and use the `ref` prop to access its instance methods.

<div class="@ns-callout @ns-intent-primary @ns-icon-info-sign">
<h5 class="@ns-heading">Working with multiple toasters</h5>
Expand All @@ -45,56 +45,57 @@ You can have multiple toasters in a single application, but you must ensure that
<div class="@ns-callout @ns-intent-primary @ns-icon-info-sign">
<h5 class="@ns-heading">Toaster focus</h5>

`Toaster` always disables `Overlay`'s `enforceFocus` behavior (meaning that you're not blocked
__OverlayToaster__ always disables Overlay's `enforceFocus` behavior (meaning that you're not blocked
from accessing other parts of the application while a toast is active), and by default also
disables `autoFocus` (meaning that focus will not switch to a toast when it appears). You can
enable `autoFocus` for an individual `Toaster` via a prop, if desired.

</div>


@interface IToasterProps
@interface OverlayToasterProps

@## Static usage

The `Toaster` component provides the static `create` method that returns a new `Toaster` instance, rendered into an
element attached to `<body>`. A `Toaster` instance
has a collection of methods to show and hide toasts in its given container.
__OverlayToaster__ provides the static `create` method that returns a new `ToasterInstance`, rendered into an
element attached to `<body>`. A toaster instance has a collection of methods to show and hide toasts in its given container.

```ts
Toaster.create(props?: IToasterProps, container = document.body): ToasterInstance
OverlayToaster.create(props?: IToasterProps, container = document.body): ToasterInstance
```

The `Toaster` will be rendered into a new element appended to the given `container`.
The toaster will be rendered into a new element appended to the given `container`.
The `container` determines which element toasts are positioned relative to; the default value of `<body>` allows them to use the entire viewport.

Note that the return type is `ToasterInstance`, which is a minimal interface that exposes only the instance
methods detailed below. It can be thought of as `Toaster` minus the `React.Component` methods,
because the `Toaster` should not be treated as a normal React component.
methods detailed below. It can be thought of as `OverlayToaster` minus the `React.Component` methods,
because the `OverlayToaster` should not be treated as a normal React component.

<div class="@ns-callout @ns-intent-warning @ns-icon-warning-sign">
<h5 class="@ns-heading">React 16 usage</h5>

`Toaster.create()` will throw an error if invoked inside a component lifecycle method in React 16, as `ReactDOM.render()` will return
`null` resulting in an inaccessible toaster instance. See the second bullet point on the [React 16 release notes](https://reactjs.org/blog/2017/09/26/react-v16.0.html#breaking-changes) for more information.
`OverlayToaster.create()` will throw an error if invoked inside a component lifecycle method in React 16,
as `ReactDOM.render()` will return `null` resulting in an inaccessible toaster instance. See the second bullet
point on the [React 16 release notes](https://reactjs.org/blog/2017/09/26/react-v16.0.html#breaking-changes)
for more information.

</div>

@interface ToasterInstance

@### Example

Your application can contain several `Toaster` instances and easily share them across the codebase as modules.
Your application can contain several `ToasterInstance`s and easily share them across the codebase as modules.

The following code samples demonstrate our preferred pattern for intergrating a toaster into a React application:

#### `toaster.ts`

```ts
import { Position, Toaster } from "@blueprintjs/core";
import { OverlayToaster, Position } from "@blueprintjs/core";

/** Singleton toaster instance. Create separate instances for different options. */
export const AppToaster = Toaster.create({
export const AppToaster = OverlayToaster.create({
className: "recipe-toaster",
position: Position.TOP,
});
Expand Down Expand Up @@ -122,32 +123,32 @@ export class App extends React.PureComponent {

@## React component usage

Render the `<Toaster>` component like any other element and supply `<Toast>` elements as `children`. You can
Render the `<OverlayToaster>` component like any other element and supply `<Toast>` elements as `children`. You can
optionally attach a `ref` handler to access the instance methods, but we strongly recommend using the
[`Toaster.create` static method](#core/components/toast.static-usage) documented above instead. Note that
[`OverlayToaster.create` static method](#core/components/toast.static-usage) documented above instead. Note that
`children` and `ref` can be used together, but `children` will always appear _after_ toasts created with
`ref.show()`.

```tsx
import { Button, Position, Toast, Toaster } from "@blueprintjs/core";
import { Button, OverlayToaster, Position, Toast, ToasterInstance } from "@blueprintjs/core";
import * as React from "react";

class MyComponent extends React.PureComponent {
public state = { toasts: [ /* ToastProps[] */ ] }

private toaster: Toaster;
private toaster: ToasterInstance;
private refHandlers = {
toaster: (ref: Toaster) => this.toaster = ref,
toaster: (ref: ToasterInstance) => this.toaster = ref,
};

public render() {
return (
<div>
<Button onClick={this.addToast} text="Procure toast" />
<Toaster position={Position.TOP_RIGHT} ref={this.refHandlers.toaster}>
<OverlayToaster position={Position.TOP_RIGHT} ref={this.refHandlers.toaster}>
{/* "Toasted!" will appear here after clicking button. */}
{this.state.toasts.map(toast => <Toast {...toast} />)}
</Toaster>
</OverlayToaster>
</div>
)
}
Expand Down
43 changes: 27 additions & 16 deletions packages/core/src/components/toast/toaster.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export type ToasterPosition =
/** @deprecated use ToasterInstance */
export type IToaster = ToasterInstance;

/** Public API methods available on a `<Toaster>` component instance. */
/** Public API methods available on a `<OverlayToaster>` component instance. */
export interface ToasterInstance {
/**
* Shows a new toast to the user, or updates an existing toast corresponding to the provided key (optional).
Expand All @@ -58,10 +58,11 @@ export interface ToasterInstance {
}

/**
* Props supported by the `<Toaster>` component.
* These props can be passed as an argument to the static `Toaster.create(props?, container?)` method.
* Props supported by the `OverlayToaster` component.
*
* These props can be passed as an argument to the static `OverlayToaster.create(props?, container?)` method.
*/
export interface IToasterProps extends Props {
export interface OverlayToasterProps extends Props {
/**
* Whether a toast should acquire application focus when it first opens.
* This is disabled by default so that toasts do not interrupt the user's flow.
Expand All @@ -85,7 +86,7 @@ export interface IToasterProps extends Props {
* Whether the toaster should be rendered into a new element attached to `document.body`.
* If `false`, then positioning will be relative to the parent element.
*
* This prop is ignored by `Toaster.create()` as that method always appends a new element
* This prop is ignored by `OverlayToaster.create()` as that method always appends a new element
* to the container.
*
* @default true
Expand Down Expand Up @@ -114,14 +115,17 @@ export interface IToasterState {
}

/**
* Toaster component.
* Default Toaster component which renders toasts inside an Overlay.
*
* @see https://blueprintjs.com/docs/#core/components/toast.toaster
*/
export class Toaster extends AbstractPureComponent2<IToasterProps, IToasterState> implements ToasterInstance {
public static displayName = `${DISPLAYNAME_PREFIX}.Toaster`;
export class OverlayToaster
extends AbstractPureComponent2<OverlayToasterProps, IToasterState>
implements ToasterInstance
{
public static displayName = `${DISPLAYNAME_PREFIX}.OverlayToaster`;

public static defaultProps: IToasterProps = {
public static defaultProps: OverlayToasterProps = {
autoFocus: false,
canEscapeKeyClear: true,
position: Position.TOP,
Expand All @@ -132,16 +136,16 @@ export class Toaster extends AbstractPureComponent2<IToasterProps, IToasterState
* Create a new `Toaster` instance that can be shared around your application.
* The `Toaster` will be rendered into a new element appended to the given container.
*/
public static create(props?: IToasterProps, container = document.body): ToasterInstance {
public static create(props?: OverlayToasterProps, container = document.body): ToasterInstance {
if (props != null && props.usePortal != null && !isNodeEnv("production")) {
console.warn(TOASTER_WARN_INLINE);
}
const containerElement = document.createElement("div");
container.appendChild(containerElement);
const toaster = ReactDOM.render<IToasterProps>(
<Toaster {...props} usePortal={false} />,
const toaster = ReactDOM.render<OverlayToasterProps>(
<OverlayToaster {...props} usePortal={false} />,
containerElement,
) as Toaster;
) as OverlayToaster;
if (toaster == null) {
throw new Error(TOASTER_CREATE_NULL);
}
Expand Down Expand Up @@ -218,7 +222,7 @@ export class Toaster extends AbstractPureComponent2<IToasterProps, IToasterState
);
}

protected validateProps({ maxToasts }: IToasterProps) {
protected validateProps({ maxToasts }: OverlayToasterProps) {
// maximum number of toasts should not be a number less than 1
if (maxToasts !== undefined && maxToasts < 1) {
throw new Error(TOASTER_MAX_TOASTS_INVALID);
Expand Down Expand Up @@ -268,5 +272,12 @@ export class Toaster extends AbstractPureComponent2<IToasterProps, IToasterState
};
}

export const OverlayToaster = Toaster;
export type OverlayToasterProps = IToasterProps;
/** @deprecated use the new, more specific component name `OverlayToaster` instead (forwards-compatible with v5) */
export const Toaster = OverlayToaster;
/** @deprecated use the new, more specific type `ToasterInstance` instead (forwards-compatible with v5) */
// eslint-disable-next-line @typescript-eslint/no-redeclare
export type Toaster = ToasterInstance;
Comment on lines +278 to +279
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, sorry about that, thanks for flagging. I suppose I could fix this by changing it to:

Suggested change
// eslint-disable-next-line @typescript-eslint/no-redeclare
export type Toaster = ToasterInstance;
// eslint-disable-next-line @typescript-eslint/no-redeclare
export type Toaster = typeof OverlayToaster;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened a PR: #6165

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should probably just be deleted until v5 to avoid complications.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think removing this line will be backwards-compatible.

Before, Toaster was a class, so it could be referenced as a type or a value.

After this change, it's re-exported as a value with const Toaster = OverlayToaster. But that will break existing usage of Toaster as a type (you could work around this with typeof Toaster).

To restore Toaster as a type in the public API, I think we need an export type Toaster = ... statement.

Copy link
Contributor

@dlech dlech May 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 makes sense.

// eslint-disable-next-line deprecation/deprecation
Toaster.displayName = `${DISPLAYNAME_PREFIX}.Toaster`;
/** @deprecated use `OverlayToasterProps` instead */
export type IToasterProps = OverlayToasterProps;
6 changes: 3 additions & 3 deletions packages/core/test/isotest.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ describe("Core isomorphic rendering", () => {
props: { lazy: false, usePortal: false },
},
OverlayToaster: {
skip: true,
props: { usePortal: false },
children: React.createElement(Core.Toast, { message: "Toast" }),
},
PanelStack: {
props: {
Expand Down Expand Up @@ -100,8 +101,7 @@ describe("Core isomorphic rendering", () => {
children: requiredChild,
},
Toaster: {
props: { usePortal: false },
children: React.createElement(Core.Toast, { message: "Toast" }),
skip: true,
},
});
});
16 changes: 8 additions & 8 deletions packages/core/test/toast/toasterTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,17 @@ import { spy } from "sinon";

import { expectPropValidationError } from "@blueprintjs/test-commons";

import { Classes, Toaster, ToasterInstance } from "../../src";
import { Classes, OverlayToaster, ToasterInstance } from "../../src";
import { TOASTER_CREATE_NULL, TOASTER_MAX_TOASTS_INVALID } from "../../src/common/errors";

describe("Toaster", () => {
describe("OverlayToaster", () => {
let testsContainerElement: HTMLElement;
let toaster: ToasterInstance;

before(() => {
testsContainerElement = document.createElement("div");
document.documentElement.appendChild(testsContainerElement);
toaster = Toaster.create({}, testsContainerElement);
toaster = OverlayToaster.create({}, testsContainerElement);
});

afterEach(() => {
Expand Down Expand Up @@ -129,7 +129,7 @@ describe("Toaster", () => {

it("reusing props object does not produce React errors", () => {
const errorSpy = spy(console, "error");
// if Toaster doesn't clone the props object before injecting key then there will be a
// if OverlayToaster doesn't clone the props object before injecting key then there will be a
// React error that both toasts have the same key, because both instances refer to the
// same object.
const toast = { message: "repeat" };
Expand All @@ -139,7 +139,7 @@ describe("Toaster", () => {
});

it("does not exceed the maximum toast limit set", () => {
toaster = Toaster.create({ maxToasts: 3 });
toaster = OverlayToaster.create({ maxToasts: 3 });
toaster.show({ message: "one" });
toaster.show({ message: "two" });
toaster.show({ message: "three" });
Expand All @@ -149,15 +149,15 @@ describe("Toaster", () => {

describe("validation", () => {
it("throws an error when max toast is set to a number less than 1", () => {
expectPropValidationError(Toaster, { maxToasts: 0 }, TOASTER_MAX_TOASTS_INVALID);
expectPropValidationError(OverlayToaster, { maxToasts: 0 }, TOASTER_MAX_TOASTS_INVALID);
});
});

describe("with autoFocus set to true", () => {
before(() => {
testsContainerElement = document.createElement("div");
document.documentElement.appendChild(testsContainerElement);
toaster = Toaster.create({ autoFocus: true }, testsContainerElement);
toaster = OverlayToaster.create({ autoFocus: true }, testsContainerElement);
});

it("focuses inside toast container", done => {
Expand All @@ -179,7 +179,7 @@ describe("Toaster", () => {

public componentDidMount() {
try {
Toaster.create();
OverlayToaster.create();
} catch (err: any) {
assert.equal(err.message, TOASTER_CREATE_NULL);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/docs-app/src/examples/core-examples/alertExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import * as React from "react";

import { Alert, Button, H5, Intent, Switch, Toaster, ToasterInstance } from "@blueprintjs/core";
import { Alert, Button, H5, Intent, OverlayToaster, Switch, ToasterInstance } from "@blueprintjs/core";
import { Example, ExampleProps, handleBooleanChange } from "@blueprintjs/docs-theme";

import { IBlueprintExampleData } from "../../tags/types";
Expand Down Expand Up @@ -106,7 +106,7 @@ export class AlertExample extends React.PureComponent<ExampleProps<IBlueprintExa
</p>
</Alert>

<Toaster ref={ref => (this.toaster = ref)} />
<OverlayToaster ref={ref => (this.toaster = ref)} />
</Example>
);
}
Expand Down
Loading