Skip to content

Commit e235fca

Browse files
committed
fix(emcn): keep ongoing status toasts behind actionable toasts
The collapsed toast stack paints newest-first and shows only the front card whole; back cards are clamped to the front card's height and peek 13px above it. A persistent status toast raised after an actionable one ("Reconnecting..." / "Joining workflow..." after "This workflow is locked") therefore covered the action, and on touch the first tap only expanded the stack. Order the stack so persistent toasts without an action sit behind every other toast; everything else stays newest-first.
1 parent 9939762 commit e235fca

2 files changed

Lines changed: 60 additions & 1 deletion

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { orderToastStack } from './toast'
3+
4+
type StackedToast = Parameters<typeof orderToastStack>[0][number]
5+
6+
function stacked(id: string, overrides: Partial<StackedToast> = {}): StackedToast {
7+
return {
8+
id,
9+
message: id,
10+
variant: 'default',
11+
duration: 5000,
12+
persistAcrossRoutes: false,
13+
...overrides,
14+
}
15+
}
16+
17+
const unlock = stacked('unlock', {
18+
duration: 0,
19+
action: { label: 'Unlock Workflow', onClick: () => {} },
20+
})
21+
const reconnecting = stacked('reconnecting', { variant: 'error', duration: 0 })
22+
23+
const frontToBack = (toasts: StackedToast[]) => orderToastStack(toasts).map((toast) => toast.id)
24+
25+
describe('orderToastStack', () => {
26+
it('keeps an actionable toast in front of a status toast raised after it', () => {
27+
expect(frontToBack([unlock, reconnecting])).toEqual(['unlock', 'reconnecting'])
28+
})
29+
30+
it('keeps a status toast behind a timed toast raised before it', () => {
31+
expect(frontToBack([stacked('saved'), reconnecting])).toEqual(['saved', 'reconnecting'])
32+
})
33+
34+
it('orders everything else newest first', () => {
35+
const joining = stacked('joining', { variant: 'error', duration: 0 })
36+
expect(frontToBack([unlock, reconnecting, stacked('saved'), joining])).toEqual([
37+
'saved',
38+
'unlock',
39+
'joining',
40+
'reconnecting',
41+
])
42+
})
43+
})

‎packages/emcn/src/components/toast/toast.tsx‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,22 @@ function toastStateReducer(state: ToastState, action: ToastStateAction): ToastSt
156156
}
157157
}
158158

159+
/**
160+
* Front-to-back paint order for the stack: newest first, except that ongoing
161+
* status toasts — persistent and without an action, like "Reconnecting..." —
162+
* sit behind every other toast. A collapsed stack shows only the front card
163+
* whole, so a status that arrives later would otherwise hide an earlier
164+
* toast's action until the stack is expanded.
165+
*/
166+
export function orderToastStack(toasts: readonly ToastData[]): ToastData[] {
167+
const isOngoingStatus = (toast: ToastData) => toast.duration <= 0 && !toast.action
168+
const newestFirst = [...toasts].reverse()
169+
return [
170+
...newestFirst.filter((toast) => !isOngoingStatus(toast)),
171+
...newestFirst.filter(isOngoingStatus),
172+
]
173+
}
174+
159175
type ToastInput = {
160176
message: string
161177
description?: string
@@ -706,7 +722,7 @@ export function ToastProvider({ children }: { children?: ReactNode }) {
706722
[dismissToast, dismissAllToasts]
707723
)
708724

709-
const ordered = [...toasts].reverse()
725+
const ordered = orderToastStack(toasts)
710726
const frontHeight = heights[ordered[0]?.id ?? ''] ?? ESTIMATED_TOAST_HEIGHT
711727
let cumulative = 0
712728
const layout = ordered.map((toast, index) => {

0 commit comments

Comments
 (0)