From 4199aab5ccf7c1edf823016004ce07389e0ba1c5 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 13 Aug 2025 15:28:31 -0500 Subject: [PATCH] Refactor API --- src/components/Toast/Toast.tsx | 15 +++--- src/components/Toast/const.ts | 2 +- src/components/Toast/index.tsx | 55 +++++++++++++------ src/components/Toast/index.web.tsx | 32 +++++++---- src/components/Toast/types.ts | 47 ++++++++-------- src/view/com/util/Toast.tsx | 8 +-- src/view/screens/Storybook/Toasts.tsx | 77 +++++++++------------------ 7 files changed, 124 insertions(+), 112 deletions(-) diff --git a/src/components/Toast/Toast.tsx b/src/components/Toast/Toast.tsx index 908b470a4a..28220cb8d3 100644 --- a/src/components/Toast/Toast.tsx +++ b/src/components/Toast/Toast.tsx @@ -13,6 +13,11 @@ type ContextType = { type: ToastType } +export type ToastComponentProps = { + type?: ToastType + content: React.ReactNode +} + export const ICONS = { default: CircleCheck, success: CircleCheck, @@ -26,13 +31,7 @@ const Context = createContext({ }) Context.displayName = 'ToastContext' -export function Toast({ - type, - content, -}: { - type: ToastType - content: React.ReactNode -}) { +export function Toast({type = 'default', content}: ToastComponentProps) { const {fonts} = useAlf() const t = useTheme() const styles = useToastStyles({type}) @@ -90,10 +89,12 @@ export function ToastText({children}: {children: React.ReactNode}) { const {textColor} = useToastStyles({type}) return ( {children} -} +export {DURATION} from '#/components/Toast/const' +/** + * Toasts are rendered in a global outlet, which is placed at the top of the + * component tree. + */ export function ToastOutlet() { return } -export const toast: ToastApi = { - show(props) { - sonner.custom( - - - , - { - duration: props.duration ?? DEFAULT_TOAST_DURATION, - }, - ) - }, +/** + * The toast UI component + */ +export function Toast({type, content}: ToastComponentProps) { + return ( + + + + ) +} + +/** + * Access the full Sonner API + */ +export const api = sonner + +/** + * Our base toast API, using the `Toast` export of this file. + */ +export function show( + content: React.ReactNode, + {type, ...options}: BaseToastOptions = {}, +) { + sonner.custom(, { + ...options, + duration: options?.duration ?? DURATION, + }) } diff --git a/src/components/Toast/index.web.tsx b/src/components/Toast/index.web.tsx index b432ca6b0f..857ed7b39a 100644 --- a/src/components/Toast/index.web.tsx +++ b/src/components/Toast/index.web.tsx @@ -1,10 +1,14 @@ import {toast as sonner, Toaster} from 'sonner' import {atoms as a} from '#/alf' -import {DEFAULT_TOAST_DURATION} from '#/components/Toast/const' +import {DURATION} from '#/components/Toast/const' import {Toast} from '#/components/Toast/Toast' -import {type ToastApi} from '#/components/Toast/types' +import {type BaseToastOptions} from '#/components/Toast/types' +/** + * Toasts are rendered in a global outlet, which is placed at the top of the + * component tree. + */ export function ToastOutlet() { return ( , { - duration: props.duration ?? DEFAULT_TOAST_DURATION, - unstyled: true, - }) - }, +/** + * Access the full Sonner API + */ +export const api = sonner + +/** + * Our base toast API, using the `Toast` export of this file. + */ +export function show( + content: React.ReactNode, + {type, ...options}: BaseToastOptions = {}, +) { + sonner(, { + unstyled: true, // required on web + ...options, + duration: options?.duration ?? DURATION, + }) } diff --git a/src/components/Toast/types.ts b/src/components/Toast/types.ts index 9f1245fa22..463e6d66ca 100644 --- a/src/components/Toast/types.ts +++ b/src/components/Toast/types.ts @@ -1,24 +1,29 @@ +import {type toast as sonner} from 'sonner-native' + +/** + * This is not exported from `sonner-native` so just hacking it in here. + */ +export type ExternalToast = Exclude< + Parameters[1], + undefined +> + export type ToastType = 'default' | 'success' | 'error' | 'warning' | 'info' -export type ToastApi = { - show: (props: { - /** - * The type of toast to show. This determines the styling and icon used. - */ - type: ToastType - /** - * A string, `Text`, or `Span` components to render inside the toast. This - * allows additional formatting of the content, but should not be used for - * interactive elements link links or buttons. - */ - content: React.ReactNode | string - /** - * Accessibility label for the toast, used for screen readers. - */ - a11yLabel: string - /** - * Defaults to `DEFAULT_TOAST_DURATION` from `#components/Toast/const`. - */ - duration?: number - }) => void +/** + * Not all properties are available on all platforms, so we pick out only those + * we support. Add more here as needed. + */ +export type BaseToastOptions = Pick< + ExternalToast, + 'duration' | 'dismissible' | 'promiseOptions' +> & { + type?: ToastType + + /** + * These methods differ between web/native implementations + */ + onDismiss?: () => void + onPress?: () => void + onAutoClose?: () => void } diff --git a/src/view/com/util/Toast.tsx b/src/view/com/util/Toast.tsx index 37ec6acb53..820c9f9d71 100644 --- a/src/view/com/util/Toast.tsx +++ b/src/view/com/util/Toast.tsx @@ -1,4 +1,4 @@ -import {toast} from '#/components/Toast' +import * as toast from '#/components/Toast' import {type ToastType} from '#/components/Toast/types' /** @@ -46,9 +46,5 @@ export function show( type: ToastType | LegacyToastType = 'default', ): void { const convertedType = convertLegacyToastType(type) - toast.show({ - type: convertedType, - content: message, - a11yLabel: message, - }) + toast.show(message, {type: convertedType}) } diff --git a/src/view/screens/Storybook/Toasts.tsx b/src/view/screens/Storybook/Toasts.tsx index 8fc6f095f7..98d5b05e32 100644 --- a/src/view/screens/Storybook/Toasts.tsx +++ b/src/view/screens/Storybook/Toasts.tsx @@ -2,8 +2,7 @@ import {Pressable, View} from 'react-native' import {show as deprecatedShow} from '#/view/com/util/Toast' import {atoms as a} from '#/alf' -import {Button, ButtonText} from '#/components/Button' -import {toast} from '#/components/Toast' +import * as toast from '#/components/Toast' import {Toast} from '#/components/Toast/Toast' import {H1} from '#/components/Typography' @@ -15,101 +14,77 @@ export function Toasts() { - toast.show({ - type: 'default', - content: 'Default toast', - a11yLabel: 'Default toast', - }) - }> - + onPress={() => toast.show(`Hey I'm a toast!`)}> + - toast.show({ - type: 'default', - content: 'Default toast, 6 seconds', - a11yLabel: 'Default toast, 6 seconds', + toast.show(`This toast will disappear after 6 seconds`, { duration: 6e3, }) }> - + - toast.show({ - type: 'default', - content: - 'This is a longer message to test how the toast handles multiple lines of text content.', - a11yLabel: - 'This is a longer message to test how the toast handles multiple lines of text content.', - }) + toast.show( + `This is a longer message to test how the toast handles multiple lines of text content.`, + ) }> - + - toast.show({ + toast.show(`Success! Yayyyyyyy :)`, { type: 'success', - content: 'Success toast', - a11yLabel: 'Success toast', }) }> - + - toast.show({ + toast.show(`I'm providing info!`, { type: 'info', - content: 'Info toast', - a11yLabel: 'Info toast', }) }> - + - toast.show({ + toast.show(`This is a warning toast`, { type: 'warning', - content: 'Warning toast', - a11yLabel: 'Warning toast', }) }> - + - toast.show({ + toast.show(`This is an error toast :(`, { type: 'error', - content: 'Error toast', - a11yLabel: 'Error toast', }) }> - + - + }> + + )