Skip to content

Commit 2588708

Browse files
committed
Bugfix/toast transition error when triggered/closing at the same tick
1 parent 342b70a commit 2588708

File tree

4 files changed

+80
-7
lines changed

4 files changed

+80
-7
lines changed

.changeset/forty-trains-type.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@skeletonlabs/skeleton": patch
3+
---
4+
5+
bugfix: Toast wrapper being removed from the DOM wrongfully after a second toast is triggered when the first one is finishing its outro transition

packages/skeleton/src/lib/utilities/Toast/Toast.svelte

+11-1
Original file line numberDiff line numberDiff line change
@@ -122,15 +122,21 @@
122122
}
123123
}
124124
125+
let wrapperVisible = false;
126+
125127
// Reactive
126128
$: classesWrapper = `${cWrapper} ${cPosition} ${zIndex} ${$$props.class || ''}`;
127129
$: classesSnackbar = `${cSnackbar} ${cAlign} ${padding}`;
128130
$: classesToast = `${cToast} ${width} ${color} ${padding} ${spacing} ${rounded} ${shadow}`;
129131
// Filtered Toast Store
130132
$: filteredToasts = Array.from($toastStore).slice(0, max);
133+
134+
$: if (filteredToasts.length) {
135+
wrapperVisible = true;
136+
}
131137
</script>
132138

133-
{#if $toastStore.length}
139+
{#if filteredToasts.length > 0 || wrapperVisible}
134140
<!-- Wrapper -->
135141
<div class="snackbar-wrapper {classesWrapper}" data-testid="snackbar-wrapper">
136142
<!-- List -->
@@ -148,6 +154,10 @@
148154
params: { x: animAxis.x, y: animAxis.y, ...transitionOutParams },
149155
enabled: transitions
150156
}}
157+
on:outroend={() => {
158+
const outroFinishedForLastToastOnQueue = filteredToasts.length === 0;
159+
if (outroFinishedForLastToastOnQueue) wrapperVisible = false;
160+
}}
151161
on:mouseenter={() => onMouseEnter(i)}
152162
on:mouseleave={() => onMouseLeave(i)}
153163
role={t.hideDismiss ? 'alert' : 'alertdialog'}

packages/skeleton/src/lib/utilities/Toast/Toast.test.ts

+53-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { render } from '@testing-library/svelte';
2-
import { describe, it, expect } from 'vitest';
3-
1+
import { render, screen } from '@testing-library/svelte';
2+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
43
import type { ToastSettings } from './types.js';
54
import ToastTest from './ToastTest.svelte';
65

@@ -16,6 +15,57 @@ const toastMessage: ToastSettings = {
1615
};
1716

1817
describe('Toast.svelte', () => {
18+
const snackbarWrapperTestId = 'snackbar-wrapper';
19+
20+
// see: https://testing-library.com/docs/svelte-testing-library/faq/#why-arent-transition-events-running
21+
beforeEach(() => {
22+
const rafMock = (fn: (_: Date) => void) => setTimeout(() => fn(new Date()), 16);
23+
vi.stubGlobal('requestAnimationFrame', rafMock);
24+
});
25+
26+
afterEach(() => {
27+
vi.unstubAllGlobals();
28+
});
29+
30+
it('does not show the toast wrapper if the toast queue is empty', () => {
31+
expect(() => screen.getByTestId(snackbarWrapperTestId)).toThrow();
32+
});
33+
34+
it('keeps the toast wrapper visible if a second toast is scheduled on the same tick as the closing of the first one, until the outro animation of the first toast is finished', async () => {
35+
const { getByTestId } = render(ToastTest, {
36+
props: {
37+
max: 2,
38+
toastSettings: [
39+
// note how toast B is scheduled to trigger at the same tick as toast A
40+
{ message: 'A', timeout: 10 },
41+
{ message: 'B', triggerDelay: 10, timeout: 10 }
42+
]
43+
}
44+
});
45+
46+
const getWrapperElementAfterTimeout = (timeout: number) =>
47+
new Promise((resolve) =>
48+
setTimeout(() => {
49+
try {
50+
const el = getByTestId(snackbarWrapperTestId);
51+
resolve(el);
52+
} catch {
53+
resolve(false);
54+
}
55+
}, timeout)
56+
);
57+
58+
const [wrapperVisibilityOnAToBChange, wrapperVisibilityAfterAOutroFinishes, wrapperVisibilityAfterBOutroFinishes] = await Promise.all([
59+
getWrapperElementAfterTimeout(10),
60+
getWrapperElementAfterTimeout(16),
61+
getWrapperElementAfterTimeout(50)
62+
]);
63+
64+
expect(wrapperVisibilityOnAToBChange).toBeTruthy();
65+
expect(wrapperVisibilityAfterAOutroFinishes).toBeTruthy();
66+
expect(wrapperVisibilityAfterBOutroFinishes).toBeFalsy();
67+
});
68+
1969
it('Renders modal alert', async () => {
2070
const { getByTestId } = render(ToastTest, { props: { toastSettings: [toastMessage] } });
2171
expect(getByTestId('toast')).toBeTruthy();

packages/skeleton/src/lib/utilities/Toast/ToastTest.svelte

+11-3
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,22 @@
33
import type { ToastSettings } from './types.js';
44
import Toast from './Toast.svelte';
55
6-
export let toastSettings: Array<ToastSettings> = [];
6+
interface TestToastSettings extends ToastSettings {
7+
triggerDelay?: number;
8+
}
9+
10+
export let toastSettings: Array<TestToastSettings> = [];
711
export let max: number | undefined = undefined;
812
913
initializeToastStore();
1014
const toastStore = getToastStore();
1115
12-
toastSettings.forEach((element) => {
13-
toastStore.trigger(element);
16+
toastSettings.forEach(({ triggerDelay, ...settings }) => {
17+
if (triggerDelay) {
18+
setTimeout(() => toastStore.trigger(settings), triggerDelay);
19+
} else {
20+
toastStore.trigger(settings);
21+
}
1422
});
1523
</script>
1624

0 commit comments

Comments
 (0)