+32
-59
src/components/Toast/Toast.tsx
+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
+8
src/components/Toast/index.e2e.tsx
+14
-26
src/components/Toast/index.tsx
+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
+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
+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
+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
/>