From 21da06d9a6edf3f12c6b39a2e059c4da70563db1 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 22 Aug 2025 11:41:49 -0500 Subject: [PATCH] Add Action component --- .eslintrc.js | 1 + src/components/Toast/Toast.tsx | 148 +++++++++++++++++++++++--- src/components/Toast/index.tsx | 18 +++- src/components/Toast/index.web.tsx | 1 + src/view/screens/Storybook/Toasts.tsx | 85 ++++++++++++--- 5 files changed, 222 insertions(+), 31 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 59c1b4eb4f..b9c89c3d5e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -34,6 +34,7 @@ module.exports = { 'P', 'Admonition', 'Admonition.Admonition', + 'Toast.Action', 'AgeAssuranceAdmonition', 'Span', ], diff --git a/src/components/Toast/Toast.tsx b/src/components/Toast/Toast.tsx index fba06e3301..218d292c00 100644 --- a/src/components/Toast/Toast.tsx +++ b/src/components/Toast/Toast.tsx @@ -2,6 +2,11 @@ import {createContext, useContext, useMemo} from 'react' import {View} from 'react-native' import {atoms as a, select, useAlf, useTheme} from '#/alf' +import { + Button, + type ButtonProps, + type UninheritableButtonProps, +} from '#/components/Button' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo' import {type Props as SVGIconProps} from '#/components/icons/common' @@ -56,15 +61,14 @@ export function Outer({ }) { } export function Text({children}: {children: React.ReactNode}) { - const {fonts} = useAlf() const {type} = useContext(Context) const {textColor} = useToastStyles({type}) - /** - * Vibes-based number, adjusts `top` of `View` that wraps the text to - * compensate for different type sizes and keep the first line of text - * aligned with the icon. - esb - */ - const fontScaleCompensation = useMemo( - () => parseInt(fonts.scale) * -1 * 0.65, - [fonts.scale], - ) + const {fontScaleCompensation} = useToastFontScaleCompensation() return ( & { + children: string + }, +) { + const t = useTheme() + const {fontScaleCompensation} = useToastFontScaleCompensation() + const {type} = useContext(Context) + const styles = useMemo(() => { + const base = { + base: { + textColor: t.palette.contrast_600, + backgroundColor: t.atoms.bg_contrast_25.backgroundColor, + }, + interacted: { + textColor: t.atoms.text.color, + backgroundColor: t.atoms.bg_contrast_50.backgroundColor, + }, + } + return { + default: base, + success: { + base: { + textColor: select(t.name, { + light: t.palette.primary_800, + dim: t.palette.primary_900, + dark: t.palette.primary_900, + }), + backgroundColor: t.palette.primary_25, + }, + interacted: { + textColor: select(t.name, { + light: t.palette.primary_900, + dim: t.palette.primary_975, + dark: t.palette.primary_975, + }), + backgroundColor: t.palette.primary_50, + }, + }, + error: { + base: { + textColor: select(t.name, { + light: t.palette.negative_700, + dim: t.palette.negative_900, + dark: t.palette.negative_900, + }), + backgroundColor: t.palette.negative_25, + }, + interacted: { + textColor: select(t.name, { + light: t.palette.negative_900, + dim: t.palette.negative_975, + dark: t.palette.negative_975, + }), + backgroundColor: t.palette.negative_50, + }, + }, + warning: base, + info: base, + }[type] + }, [t, type]) + + return ( + + + + ) +} + +/** + * Vibes-based number, provides t `top` value to wrap the text to compensate + * for different type sizes and keep the first line of text aligned with the + * icon. - esb + */ +function useToastFontScaleCompensation() { + const {fonts} = useAlf() + const fontScaleCompensation = useMemo( + () => parseInt(fonts.scale) * -1 * 0.65, + [fonts.scale], + ) + return useMemo( + () => ({ + fontScaleCompensation, + }), + [fontScaleCompensation], + ) +} + function useToastStyles({type}: {type: ToastType}) { const t = useTheme() return useMemo(() => { diff --git a/src/components/Toast/index.tsx b/src/components/Toast/index.tsx index 7a99eebce7..057dc8205b 100644 --- a/src/components/Toast/index.tsx +++ b/src/components/Toast/index.tsx @@ -6,12 +6,14 @@ import {atoms as a} from '#/alf' import {DURATION} from '#/components/Toast/const' import { Default as DefaultToast, + Outer as BaseOuter, type ToastComponentProps, } from '#/components/Toast/Toast' import {type BaseToastOptions} from '#/components/Toast/types' export {DURATION} from '#/components/Toast/const' -export {Icon, Outer, Text} from '#/components/Toast/Toast' +export {Action, Icon, Text} from '#/components/Toast/Toast' +export {type ToastType} from '#/components/Toast/types' /** * Toasts are rendered in a global outlet, which is placed at the top of the @@ -32,6 +34,20 @@ export function Default({type, content}: ToastComponentProps) { ) } +export function Outer({ + children, + type = 'default', +}: { + children: React.ReactNode + type?: ToastComponentProps['type'] +}) { + return ( + + {children} + + ) +} + /** * Access the full Sonner API */ diff --git a/src/components/Toast/index.web.tsx b/src/components/Toast/index.web.tsx index 06b33b5834..29d16d67e3 100644 --- a/src/components/Toast/index.web.tsx +++ b/src/components/Toast/index.web.tsx @@ -8,6 +8,7 @@ import {type BaseToastOptions} from '#/components/Toast/types' export {DURATION} from '#/components/Toast/const' export * from '#/components/Toast/Toast' +export {type ToastType} from '#/components/Toast/types' /** * Toasts are rendered in a global outlet, which is placed at the top of the diff --git a/src/view/screens/Storybook/Toasts.tsx b/src/view/screens/Storybook/Toasts.tsx index 9d289b9fc0..d06b301191 100644 --- a/src/view/screens/Storybook/Toasts.tsx +++ b/src/view/screens/Storybook/Toasts.tsx @@ -2,74 +2,125 @@ import {Pressable, View} from 'react-native' import {show as deprecatedShow} from '#/view/com/util/Toast' import {atoms as a} from '#/alf' -import * as toast from '#/components/Toast' -import {Default as Toast} from '#/components/Toast/Toast' +import {Globe_Stroke2_Corner0_Rounded as GlobeIcon} from '#/components/icons/Globe' +import * as Toast from '#/components/Toast' +import {Default} from '#/components/Toast/Toast' import {H1} from '#/components/Typography' +function ToastWithAction({type = 'default'}: {type?: Toast.ToastType}) { + return ( + + + This toast has an action button + alert('Action clicked!')}> + Action + + + ) +} + +function LongToastWithAction({type = 'default'}: {type?: Toast.ToastType}) { + return ( + + + + This is a longer message to test how the toast handles multiple lines of + text content. + + alert('Action clicked!')}> + Action + + + ) +} + export function Toasts() { return (

Toast Examples

+ + Toast.show()}> + + + Toast.show()}> + + + Toast.show()}> + + + Toast.show()}> + + + + toast.show(`Hey I'm a toast!`)}> - + onPress={() => Toast.show(`Hey I'm a toast!`)}> + - toast.show(`This toast will disappear after 6 seconds`, { + Toast.show(`This toast will disappear after 6 seconds`, { duration: 6e3, }) }> - + - toast.show( + Toast.show( `This is a longer message to test how the toast handles multiple lines of text content.`, ) }> - + - toast.show(`Success! Yayyyyyyy :)`, { + Toast.show(`Success! Yayyyyyyy :)`, { type: 'success', }) }> - + - toast.show(`I'm providing info!`, { + Toast.show(`I'm providing info!`, { type: 'info', }) }> - + - toast.show(`This is a warning toast`, { + Toast.show(`This is a warning toast`, { type: 'warning', }) }> - + - toast.show(`This is an error toast :(`, { + Toast.show(`This is an error toast :(`, { type: 'error', }) }> - + -