Skip to content

Commit 3c78a30

Browse files
authored
Merge pull request #2627 from skeletonlabs/dev
Merge for release April 23, 2024
2 parents dd99827 + 59e4e51 commit 3c78a30

File tree

19 files changed

+267
-97
lines changed

19 files changed

+267
-97
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: Resolved a timing issue that could cause Toasts to animate incorrectly on close.

.changeset/four-cherries-arrive.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"skeleton.dev": minor
3+
---
4+
5+
Feat: Avatar component updated to support restProps and fallback slot.

packages/skeleton/src/lib/components/Avatar/Avatar.svelte

+6-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
66
// Props (initials)
77
/** Initials only - Provide up to two text characters. */
8-
export let initials = 'AB';
8+
export let initials = '';
99
/** Initials only - Provide classes to set the SVG text fill color. */
1010
export let fill: CssClasses = 'fill-token';
1111
/** Initials only - Set the base font size for the scalable SVG text. */
@@ -53,18 +53,17 @@
5353

5454
<!-- FIXME: resolve a11y warnings -->
5555
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
56-
<figure class="avatar {classesBase}" data-testid="avatar" on:click on:keydown on:keyup on:keypress>
57-
{#if src}
56+
<figure class="avatar {classesBase}" data-testid="avatar" {...prunedRestProps()} on:click on:keydown on:keyup on:keypress>
57+
{#if src || fallback}
5858
<img
5959
class="avatar-image {cImage}"
6060
style={$$props.style ?? ''}
6161
{src}
6262
alt={$$props.alt || ''}
6363
use:action={actionParams}
6464
on:error={() => (src = fallback)}
65-
{...prunedRestProps()}
6665
/>
67-
{:else}
66+
{:else if initials}
6867
<svg class="avatar-initials w-full h-full" viewBox="0 0 512 512">
6968
<text
7069
x="50%"
@@ -78,5 +77,7 @@
7877
{String(initials).substring(0, 2).toUpperCase()}
7978
</text>
8079
</svg>
80+
{:else}
81+
<slot />
8182
{/if}
8283
</figure>

packages/skeleton/src/lib/components/Avatar/Avatar.test.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ describe('Avatar.svelte', () => {
3636
});
3737

3838
it('Initials shown when no image source provided', async () => {
39-
const { getByTestId } = render(Avatar);
40-
expect(getByTestId('avatar').querySelector('.avatar-initials')?.textContent).eq('AB');
39+
const testInitials = 'SK';
40+
const { getByTestId } = render(Avatar, { props: { initials: testInitials } });
41+
expect(getByTestId('avatar').querySelector('.avatar-initials')?.textContent).eq(testInitials);
4142
});
4243
});

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

sites/skeleton.dev/src/lib/links.ts

