Bluesky app fork with some witchin' additions 💫

Fix toast type (#8909)

* Fix confusing toast API

* Provide all exports to e2e file

* Fix first usage in Composer

* Loosen type, add Trans tag

authored by Eric Bailey and committed by GitHub 9f7c9203 0555d362

Changed files
+123 -135
src
components
view
com
composer
screens
Storybook
+32 -59
src/components/Toast/Toast.tsx
··· 16 16 import {type ToastType} from '#/components/Toast/types' 17 17 import {Text as BaseText} from '#/components/Typography' 18 18 19 - type ToastConfigContextType = { 20 - id: string 21 - } 22 - 23 - type ToastThemeContextType = { 24 - type: ToastType 25 - } 26 - 27 - export type ToastComponentProps = { 28 - type?: ToastType 29 - content: string 30 - } 31 - 32 19 export const ICONS = { 33 20 default: CircleCheck, 34 21 success: CircleCheck, ··· 37 24 info: CircleInfo, 38 25 } 39 26 40 - const ToastConfigContext = createContext<ToastConfigContextType>({ 27 + const ToastConfigContext = createContext<{ 28 + id: string 29 + type: ToastType 30 + }>({ 41 31 id: '', 32 + type: 'default', 42 33 }) 43 34 ToastConfigContext.displayName = 'ToastConfigContext' 44 35 45 36 export function ToastConfigProvider({ 46 37 children, 47 38 id, 39 + type, 48 40 }: { 49 41 children: React.ReactNode 50 42 id: string 43 + type: ToastType 51 44 }) { 52 45 return ( 53 - <ToastConfigContext.Provider value={useMemo(() => ({id}), [id])}> 46 + <ToastConfigContext.Provider 47 + value={useMemo(() => ({id, type}), [id, type])}> 54 48 {children} 55 49 </ToastConfigContext.Provider> 56 50 ) 57 51 } 58 52 59 - const ToastThemeContext = createContext<ToastThemeContextType>({ 60 - type: 'default', 61 - }) 62 - ToastThemeContext.displayName = 'ToastThemeContext' 63 - 64 - export function Default({type = 'default', content}: ToastComponentProps) { 65 - return ( 66 - <Outer type={type}> 67 - <Icon /> 68 - <Text>{content}</Text> 69 - </Outer> 70 - ) 71 - } 72 - 73 - export function Outer({ 74 - children, 75 - type = 'default', 76 - }: { 77 - children: React.ReactNode 78 - type?: ToastType 79 - }) { 53 + export function Outer({children}: {children: React.ReactNode}) { 80 54 const t = useTheme() 55 + const {type} = useContext(ToastConfigContext) 81 56 const styles = useToastStyles({type}) 82 57 83 58 return ( 84 - <ToastThemeContext.Provider value={useMemo(() => ({type}), [type])}> 85 - <View 86 - style={[ 87 - a.flex_1, 88 - a.p_lg, 89 - a.rounded_md, 90 - a.border, 91 - a.flex_row, 92 - a.gap_sm, 93 - t.atoms.shadow_sm, 94 - { 95 - paddingVertical: 14, // 16 seems too big 96 - backgroundColor: styles.backgroundColor, 97 - borderColor: styles.borderColor, 98 - }, 99 - ]}> 100 - {children} 101 - </View> 102 - </ToastThemeContext.Provider> 59 + <View 60 + style={[ 61 + a.flex_1, 62 + a.p_lg, 63 + a.rounded_md, 64 + a.border, 65 + a.flex_row, 66 + a.gap_sm, 67 + t.atoms.shadow_sm, 68 + { 69 + paddingVertical: 14, // 16 seems too big 70 + backgroundColor: styles.backgroundColor, 71 + borderColor: styles.borderColor, 72 + }, 73 + ]}> 74 + {children} 75 + </View> 103 76 ) 104 77 } 105 78 106 79 export function Icon({icon}: {icon?: React.ComponentType<SVGIconProps>}) { 107 - const {type} = useContext(ToastThemeContext) 80 + const {type} = useContext(ToastConfigContext) 108 81 const styles = useToastStyles({type}) 109 82 const IconComponent = icon || ICONS[type] 110 83 return <IconComponent size="md" fill={styles.iconColor} /> 111 84 } 112 85 113 86 export function Text({children}: {children: React.ReactNode}) { 114 - const {type} = useContext(ToastThemeContext) 87 + const {type} = useContext(ToastConfigContext) 115 88 const {textColor} = useToastStyles({type}) 116 89 const {fontScaleCompensation} = useToastFontScaleCompensation() 117 90 return ( ··· 142 115 143 116 export function Action( 144 117 props: Omit<ButtonProps, UninheritableButtonProps | 'children'> & { 145 - children: string 118 + children: React.ReactNode 146 119 }, 147 120 ) { 148 121 const t = useTheme() 149 122 const {fontScaleCompensation} = useToastFontScaleCompensation() 150 - const {type} = useContext(ToastThemeContext) 123 + const {type} = useContext(ToastConfigContext) 151 124 const {id} = useContext(ToastConfigContext) 152 125 const styles = useMemo(() => { 153 126 const base = {
+8
src/components/Toast/index.e2e.tsx
··· 1 + export const DURATION = 0 2 + 3 + export const Action = () => null 4 + export const Icon = () => null 5 + export const Outer = () => null 6 + export const Text = () => null 7 + export const ToastConfigProvider = () => null 8 + 1 9 export function ToastOutlet() { 2 10 return null 3 11 }
+14 -26
src/components/Toast/index.tsx
··· 6 6 import {atoms as a} from '#/alf' 7 7 import {DURATION} from '#/components/Toast/const' 8 8 import { 9 - Default as DefaultToast, 9 + Icon as ToastIcon, 10 10 Outer as BaseOuter, 11 - type ToastComponentProps, 11 + Text as ToastText, 12 12 ToastConfigProvider, 13 13 } from '#/components/Toast/Toast' 14 14 import {type BaseToastOptions} from '#/components/Toast/types' 15 15 16 16 export {DURATION} from '#/components/Toast/const' 17 - export {Action, Icon, Text} from '#/components/Toast/Toast' 17 + export {Action, Icon, Text, ToastConfigProvider} from '#/components/Toast/Toast' 18 18 export {type ToastType} from '#/components/Toast/types' 19 19 20 20 /** ··· 25 25 return <Toaster pauseWhenPageIsHidden gap={a.gap_sm.gap} /> 26 26 } 27 27 28 - /** 29 - * The toast UI component 30 - */ 31 - export function Default({type, content}: ToastComponentProps) { 32 - return ( 33 - <View style={[a.px_xl, a.w_full]}> 34 - <DefaultToast content={content} type={type} /> 35 - </View> 36 - ) 37 - } 38 - 39 - export function Outer({ 40 - children, 41 - type = 'default', 42 - }: { 43 - children: React.ReactNode 44 - type?: ToastComponentProps['type'] 45 - }) { 28 + export function Outer({children}: {children: React.ReactNode}) { 46 29 return ( 47 30 <View style={[a.px_xl, a.w_full]}> 48 - <BaseOuter type={type}>{children}</BaseOuter> 31 + <BaseOuter>{children}</BaseOuter> 49 32 </View> 50 33 ) 51 34 } ··· 60 43 */ 61 44 export function show( 62 45 content: React.ReactNode, 63 - {type, ...options}: BaseToastOptions = {}, 46 + {type = 'default', ...options}: BaseToastOptions = {}, 64 47 ) { 65 48 const id = nanoid() 66 49 67 50 if (typeof content === 'string') { 68 51 sonner.custom( 69 - <ToastConfigProvider id={id}> 70 - <Default content={content} type={type} /> 52 + <ToastConfigProvider id={id} type={type}> 53 + <Outer> 54 + <ToastIcon /> 55 + <ToastText>{content}</ToastText> 56 + </Outer> 71 57 </ToastConfigProvider>, 72 58 { 73 59 ...options, ··· 77 63 ) 78 64 } else if (React.isValidElement(content)) { 79 65 sonner.custom( 80 - <ToastConfigProvider id={id}>{content}</ToastConfigProvider>, 66 + <ToastConfigProvider id={id} type={type}> 67 + {content} 68 + </ToastConfigProvider>, 81 69 { 82 70 ...options, 83 71 id,
+20 -10
src/components/Toast/index.web.tsx
··· 5 5 import {atoms as a} from '#/alf' 6 6 import {DURATION} from '#/components/Toast/const' 7 7 import { 8 - Default as DefaultToast, 8 + Icon as ToastIcon, 9 + Outer as ToastOuter, 10 + Text as ToastText, 9 11 ToastConfigProvider, 10 12 } from '#/components/Toast/Toast' 11 13 import {type BaseToastOptions} from '#/components/Toast/types' ··· 39 41 */ 40 42 export function show( 41 43 content: React.ReactNode, 42 - {type, ...options}: BaseToastOptions = {}, 44 + {type = 'default', ...options}: BaseToastOptions = {}, 43 45 ) { 44 46 const id = nanoid() 45 47 46 48 if (typeof content === 'string') { 47 49 sonner( 48 - <ToastConfigProvider id={id}> 49 - <DefaultToast content={content} type={type} /> 50 + <ToastConfigProvider id={id} type={type}> 51 + <ToastOuter> 52 + <ToastIcon /> 53 + <ToastText>{content}</ToastText> 54 + </ToastOuter> 50 55 </ToastConfigProvider>, 51 56 { 52 57 ...options, ··· 56 61 }, 57 62 ) 58 63 } else if (React.isValidElement(content)) { 59 - sonner(<ToastConfigProvider id={id}>{content}</ToastConfigProvider>, { 60 - ...options, 61 - unstyled: true, // required on web 62 - id, 63 - duration: options?.duration ?? DURATION, 64 - }) 64 + sonner( 65 + <ToastConfigProvider id={id} type={type}> 66 + {content} 67 + </ToastConfigProvider>, 68 + { 69 + ...options, 70 + unstyled: true, // required on web 71 + id, 72 + duration: options?.duration ?? DURATION, 73 + }, 74 + ) 65 75 } else { 66 76 throw new Error( 67 77 `Toast can be a string or a React element, got ${typeof content}`,
+5 -4
src/view/com/composer/Composer.tsx
··· 126 126 import {UserAvatar} from '#/view/com/util/UserAvatar' 127 127 import {atoms as a, native, useTheme, web} from '#/alf' 128 128 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 129 - import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheckIcon} from '#/components/icons/CircleCheck' 130 129 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 131 130 import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji' 132 131 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' ··· 527 526 } 528 527 onClose() 529 528 Toast.show( 530 - <Toast.Outer type="success"> 531 - <Toast.Icon icon={CircleCheckIcon} /> 529 + <Toast.Outer> 530 + <Toast.Icon /> 532 531 <Toast.Text> 533 532 {thread.posts.length > 1 534 533 ? _(msg`Your posts were sent`) ··· 543 542 const {host: name, rkey} = new AtUri(postUri) 544 543 navigation.navigate('PostThread', {name, rkey}) 545 544 }}> 546 - View 545 + <Trans context="Action to view the post the user just created"> 546 + View 547 + </Trans> 547 548 </Toast.Action> 548 549 )} 549 550 </Toast.Outer>,
+44 -36
src/view/screens/Storybook/Toasts.tsx
··· 4 4 import {atoms as a} from '#/alf' 5 5 import {Globe_Stroke2_Corner0_Rounded as GlobeIcon} from '#/components/icons/Globe' 6 6 import * as Toast from '#/components/Toast' 7 - import {Default} from '#/components/Toast/Toast' 8 7 import {H1} from '#/components/Typography' 9 8 10 - function ToastWithAction({type = 'default'}: {type?: Toast.ToastType}) { 9 + function DefaultToast({ 10 + content, 11 + type = 'default', 12 + }: { 13 + content: string 14 + type?: Toast.ToastType 15 + }) { 11 16 return ( 12 - <Toast.Outer type={type}> 17 + <Toast.ToastConfigProvider id="default-toast" type={type}> 18 + <Toast.Outer> 19 + <Toast.Icon icon={GlobeIcon} /> 20 + <Toast.Text>{content}</Toast.Text> 21 + </Toast.Outer> 22 + </Toast.ToastConfigProvider> 23 + ) 24 + } 25 + 26 + function ToastWithAction() { 27 + return ( 28 + <Toast.Outer> 13 29 <Toast.Icon icon={GlobeIcon} /> 14 30 <Toast.Text>This toast has an action button</Toast.Text> 15 31 <Toast.Action ··· 21 37 ) 22 38 } 23 39 24 - function LongToastWithAction({type = 'default'}: {type?: Toast.ToastType}) { 40 + function LongToastWithAction() { 25 41 return ( 26 - <Toast.Outer type={type}> 42 + <Toast.Outer> 27 43 <Toast.Icon icon={GlobeIcon} /> 28 44 <Toast.Text> 29 45 This is a longer message to test how the toast handles multiple lines of ··· 44 60 <H1>Toast Examples</H1> 45 61 46 62 <View style={[a.gap_md]}> 47 - <View style={[a.gap_md, {marginHorizontal: a.px_xl.paddingLeft * -1}]}> 48 - <Pressable 49 - accessibilityRole="button" 50 - onPress={() => Toast.show(<ToastWithAction />)}> 51 - <ToastWithAction /> 52 - </Pressable> 53 - <Pressable 54 - accessibilityRole="button" 55 - onPress={() => Toast.show(<LongToastWithAction />)}> 56 - <LongToastWithAction /> 57 - </Pressable> 58 - <Pressable 59 - accessibilityRole="button" 60 - onPress={() => Toast.show(<ToastWithAction type="success" />)}> 61 - <ToastWithAction type="success" /> 62 - </Pressable> 63 - <Pressable 64 - accessibilityRole="button" 65 - onPress={() => Toast.show(<ToastWithAction type="error" />)}> 66 - <ToastWithAction type="error" /> 67 - </Pressable> 68 - </View> 69 - 63 + <Pressable 64 + accessibilityRole="button" 65 + onPress={() => Toast.show(<ToastWithAction />, {type: 'success'})}> 66 + <ToastWithAction /> 67 + </Pressable> 68 + <Pressable 69 + accessibilityRole="button" 70 + onPress={() => Toast.show(<ToastWithAction />, {type: 'error'})}> 71 + <ToastWithAction /> 72 + </Pressable> 73 + <Pressable 74 + accessibilityRole="button" 75 + onPress={() => Toast.show(<LongToastWithAction />)}> 76 + <LongToastWithAction /> 77 + </Pressable> 70 78 <Pressable 71 79 accessibilityRole="button" 72 80 onPress={() => Toast.show(`Hey I'm a toast!`)}> 73 - <Default content="Hey I'm a toast!" /> 81 + <DefaultToast content="Hey I'm a toast!" /> 74 82 </Pressable> 75 83 <Pressable 76 84 accessibilityRole="button" ··· 79 87 duration: 6e3, 80 88 }) 81 89 }> 82 - <Default content="This toast will disappear after 6 seconds" /> 90 + <DefaultToast content="This toast will disappear after 6 seconds" /> 83 91 </Pressable> 84 92 <Pressable 85 93 accessibilityRole="button" ··· 88 96 `This is a longer message to test how the toast handles multiple lines of text content.`, 89 97 ) 90 98 }> 91 - <Default content="This is a longer message to test how the toast handles multiple lines of text content." /> 99 + <DefaultToast content="This is a longer message to test how the toast handles multiple lines of text content." /> 92 100 </Pressable> 93 101 <Pressable 94 102 accessibilityRole="button" ··· 97 105 type: 'success', 98 106 }) 99 107 }> 100 - <Default content="Success! Yayyyyyyy :)" type="success" /> 108 + <DefaultToast content="Success! Yayyyyyyy :)" type="success" /> 101 109 </Pressable> 102 110 <Pressable 103 111 accessibilityRole="button" ··· 106 114 type: 'info', 107 115 }) 108 116 }> 109 - <Default content="I'm providing info!" type="info" /> 117 + <DefaultToast content="I'm providing info!" type="info" /> 110 118 </Pressable> 111 119 <Pressable 112 120 accessibilityRole="button" ··· 115 123 type: 'warning', 116 124 }) 117 125 }> 118 - <Default content="This is a warning toast" type="warning" /> 126 + <DefaultToast content="This is a warning toast" type="warning" /> 119 127 </Pressable> 120 128 <Pressable 121 129 accessibilityRole="button" ··· 124 132 type: 'error', 125 133 }) 126 134 }> 127 - <Default content="This is an error toast :(" type="error" /> 135 + <DefaultToast content="This is an error toast :(" type="error" /> 128 136 </Pressable> 129 137 130 138 <Pressable ··· 135 143 'exclamation-circle', 136 144 ) 137 145 }> 138 - <Default 146 + <DefaultToast 139 147 content="This is a test of the deprecated API" 140 148 type="warning" 141 149 />