srcdev-nuxt-components 9.5.7 → 9.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/component-compliance-checklist.md +8 -2
- package/.claude/skills/component-dynamic-slots.md +3 -3
- package/.claude/skills/component-export-types.md +10 -16
- package/.claude/skills/component-naming.md +1 -1
- package/.claude/skills/components/alert-content.md +9 -1
- package/.claude/skills/components/breadcrumb.md +5 -0
- package/.claude/skills/components/container-glow.md +21 -8
- package/.claude/skills/components/display-avatar.md +13 -7
- package/.claude/skills/components/display-chip.md +37 -24
- package/.claude/skills/components/display-pill.md +58 -8
- package/.claude/skills/components/display-prompt.md +8 -5
- package/.claude/skills/components/display-toast.md +21 -4
- package/.claude/skills/components/glowing-border.md +20 -2
- package/.claude/skills/components/input-button.md +3 -0
- package/.claude/skills/components/responsive-header.md +59 -0
- package/.claude/skills/components/select-menu.md +54 -5
- package/.claude/skills/components/service-detail.md +32 -0
- package/.claude/skills/components/service-summary.md +8 -0
- package/.claude/skills/components/services-card-grid.md +5 -3
- package/.claude/skills/components/services-card.md +22 -12
- package/.claude/skills/components/site-header.md +2 -2
- package/.claude/skills/components/slider-gallery.md +10 -4
- package/.claude/skills/components/tabbed-content.md +300 -0
- package/.claude/skills/composable-anchor-scroll.md +16 -15
- package/.claude/skills/index.md +4 -5
- package/.claude/skills/storybook-add-story.md +53 -0
- package/.claude/skills/theming-status-tokens.md +60 -0
- package/.vscode/srcdev-component-display-avatar.code-snippets +2 -1
- package/.vscode/srcdev-component-display-chip.code-snippets +3 -2
- package/.vscode/srcdev-component-display-pill.code-snippets +10 -0
- package/.vscode/srcdev-component-responsive-header.code-snippets +16 -0
- package/.vscode/srcdev-component-select-menu.code-snippets +28 -0
- package/.vscode/srcdev-component-service-detail.code-snippets +1 -0
- package/.vscode/srcdev-component-service-summary.code-snippets +3 -1
- package/.vscode/srcdev-component-services-card.code-snippets +38 -36
- package/.vscode/srcdev-component-services-grids.code-snippets +2 -1
- package/.vscode/{srcdev-component-tabs-core.code-snippets → srcdev-component-tabbed-content.code-snippets} +35 -16
- package/app/assets/styles/setup/03.theming/_status.css +24 -0
- package/app/assets/styles/setup/03.theming/index.css +1 -0
- package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +39 -35
- package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +25 -15
- package/app/components/01.atoms/animations/container-glow/stories/ContainerGlow.stories.ts +130 -24
- package/app/components/01.atoms/animations/container-glow/tests/ContainerGlow.spec.ts +45 -0
- package/app/components/01.atoms/animations/container-glow/tests/__snapshots__/ContainerGlow.spec.ts.snap +2 -2
- package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +76 -19
- package/app/components/01.atoms/animations/glowing-border/GlowingBorder.vue +12 -10
- package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +201 -42
- package/app/components/01.atoms/animations/glowing-border/tests/GlowingBorder.spec.ts +15 -4
- package/app/components/01.atoms/animations/glowing-border/tests/__snapshots__/GlowingBorder.spec.ts.snap +1 -1
- package/app/components/01.atoms/display-pill/CONSUMER-STYLING.md +78 -9
- package/app/components/01.atoms/display-pill/DisplayPill.vue +59 -23
- package/app/components/01.atoms/display-pill/stories/DisplayPill.stories.ts +240 -16
- package/app/components/01.atoms/display-pill/tests/DisplayPill.spec.ts +43 -14
- package/app/components/01.atoms/display-pill/tests/__snapshots__/DisplayPill.spec.ts.snap +3 -3
- package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +16 -4
- package/app/components/01.atoms/navigation/breadcrumb/CONSUMER-STYLING.md +28 -5
- package/app/components/01.atoms/navigation/breadcrumb/stories/Breadcrumb.stories.ts +94 -0
- package/app/components/01.atoms/navigation/breadcrumb/tests/Breadcrumb.spec.ts +27 -0
- package/app/components/01.atoms/tabbed-content/CONSUMER-STYLING.md +201 -0
- package/app/components/01.atoms/tabbed-content/TabbedContent.vue +750 -0
- package/app/components/01.atoms/tabbed-content/stories/TabbedContent.stories.ts +279 -0
- package/app/components/01.atoms/tabbed-content/tests/TabbedContent.spec.ts +816 -0
- package/app/components/02.molecules/alert-content/AlertContent.vue +11 -2
- package/app/components/02.molecules/alert-content/AlertContentInner.vue +19 -0
- package/app/components/02.molecules/alert-content/CONSUMER-STYLING.md +17 -4
- package/app/components/02.molecules/alert-content/stories/AlertContent.stories.ts +200 -17
- package/app/components/02.molecules/alert-content/tests/AlertContent.spec.ts +33 -0
- package/app/components/02.molecules/display-avatar/CONSUMER-STYLING.md +7 -5
- package/app/components/02.molecules/display-avatar/DisplayAvatar.vue +6 -2
- package/app/components/02.molecules/display-avatar/stories/DisplayAvatar.stories.ts +24 -3
- package/app/components/02.molecules/display-avatar/tests/DisplayAvatar.spec.ts +19 -0
- package/app/components/02.molecules/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap +2 -1
- package/app/components/02.molecules/display-chip/CONSUMER-STYLING.md +52 -19
- package/app/components/02.molecules/display-chip/DisplayChip.vue +72 -54
- package/app/components/02.molecules/display-chip/stories/DisplayChip.stories.ts +103 -6
- package/app/components/02.molecules/display-chip/tests/DisplayChip.spec.ts +98 -29
- package/app/components/02.molecules/display-chip/tests/__snapshots__/DisplayChip.spec.ts.snap +8 -4
- package/app/components/02.molecules/prompt/CONSUMER-STYLING.md +9 -1
- package/app/components/02.molecules/prompt/DisplayPrompt.vue +2 -2
- package/app/components/02.molecules/prompt/stories/DisplayPrompt.stories.ts +208 -71
- package/app/components/02.molecules/prompt/tests/DisplayPrompt.spec.ts +19 -6
- package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +66 -8
- package/app/components/02.molecules/select-menu/SelectMenu.vue +141 -18
- package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +232 -2
- package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +151 -0
- package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +6 -2
- package/app/components/02.molecules/toast/CONSUMER-STYLING.md +91 -27
- package/app/components/02.molecules/toast/DisplayToast.vue +109 -50
- package/app/components/02.molecules/toast/DisplayToastProvider.vue +109 -45
- package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +23 -0
- package/app/components/02.molecules/toast/stories/DisplayToastProvider.stories.ts +73 -0
- package/app/components/02.molecules/toast/tests/DisplayToast.spec.ts +132 -18
- package/app/components/02.molecules/toast/tests/DisplayToastProvider.spec.ts +105 -39
- package/app/components/03.organisms/image-galleries/slider-gallery/CONSUMER-STYLING.md +19 -1
- package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +65 -7
- package/app/components/03.organisms/image-galleries/slider-gallery/stories/SliderGallery.stories.ts +201 -63
- package/app/components/03.organisms/image-galleries/slider-gallery/tests/SliderGallery.spec.ts +41 -2
- package/app/components/03.organisms/image-galleries/slider-gallery/tests/__snapshots__/SliderGallery.spec.ts.snap +6 -6
- package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +42 -11
- package/app/components/03.organisms/responsive-header/NavigationItems.vue +60 -16
- package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +86 -24
- package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +52 -0
- package/app/components/03.organisms/responsive-header/tests/NavigationItems.spec.ts +20 -0
- package/app/components/03.organisms/responsive-header/tests/ResponsiveHeader.spec.ts +40 -0
- package/app/components/03.organisms/services/service-detail/CONSUMER-STYLING.md +92 -41
- package/app/components/03.organisms/services/service-detail/ServiceDetail.vue +198 -73
- package/app/components/03.organisms/services/service-detail/stories/ServiceDetail.stories.ts +157 -9
- package/app/components/03.organisms/services/service-detail/tests/ServiceDetail.spec.ts +58 -0
- package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +5 -4
- package/app/components/03.organisms/services/service-summary/CONSUMER-STYLING.md +30 -0
- package/app/components/03.organisms/services/service-summary/ServiceSummary.vue +32 -8
- package/app/components/03.organisms/services/service-summary/stories/ServiceSummary.stories.ts +147 -0
- package/app/components/03.organisms/services/service-summary/tests/ServiceSummary.spec.ts +26 -0
- package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +2 -2
- package/app/components/03.organisms/services/services-card/CONSUMER-STYLING.md +104 -63
- package/app/components/03.organisms/services/services-card/ServicesCard.vue +92 -66
- package/app/components/03.organisms/services/services-card/stories/ServicesCard.stories.ts +194 -27
- package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +34 -8
- package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +8 -8
- package/app/components/03.organisms/services/services-grids/CONSUMER-STYLING.md +9 -0
- package/app/components/03.organisms/services/services-grids/ServicesCardGrid.vue +2 -1
- package/app/components/03.organisms/services/services-grids/stories/ServiceSummaryGrid.stories.ts +46 -6
- package/app/components/03.organisms/services/services-grids/stories/ServicesCardGrid.stories.ts +89 -11
- package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +8 -0
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +6 -6
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +24 -24
- package/app/components/05.forms/input-button/CONSUMER-STYLING.md +8 -0
- package/app/components/05.forms/input-button/InputButton.vue +3 -1
- package/app/composables/tests/useAnchorScroll.spec.ts +8 -0
- package/app/composables/useAnchorScroll.ts +1 -1
- package/app/composables/useTabs.ts +148 -206
- package/app/types/components/display-chip.d.ts +8 -4
- package/app/types/components/display-toast.d.ts +2 -0
- package/app/types/components/index.ts +0 -1
- package/app/types/components/select-menu.d.ts +2 -0
- package/app/utils/pausableTimeout.ts +41 -0
- package/nuxt.config.ts +1 -1
- package/package.json +1 -1
- package/.claude/skills/components/site-navigation.md +0 -145
- package/.claude/skills/components/tab-navigation.md +0 -272
- package/.claude/skills/components/tabs-core.md +0 -187
- package/.vscode/srcdev-component-site-navigation.code-snippets +0 -38
- package/.vscode/srcdev-component-tab-navigation.code-snippets +0 -30
- package/app/components/01.atoms/navigation/tabs/CONSUMER-STYLING.md +0 -120
- package/app/components/01.atoms/navigation/tabs/TabsCore.vue +0 -272
- package/app/components/01.atoms/navigation/tabs/stories/TabsCore.stories.ts +0 -199
- package/app/components/01.atoms/navigation/tabs/tests/TabsCore.spec.ts +0 -274
- package/app/components/02.molecules/navigation/site-navigation/CONSUMER-STYLING.md +0 -109
- package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +0 -751
- package/app/components/02.molecules/navigation/site-navigation/stories/SiteNavigation.stories.ts +0 -356
- package/app/components/02.molecules/navigation/site-navigation/tests/SiteNavigation.spec.ts +0 -367
- package/app/components/02.molecules/navigation/site-navigation/tests/__snapshots__/SiteNavigation.spec.ts.snap +0 -30
- package/app/components/02.molecules/navigation/tab-navigation/CONSUMER-STYLING.md +0 -150
- package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +0 -518
- package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts +0 -340
- package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +0 -339
- package/app/components/02.molecules/navigation/tab-navigation/tests/__snapshots__/TabNavigation.spec.ts.snap +0 -33
- package/app/composables/useNavCollapse.ts +0 -86
- package/app/types/components/nav-item.d.ts +0 -11
|
@@ -140,3 +140,76 @@ FullWidth.args = {
|
|
|
140
140
|
theme: "warning",
|
|
141
141
|
autoDismiss: false,
|
|
142
142
|
};
|
|
143
|
+
|
|
144
|
+
const longUnbroken = "Donaudampfschifffahrtselektrizitätenhauptbetriebswerkbauunterbeamtengesellschaft";
|
|
145
|
+
|
|
146
|
+
const stressToasts = [
|
|
147
|
+
{
|
|
148
|
+
theme: "error" as const,
|
|
149
|
+
title: "Ihre Zahlung konnte leider nicht verarbeitet werden, bitte überprüfen Sie Ihre Kartendaten",
|
|
150
|
+
description:
|
|
151
|
+
"Die Bank hat die Transaktion abgelehnt. Bitte versuchen Sie es mit einer anderen Zahlungsmethode erneut oder wenden Sie sich an Ihre Bank, um weitere Informationen zu erhalten. ".repeat(
|
|
152
|
+
2
|
|
153
|
+
),
|
|
154
|
+
},
|
|
155
|
+
{ theme: "warning" as const, title: longUnbroken, description: "https://example.com/" + "a-long-path-segment".repeat(6) },
|
|
156
|
+
{ theme: "success" as const, title: "🎉 Emoji first", description: "<b>Not bold</b> <script>alert('x')</script>" },
|
|
157
|
+
{ theme: "info" as const, title: "تم حفظ التغييرات بنجاح", description: "نص عربي طويل من اليمين إلى اليسار للتحقق من الاتجاه" },
|
|
158
|
+
{ theme: "info" as const, title: "A", description: "" },
|
|
159
|
+
{ theme: "info" as const, title: "", description: "Description only, no title." },
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
export const StressTest: StoryFn<StoryArgs> = (args) => ({
|
|
163
|
+
components: { DisplayToastProvider },
|
|
164
|
+
setup() {
|
|
165
|
+
const { show, clear } = useToastQueue();
|
|
166
|
+
const queueStress = () => {
|
|
167
|
+
stressToasts.forEach(({ theme, title, description }) => {
|
|
168
|
+
show({
|
|
169
|
+
appearance: { theme },
|
|
170
|
+
behavior: { autoDismiss: args.autoDismiss, duration: args.duration },
|
|
171
|
+
content: { title, description, dismissLabel: "Benachrichtigung schließen" },
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
};
|
|
175
|
+
const flood = () => {
|
|
176
|
+
for (let index = 1; index <= 20; index++) {
|
|
177
|
+
show({ behavior: { autoDismiss: true, duration: 1500 }, content: { title: `Toast ${index} of 20` } });
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
return { args, queueStress, flood, clear };
|
|
181
|
+
},
|
|
182
|
+
template: `
|
|
183
|
+
<div style="padding: 2rem; min-height: 200px;">
|
|
184
|
+
<div style="display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
|
185
|
+
<button @click="queueStress" style="padding: 0.6rem 1.4rem; cursor: pointer; border: 1px solid #ccc; border-radius: 4px;">Queue worst-case toasts</button>
|
|
186
|
+
<button @click="flood" style="padding: 0.6rem 1.4rem; cursor: pointer; border: 1px solid #ccc; border-radius: 4px;">Flood 20 toasts</button>
|
|
187
|
+
<button @click="clear" style="padding: 0.6rem 1.4rem; cursor: pointer; border: 1px solid #ccc; border-radius: 4px;">Clear Queue</button>
|
|
188
|
+
</div>
|
|
189
|
+
<DisplayToastProvider
|
|
190
|
+
:position="args.position"
|
|
191
|
+
:alignment="args.alignment"
|
|
192
|
+
:full-width="args.fullWidth"
|
|
193
|
+
:max-visible="args.maxVisible"
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
`,
|
|
197
|
+
});
|
|
198
|
+
StressTest.storyName = "Stress Test (Worst-Case Data)";
|
|
199
|
+
StressTest.args = {
|
|
200
|
+
maxVisible: 5,
|
|
201
|
+
autoDismiss: true,
|
|
202
|
+
duration: 8000,
|
|
203
|
+
};
|
|
204
|
+
StressTest.parameters = {
|
|
205
|
+
docs: {
|
|
206
|
+
description: {
|
|
207
|
+
story:
|
|
208
|
+
"Hostile toast content: long German copy, an unbroken word as the title, a long URL, emoji first, HTML-like text " +
|
|
209
|
+
"(must render as text), RTL, a one-character title, empty title and empty description, plus a flood of 20 queued toasts. " +
|
|
210
|
+
"Toasts are fixed to the viewport, so use Storybook's viewport toolbar (not a canvas width) to check narrow screens. " +
|
|
211
|
+
"Check that every toast stays inside the viewport and caps at 48rem on wide screens, unbroken text wraps, hovering a toast " +
|
|
212
|
+
"or tabbing into it freezes its progress bar and timer, and the flood drains in order without stacking past maxVisible.",
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
};
|
|
@@ -10,6 +10,9 @@ const { useAppConfigMock } = vi.hoisted(() => ({
|
|
|
10
10
|
|
|
11
11
|
mockNuxtImport("useAppConfig", () => useAppConfigMock);
|
|
12
12
|
|
|
13
|
+
const mounted: { unmount: () => void }[] = [];
|
|
14
|
+
let focusVisibleSpy: { mockRestore: () => void } | null = null;
|
|
15
|
+
|
|
13
16
|
// Helper: mount the toast and activate it so the teleported element is in the DOM.
|
|
14
17
|
async function mountAndShow(props: Record<string, unknown> = {}, slots: Record<string, string> = {}) {
|
|
15
18
|
const { config, styleClassPassthrough, ...rest } = props;
|
|
@@ -17,6 +20,7 @@ async function mountAndShow(props: Record<string, unknown> = {}, slots: Record<s
|
|
|
17
20
|
props: { config, styleClassPassthrough } as Record<string, unknown>,
|
|
18
21
|
slots,
|
|
19
22
|
});
|
|
23
|
+
mounted.push(wrapper);
|
|
20
24
|
await wrapper.setProps({ ...rest, modelValue: true });
|
|
21
25
|
await nextTick();
|
|
22
26
|
return wrapper;
|
|
@@ -27,7 +31,11 @@ function toast() {
|
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
describe("DisplayToast", () => {
|
|
34
|
+
// Unmount first: a leftover toast's pending timers would otherwise re-render into the cleared body.
|
|
30
35
|
afterEach(() => {
|
|
36
|
+
mounted.splice(0).forEach((wrapper) => wrapper.unmount());
|
|
37
|
+
focusVisibleSpy?.mockRestore();
|
|
38
|
+
focusVisibleSpy = null;
|
|
31
39
|
document.body.innerHTML = "";
|
|
32
40
|
});
|
|
33
41
|
|
|
@@ -50,9 +58,9 @@ describe("DisplayToast", () => {
|
|
|
50
58
|
expect(toast()).not.toBeNull();
|
|
51
59
|
});
|
|
52
60
|
|
|
53
|
-
it("has
|
|
61
|
+
it("has data-state='show' when activated", async () => {
|
|
54
62
|
await mountAndShow();
|
|
55
|
-
expect(toast()!.
|
|
63
|
+
expect(toast()!.getAttribute("data-state")).toBe("show");
|
|
56
64
|
});
|
|
57
65
|
|
|
58
66
|
// ─── data-theme ───────────────────────────────────────────────────────────
|
|
@@ -83,26 +91,28 @@ describe("DisplayToast", () => {
|
|
|
83
91
|
expect(toast()!.getAttribute("aria-live")).toBe(live);
|
|
84
92
|
});
|
|
85
93
|
|
|
86
|
-
// ─── Position
|
|
94
|
+
// ─── Position hooks ───────────────────────────────────────────────────────
|
|
87
95
|
|
|
88
|
-
it.each(["top", "bottom"] as const)("
|
|
96
|
+
it.each(["top", "bottom"] as const)("sets data-position='%s'", async (position) => {
|
|
89
97
|
await mountAndShow({ config: { appearance: { position } } });
|
|
90
|
-
expect(toast()!.
|
|
98
|
+
expect(toast()!.getAttribute("data-position")).toBe(position);
|
|
91
99
|
});
|
|
92
100
|
|
|
93
|
-
it.each(["left", "center", "right"] as const)("
|
|
101
|
+
it.each(["left", "center", "right"] as const)("sets data-alignment='%s'", async (alignment) => {
|
|
94
102
|
await mountAndShow({ config: { appearance: { alignment } } });
|
|
95
|
-
expect(toast()!.
|
|
103
|
+
expect(toast()!.getAttribute("data-alignment")).toBe(alignment);
|
|
96
104
|
});
|
|
97
105
|
|
|
98
|
-
it("
|
|
99
|
-
await mountAndShow({ config: { appearance: { fullWidth: true } } });
|
|
100
|
-
expect(toast()!.
|
|
106
|
+
it("sets data-alignment='full-width' when fullWidth is true, overriding alignment", async () => {
|
|
107
|
+
await mountAndShow({ config: { appearance: { fullWidth: true, alignment: "left" } } });
|
|
108
|
+
expect(toast()!.getAttribute("data-alignment")).toBe("full-width");
|
|
101
109
|
});
|
|
102
110
|
|
|
103
|
-
it("
|
|
104
|
-
await mountAndShow({ config: { appearance: {
|
|
105
|
-
|
|
111
|
+
it("adds no generic position or state classes a consumer's CSS could match", async () => {
|
|
112
|
+
await mountAndShow({ config: { appearance: { position: "top", alignment: "center" } } });
|
|
113
|
+
for (const name of ["top", "bottom", "left", "center", "right", "show", "hide", "full-width"]) {
|
|
114
|
+
expect(toast()!.classList).not.toContain(name);
|
|
115
|
+
}
|
|
106
116
|
});
|
|
107
117
|
|
|
108
118
|
// ─── tabindex / aria-describedby ──────────────────────────────────────────
|
|
@@ -194,7 +204,7 @@ describe("DisplayToast", () => {
|
|
|
194
204
|
await nextTick();
|
|
195
205
|
toast()!.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
|
|
196
206
|
await nextTick();
|
|
197
|
-
expect(toast()!.
|
|
207
|
+
expect(toast()!.getAttribute("data-state")).toBe("hide");
|
|
198
208
|
});
|
|
199
209
|
|
|
200
210
|
// ─── Dismiss via close button ─────────────────────────────────────────────
|
|
@@ -205,7 +215,111 @@ describe("DisplayToast", () => {
|
|
|
205
215
|
await nextTick();
|
|
206
216
|
(document.querySelector("[data-test-id='alert-dismiss']") as HTMLElement).click();
|
|
207
217
|
await nextTick();
|
|
208
|
-
expect(toast()!.
|
|
218
|
+
expect(toast()!.getAttribute("data-state")).toBe("hide");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// ─── Dismiss label ────────────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
it("passes config.content.dismissLabel to the close button's screen-reader text", async () => {
|
|
224
|
+
await mountAndShow({ config: { behavior: { autoDismiss: false }, content: { dismissLabel: "Schließen" } } });
|
|
225
|
+
expect(document.querySelector("[data-test-id='alert-dismiss']")!.textContent).toContain("Schließen");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// ─── Auto-dismiss pause ──────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
const autoConfig = { behavior: { autoDismiss: true, duration: 1000 } };
|
|
231
|
+
|
|
232
|
+
// jsdom matches :focus-visible whenever an element is focused; real browsers only do after keyboard input.
|
|
233
|
+
const originalMatches = Element.prototype.matches;
|
|
234
|
+
function setFocusVisible(keyboard: boolean) {
|
|
235
|
+
const spy = vi.spyOn(Element.prototype, "matches");
|
|
236
|
+
spy.mockImplementation(function (this: Element, selector: string) {
|
|
237
|
+
return selector === ":focus-visible" ? keyboard : originalMatches.call(this, selector);
|
|
238
|
+
});
|
|
239
|
+
focusVisibleSpy = spy;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
it("auto-dismisses after the duration", async () => {
|
|
243
|
+
await mountAndShow({ config: autoConfig });
|
|
244
|
+
vi.advanceTimersByTime(1000);
|
|
245
|
+
await nextTick();
|
|
246
|
+
expect(toast()!.getAttribute("data-state")).toBe("hide");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("pauses auto-dismiss while hovered and resumes with the remaining time", async () => {
|
|
250
|
+
setFocusVisible(false);
|
|
251
|
+
await mountAndShow({ config: autoConfig });
|
|
252
|
+
vi.advanceTimersByTime(400);
|
|
253
|
+
toast()!.dispatchEvent(new Event("pointerenter"));
|
|
254
|
+
await nextTick();
|
|
255
|
+
expect(toast()!.hasAttribute("data-paused")).toBe(true);
|
|
256
|
+
vi.advanceTimersByTime(5000);
|
|
257
|
+
await nextTick();
|
|
258
|
+
expect(toast()!.getAttribute("data-state")).toBe("show");
|
|
259
|
+
toast()!.dispatchEvent(new Event("pointerleave"));
|
|
260
|
+
await nextTick();
|
|
261
|
+
expect(toast()!.hasAttribute("data-paused")).toBe(false);
|
|
262
|
+
vi.advanceTimersByTime(599);
|
|
263
|
+
await nextTick();
|
|
264
|
+
expect(toast()!.getAttribute("data-state")).toBe("show");
|
|
265
|
+
vi.advanceTimersByTime(1);
|
|
266
|
+
await nextTick();
|
|
267
|
+
expect(toast()!.getAttribute("data-state")).toBe("hide");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("pauses while focus is on something inside the toast", async () => {
|
|
271
|
+
await mountAndShow({ config: autoConfig }, { default: "<a href='#' class='inner-link'>Undo</a>" });
|
|
272
|
+
const link = document.querySelector(".inner-link") as HTMLElement;
|
|
273
|
+
link.dispatchEvent(new FocusEvent("focusin", { bubbles: true }));
|
|
274
|
+
await nextTick();
|
|
275
|
+
vi.advanceTimersByTime(5000);
|
|
276
|
+
await nextTick();
|
|
277
|
+
expect(toast()!.getAttribute("data-state")).toBe("show");
|
|
278
|
+
link.dispatchEvent(new FocusEvent("focusout", { bubbles: true, relatedTarget: null }));
|
|
279
|
+
await nextTick();
|
|
280
|
+
vi.advanceTimersByTime(1000);
|
|
281
|
+
await nextTick();
|
|
282
|
+
expect(toast()!.getAttribute("data-state")).toBe("hide");
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("does not pause when the toast takes focus after mouse input", async () => {
|
|
286
|
+
setFocusVisible(false);
|
|
287
|
+
await mountAndShow({ config: autoConfig });
|
|
288
|
+
vi.advanceTimersByTime(100); // the toast focuses itself
|
|
289
|
+
await nextTick();
|
|
290
|
+
expect(document.activeElement).toBe(toast());
|
|
291
|
+
expect(toast()!.hasAttribute("data-paused")).toBe(false);
|
|
292
|
+
vi.advanceTimersByTime(900);
|
|
293
|
+
await nextTick();
|
|
294
|
+
expect(toast()!.getAttribute("data-state")).toBe("hide");
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("pauses when the toast takes focus after keyboard input, until focus leaves", async () => {
|
|
298
|
+
setFocusVisible(true);
|
|
299
|
+
await mountAndShow({ config: autoConfig });
|
|
300
|
+
vi.advanceTimersByTime(100);
|
|
301
|
+
await nextTick();
|
|
302
|
+
expect(toast()!.hasAttribute("data-paused")).toBe(true);
|
|
303
|
+
vi.advanceTimersByTime(5000);
|
|
304
|
+
await nextTick();
|
|
305
|
+
expect(toast()!.getAttribute("data-state")).toBe("show");
|
|
306
|
+
(toast() as HTMLElement).blur();
|
|
307
|
+
await nextTick();
|
|
308
|
+
vi.advanceTimersByTime(900);
|
|
309
|
+
await nextTick();
|
|
310
|
+
expect(toast()!.getAttribute("data-state")).toBe("hide");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("does not let an earlier show's timer dismiss a toast re-shown after a manual dismiss", async () => {
|
|
314
|
+
const wrapper = await mountAndShow({ config: autoConfig });
|
|
315
|
+
await wrapper.setProps({ modelValue: false });
|
|
316
|
+
vi.advanceTimersByTime(550);
|
|
317
|
+
await nextTick();
|
|
318
|
+
await wrapper.setProps({ modelValue: true });
|
|
319
|
+
await nextTick();
|
|
320
|
+
vi.advanceTimersByTime(600);
|
|
321
|
+
await nextTick();
|
|
322
|
+
expect(toast()!.getAttribute("data-state")).toBe("show");
|
|
209
323
|
});
|
|
210
324
|
|
|
211
325
|
// ─── styleClassPassthrough ────────────────────────────────────────────────
|
|
@@ -236,8 +350,8 @@ describe("DisplayToast", () => {
|
|
|
236
350
|
it("uses hardcoded fallbacks when app.config has no displayToast key", async () => {
|
|
237
351
|
await mountAndShow();
|
|
238
352
|
expect(toast()!.getAttribute("data-theme")).toBe("info");
|
|
239
|
-
expect(toast()!.
|
|
240
|
-
expect(toast()!.
|
|
353
|
+
expect(toast()!.getAttribute("data-position")).toBe("top");
|
|
354
|
+
expect(toast()!.getAttribute("data-alignment")).toBe("right");
|
|
241
355
|
expect(document.querySelector(".display-toast-progress")).not.toBeNull();
|
|
242
356
|
});
|
|
243
357
|
|
|
@@ -260,7 +374,7 @@ describe("DisplayToast", () => {
|
|
|
260
374
|
}));
|
|
261
375
|
await mountAndShow({ config: { appearance: { theme: "warning", position: "top" } } });
|
|
262
376
|
expect(toast()!.getAttribute("data-theme")).toBe("warning");
|
|
263
|
-
expect(toast()!.
|
|
377
|
+
expect(toast()!.getAttribute("data-position")).toBe("top");
|
|
264
378
|
});
|
|
265
379
|
});
|
|
266
380
|
});
|
|
@@ -4,6 +4,15 @@ import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
|
4
4
|
import DisplayToastProvider from "../DisplayToastProvider.vue";
|
|
5
5
|
import { useToastQueue } from "~/composables/useToastQueue";
|
|
6
6
|
|
|
7
|
+
const mounted: { unmount: () => void }[] = [];
|
|
8
|
+
|
|
9
|
+
// Every provider shares the global queue, so a leftover one from an earlier test would run its own timers.
|
|
10
|
+
async function mountProvider(...args: Parameters<typeof mountSuspended<typeof DisplayToastProvider>>) {
|
|
11
|
+
const wrapper = await mountSuspended(...args);
|
|
12
|
+
mounted.push(wrapper);
|
|
13
|
+
return wrapper;
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
function item() {
|
|
8
17
|
return document.querySelector(".display-toast-provider-item");
|
|
9
18
|
}
|
|
@@ -24,6 +33,7 @@ describe("DisplayToastProvider", () => {
|
|
|
24
33
|
});
|
|
25
34
|
|
|
26
35
|
afterEach(() => {
|
|
36
|
+
mounted.splice(0).forEach((wrapper) => wrapper.unmount());
|
|
27
37
|
clear();
|
|
28
38
|
document.body.innerHTML = "";
|
|
29
39
|
});
|
|
@@ -31,47 +41,47 @@ describe("DisplayToastProvider", () => {
|
|
|
31
41
|
// ─── Mount ────────────────────────────────────────────────────────────────
|
|
32
42
|
|
|
33
43
|
it("mounts without error", async () => {
|
|
34
|
-
const wrapper = await
|
|
44
|
+
const wrapper = await mountProvider(DisplayToastProvider);
|
|
35
45
|
expect(wrapper.vm).toBeTruthy();
|
|
36
46
|
});
|
|
37
47
|
|
|
38
48
|
it("renders the provider container", async () => {
|
|
39
|
-
await
|
|
49
|
+
await mountProvider(DisplayToastProvider);
|
|
40
50
|
expect(provider()).not.toBeNull();
|
|
41
51
|
});
|
|
42
52
|
|
|
43
53
|
// ─── Empty state ──────────────────────────────────────────────────────────
|
|
44
54
|
|
|
45
55
|
it("renders no items when queue is empty", async () => {
|
|
46
|
-
await
|
|
56
|
+
await mountProvider(DisplayToastProvider);
|
|
47
57
|
expect(items().length).toBe(0);
|
|
48
58
|
});
|
|
49
59
|
|
|
50
60
|
// ─── Show / queue ─────────────────────────────────────────────────────────
|
|
51
61
|
|
|
52
62
|
it("promotes a pending entry to visible on show()", async () => {
|
|
53
|
-
await
|
|
63
|
+
await mountProvider(DisplayToastProvider);
|
|
54
64
|
show({ content: { text: "Hello" } });
|
|
55
65
|
await nextTick();
|
|
56
66
|
expect(items().length).toBe(1);
|
|
57
67
|
});
|
|
58
68
|
|
|
59
69
|
it("renders the toast message text", async () => {
|
|
60
|
-
await
|
|
70
|
+
await mountProvider(DisplayToastProvider);
|
|
61
71
|
show({ content: { text: "Toast message" } });
|
|
62
72
|
await nextTick();
|
|
63
73
|
expect(document.querySelector(".alert-content-body")!.textContent).toContain("Toast message");
|
|
64
74
|
});
|
|
65
75
|
|
|
66
76
|
it("renders the toast title", async () => {
|
|
67
|
-
await
|
|
77
|
+
await mountProvider(DisplayToastProvider);
|
|
68
78
|
show({ content: { title: "My Title" } });
|
|
69
79
|
await nextTick();
|
|
70
80
|
expect(document.querySelector("[data-test-id='alert-title']")!.textContent).toContain("My Title");
|
|
71
81
|
});
|
|
72
82
|
|
|
73
83
|
it("renders the toast description", async () => {
|
|
74
|
-
await
|
|
84
|
+
await mountProvider(DisplayToastProvider);
|
|
75
85
|
show({ content: { description: "My description" } });
|
|
76
86
|
await nextTick();
|
|
77
87
|
expect(document.querySelector("[data-test-id='alert-content']")!.textContent).toContain("My description");
|
|
@@ -82,7 +92,7 @@ describe("DisplayToastProvider", () => {
|
|
|
82
92
|
it.each(["info", "success", "warning", "error"] as const)(
|
|
83
93
|
"sets data-theme='%s' from config",
|
|
84
94
|
async (theme) => {
|
|
85
|
-
await
|
|
95
|
+
await mountProvider(DisplayToastProvider);
|
|
86
96
|
show({ appearance: { theme } });
|
|
87
97
|
await nextTick();
|
|
88
98
|
expect(item()!.getAttribute("data-theme")).toBe(theme);
|
|
@@ -90,7 +100,7 @@ describe("DisplayToastProvider", () => {
|
|
|
90
100
|
);
|
|
91
101
|
|
|
92
102
|
it("defaults to data-theme='info'", async () => {
|
|
93
|
-
await
|
|
103
|
+
await mountProvider(DisplayToastProvider);
|
|
94
104
|
show({});
|
|
95
105
|
await nextTick();
|
|
96
106
|
expect(item()!.getAttribute("data-theme")).toBe("info");
|
|
@@ -104,7 +114,7 @@ describe("DisplayToastProvider", () => {
|
|
|
104
114
|
{ theme: "warning" as const, role: "alert", live: "assertive" },
|
|
105
115
|
{ theme: "error" as const, role: "alert", live: "assertive" },
|
|
106
116
|
])("sets role=$role and aria-live=$live for theme='$theme'", async ({ theme, role, live }) => {
|
|
107
|
-
await
|
|
117
|
+
await mountProvider(DisplayToastProvider);
|
|
108
118
|
show({ appearance: { theme } });
|
|
109
119
|
await nextTick();
|
|
110
120
|
expect(item()!.getAttribute("role")).toBe(role);
|
|
@@ -112,48 +122,56 @@ describe("DisplayToastProvider", () => {
|
|
|
112
122
|
});
|
|
113
123
|
|
|
114
124
|
it("sets aria-describedby on each item", async () => {
|
|
115
|
-
await
|
|
125
|
+
await mountProvider(DisplayToastProvider);
|
|
116
126
|
show({});
|
|
117
127
|
await nextTick();
|
|
118
128
|
expect(item()!.getAttribute("aria-describedby")).not.toBeNull();
|
|
119
129
|
});
|
|
120
130
|
|
|
121
131
|
it("sets tabindex='0' on each item", async () => {
|
|
122
|
-
await
|
|
132
|
+
await mountProvider(DisplayToastProvider);
|
|
123
133
|
show({});
|
|
124
134
|
await nextTick();
|
|
125
135
|
expect(item()!.getAttribute("tabindex")).toBe("0");
|
|
126
136
|
});
|
|
127
137
|
|
|
128
|
-
// ─── Position / alignment
|
|
138
|
+
// ─── Position / alignment hooks ──────────────────────────────────────────
|
|
129
139
|
|
|
130
|
-
it.each(["top", "bottom"] as const)("
|
|
131
|
-
await
|
|
132
|
-
expect(provider()!.
|
|
140
|
+
it.each(["top", "bottom"] as const)("sets data-position='%s' on the provider", async (position) => {
|
|
141
|
+
await mountProvider(DisplayToastProvider, { props: { position } });
|
|
142
|
+
expect(provider()!.getAttribute("data-position")).toBe(position);
|
|
133
143
|
});
|
|
134
144
|
|
|
135
|
-
it.each(["left", "center", "right"] as const)(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
145
|
+
it.each(["left", "center", "right"] as const)("sets data-alignment='%s' on the provider", async (alignment) => {
|
|
146
|
+
await mountProvider(DisplayToastProvider, { props: { alignment } });
|
|
147
|
+
expect(provider()!.getAttribute("data-alignment")).toBe(alignment);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("sets data-alignment='full-width' when fullWidth is true, overriding alignment", async () => {
|
|
151
|
+
await mountProvider(DisplayToastProvider, { props: { fullWidth: true, alignment: "left" } });
|
|
152
|
+
expect(provider()!.getAttribute("data-alignment")).toBe("full-width");
|
|
153
|
+
});
|
|
142
154
|
|
|
143
|
-
it("
|
|
144
|
-
await
|
|
145
|
-
|
|
155
|
+
it("adds no generic position classes a consumer's CSS could match", async () => {
|
|
156
|
+
await mountProvider(DisplayToastProvider, { props: { position: "top", alignment: "center" } });
|
|
157
|
+
for (const name of ["top", "bottom", "left", "center", "right", "full-width"]) {
|
|
158
|
+
expect(provider()!.classList).not.toContain(name);
|
|
159
|
+
}
|
|
146
160
|
});
|
|
147
161
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
162
|
+
// ─── Dismiss label ────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
it("passes config.content.dismissLabel to the close button's screen-reader text", async () => {
|
|
165
|
+
await mountProvider(DisplayToastProvider);
|
|
166
|
+
show({ behavior: { autoDismiss: false }, content: { text: "Saved", dismissLabel: "Fermer" } });
|
|
167
|
+
await nextTick();
|
|
168
|
+
expect(document.querySelector("[data-test-id='alert-dismiss']")!.textContent).toContain("Fermer");
|
|
151
169
|
});
|
|
152
170
|
|
|
153
171
|
// ─── maxVisible ───────────────────────────────────────────────────────────
|
|
154
172
|
|
|
155
173
|
it("shows only one item at a time when maxVisible=1", async () => {
|
|
156
|
-
await
|
|
174
|
+
await mountProvider(DisplayToastProvider, { props: { maxVisible: 1 } });
|
|
157
175
|
show({ content: { text: "First" } });
|
|
158
176
|
show({ content: { text: "Second" } });
|
|
159
177
|
await nextTick();
|
|
@@ -162,7 +180,7 @@ describe("DisplayToastProvider", () => {
|
|
|
162
180
|
});
|
|
163
181
|
|
|
164
182
|
it("shows up to maxVisible items simultaneously", async () => {
|
|
165
|
-
await
|
|
183
|
+
await mountProvider(DisplayToastProvider, { props: { maxVisible: 2 } });
|
|
166
184
|
show({ content: { text: "First" } });
|
|
167
185
|
show({ content: { text: "Second" } });
|
|
168
186
|
show({ content: { text: "Third" } });
|
|
@@ -173,7 +191,7 @@ describe("DisplayToastProvider", () => {
|
|
|
173
191
|
// ─── Dismiss ──────────────────────────────────────────────────────────────
|
|
174
192
|
|
|
175
193
|
it("removes item when close button is clicked", async () => {
|
|
176
|
-
await
|
|
194
|
+
await mountProvider(DisplayToastProvider);
|
|
177
195
|
show({ behavior: { autoDismiss: false } });
|
|
178
196
|
await nextTick();
|
|
179
197
|
(document.querySelector("[data-test-id='alert-dismiss']") as HTMLElement).click();
|
|
@@ -182,7 +200,7 @@ describe("DisplayToastProvider", () => {
|
|
|
182
200
|
});
|
|
183
201
|
|
|
184
202
|
it("removes item when Escape is pressed", async () => {
|
|
185
|
-
await
|
|
203
|
+
await mountProvider(DisplayToastProvider);
|
|
186
204
|
show({ behavior: { autoDismiss: false } });
|
|
187
205
|
await nextTick();
|
|
188
206
|
item()!.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
|
|
@@ -193,7 +211,7 @@ describe("DisplayToastProvider", () => {
|
|
|
193
211
|
// ─── Auto-dismiss timer ───────────────────────────────────────────────────
|
|
194
212
|
|
|
195
213
|
it("auto-dismisses after duration", async () => {
|
|
196
|
-
await
|
|
214
|
+
await mountProvider(DisplayToastProvider);
|
|
197
215
|
show({ behavior: { autoDismiss: true, duration: 3000 } });
|
|
198
216
|
await nextTick();
|
|
199
217
|
expect(items().length).toBe(1);
|
|
@@ -202,8 +220,56 @@ describe("DisplayToastProvider", () => {
|
|
|
202
220
|
expect(items().length).toBe(0);
|
|
203
221
|
});
|
|
204
222
|
|
|
223
|
+
it("pauses auto-dismiss while hovered and resumes with the remaining time", async () => {
|
|
224
|
+
await mountProvider(DisplayToastProvider);
|
|
225
|
+
show({ behavior: { autoDismiss: true, duration: 1000 } });
|
|
226
|
+
await nextTick();
|
|
227
|
+
vi.advanceTimersByTime(400);
|
|
228
|
+
item()!.dispatchEvent(new Event("pointerenter"));
|
|
229
|
+
await nextTick();
|
|
230
|
+
expect(item()!.hasAttribute("data-paused")).toBe(true);
|
|
231
|
+
vi.advanceTimersByTime(5000);
|
|
232
|
+
await nextTick();
|
|
233
|
+
expect(items().length).toBe(1);
|
|
234
|
+
item()!.dispatchEvent(new Event("pointerleave"));
|
|
235
|
+
await nextTick();
|
|
236
|
+
vi.advanceTimersByTime(599);
|
|
237
|
+
await nextTick();
|
|
238
|
+
expect(items().length).toBe(1);
|
|
239
|
+
vi.advanceTimersByTime(1);
|
|
240
|
+
await nextTick();
|
|
241
|
+
expect(items().length).toBe(0);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("pauses while focus is on something inside a toast, and resumes when it leaves", async () => {
|
|
245
|
+
await mountProvider(DisplayToastProvider);
|
|
246
|
+
show({ behavior: { autoDismiss: true, duration: 1000 } });
|
|
247
|
+
await nextTick();
|
|
248
|
+
const inner = item()!.querySelector(".alert-content-body") as HTMLElement;
|
|
249
|
+
inner.dispatchEvent(new FocusEvent("focusin", { bubbles: true }));
|
|
250
|
+
await nextTick();
|
|
251
|
+
expect(item()!.hasAttribute("data-paused")).toBe(true);
|
|
252
|
+
vi.advanceTimersByTime(5000);
|
|
253
|
+
await nextTick();
|
|
254
|
+
expect(items().length).toBe(1);
|
|
255
|
+
inner.dispatchEvent(new FocusEvent("focusout", { bubbles: true, relatedTarget: null }));
|
|
256
|
+
await nextTick();
|
|
257
|
+
vi.advanceTimersByTime(1000);
|
|
258
|
+
await nextTick();
|
|
259
|
+
expect(items().length).toBe(0);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("does not mark a non-auto-dismiss toast as paused", async () => {
|
|
263
|
+
await mountProvider(DisplayToastProvider);
|
|
264
|
+
show({ behavior: { autoDismiss: false } });
|
|
265
|
+
await nextTick();
|
|
266
|
+
item()!.dispatchEvent(new Event("pointerenter"));
|
|
267
|
+
await nextTick();
|
|
268
|
+
expect(item()!.hasAttribute("data-paused")).toBe(false);
|
|
269
|
+
});
|
|
270
|
+
|
|
205
271
|
it("does not auto-dismiss when autoDismiss is false", async () => {
|
|
206
|
-
await
|
|
272
|
+
await mountProvider(DisplayToastProvider);
|
|
207
273
|
show({ behavior: { autoDismiss: false } });
|
|
208
274
|
await nextTick();
|
|
209
275
|
vi.advanceTimersByTime(10000);
|
|
@@ -214,7 +280,7 @@ describe("DisplayToastProvider", () => {
|
|
|
214
280
|
// ─── Queue progression ────────────────────────────────────────────────────
|
|
215
281
|
|
|
216
282
|
it("promotes next pending entry after current is dismissed", async () => {
|
|
217
|
-
await
|
|
283
|
+
await mountProvider(DisplayToastProvider, { props: { maxVisible: 1 } });
|
|
218
284
|
show({ content: { text: "First" }, behavior: { autoDismiss: false } });
|
|
219
285
|
show({ content: { text: "Second" }, behavior: { autoDismiss: false } });
|
|
220
286
|
await nextTick();
|
|
@@ -228,14 +294,14 @@ describe("DisplayToastProvider", () => {
|
|
|
228
294
|
// ─── Progress bar ─────────────────────────────────────────────────────────
|
|
229
295
|
|
|
230
296
|
it("renders progress bar when autoDismiss is true", async () => {
|
|
231
|
-
await
|
|
297
|
+
await mountProvider(DisplayToastProvider);
|
|
232
298
|
show({ behavior: { autoDismiss: true } });
|
|
233
299
|
await nextTick();
|
|
234
300
|
expect(document.querySelector(".display-toast-provider-progress")).not.toBeNull();
|
|
235
301
|
});
|
|
236
302
|
|
|
237
303
|
it("does not render progress bar when autoDismiss is false", async () => {
|
|
238
|
-
await
|
|
304
|
+
await mountProvider(DisplayToastProvider);
|
|
239
305
|
show({ behavior: { autoDismiss: false } });
|
|
240
306
|
await nextTick();
|
|
241
307
|
expect(document.querySelector(".display-toast-provider-progress")).toBeNull();
|
|
@@ -245,7 +311,7 @@ describe("DisplayToastProvider", () => {
|
|
|
245
311
|
|
|
246
312
|
it("removes all items on clear()", async () => {
|
|
247
313
|
const { clear: clearQueue } = useToastQueue();
|
|
248
|
-
await
|
|
314
|
+
await mountProvider(DisplayToastProvider, { props: { maxVisible: 3 } });
|
|
249
315
|
show({});
|
|
250
316
|
show({});
|
|
251
317
|
await nextTick();
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
| Token | Default | Controls |
|
|
8
8
|
|---|---|---|
|
|
9
|
-
| `--slider-gallery-height` | `100svh` | Gallery height
|
|
9
|
+
| `--slider-gallery-height` | `100svh` | Gallery height |
|
|
10
|
+
| `--slider-gallery-width` | `100vw` | Gallery width. Set `100%` to fill a positioned container instead of the viewport |
|
|
10
11
|
| `--slider-gallery-z-index` | `9999` | Stacking order (full-screen takeover) |
|
|
11
12
|
| `--slider-gallery-accent` | `#f1683a` | Loading spinner and progress bar colour |
|
|
12
13
|
| `--slider-gallery-loading-background` | `var(--page-bg)` | Loading overlay background |
|
|
@@ -25,6 +26,15 @@
|
|
|
25
26
|
| `--slider-gallery-cta-background` | `#99999975` | See-more link background |
|
|
26
27
|
| `--slider-gallery-cta-border-colour` | `#fff` | See-more link border |
|
|
27
28
|
| `--slider-gallery-cta-colour` | `#fff` | See-more link text |
|
|
29
|
+
| `--slider-gallery-author-line-clamp` | `none` | Max lines of the stylist line |
|
|
30
|
+
| `--slider-gallery-title-line-clamp` | `none` | Max lines of the title |
|
|
31
|
+
| `--slider-gallery-topic-line-clamp` | `none` | Max lines of the category line |
|
|
32
|
+
| `--slider-gallery-description-line-clamp` | `none` | Max lines of the description |
|
|
33
|
+
|
|
34
|
+
Line-clamp tokens: `1` is single-line ellipsis, any other number caps the lines with an ellipsis
|
|
35
|
+
on the last, `none` shows everything. All slide text wraps long unbroken strings (`overflow-wrap: anywhere`).
|
|
36
|
+
The slide text is absolutely positioned, so very long copy can run into the thumbnails and arrows:
|
|
37
|
+
clamp the description if your CMS copy is unbounded.
|
|
28
38
|
|
|
29
39
|
### Thumbnails
|
|
30
40
|
|
|
@@ -43,6 +53,11 @@
|
|
|
43
53
|
| `--slider-gallery-thumbnail-outline-offset-hover` | the resting offset | Outline gap on hover |
|
|
44
54
|
| `--slider-gallery-thumbnail-border-radius` | `20px` | Thumbnail rounding |
|
|
45
55
|
| `--slider-gallery-thumbnail-overlay` | `#0004` | Tint over thumbnail images |
|
|
56
|
+
| `--slider-gallery-thumbnail-title-line-clamp` | `2` | Max lines of a thumbnail title |
|
|
57
|
+
| `--slider-gallery-thumbnail-description-line-clamp` | `2` | Max lines of a thumbnail description |
|
|
58
|
+
|
|
59
|
+
The thumbnail clamps default to `2` because the card is a fixed size with its text anchored to the
|
|
60
|
+
bottom: uncapped, long text grows upwards and its first lines are cut off.
|
|
46
61
|
|
|
47
62
|
### Arrows
|
|
48
63
|
|
|
@@ -83,6 +98,9 @@ Private (not public API): `--_animation-duration` (from the `animationDuration`
|
|
|
83
98
|
`--_arrow-border-*`/`--_arrow-outline-*` (resting values the hover tokens fall back to) and
|
|
84
99
|
`--_arrows-top`/`-right`/`-width`/`-max-width` (the resolved, breakpoint-switched row position).
|
|
85
100
|
|
|
101
|
+
> Changed 2026-10-06: added `--slider-gallery-width` (was a fixed `100vw`) and the line-clamp tokens;
|
|
102
|
+
> slide, thumbnail, see-more and loading text now wrap unbroken strings.
|
|
103
|
+
|
|
86
104
|
> Changed 2026-09-28: the thumbnail sizing/border/radius overrides used to be private names
|
|
87
105
|
> (`--_thumbnailMobileWidth`, `--_thumbnailBorderRadius`, etc.) with no public token; colours were
|
|
88
106
|
> hardcoded. The arrow icon is now sized by `--slider-gallery-arrow-icon-size` (width/height), separately from the button, and the `--_translate-x` icon nudge was removed. The wide thumbnail size now also applies to the image
|