Refactor API

This commit is contained in:
Eric Bailey
2025-08-13 15:28:31 -05:00
committed by Samuel Newman
parent 185840d389
commit 4199aab5cc
7 changed files with 124 additions and 112 deletions
+8 -7
View File
@@ -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<ContextType>({
})
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 (
<Text
selectable={false}
style={[
a.text_md,
a.font_bold,
a.leading_snug,
a.pointer_events_none,
{
color: textColor,
},
+1 -1
View File
@@ -1 +1 @@
export const DEFAULT_TOAST_DURATION = 3e3
export const DURATION = 3e3
+38 -17
View File
@@ -2,27 +2,48 @@ import {View} from 'react-native'
import {toast as sonner, Toaster} from 'sonner-native'
import {atoms as a} from '#/alf'
import {DEFAULT_TOAST_DURATION} from '#/components/Toast/const'
import {Toast} from '#/components/Toast/Toast'
import {type ToastApi} from '#/components/Toast/types'
import {DURATION} from '#/components/Toast/const'
import {
Toast as BaseToast,
type ToastComponentProps,
} from '#/components/Toast/Toast'
import {type BaseToastOptions} from '#/components/Toast/types'
function ToastOuter({children}: {children: React.ReactNode}) {
return <View style={[a.px_xl, a.w_full]}>{children}</View>
}
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 <Toaster pauseWhenPageIsHidden gap={a.gap_sm.gap} />
}
export const toast: ToastApi = {
show(props) {
sonner.custom(
<ToastOuter>
<Toast content={props.content} type={props.type} />
</ToastOuter>,
{
duration: props.duration ?? DEFAULT_TOAST_DURATION,
},
)
},
/**
* The toast UI component
*/
export function Toast({type, content}: ToastComponentProps) {
return (
<View style={[a.px_xl, a.w_full]}>
<BaseToast content={content} type={type} />
</View>
)
}
/**
* 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(<Toast content={content} type={type} />, {
...options,
duration: options?.duration ?? DURATION,
})
}
+23 -9
View File
@@ -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 (
<Toaster
@@ -16,11 +20,21 @@ export function ToastOutlet() {
)
}
export const toast: ToastApi = {
show(props) {
sonner(<Toast content={props.content} type={props.type} />, {
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(<Toast content={content} type={type} />, {
unstyled: true, // required on web
...options,
duration: options?.duration ?? DURATION,
})
}
+26 -21
View File
@@ -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<typeof sonner.custom>[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
}
+2 -6
View File
@@ -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})
}
+26 -51
View File
@@ -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() {
<View style={[a.gap_md]}>
<Pressable
accessibilityRole="button"
onPress={() =>
toast.show({
type: 'default',
content: 'Default toast',
a11yLabel: 'Default toast',
})
}>
<Toast content="Default toast" type="default" />
onPress={() => toast.show(`Hey I'm a toast!`)}>
<Toast content="Hey I'm a toast!" />
</Pressable>
<Pressable
accessibilityRole="button"
onPress={() =>
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 content="Default toast, 6 seconds" type="default" />
<Toast content="This toast will disappear after 6 seconds" />
</Pressable>
<Pressable
accessibilityRole="button"
onPress={() =>
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
content="This is a longer message to test how the toast handles multiple lines of text content."
type="default"
/>
<Toast content="This is a longer message to test how the toast handles multiple lines of text content." />
</Pressable>
<Pressable
accessibilityRole="button"
onPress={() =>
toast.show({
toast.show(`Success! Yayyyyyyy :)`, {
type: 'success',
content: 'Success toast',
a11yLabel: 'Success toast',
})
}>
<Toast content="Success toast" type="success" />
<Toast content="Success! Yayyyyyyy :)" type="success" />
</Pressable>
<Pressable
accessibilityRole="button"
onPress={() =>
toast.show({
toast.show(`I'm providing info!`, {
type: 'info',
content: 'Info toast',
a11yLabel: 'Info toast',
})
}>
<Toast content="Info" type="info" />
<Toast content="I'm providing info!" type="info" />
</Pressable>
<Pressable
accessibilityRole="button"
onPress={() =>
toast.show({
toast.show(`This is a warning toast`, {
type: 'warning',
content: 'Warning toast',
a11yLabel: 'Warning toast',
})
}>
<Toast content="Warning" type="warning" />
<Toast content="This is a warning toast" type="warning" />
</Pressable>
<Pressable
accessibilityRole="button"
onPress={() =>
toast.show({
toast.show(`This is an error toast :(`, {
type: 'error',
content: 'Error toast',
a11yLabel: 'Error toast',
})
}>
<Toast content="Error" type="error" />
<Toast content="This is an error toast :(" type="error" />
</Pressable>
<Button
label="Deprecated toast example"
<Pressable
accessibilityRole="button"
onPress={() =>
deprecatedShow(
'This is a deprecated toast example',
`This is a test of the deprecated API`,
'exclamation-circle',
)
}
size="large"
variant="solid"
color="secondary">
<ButtonText>Deprecated toast example</ButtonText>
</Button>
}>
<Toast
content="This is a test of the deprecated API"
type="warning"
/>
</Pressable>
</View>
</View>
)