+35-11
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,19 @@ export const menuNavLinks: Record<string, Array<{ title: string; list: List }>>
2323
href: '/docs/transitions',
2424
label: 'Transitions',
2525
keywords: 'transition, transitions, blur, fade, fly, slide, scale, draw, crossfade, prefers, reduced, motion'
26+
},
27+
{
28+
href: '/docs/dark-mode',
29+
label: 'Dark Mode',
30+
keywords: 'light, dark, toggle, prefer, color, scheme, lightswitch'
2631
}
2732
]
2833
},
2934
{
3035
title: 'Resources',
3136
list: [
3237
{ href: '/docs/generator', label: 'Theme Generator', keywords: 'create, custom, style, css, design' },
33-
{ href: '/docs/figma', label: 'Figma', keywords: 'figma, design, mock, wireframe, ui, kit' }, // , badge: 'New'
34-
{ href: '/docs/purgecss', label: 'PurgeCSS', keywords: 'purgecss, vite, tree, shaking, bundle, optimize' },
38+
{ href: '/docs/figma', label: 'Figma', keywords: 'figma, design, mock, wireframe, ui, kit' },
3539
{ href: '/docs/contributing', label: 'Contributing', keywords: 'branch, pr' },
3640
{
3741
href: '/docs/sponsorship',
@@ -43,8 +47,9 @@ export const menuNavLinks: Record<string, Array<{ title: string; list: List }>>
4347
{
4448
title: 'Integrations',
4549
list: [
50+
{ href: '/docs/purgecss', label: 'PurgeCSS', keywords: 'purgecss, vite, tree, shaking, bundle, optimize' },
4651
{ href: '/docs/tauri', label: 'Tauri', keywords: 'Tauri, desktop, setup, install' },
47-
{ href: '/docs/ssd', label: 'Datatables', keywords: 'datatables, tables, datagrid, simple', badge: 'New' }
52+
{ href: '/docs/ssd', label: 'Datatables', keywords: 'datatables, tables, datagrid, simple' } // badge: 'New'
4853
]
4954
}
5055
],
@@ -105,7 +110,6 @@ export const menuNavLinks: Record<string, Array<{ title: string; list: List }>>
105110
{ href: '/components/accordions', label: 'Accordions', keywords: 'collapse' },
106111
{ href: '/components/app-bar', label: 'App Bar', keywords: 'header, top, bar, title' },
107112
{ href: '/components/app-rail', label: 'App Rail', keywords: 'nav, navigation, tile, sidebar' },
108-
{ href: '/components/app-shell', label: 'App Shell', keywords: 'layout, header, footer, sidebar, page, content' },
109113
{ href: '/components/autocomplete', label: 'Autocomplete', keywords: 'input, filter, fuzzy, auto, complete, suggest' },
110114
{ href: '/components/avatars', label: 'Avatars', keywords: 'image, initial, filter' },
111115
{ href: '/components/conic-gradients', label: 'Conic Gradients', keywords: 'chart, graph, circle, pie, spinner, legend' },
@@ -120,10 +124,22 @@ export const menuNavLinks: Record<string, Array<{ title: string; list: List }>>
120124
{ href: '/components/radio-groups', label: 'Radio Groups', keywords: 'input, form, select, selection' },
121125
{ href: '/components/range-sliders', label: 'Range Sliders', keywords: 'value, min, max, step,, tick, input, form' },
122126
{ href: '/components/slide-toggles', label: 'Slide Toggles', keywords: 'check, checkbox, toggle, input, form' },
123-
{ href: '/components/steppers', label: 'Steppers', keywords: 'intro, onboard, onboarding, form, progress' },
124127
{ href: '/components/tabs', label: 'Tabs', keywords: 'select, selection, panel' },
125-
{ href: '/components/tables', label: 'Tables', keywords: 'data, entry' },
126-
{ href: '/components/tree-views', label: 'Tree Views', keywords: 'tree, view, node', badge: 'Beta' }
128+
{ href: '/components/tree-views', label: 'Tree Views', keywords: 'tree, view, node' }
129+
]
130+
},
131+
// Deprecated
132+
{
133+
title: '',
134+
list: [
135+
{
136+
href: '/components/app-shell',
137+
label: 'App Shell',
138+
keywords: 'layout, header, footer, sidebar, page, content',
139+
badge: 'Deprecated'
140+
},
141+
{ href: '/components/tables', label: 'Tables', keywords: 'data, entry', badge: 'Deprecated' },
142+
{ href: '/components/steppers', label: 'Steppers', keywords: 'intro, onboard, onboarding, form, progress', badge: 'Deprecated' }
127143
]
128144
}
129145
],
@@ -133,8 +149,6 @@ export const menuNavLinks: Record<string, Array<{ title: string; list: List }>>
133149
list: [
134150
{ href: '/utilities/codeblocks', label: 'Code Blocks', keywords: 'highlight, syntax, code' },
135151
{ href: '/utilities/drawers', label: 'Drawers', keywords: 'overlay, slide, panel, sidebar' },
136-
{ href: '/utilities/lightswitches', label: 'Lightswitch', keywords: 'light, dark, toggle, prefer, color, scheme' },
137-
{ href: '/utilities/local-storage-stores', label: 'Local Storage Stores', keywords: 'svelte, writable, get, cache, persist' },
138152
{
139153
href: '/utilities/modals',
140154
label: 'Modals',
@@ -143,8 +157,18 @@ export const menuNavLinks: Record<string, Array<{ title: string; list: List }>>
143157
{ href: '/utilities/popups', label: 'Popups', keywords: 'menu, tooltip, overlay, dropdown, combobox, drop, down, select' },
144158
{ href: '/utilities/toasts', label: 'Toasts', keywords: 'overlay, snack, snackbar, bar, action, alert, notification' },
145159
{ href: '/utilities/table-of-contents', label: 'Table of Contents', keywords: 'page, results, links, navigation' }
146-
// DELISTED UNTIL FURTHER NOTICE
147-
// { href: '/utilities/data-tables', label: 'Data Tables', keywords: 'search, sort, page, pagination, async', badge: 'Experimental' }
160+
]
161+
},
162+
// Deprecated
163+
{
164+
title: '',
165+
list: [
166+
{
167+
href: '/utilities/local-storage-stores',
168+
label: 'Persisted Store',
169+
keywords: 'svelte, writable, get, cache, persist',
170+
badge: 'Deprecated'
171+
}
148172
]
149173
}
150174
]

sites/skeleton.dev/src/routes/(inner)/components/app-shell/+page.svelte

