diff --git a/packages/@react-aria/toast/package.json b/packages/@react-aria/toast/package.json index 693a2c46fa2..bed9a0a4684 100644 --- a/packages/@react-aria/toast/package.json +++ b/packages/@react-aria/toast/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/toast", - "version": "3.0.0-alpha.2", + "version": "3.0.0-alpha.1", "private": true, "description": "Spectrum UI components in React", "main": "dist/main.js", @@ -19,8 +19,9 @@ "@babel/runtime": "^7.6.2", "@react-aria/i18n": "^3.0.0-alpha.2", "@react-aria/interactions": "^3.0.0-alpha.2", + "@react-aria/utils": "^3.0.0-alpha.2", "@react-types/shared": "^3.0.0-alpha.2", - "@react-types/toast": "^3.0.0-alpha.2" + "@react-types/toast": "^3.0.0-alpha.1" }, "peerDependencies": { "react": "^16.8.0" diff --git a/packages/@react-aria/toast/src/useToast.ts b/packages/@react-aria/toast/src/useToast.ts index 70b191786e1..bd4d625a354 100644 --- a/packages/@react-aria/toast/src/useToast.ts +++ b/packages/@react-aria/toast/src/useToast.ts @@ -3,8 +3,13 @@ import {HTMLAttributes, ImgHTMLAttributes} from 'react'; import intlMessages from '../intl/*.json'; import {PressProps} from '@react-aria/interactions'; import {ToastProps} from '@react-types/toast'; +import {useId} from '@react-aria/utils'; import {useMessageFormatter} from '@react-aria/i18n'; +interface AriaToastProps extends ToastProps { + id?: string +} + interface ToastAria { toastProps: HTMLAttributes, iconProps: ImgHTMLAttributes, @@ -12,8 +17,9 @@ interface ToastAria { closeButtonProps: DOMProps & PressProps } -export function useToast(props: ToastProps): ToastAria { +export function useToast(props: AriaToastProps): ToastAria { let { + id, onAction, onClose, shouldCloseOnAction, @@ -35,6 +41,7 @@ export function useToast(props: ToastProps): ToastAria { return { toastProps: { + id: useId(id), role: 'alert' }, iconProps, diff --git a/packages/@react-spectrum/button/package.json b/packages/@react-spectrum/button/package.json index b98d27bd235..b8289e137a8 100644 --- a/packages/@react-spectrum/button/package.json +++ b/packages/@react-spectrum/button/package.json @@ -33,7 +33,6 @@ "@react-aria/interactions": "^3.0.0-rc.1", "@react-aria/utils": "^3.0.0-rc.1", "@react-aria/selection": "^3.0.0-alpha.2", - "@react-spectrum/provider": "^3.0.0-rc.1", "@react-spectrum/utils": "^3.0.0-rc.1", "@react-stately/button": "^3.0.0-alpha.2", "@react-types/button": "^3.0.0-rc.1", @@ -46,7 +45,8 @@ }, "peerDependencies": { "react": "^16.8.0", - "react-dom": "^16.8.0" + "react-dom": "^16.8.0", + "@react-spectrum/provider": "^3.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-spectrum/icon/package.json b/packages/@react-spectrum/icon/package.json index 761d2728985..a1a72d512d7 100644 --- a/packages/@react-spectrum/icon/package.json +++ b/packages/@react-spectrum/icon/package.json @@ -28,7 +28,6 @@ }, "dependencies": { "@babel/runtime": "^7.6.2", - "@react-spectrum/provider": "^3.0.0-rc.1", "@react-spectrum/utils": "^3.0.0-rc.1", "@react-types/shared": "^3.0.0-rc.1" }, @@ -36,7 +35,8 @@ "@adobe/spectrum-css-temp": "^3.0.0-alpha.2" }, "peerDependencies": { - "react": "^16.8.0" + "react": "^16.8.0", + "@react-spectrum/provider": "^3.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-spectrum/provider/package.json b/packages/@react-spectrum/provider/package.json index d14eeade39a..0c7917c2e55 100644 --- a/packages/@react-spectrum/provider/package.json +++ b/packages/@react-spectrum/provider/package.json @@ -30,6 +30,7 @@ "@babel/runtime": "^7.6.2", "@react-aria/dialog": "^3.0.0-alpha.2", "@react-aria/i18n": "^3.0.0-rc.1", + "@react-spectrum/toast": "^3.0.0-alpha.1", "@react-spectrum/utils": "^3.0.0-rc.1", "@react-types/provider": "^3.0.0-rc.1", "@react-types/shared": "^3.0.0-rc.1", diff --git a/packages/@react-spectrum/provider/src/Provider.tsx b/packages/@react-spectrum/provider/src/Provider.tsx index 7d84068161a..86bdd5ace94 100644 --- a/packages/@react-spectrum/provider/src/Provider.tsx +++ b/packages/@react-spectrum/provider/src/Provider.tsx @@ -7,6 +7,7 @@ import {ModalProvider, useModalProvider} from '@react-aria/dialog'; import {ProviderContext, ProviderProps} from '@react-types/provider'; import React, {useContext, useEffect} from 'react'; import styles from '@adobe/spectrum-css-temp/components/page/vars.css'; +import {ToastProvider} from '@react-spectrum/toast'; import typographyStyles from '@adobe/spectrum-css-temp/components/typography/index.css'; import {useColorScheme, useScale} from './mediaQueries'; // @ts-ignore @@ -67,7 +68,9 @@ function Provider(props: ProviderProps, ref: DOMRef) { if (!prevContext || theme !== prevContext.theme || colorScheme !== prevContext.colorScheme || scale !== prevContext.scale || Object.keys(domProps).length > 0 || otherProps.UNSAFE_className || Object.keys(styleProps.style).length > 0) { contents = ( - {contents} + + {contents} + ); } diff --git a/packages/@react-spectrum/toast/package.json b/packages/@react-spectrum/toast/package.json index 4464118c3f1..4f14142f4f5 100644 --- a/packages/@react-spectrum/toast/package.json +++ b/packages/@react-spectrum/toast/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/toast", - "version": "3.0.0-alpha.2", + "version": "3.0.0-alpha.1", "private": true, "description": "Spectrum UI components in React", "main": "dist/main.js", @@ -29,12 +29,12 @@ }, "dependencies": { "@babel/runtime": "^7.6.2", - "@react-aria/toast": "^3.0.0-alpha.2", - "@react-spectrum/button": "^3.0.0-alpha.2", - "@react-spectrum/provider": "^3.0.0-alpha.2", + "@react-aria/toast": "^3.0.0-alpha.1", + "@react-spectrum/button": "^3.0.0-rc.1", "@react-spectrum/utils": "^3.0.0-alpha.2", + "@react-stately/toast": "^3.0.0-alpha.1", "@react-types/shared": "^3.0.0-rc.1", - "@react-types/toast": "^3.0.0-alpha.2", + "@react-types/toast": "^3.0.0-alpha.1", "@spectrum-icons/ui": "^3.0.0-alpha.2" }, "devDependencies": { @@ -43,7 +43,8 @@ }, "peerDependencies": { "react": "^16.8.0", - "react-dom": "^16.8.0" + "react-dom": "^16.8.0", + "@react-spectrum/provider": "^3.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-spectrum/toast/src/Toast.tsx b/packages/@react-spectrum/toast/src/Toast.tsx index 8d13558ff1e..07891c2e2e9 100644 --- a/packages/@react-spectrum/toast/src/Toast.tsx +++ b/packages/@react-spectrum/toast/src/Toast.tsx @@ -1,20 +1,3 @@ -/************************************************************************* -* ADOBE CONFIDENTIAL -* ___________________ -* -* Copyright 2019 Adobe -* All Rights Reserved. -* -* NOTICE: All information contained herein is, and remains -* the property of Adobe and its suppliers, if any. The intellectual -* and technical concepts contained herein are proprietary to Adobe -* and its suppliers and are protected by all applicable intellectual -* property laws, including trade secret and copyright laws. -* Dissemination of this information or reproduction of this material -* is strictly forbidden unless prior written permission is obtained -* from Adobe. -**************************************************************************/ - import AlertMedium from '@spectrum-icons/ui/AlertMedium'; import {Button, ClearButton} from '@react-spectrum/button'; import {classNames, filterDOMProps, useDOMRef, useStyleProps} from '@react-spectrum/utils'; @@ -25,6 +8,7 @@ import React from 'react'; import {SpectrumToastProps} from '@react-types/toast'; import styles from '@adobe/spectrum-css-temp/components/toast/vars.css'; import SuccessMedium from '@spectrum-icons/ui/SuccessMedium'; +import toastContainerStyles from './toastContainer.css'; import {useToast} from '@react-aria/toast'; export const ICONS = { @@ -59,7 +43,11 @@ function Toast(props: SpectrumToastProps, ref: DOMRef) { className={classNames(styles, 'spectrum-Toast', {['spectrum-Toast--' + variant]: variant}, - styleProps.className + styleProps.className, + classNames( + toastContainerStyles, + 'spectrum-Toast' + ) )}> {Icon && props.toasts.map((toast) => + ({toast.content}) + ); + + return ( +
+ {renderToasts()} +
+ ); +} diff --git a/packages/@react-spectrum/toast/src/ToastProvider.tsx b/packages/@react-spectrum/toast/src/ToastProvider.tsx new file mode 100644 index 00000000000..66680a4259d --- /dev/null +++ b/packages/@react-spectrum/toast/src/ToastProvider.tsx @@ -0,0 +1,53 @@ +import React, {ReactElement, ReactNode, useContext} from 'react'; +import {ToastContainer} from './'; +import {ToastOptions, ToastStateBase} from '@react-types/toast'; +import {useProviderProps} from '@react-spectrum/provider'; +import {useToastState} from '@react-stately/toast'; + +interface ToastContextProps { + setToasts?: (any) => void, + toasts?: ToastStateBase[], + positive?: (content: ReactNode, options: ToastOptions) => void, + negative?: (content: ReactNode, options: ToastOptions) => void, + neutral?: (content: ReactNode, options: ToastOptions) => void, + info?: (content: ReactNode, options: ToastOptions) => void +} + +interface ToastProviderProps { + children: ReactNode +} + +export const ToastContext = React.createContext(null); + +export function useToastProvider() { + return useContext(ToastContext); +} + +export function ToastProvider(props: ToastProviderProps): ReactElement { + let {onAdd, toasts} = useToastState(); + let { + children + } = useProviderProps(props); + + let contextValue = { + neutral: (content: ReactNode, options: ToastOptions = {}) => { + onAdd(content, options); + }, + positive: (content: ReactNode, options: ToastOptions = {}) => { + onAdd(content, {...options, variant: 'positive'}); + }, + negative: (content: ReactNode, options: ToastOptions = {}) => { + onAdd(content, {...options, variant: 'negative'}); + }, + info: (content: ReactNode, options: ToastOptions = {}) => { + onAdd(content, {...options, variant: 'info'}); + } + }; + + return ( + + + {children} + + ); +} diff --git a/packages/@react-spectrum/toast/src/index.ts b/packages/@react-spectrum/toast/src/index.ts index 1b794ee7863..3045efc1c58 100644 --- a/packages/@react-spectrum/toast/src/index.ts +++ b/packages/@react-spectrum/toast/src/index.ts @@ -1 +1,3 @@ export * from './Toast'; +export * from './ToastContainer'; +export * from './ToastProvider'; diff --git a/packages/@react-spectrum/toast/src/toastContainer.css b/packages/@react-spectrum/toast/src/toastContainer.css new file mode 100644 index 00000000000..44c545ee770 --- /dev/null +++ b/packages/@react-spectrum/toast/src/toastContainer.css @@ -0,0 +1,35 @@ +.react-spectrum-ToastContainer { + position: fixed; + top: unset; + bottom: 0; + inset-inline-start: 0; + inset-inline-end: 0; + z-index: 100050; /* above modals */ + display: flex; + flex-direction: column-reverse; + align-items: center; + pointer-events: none; + + .spectrum-Toast { + margin: 8px; + pointer-events: all; + } +} +.react-spectrum-ToastContainer--top { + top: 0; + flex-direction: column; + bottom: unset; +} +.react-spectrum-ToastContainer--bottom { + flex-direction: column-reverse; + bottom: 0; +} +.react-spectrum-ToastContainer--left { + align-items: flex-start; +} +.react-spectrum-ToastContainer--center { + align-items: center; +} +.react-spectrum-ToastContainer--right { + align-items: flex-end; +} diff --git a/packages/@react-spectrum/toast/stories/Toast.stories.tsx b/packages/@react-spectrum/toast/stories/Toast.stories.tsx index a91ecf489a3..c0a90181b1b 100644 --- a/packages/@react-spectrum/toast/stories/Toast.stories.tsx +++ b/packages/@react-spectrum/toast/stories/Toast.stories.tsx @@ -1,8 +1,10 @@ import {action} from '@storybook/addon-actions'; +import {Button} from '@react-spectrum/button'; import React from 'react'; import {storiesOf} from '@storybook/react'; import {Toast} from '../'; import {ToastProps} from '@react-types/toast'; +import {useToastProvider} from '../'; storiesOf('Toast', module) .add( @@ -28,6 +30,9 @@ storiesOf('Toast', module) .add( 'action triggers close', () => render({actionLabel: 'Undo', onAction: action('onAction'), shouldCloseOnAction: true, onClose: action('onClose')}, 'Close on untoasting of the toast') + ).add( + 'add via provider', + () => ); function render(props:ToastProps = {}, message:String) { @@ -37,3 +42,32 @@ function render(props:ToastProps = {}, message:String) { ); } + +function RenderProvider() { + let toastContext = useToastProvider(); + + return ( +
+ + + + +
+ ); +} diff --git a/packages/@react-spectrum/toast/test/ToastContainer.test.js b/packages/@react-spectrum/toast/test/ToastContainer.test.js new file mode 100644 index 00000000000..f680be00caf --- /dev/null +++ b/packages/@react-spectrum/toast/test/ToastContainer.test.js @@ -0,0 +1,45 @@ +import {Button} from '@react-spectrum/button'; +import {cleanup, render} from '@testing-library/react'; +import React from 'react'; +import {ToastProvider, useToastProvider} from '../'; +import {triggerPress} from '@react-spectrum/test-utils'; + +function RenderToastButton() { + let toastContext = useToastProvider(); + + return ( +
+ +
+ ); +} + +function renderComponent(contents) { + return render( + {contents} + ); +} + +describe('Toast', function () { + afterEach(() => { + cleanup(); + }); + + it('Renders a button that triggers a toast via the provider', async function () { + let {getByRole, queryAllByRole} = renderComponent(); + let button = getByRole('button'); + + expect(() => { + getByRole('alert'); + }).toThrow(); + + triggerPress(button); + + expect(queryAllByRole('alert').length).toBe(1); + expect(getByRole('alert')).toBeVisible(); + }); +}); diff --git a/packages/@react-stately/toast/index.ts b/packages/@react-stately/toast/index.ts new file mode 100644 index 00000000000..8420b1093fd --- /dev/null +++ b/packages/@react-stately/toast/index.ts @@ -0,0 +1 @@ +export * from './src'; diff --git a/packages/@react-stately/toast/package.json b/packages/@react-stately/toast/package.json new file mode 100644 index 00000000000..c1c0eb58f4c --- /dev/null +++ b/packages/@react-stately/toast/package.json @@ -0,0 +1,26 @@ +{ + "name": "@react-stately/toast", + "version": "3.0.0-alpha.1", + "description": "Spectrum UI components in React", + "main": "dist/main.js", + "module": "dist/module.js", + "types": "dist/types.d.ts", + "source": "src/index.ts", + "files": [ + "dist" + ], + "sideEffects": false, + "repository": { + "type": "git", + "url": "https://github.com/adobe/react-spectrum" + }, + "dependencies": { + "@babel/runtime": "^7.6.2" + }, + "peerDependencies": { + "react": "^16.8.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/@react-stately/toast/src/index.ts b/packages/@react-stately/toast/src/index.ts new file mode 100644 index 00000000000..8b7d0dd438d --- /dev/null +++ b/packages/@react-stately/toast/src/index.ts @@ -0,0 +1 @@ +export * from './useToastState'; diff --git a/packages/@react-stately/toast/src/useToastState.ts b/packages/@react-stately/toast/src/useToastState.ts new file mode 100644 index 00000000000..a669036a756 --- /dev/null +++ b/packages/@react-stately/toast/src/useToastState.ts @@ -0,0 +1,26 @@ +import {ToastStateBase} from '@react-types/toast'; +import {useState} from 'react'; + + +interface ToastStateProps extends React.HTMLAttributes { + value?: ToastStateBase[] +} + +export function useToastState(props?: ToastStateProps) { + const [toasts, setToasts] = useState(props && props.value || []); + + const onAdd = (content, options) => { + let tempToasts = [...toasts]; + tempToasts.push({ + content, + props: options + }); + setToasts(tempToasts); + }; + + return { + onAdd, + toasts, + setToasts + }; +} diff --git a/packages/@react-stately/toast/test/useToastState.test.js b/packages/@react-stately/toast/test/useToastState.test.js new file mode 100644 index 00000000000..3b7e1552031 --- /dev/null +++ b/packages/@react-stately/toast/test/useToastState.test.js @@ -0,0 +1,38 @@ +import {act, renderHook} from 'react-hooks-testing-library'; +import {useToastState} from '../'; + +describe('useToastState', () => { + let newValue = [{ + content: 'Toast Message', + props: {} + }]; + + it('should be able to update via setToasts', () => { + let {result} = renderHook(() => useToastState()); + expect(result.current.toasts).toStrictEqual([]); + act(() => result.current.setToasts(newValue)); + expect(result.current.toasts).toStrictEqual(newValue); + }); + + it('should add a new toast via onAdd', () => { + let {result} = renderHook(() => useToastState()); + expect(result.current.toasts).toStrictEqual([]); + act(() => result.current.onAdd(newValue[0].content, newValue[0].props)); + expect(result.current.toasts).toStrictEqual(newValue); + }); + + it('should be able to add multiple toasts', () => { + let secondToast = { + content: 'Second Toast', + props: {variant: 'info'} + }; + let {result} = renderHook(() => useToastState()); + expect(result.current.toasts).toStrictEqual([]); + act(() => result.current.onAdd(newValue[0].content, newValue[0].props)); + expect(result.current.toasts).toStrictEqual(newValue); + act(() => result.current.onAdd(secondToast.content, secondToast.props)); + expect(result.current.toasts.length).toBe(2); + expect(result.current.toasts[0]).toStrictEqual(newValue[0]); + expect(result.current.toasts[1]).toStrictEqual(secondToast); + }); +}); diff --git a/packages/@react-types/toast/package.json b/packages/@react-types/toast/package.json index 3aeb79399b8..6559f8d5332 100644 --- a/packages/@react-types/toast/package.json +++ b/packages/@react-types/toast/package.json @@ -1,6 +1,6 @@ { "name": "@react-types/toast", - "version": "3.0.0-alpha.2", + "version": "3.0.0-alpha.1", "description": "Spectrum UI components in React", "types": "src/index.d.ts", "repository": { diff --git a/packages/@react-types/toast/src/index.d.ts b/packages/@react-types/toast/src/index.d.ts index eda534a548b..7c337cc9c25 100644 --- a/packages/@react-types/toast/src/index.d.ts +++ b/packages/@react-types/toast/src/index.d.ts @@ -11,7 +11,12 @@ export interface ToastOptions { interface ToastProps extends ToastOptions { children?: ReactNode, - variant?: 'positive' | 'negative' | 'info' // TODO: move this into react-spectrum + variant?: 'positive' | 'negative' | 'info' } export interface SpectrumToastProps extends ToastProps, DOMProps, StyleProps {} + +export interface ToastStateBase { + content: ReactNode, + props: ToastOptions +}