Skip to content

Commit 801f8b6

Browse files
authored
fix: tighten toast typing and restore focus visibility (#33591)
1 parent 4c5f7ef commit 801f8b6

File tree

3 files changed

+54
-43
lines changed

3 files changed

+54
-43
lines changed

web/app/components/base/ui/toast/__tests__/index.spec.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ describe('base/ui/toast', () => {
3737
expect(viewport).toHaveAttribute('aria-live', 'polite')
3838
expect(viewport).toHaveClass('z-[1101]')
3939
expect(viewport.firstElementChild).toHaveClass('top-4')
40+
expect(screen.getByRole('dialog')).not.toHaveClass('outline-none')
4041
expect(document.body.querySelector('[aria-hidden="true"].i-ri-checkbox-circle-fill')).toBeInTheDocument()
4142
expect(document.body.querySelector('button[aria-label="common.toast.close"][aria-hidden="true"]')).toBeInTheDocument()
4243
})

web/app/components/base/ui/toast/index.tsx

Lines changed: 52 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import type {
44
ToastManagerAddOptions,
5-
ToastManagerPromiseOptions,
65
ToastManagerUpdateOptions,
76
ToastObject,
87
} from '@base-ui/react/toast'
@@ -11,20 +10,46 @@ import { useTranslation } from 'react-i18next'
1110
import { cn } from '@/utils/classnames'
1211

1312
type ToastData = Record<string, never>
14-
type ToastType = 'success' | 'error' | 'warning' | 'info'
13+
type ToastToneStyle = {
14+
gradientClassName: string
15+
iconClassName: string
16+
}
17+
18+
const TOAST_TONE_STYLES = {
19+
success: {
20+
iconClassName: 'i-ri-checkbox-circle-fill text-text-success',
21+
gradientClassName: 'from-components-badge-status-light-success-halo to-background-gradient-mask-transparent',
22+
},
23+
error: {
24+
iconClassName: 'i-ri-error-warning-fill text-text-destructive',
25+
gradientClassName: 'from-components-badge-status-light-error-halo to-background-gradient-mask-transparent',
26+
},
27+
warning: {
28+
iconClassName: 'i-ri-alert-fill text-text-warning-secondary',
29+
gradientClassName: 'from-components-badge-status-light-warning-halo to-background-gradient-mask-transparent',
30+
},
31+
info: {
32+
iconClassName: 'i-ri-information-2-fill text-text-accent',
33+
gradientClassName: 'from-components-badge-status-light-normal-halo to-background-gradient-mask-transparent',
34+
},
35+
} satisfies Record<string, ToastToneStyle>
36+
37+
export type ToastType = keyof typeof TOAST_TONE_STYLES
1538

16-
type ToastAddOptions = Omit<ToastManagerAddOptions<ToastData>, 'data' | 'positionerProps' | 'type'> & {
39+
export type ToastAddOptions = Omit<ToastManagerAddOptions<ToastData>, 'data' | 'positionerProps' | 'type'> & {
1740
type?: ToastType
1841
}
1942

20-
type ToastUpdateOptions = Omit<ToastManagerUpdateOptions<ToastData>, 'data' | 'positionerProps' | 'type'> & {
43+
export type ToastUpdateOptions = Omit<ToastManagerUpdateOptions<ToastData>, 'data' | 'positionerProps' | 'type'> & {
2144
type?: ToastType
2245
}
2346

24-
type ToastPromiseOptions<Value> = {
47+
type ToastPromiseResultOption<Value> = string | ToastUpdateOptions | ((value: Value) => string | ToastUpdateOptions)
48+
49+
export type ToastPromiseOptions<Value> = {
2550
loading: string | ToastUpdateOptions
26-
success: string | ToastUpdateOptions | ((result: Value) => string | ToastUpdateOptions)
27-
error: string | ToastUpdateOptions | ((error: unknown) => string | ToastUpdateOptions)
51+
success: ToastPromiseResultOption<Value>
52+
error: ToastPromiseResultOption<unknown>
2853
}
2954

3055
export type ToastHostProps = {
@@ -34,6 +59,14 @@ export type ToastHostProps = {
3459

3560
const toastManager = BaseToast.createToastManager<ToastData>()
3661

62+
function isToastType(type: string): type is ToastType {
63+
return Object.prototype.hasOwnProperty.call(TOAST_TONE_STYLES, type)
64+
}
65+
66+
function getToastType(type?: string): ToastType | undefined {
67+
return type && isToastType(type) ? type : undefined
68+
}
69+
3770
export const toast = {
3871
add(options: ToastAddOptions) {
3972
return toastManager.add(options)
@@ -45,43 +78,19 @@ export const toast = {
4578
toastManager.update(toastId, options)
4679
},
4780
promise<Value>(promiseValue: Promise<Value>, options: ToastPromiseOptions<Value>) {
48-
return toastManager.promise(promiseValue, options as ToastManagerPromiseOptions<Value, ToastData>)
81+
return toastManager.promise(promiseValue, options)
4982
},
5083
}
5184

52-
function ToastIcon({ type }: { type?: string }) {
53-
if (type === 'success') {
54-
return <span aria-hidden="true" className="i-ri-checkbox-circle-fill h-5 w-5 text-text-success" />
55-
}
56-
57-
if (type === 'error') {
58-
return <span aria-hidden="true" className="i-ri-error-warning-fill h-5 w-5 text-text-destructive" />
59-
}
60-
61-
if (type === 'warning') {
62-
return <span aria-hidden="true" className="i-ri-alert-fill h-5 w-5 text-text-warning-secondary" />
63-
}
64-
65-
if (type === 'info') {
66-
return <span aria-hidden="true" className="i-ri-information-2-fill h-5 w-5 text-text-accent" />
67-
}
68-
69-
return null
85+
function ToastIcon({ type }: { type?: ToastType }) {
86+
return type
87+
? <span aria-hidden="true" className={cn('h-5 w-5', TOAST_TONE_STYLES[type].iconClassName)} />
88+
: null
7089
}
7190

72-
function getToneGradientClasses(type?: string) {
73-
if (type === 'success')
74-
return 'from-components-badge-status-light-success-halo to-background-gradient-mask-transparent'
75-
76-
if (type === 'error')
77-
return 'from-components-badge-status-light-error-halo to-background-gradient-mask-transparent'
78-
79-
if (type === 'warning')
80-
return 'from-components-badge-status-light-warning-halo to-background-gradient-mask-transparent'
81-
82-
if (type === 'info')
83-
return 'from-components-badge-status-light-normal-halo to-background-gradient-mask-transparent'
84-
91+
function getToneGradientClasses(type?: ToastType) {
92+
if (type)
93+
return TOAST_TONE_STYLES[type].gradientClassName
8594
return 'from-background-default-subtle to-background-gradient-mask-transparent'
8695
}
8796

@@ -93,12 +102,13 @@ function ToastCard({
93102
showHoverBridge?: boolean
94103
}) {
95104
const { t } = useTranslation('common')
105+
const toastType = getToastType(toastItem.type)
96106

97107
return (
98108
<BaseToast.Root
99109
toast={toastItem}
100110
className={cn(
101-
'pointer-events-auto absolute right-0 top-0 w-[360px] max-w-[calc(100vw-2rem)] origin-top-right cursor-default select-none outline-none',
111+
'pointer-events-auto absolute right-0 top-0 w-[360px] max-w-[calc(100vw-2rem)] origin-top cursor-default select-none',
102112
'[--toast-current-height:var(--toast-frontmost-height,var(--toast-height))] [--toast-gap:8px] [--toast-peek:5px] [--toast-scale:calc(1-(var(--toast-index)*0.0225))] [--toast-shrink:calc(1-var(--toast-scale))]',
103113
'[height:var(--toast-current-height)] [z-index:calc(100-var(--toast-index))]',
104114
'[transition:transform_500ms_cubic-bezier(0.22,1,0.36,1),opacity_500ms,height_150ms] motion-reduce:transition-none',
@@ -110,11 +120,11 @@ function ToastCard({
110120
<div className="relative overflow-hidden rounded-xl border border-components-panel-border bg-components-panel-bg-blur shadow-lg shadow-shadow-shadow-5 backdrop-blur-[5px]">
111121
<div
112122
aria-hidden="true"
113-
className={cn('absolute inset-[-1px] bg-gradient-to-r opacity-40', getToneGradientClasses(toastItem.type))}
123+
className={cn('absolute inset-[-1px] bg-gradient-to-r opacity-40', getToneGradientClasses(toastType))}
114124
/>
115125
<BaseToast.Content className="relative flex items-start gap-1 overflow-hidden p-3 transition-opacity duration-200 data-[behind]:opacity-0 data-[expanded]:opacity-100">
116126
<div className="flex shrink-0 items-center justify-center p-0.5">
117-
<ToastIcon type={toastItem.type} />
127+
<ToastIcon type={toastType} />
118128
</div>
119129
<div className="min-w-0 flex-1 p-1">
120130
<div className="flex w-full items-center gap-1">

web/app/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ const LocaleLayout = async ({
7171
<SentryInitializer>
7272
<TanstackQueryInitializer>
7373
<I18nServerProvider>
74-
<ToastHost timeout={5000} />
74+
<ToastHost timeout={5000} limit={3} />
7575
<ToastProvider>
7676
<GlobalPublicStoreProvider>
7777
<TooltipProvider delay={300} closeDelay={200}>

0 commit comments

Comments
 (0)