Back to React Spectrum

useToast

packages/react-aria/docs/toast/useToast.mdx

2022-12-1612.4 KB
Original Source

{/* Copyright 2020 Adobe. All rights reserved. This file is licensed to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */}

import {Layout} from '@react-spectrum/docs'; export default Layout;

import docs from 'docs:@react-aria/toast'; import statelyDocs from 'docs:@react-stately/toast'; import {HeaderInfo, FunctionAPI, TypeContext, InterfaceType, PageDescription, TypeLink} from '@react-spectrum/docs'; import packageData from '@react-aria/toast/package.json'; import Anatomy from './toast-anatomy.svg'; import ChevronRight from '@spectrum-icons/workflow/ChevronRight'; import {Keyboard} from '@react-spectrum/text';

tsx
import {useToastRegion} from '@react-aria/toast';

category: Status keywords: [toast, notifications, alert, aria]

useToast

<PageDescription>{docs.exports.useToast.description}</PageDescription>

<HeaderInfo packageData={packageData} componentNames={['useToastRegion', 'useToast']} sourceData={[ {type: 'W3C', url: 'https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/'} ]} />

API

<FunctionAPI function={docs.exports.useToastRegion} links={docs.links} /> <FunctionAPI function={docs.exports.useToast} links={docs.links} />

Features

There is no built in way to display toast notifications in HTML. <TypeLink links={docs.links} type={docs.exports.useToastRegion} /> and <TypeLink links={docs.links} type={docs.exports.useToast} /> help achieve accessible toasts that can be styled as needed.

  • Accessible – Toasts follow the ARIA alertdialog pattern. They are rendered in a landmark region, which keyboard and screen reader users can easily jump to when an alert is announced.
  • Focus management – When a toast unmounts, focus is moved to the next toast if any. Otherwise, focus is restored to where it was before navigating to the toast region. Tabbing through the Toast region will move from newest to oldest.

Anatomy

<Anatomy role="img" aria-label="Toast anatomy diagram, showing the toast's title and close button within the toast region." />

A toast region is an ARIA landmark region labeled "Notifications" by default. A toast region contains one or more visible toasts, in chronological order. When the limit is reached, additional toasts are queued until the user dismisses one. Each toast is a non-modal ARIA alertdialog, containing the content of the notification and a close button.

Landmark regions including the toast container can be navigated using the keyboard by pressing the <Keyboard>F6</Keyboard> key to move forward, and the <Keyboard>Shift</Keyboard> + <Keyboard>F6</Keyboard> key to move backward. This provides an easy way for keyboard users to jump to the toasts from anywhere in the app. When the last toast is closed, keyboard focus is restored.

useToastRegion returns props that you should spread onto the toast container element:

<TypeContext.Provider value={docs.links}> <InterfaceType properties={docs.links[docs.exports.useToastRegion.return.id].properties} /> </TypeContext.Provider>

useToast returns props that you should spread onto an individual toast and its child elements:

<TypeContext.Provider value={docs.links}> <InterfaceType properties={docs.links[docs.exports.useToast.return.id].properties} /> </TypeContext.Provider>

Example

Toasts consist of three components. The first is a ToastProvider component which will manage the state for the toast queue with the <TypeLink links={statelyDocs.links} type={statelyDocs.exports.useToastState} /> hook. Alternatively, you could use a global toast queue (see below).

tsx
import {useToastState} from '@react-stately/toast';

function ToastProvider({children, ...props}) {
  let state = useToastState({
    maxVisibleToasts: 5
  });

  return (
    <>
      {children(state)}
      {state.visibleToasts.length > 0 && <ToastRegion {...props} state={state} />}
    </>
  );
}
tsx
// Actual implementation we use in the docs, using global queue.
function ToastProvider({children}) {
  return children(toastQueue);
}

The ToastRegion component will be rendered when there are toasts to display. It uses the <TypeLink links={docs.links} type={docs.exports.useToastRegion} /> hook to create a landmark region, allowing keyboard and screen reader users to easily navigate to it.

tsx
import type {AriaToastRegionProps} from '@react-aria/toast';
import type {ToastState} from '@react-stately/toast';
import {useToastRegion} from '@react-aria/toast';

interface ToastRegionProps<T> extends AriaToastRegionProps {
  state: ToastState<T>
}

function ToastRegion<T extends React.ReactNode>({state, ...props}: ToastRegionProps<T>) {
  let ref = React.useRef(null);
  let {regionProps} = useToastRegion(props, state, ref);

  return (
    <div {...regionProps} ref={ref} className="toast-region">
      {state.visibleToasts.map(toast => (
        <Toast key={toast.key} toast={toast} state={state} />
      ))}
    </div>
  );
}

Finally, we need the Toast component to render an individual toast within a ToastRegion, built with <TypeLink links={docs.links} type={docs.exports.useToast} />.

tsx
import type {AriaToastProps} from '@react-aria/toast';
import {useToast} from '@react-aria/toast';

// Reuse the Button from your component library. See below for details.
import {Button} from 'your-component-library';

interface ToastProps<T> extends AriaToastProps<T> {
  state: ToastState<T>
}

function Toast<T extends React.ReactNode>({state, ...props}: ToastProps<T>) {
  let ref = React.useRef(null);
  let {toastProps, contentProps, titleProps, closeButtonProps} = useToast(props, state, ref);

  return (
    <div {...toastProps} ref={ref} className="toast">
      <div {...contentProps}>
        <div {...titleProps}>{props.toast.content}</div>
      </div>
      <Button {...closeButtonProps}>x</Button>
    </div>
  );
}
tsx
<ToastProvider>
  {state => (
    <Button onPress={() => state.add('Toast is done!')}>Show toast</Button>
  )}