+14
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,20 @@
3636
<DocsShell {settings}>
3737
<!-- Slot: Sandbox -->
3838
<svelte:fragment slot="sandbox">
39+
<!-- Alert -->
40+
<aside class="alert variant-ghost-error">
41+
<i class="fa-solid fa-triangle-exclamation text-4xl" />
42+
<div class="alert-message" data-toc-ignore>
43+
<h3 class="h3" data-toc-ignore>Deprecated</h3>
44+
<!-- prettier-ignore -->
45+
<p>
46+
This feature is being phased out as we transition to <a class="underline" href="https://github.com/skeletonlabs/skeleton/discussions/2375" target="_blank">Skeleton v3</a>. This will remain functional for all 2.x releases, but we recommend you migrate your apps to custom layouts as soon as possible. Expect more guidance around this in the future.
47+
</p>
48+
</div>
49+
<div class="alert-actions">
50+
<a class="btn variant-filled" href="https://github.com/skeletonlabs/skeleton/issues/2383" target="_blank"> Learn More </a>
51+
</div>
52+
</aside>
3953
<div class="space-y-2">
4054
<DocsPreview regionPreview="h-[280px]">
4155
<svelte:fragment slot="lead">

sites/skeleton.dev/src/routes/(inner)/components/avatars/+page.svelte

+41-24
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
8: 'rounded-full'
3838
};
3939
let rangeSliderValue: keyof typeof roundedMapping = 8;
40-
let fallback = '';
40+
let fallback = imgPlaceholder;
4141
4242
// Reactive
4343
$: actionParams = '#Apollo';
@@ -85,30 +85,11 @@
8585
</svelte:fragment>
8686
</DocsPreview>
8787
</section>
88-
<section class="space-y-4">
89-
<h2 class="h2">Interactive Border</h2>
90-
<p>Apply the following styles using the <code class="code">border</code> and <code class="code">cursor</code> properties.</p>
91-
<DocsPreview background="neutral">
92-
<svelte:fragment slot="preview">
93-
<Avatar border={borderStyles} cursor="cursor-pointer" />
94-
</svelte:fragment>
95-
<svelte:fragment slot="source">
96-
<CodeBlock
97-
language="html"
98-
code={`
99-
<Avatar
100-
border="${borderStyles}"
101-
cursor="cursor-pointer"
102-
/>
103-
`}
104-
/>
105-
</svelte:fragment>
106-
</DocsPreview>
107-
</section>
108-
<section class="space-y-4">
109-
<h2 class="h2">Handling Fallbacks</h2>
88+
<!-- DEPRECATED: use fallback slot instead -->
89+
<!-- <section class="space-y-4">
90+
<h2 class="h2">Fallback Image</h2>
11091
<p>
111-
Use the <code class="code">fallback</code> property to specify a fallback when images fail to load, or supply the user's initials.
92+
Use the <code class="code">fallback</code> property to specify a fallback when images fail to load.
11293
</p>
11394
<DocsPreview background="neutral" regionFooter="text-center">
11495
<svelte:fragment slot="preview">
@@ -129,6 +110,42 @@
129110
</select>
130111
</svelte:fragment>
131112
</DocsPreview>
113+
</section> -->
114+
<section class="space-y-4">
115+
<h2 class="h2">Fallback</h2>
116+
<p>Use the default slot to provide fallback images, icons, or text.</p>
117+
<DocsPreview background="neutral" regionFooter="text-center">
118+
<svelte:fragment slot="preview">
119+
{#key fallback}
120+
<Avatar background="bg-secondary-500">
121+
<i class="fa-solid fa-skull text-xl"></i>
122+
</Avatar>
123+
{/key}
124+
</svelte:fragment>
125+
<svelte:fragment slot="source">
126+
<CodeBlock language="html" code={`<Avatar background="bg-secondary-500">(fallback)</Avatar>`} />
127+
</svelte:fragment>
128+
</DocsPreview>
129+
</section>
130+
<section class="space-y-4">
131+
<h2 class="h2">Interactive Border</h2>
132+
<p>Apply the following styles using the <code class="code">border</code> and <code class="code">cursor</code> properties.</p>
133+
<DocsPreview background="neutral">
134+
<svelte:fragment slot="preview">
135+
<Avatar initials="SK" border={borderStyles} cursor="cursor-pointer"></Avatar>
136+
</svelte:fragment>
137+
<svelte:fragment slot="source">
138+
<CodeBlock
139+
language="html"
140+
code={`
141+
<Avatar
142+
border="${borderStyles}"
143+
cursor="cursor-pointer"
144+
/>
145+
`}
146+
/>
147+
</svelte:fragment>
148+
</DocsPreview>
132149
</section>
133150
<section class="space-y-4">
134151
<h2 class="h2">Applying Filters</h2>

0 commit comments

Comments
 (0)