diff --git a/packages/@react-spectrum/toast/docs/Toast.mdx b/packages/@react-spectrum/toast/docs/Toast.mdx index 213a6e43fc0..df60f7689ea 100644 --- a/packages/@react-spectrum/toast/docs/Toast.mdx +++ b/packages/@react-spectrum/toast/docs/Toast.mdx @@ -165,6 +165,14 @@ function Example() { } ``` +## Placement + +By default, toasts are displayed at the bottom center of the screen. This can be changed by setting the `placement` prop on the `ToastContainer` to `'top start'`, `'top'`, `'top end'`, `'bottom start'`, `'bottom'`, or `'bottom end'`. + +```tsx example render=false hidden + +``` + ## API ### ToastQueue diff --git a/packages/@react-spectrum/toast/src/ToastContainer.tsx b/packages/@react-spectrum/toast/src/ToastContainer.tsx index e5af9d47814..56b144ef884 100644 --- a/packages/@react-spectrum/toast/src/ToastContainer.tsx +++ b/packages/@react-spectrum/toast/src/ToastContainer.tsx @@ -21,7 +21,11 @@ import {Toaster} from './Toaster'; import {ToastOptions, ToastQueue, useToastQueue} from '@react-stately/toast'; import {useSyncExternalStore} from 'use-sync-external-store/shim/index.js'; -export interface SpectrumToastContainerProps extends AriaToastRegionProps {} +export type ToastPlacement = 'top start' | 'top' | 'top end' | 'bottom start' | 'bottom' | 'bottom end'; + +export interface SpectrumToastContainerProps extends AriaToastRegionProps { + placement?: ToastPlacement +} export interface SpectrumToastOptions extends Omit, DOMProps { /** A label for the action button within the toast. */ diff --git a/packages/@react-spectrum/toast/src/Toaster.tsx b/packages/@react-spectrum/toast/src/Toaster.tsx index 8be59a78644..356fc97fc4c 100644 --- a/packages/@react-spectrum/toast/src/Toaster.tsx +++ b/packages/@react-spectrum/toast/src/Toaster.tsx @@ -15,15 +15,17 @@ import {classNames} from '@react-spectrum/utils'; import {FocusScope, useFocusRing} from '@react-aria/focus'; import {mergeProps} from '@react-aria/utils'; import {Provider} from '@react-spectrum/provider'; -import React, {createContext, ReactElement, ReactNode, useRef} from 'react'; +import React, {createContext, ReactElement, ReactNode, useMemo, useRef} from 'react'; import ReactDOM from 'react-dom'; import toastContainerStyles from './toastContainer.css'; +import type {ToastPlacement} from './ToastContainer'; import {ToastState} from '@react-stately/toast'; import {useUNSTABLE_PortalContext} from '@react-aria/overlays'; interface ToastContainerProps extends AriaToastRegionProps { children: ReactNode, - state: ToastState + state: ToastState, + placement?: ToastPlacement } export const ToasterContext = createContext(false); @@ -39,6 +41,11 @@ export function Toaster(props: ToastContainerProps): ReactElement { let {focusProps, isFocusVisible} = useFocusRing(); let {getContainer} = useUNSTABLE_PortalContext(); + let [position, placement] = useMemo(() => { + let [pos = 'bottom', place = 'center'] = props.placement?.split(' ') || []; + return [pos, place]; + }, [props.placement]); + let contents = ( @@ -46,8 +53,8 @@ export function Toaster(props: ToastContainerProps): ReactElement {
( + (story, {parameters, args}) => ( <> - {!parameters.disableToastContainer && } + {!parameters.disableToastContainer && } {story()} ) ], args: { shouldCloseOnAction: false, - timeout: null + timeout: null, + placement: undefined }, argTypes: { timeout: { control: 'radio', options: [null, 5000] + }, + placement: { + control: 'select', + options: [undefined, 'top start', 'top', 'top end', 'bottom start', 'bottom', 'bottom end'] } } };