-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ui): add toast for status messages (#116)
## Description Add the ability to create toast messages using `addToast()` to write to a common store ## Related Issue - #115
- Loading branch information
1 parent
98916d6
commit 983ae6a
Showing
6 changed files
with
263 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
<script lang="ts"> | ||
import { CheckmarkOutline, Close, Information, Warning } from 'carbon-icons-svelte' | ||
import { removeToast, toast } from './store' | ||
</script> | ||
|
||
<div class="fixed top-20 right-5 p-4 z-40"> | ||
{#each $toast as toast} | ||
<div | ||
class="flex items-center justify-between shadow-gray-900 w-full max-w-xs p-4 text-gray-500 bg-white rounded-lg shadow-lg dark:text-gray-400 dark:bg-gray-800 mb-4" | ||
> | ||
<div class="flex items-center space-x-4"> | ||
<div class="flex-shrink-0 w-8 h-8 rounded-lg"> | ||
{#if toast.type === 'error'} | ||
<Warning class="w-8 h-8 text-red-500" /> | ||
{:else if toast.type === 'warning'} | ||
<Warning class="w-8 h-8 text-yellow-500" /> | ||
{:else if toast.type === 'info'} | ||
<Information class="w-8 h-8 text-blue-500" /> | ||
{:else if toast.type === 'success'} | ||
<CheckmarkOutline class="w-8 h-8 text-green-500" /> | ||
{/if} | ||
</div> | ||
<div class="text-sm font-normal">{toast.message}</div> | ||
</div> | ||
<button | ||
type="button" | ||
class="flex-shrink-0 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex items-center justify-center h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" | ||
on:click={() => removeToast(toast.id)} | ||
> | ||
<Close class="w-5 h-5" /> | ||
</button> | ||
</div> | ||
{/each} | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// SPDX-FileCopyrightText: 2024-Present The UDS Authors | ||
|
||
import { fireEvent, render, screen } from '@testing-library/svelte' | ||
import { get } from 'svelte/store' | ||
import { beforeEach, describe, expect, it, vi } from 'vitest' | ||
import ToastComponent from './component.svelte' | ||
import { addToast, toast } from './store' | ||
|
||
// Mock the icon components | ||
vi.mock('carbon-icons-svelte', () => { | ||
const mockComponent = () => ({ | ||
$$: { | ||
on_mount: [], | ||
on_destroy: [], | ||
before_update: [], | ||
after_update: [], | ||
}, | ||
}) | ||
|
||
return { | ||
CheckmarkOutline: vi.fn().mockImplementation(mockComponent), | ||
Close: vi.fn().mockImplementation(mockComponent), | ||
Information: vi.fn().mockImplementation(mockComponent), | ||
Warning: vi.fn().mockImplementation(mockComponent), | ||
} | ||
}) | ||
|
||
describe('Toast Component', () => { | ||
beforeEach(() => { | ||
// Reset the store before each test | ||
toast.set([]) | ||
}) | ||
|
||
it('renders nothing when there are no toasts', () => { | ||
const { container } = render(ToastComponent) | ||
expect(container.firstChild?.childNodes.length).toBe(0) | ||
}) | ||
|
||
it('renders a toast message', () => { | ||
addToast({ message: 'Test toast', timeoutSecs: 3, type: 'info' }) | ||
render(ToastComponent) | ||
expect(screen.getByText('Test toast')).toBeInTheDocument() | ||
}) | ||
|
||
it('renders multiple toast messages', () => { | ||
addToast({ message: 'Toast 1', timeoutSecs: 3, type: 'info' }) | ||
addToast({ message: 'Toast 2', timeoutSecs: 3, type: 'success' }) | ||
render(ToastComponent) | ||
expect(screen.getByText('Toast 1')).toBeInTheDocument() | ||
expect(screen.getByText('Toast 2')).toBeInTheDocument() | ||
}) | ||
|
||
it('renders the correct number of icons for each toast type', () => { | ||
addToast({ message: 'Error toast', timeoutSecs: 3, type: 'error' }) | ||
addToast({ message: 'Warning toast', timeoutSecs: 3, type: 'warning' }) | ||
addToast({ message: 'Info toast', timeoutSecs: 3, type: 'info' }) | ||
addToast({ message: 'Success toast', timeoutSecs: 3, type: 'success' }) | ||
const { container } = render(ToastComponent) | ||
|
||
const icons = container.querySelectorAll('.w-8.h-8') | ||
// 4 toasts * 2 icons per toast (icon + close button) | ||
expect(icons.length).toBe(8) | ||
}) | ||
|
||
it('removes a toast when the close button is clicked', async () => { | ||
addToast({ message: 'Test toast', timeoutSecs: 3, type: 'info' }) | ||
render(ToastComponent) | ||
|
||
const closeButton = screen.getByRole('button') | ||
await fireEvent.click(closeButton) | ||
|
||
expect(get(toast)).toHaveLength(0) | ||
expect(screen.queryByText('Test toast')).not.toBeInTheDocument() | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// SPDX-FileCopyrightText: 2024-Present The UDS Authors | ||
|
||
export { default as ToastPanel } from './component.svelte' | ||
export { addToast } from './store' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// SPDX-FileCopyrightText: 2024-Present The UDS Authors | ||
|
||
import { get } from 'svelte/store' | ||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' | ||
import { addToast, removeToast, toast, type Toast } from './store' | ||
|
||
describe('Toast Store', () => { | ||
beforeEach(() => { | ||
// Reset the store before each test | ||
toast.set([]) | ||
}) | ||
|
||
afterEach(() => { | ||
// Clear all timers after each test | ||
vi.useRealTimers() | ||
}) | ||
|
||
it('should initialize with an empty array', () => { | ||
expect(get(toast)).toEqual([]) | ||
}) | ||
|
||
it('should add a toast to the store', () => { | ||
const newToast: Toast = { | ||
message: 'Test toast', | ||
timeoutSecs: 3, | ||
type: 'info', | ||
} | ||
addToast(newToast) | ||
const toasts = get(toast) | ||
expect(toasts).toHaveLength(1) | ||
expect(toasts[0]).toMatchObject(newToast) | ||
expect(toasts[0].id).toBeDefined() | ||
}) | ||
|
||
it('should remove a toast from the store', () => { | ||
const newToast: Toast = { | ||
message: 'Test toast', | ||
timeoutSecs: 3, | ||
type: 'info', | ||
} | ||
addToast(newToast) | ||
const toasts = get(toast) | ||
const toastId = toasts[0].id | ||
|
||
removeToast(toastId) | ||
expect(get(toast)).toHaveLength(0) | ||
}) | ||
|
||
it('should automatically remove a toast after the specified timeout', () => { | ||
vi.useFakeTimers() | ||
const newToast: Toast = { | ||
message: 'Test toast', | ||
timeoutSecs: 3, | ||
type: 'info', | ||
} | ||
addToast(newToast) | ||
expect(get(toast)).toHaveLength(1) | ||
|
||
vi.advanceTimersByTime(3000) | ||
expect(get(toast)).toHaveLength(0) | ||
}) | ||
|
||
it('should not remove a toast if timeout is not specified', () => { | ||
vi.useFakeTimers() | ||
const newToast: Toast = { | ||
message: 'Test toast', | ||
timeoutSecs: 0, | ||
type: 'info', | ||
} | ||
addToast(newToast) | ||
expect(get(toast)).toHaveLength(1) | ||
|
||
vi.advanceTimersByTime(10000) | ||
expect(get(toast)).toHaveLength(1) | ||
}) | ||
|
||
it('should add multiple toasts and maintain their order', () => { | ||
const toast1: Toast = { message: 'Toast 1', timeoutSecs: 3, type: 'info' } | ||
const toast2: Toast = { message: 'Toast 2', timeoutSecs: 3, type: 'success' } | ||
const toast3: Toast = { message: 'Toast 3', timeoutSecs: 3, type: 'error' } | ||
|
||
addToast(toast1) | ||
addToast(toast2) | ||
addToast(toast3) | ||
|
||
const toasts = get(toast) | ||
expect(toasts).toHaveLength(3) | ||
expect(toasts[0].message).toBe('Toast 1') | ||
expect(toasts[1].message).toBe('Toast 2') | ||
expect(toasts[2].message).toBe('Toast 3') | ||
}) | ||
|
||
it('should not remove other toasts when removing a specific toast', () => { | ||
const toast1: Toast = { message: 'Toast 1', timeoutSecs: 3, type: 'info' } | ||
const toast2: Toast = { message: 'Toast 2', timeoutSecs: 3, type: 'success' } | ||
|
||
addToast(toast1) | ||
addToast(toast2) | ||
|
||
const toasts = get(toast) | ||
removeToast(toasts[0].id) | ||
|
||
const updatedToasts = get(toast) | ||
expect(updatedToasts).toHaveLength(1) | ||
expect(updatedToasts[0].message).toBe('Toast 2') | ||
}) | ||
|
||
it('should do nothing when trying to remove a non-existent toast', () => { | ||
const toast1: Toast = { message: 'Toast 1', timeoutSecs: 3, type: 'info' } | ||
addToast(toast1) | ||
|
||
removeToast(12345) // Non-existent ID | ||
|
||
expect(get(toast)).toHaveLength(1) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// SPDX-FileCopyrightText: 2024-Present The UDS Authors | ||
|
||
import { writable } from 'svelte/store' | ||
|
||
export type Toast = { | ||
id?: number | ||
message: string | ||
timeoutSecs: number | ||
type: 'success' | 'info' | 'warning' | 'error' | ||
} | ||
|
||
export const toast = writable<Toast[]>([]) | ||
|
||
export const addToast = (newToast: Toast) => { | ||
toast.update((toasts) => { | ||
const id = Date.now() + Math.random() | ||
const toast = { id, ...newToast } | ||
|
||
if (toast.timeoutSecs) { | ||
setTimeout(() => removeToast(id), toast.timeoutSecs * 1000) | ||
} | ||
return [...toasts, toast] | ||
}) | ||
} | ||
|
||
export const removeToast = (id?: number) => { | ||
toast.update((toasts) => toasts.filter((toast) => toast.id !== id)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters