Skip to content

Commit

Permalink
Bugfix/toast transition error when triggered/closing at the same tick (
Browse files Browse the repository at this point in the history
  • Loading branch information
VitAndrGuid authored Apr 20, 2024
1 parent 342b70a commit 0020c67
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changeset/forty-trains-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@skeletonlabs/skeleton": patch
---

bugfix: Toast wrapper being removed from the DOM wrongfully after a second toast is triggered when the first one is finishing its outro transition
12 changes: 11 additions & 1 deletion packages/skeleton/src/lib/utilities/Toast/Toast.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -122,15 +122,21 @@
}
}
let wrapperVisible = false;
// Reactive
$: classesWrapper = `${cWrapper} ${cPosition} ${zIndex} ${$$props.class || ''}`;
$: classesSnackbar = `${cSnackbar} ${cAlign} ${padding}`;
$: classesToast = `${cToast} ${width} ${color} ${padding} ${spacing} ${rounded} ${shadow}`;
// Filtered Toast Store
$: filteredToasts = Array.from($toastStore).slice(0, max);
$: if (filteredToasts.length) {
wrapperVisible = true;
}
</script>

{#if $toastStore.length}
{#if filteredToasts.length > 0 || wrapperVisible}
<!-- Wrapper -->
<div class="snackbar-wrapper {classesWrapper}" data-testid="snackbar-wrapper">
<!-- List -->
Expand All @@ -148,6 +154,10 @@
params: { x: animAxis.x, y: animAxis.y, ...transitionOutParams },
enabled: transitions
}}
on:outroend={() => {
const outroFinishedForLastToastOnQueue = filteredToasts.length === 0;
if (outroFinishedForLastToastOnQueue) wrapperVisible = false;
}}
on:mouseenter={() => onMouseEnter(i)}
on:mouseleave={() => onMouseLeave(i)}
role={t.hideDismiss ? 'alert' : 'alertdialog'}
Expand Down
56 changes: 53 additions & 3 deletions packages/skeleton/src/lib/utilities/Toast/Toast.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { render } from '@testing-library/svelte';
import { describe, it, expect } from 'vitest';

import { render, screen } from '@testing-library/svelte';
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import type { ToastSettings } from './types.js';
import ToastTest from './ToastTest.svelte';

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

describe('Toast.svelte', () => {
const snackbarWrapperTestId = 'snackbar-wrapper';

// see: https://testing-library.com/docs/svelte-testing-library/faq/#why-arent-transition-events-running
beforeEach(() => {
const rafMock = (fn: (_: Date) => void) => setTimeout(() => fn(new Date()), 16);
vi.stubGlobal('requestAnimationFrame', rafMock);
});

afterEach(() => {
vi.unstubAllGlobals();
});

it('does not show the toast wrapper if the toast queue is empty', () => {
expect(() => screen.getByTestId(snackbarWrapperTestId)).toThrow();
});

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 () => {
const { getByTestId } = render(ToastTest, {
props: {
max: 2,
toastSettings: [
// note how toast B is scheduled to trigger at the same tick as toast A
{ message: 'A', timeout: 10 },
{ message: 'B', triggerDelay: 10, timeout: 10 }
]
}
});

const getWrapperElementAfterTimeout = (timeout: number) =>
new Promise((resolve) =>
setTimeout(() => {
try {
const el = getByTestId(snackbarWrapperTestId);
resolve(el);
} catch {
resolve(false);
}
}, timeout)
);

const [wrapperVisibilityOnAToBChange, wrapperVisibilityAfterAOutroFinishes, wrapperVisibilityAfterBOutroFinishes] = await Promise.all([
getWrapperElementAfterTimeout(10),
getWrapperElementAfterTimeout(16),
getWrapperElementAfterTimeout(50)
]);

expect(wrapperVisibilityOnAToBChange).toBeTruthy();
expect(wrapperVisibilityAfterAOutroFinishes).toBeTruthy();
expect(wrapperVisibilityAfterBOutroFinishes).toBeFalsy();
});

it('Renders modal alert', async () => {
const { getByTestId } = render(ToastTest, { props: { toastSettings: [toastMessage] } });
expect(getByTestId('toast')).toBeTruthy();
Expand Down
14 changes: 11 additions & 3 deletions packages/skeleton/src/lib/utilities/Toast/ToastTest.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,22 @@
import type { ToastSettings } from './types.js';
import Toast from './Toast.svelte';
export let toastSettings: Array<ToastSettings> = [];
interface TestToastSettings extends ToastSettings {
triggerDelay?: number;
}
export let toastSettings: Array<TestToastSettings> = [];
export let max: number | undefined = undefined;
initializeToastStore();
const toastStore = getToastStore();
toastSettings.forEach((element) => {
toastStore.trigger(element);
toastSettings.forEach(({ triggerDelay, ...settings }) => {
if (triggerDelay) {
setTimeout(() => toastStore.trigger(settings), triggerDelay);
} else {
toastStore.trigger(settings);
}
});
</script>

Expand Down

0 comments on commit 0020c67

Please sign in to comment.