</ToastProvider>
<details> <summary style={{fontWeight: 'bold'}}><ChevronRight size="S" /> Show CSS</summary>
css
.toast-region {
  position: fixed;
  bottom: 16px;
  right: 16px;
  display: flex;
  flex-direction: column-reverse;
  gap: 8px;
}

.toast {
  display: flex;
  align-items: center;
  gap: 16px;
  background: slateblue;
  color: white;
  padding: 12px 16px;
  border-radius: 8px;
}

.toast button {
  background: none;
  border: none;
  appearance: none;
  border-radius: 50%;
  height: 32px;
  width: 32px;
  font-size: 16px;
  border: 1px solid white;
  color: white;
  padding: 0;
}

.toast button:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px slateblue, 0 0 0 4px white;
}

.toast button:active {
  background: rgba(255, 255, 255, 0.2);
}
</details>

Button

The Button component is used in the above example to close a toast. It is built using the useButton hook, and can be shared with many other components.

<details> <summary style={{fontWeight: 'bold'}}><ChevronRight size="S" /> Show code</summary>
tsx
import {useButton} from '@react-aria/button';

function Button(props) {
  let ref = React.useRef(null);
  let {buttonProps} = useButton(props, ref);
  return <button {...buttonProps} ref={ref}>{props.children}</button>;
}
</details>

Usage

The following examples show how to use the ToastProvider component created in the above example.

Auto-dismiss

Toasts support a timeout option to automatically hide them after a certain amount of time. For accessibility, toasts should have a minimum timeout of 5 seconds to give users enough time to read them. If a toast includes action buttons or other interactive elements it should not auto dismiss. In addition, timers will automatically pause when the user focuses or hovers over a toast.

Be sure only to automatically dismiss toasts when the information is not important, or may be found elsewhere. Some users may require additional time to read a toast message, and screen zoom users may miss toasts entirely.

tsx
<ToastProvider>
  {state => (
    ///- begin highlight -///
    <Button onPress={() => state.add('Toast still toasting!', {timeout: 5000})}>
      Show toast
    </Button>
  )}
</ToastProvider>

Programmatic dismissal

Toasts may be programmatically dismissed if they become irrelevant before the user manually closes them. state.add returns a key for the toast which may be passed to state.close to dismiss the toast.

tsx
function Example() {
  let [toastKey, setToastKey] = React.useState(null);

  return (
    <ToastProvider>
      {state => (
        <Button
          onPress={() => {
            if (!toastKey) {
              ///- begin highlight -///
              setToastKey(state.add('Unable to save', {onClose: () => setToastKey(null)}));
              ///- end highlight -///
            } else {
              ///- begin highlight -///
              state.close(toastKey);
              ///- end highlight -///
            }
          }}>
          {toastKey ? 'Hide' : 'Show'} Toast
        </Button>
      )}
    </ToastProvider>
  );
}

Advanced topics

Global toast queue

In the above examples, each ToastProvider has a separate queue. This setup is simple, and fine for most cases where you can wrap the entire app in a single ToastProvider. However, in more complex situations, you may want to keep the toast queue outside the React tree so that toasts can be queued from anywhere. This can be done by creating your own <TypeLink links={statelyDocs.links} type={statelyDocs.exports.ToastQueue} /> and subscribing to it using the <TypeLink links={statelyDocs.links} type={statelyDocs.exports.useToastQueue} /> hook rather than useToastState.

tsx
import {ToastQueue, useToastQueue} from '@react-stately/toast';
import {createPortal} from 'react-dom';

// Create a global toast queue.
///- begin highlight -///
const toastQueue = new ToastQueue({
  maxVisibleToasts: 5
});
///- end highlight -///

function GlobalToastRegion(props) {
  // Subscribe to it.
  ///- begin highlight -///
  let state = useToastQueue(toastQueue);
  ///- end highlight -///

  // Render toast region.
  return state.visibleToasts.length > 0
    ? createPortal(<ToastRegion {...props} state={state} />, document.body)
    : null;
}

// Render it somewhere in your app.
<GlobalToastRegion />

Now you can queue a toast from anywhere:

tsx
<Button onPress={() => toastQueue.add('Toast is done!')}>Show toast</Button>

TypeScript

A ToastQueue and useToastState use a generic type to represent toast content. The examples so far have used strings, but you can type this however you want to enable passing custom objects or options. This example uses a custom object to support toasts with both a title and description.

tsx
import type {QueuedToast} from '@react-stately/toast';

/*- begin highlight -*/
interface MyToast {
  title: string,
  description: string
}
/*- end highlight -*/

function ToastProvider() {
  /*- begin highlight -*/
  let state = useToastState<MyToast>();
  /*- end highlight -*/

  // ...
}

interface ToastProps {
  /*- begin highlight -*/
  toast: QueuedToast<MyToast>
  /*- end highlight -*/
}

function Toast(props: ToastProps) {
  // ...

  let {toastProps, titleProps, descriptionProps, closeButtonProps} = useToast(props, state, ref);

  return (
    <div {...toastProps} ref={ref} className="toast">
      <div>
        <div {...titleProps}>{props.toast.content.title}</div>
        <div {...descriptionProps}>{props.toast.content.description}</div>
      </div>
      <Button {...closeButtonProps}>x</Button>
    </div>
  );
}

// Queuing a toast
/*- begin highlight -*/
state.add({title: 'Success!', description: 'Toast is done.'});
/*- end highlight -*/