srcdev-nuxt-components 9.6.4 → 9.6.6
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/rules/consumer/a11y-no-focus-steal.md +14 -0
- package/.claude/skills/component-compliance-checklist.md +14 -2
- package/.claude/skills/components/display-toast.md +6 -3
- package/.claude/skills/components/progress-tracker.md +140 -0
- package/.claude/skills/components/social-icons-list.md +32 -5
- package/.claude/skills/components/stepper-list.md +10 -3
- package/.claude/skills/components/tabbed-content.md +4 -0
- package/.claude/skills/css-dynamic-border-radius.md +114 -0
- package/.claude/skills/index.md +4 -2
- package/.claude/skills/storybook-add-story.md +12 -0
- package/.vscode/srcdev-component-display-toast.code-snippets +6 -8
- package/.vscode/srcdev-component-progress-tracker.code-snippets +45 -0
- package/.vscode/srcdev-component-social-icons-list.code-snippets +14 -0
- package/.vscode/srcdev-component-stepper-list.code-snippets +3 -2
- package/.vscode/srcdev-component-tabbed-content.code-snippets +1 -0
- package/.vscode/srcdev-utility-dynamic-border-radius.code-snippets +31 -0
- package/app/assets/styles/setup/06.utility-classes/_dynamic-border-radius.css +27 -0
- package/app/assets/styles/setup/06.utility-classes/index.css +1 -0
- package/app/components/01.atoms/tabbed-content/CONSUMER-STYLING.md +5 -1
- package/app/components/01.atoms/tabbed-content/TabbedContent.vue +2 -2
- package/app/components/01.atoms/tabbed-content/stories/TabbedContent.stories.ts +26 -0
- package/app/components/02.molecules/contact-section/tests/__snapshots__/ContactSection.spec.ts.snap +28 -28
- package/app/components/02.molecules/progress-tracker/CONSUMER-STYLING.md +116 -0
- package/app/components/02.molecules/progress-tracker/ProgressTracker.vue +275 -0
- package/app/components/02.molecules/progress-tracker/stories/ProgressTracker.stories.ts +424 -0
- package/app/components/02.molecules/progress-tracker/tests/ProgressTracker.spec.ts +253 -0
- package/app/components/02.molecules/progress-tracker/tests/__snapshots__/ProgressTracker.spec.ts.snap +46 -0
- package/app/components/02.molecules/social-icons-list/CONSUMER-STYLING.md +97 -8
- package/app/components/02.molecules/social-icons-list/SocialIconsList.vue +80 -14
- package/app/components/02.molecules/social-icons-list/stories/SocialIconsList.stories.ts +230 -2
- package/app/components/02.molecules/social-icons-list/stories/social-icons-list-recipes.css +108 -0
- package/app/components/02.molecules/social-icons-list/tests/SocialIconsList.spec.ts +123 -52
- package/app/components/02.molecules/social-icons-list/tests/__snapshots__/SocialIconsList.spec.ts.snap +10 -4
- package/app/components/02.molecules/stepper-list/CONSUMER-STYLING.md +42 -7
- package/app/components/02.molecules/stepper-list/StepperList.vue +42 -19
- package/app/components/02.molecules/stepper-list/stories/StepperList.stories.ts +288 -2
- package/app/components/02.molecules/stepper-list/tests/StepperList.spec.ts +161 -34
- package/app/components/02.molecules/stepper-list/tests/__snapshots__/StepperList.spec.ts.snap +40 -40
- package/app/components/02.molecules/toast/CONSUMER-STYLING.md +9 -3
- package/app/components/02.molecules/toast/DisplayToast.vue +10 -10
- package/app/components/02.molecules/toast/DisplayToastProvider.vue +12 -4
- package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +200 -2
- package/app/components/02.molecules/toast/tests/DisplayToast.spec.ts +33 -12
- package/app/components/02.molecules/toast/tests/DisplayToastProvider.spec.ts +20 -0
- package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +7 -7
- package/app/types/components/index.ts +1 -0
- package/app/types/components/progress-tracker.d.ts +6 -0
- package/app/types/components/social-icons-list.d.ts +9 -0
- package/package.json +1 -1
|
@@ -1,12 +1,56 @@
|
|
|
1
1
|
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
-
import { ref, onMounted } from "vue";
|
|
2
|
+
import { ref, computed, watchEffect, onMounted, onUnmounted } from "vue";
|
|
3
3
|
import StorybookComponent from "../DisplayToast.vue";
|
|
4
4
|
|
|
5
|
+
type CanvasKey = "none" | "gradient" | "lake" | "portrait" | "dark-photo" | "stripes" | "page-text";
|
|
6
|
+
|
|
7
|
+
const busyBackdrop = [
|
|
8
|
+
"repeating-linear-gradient(45deg, oklch(100% 0 0 / 0.18) 0 1.2rem, transparent 1.2rem 2.4rem)",
|
|
9
|
+
"linear-gradient(120deg, oklch(70% 0.2 30), oklch(75% 0.18 90) 35%, oklch(65% 0.2 200) 70%, oklch(55% 0.22 300))",
|
|
10
|
+
].join(", ");
|
|
11
|
+
|
|
12
|
+
const canvases: Record<CanvasKey, string> = {
|
|
13
|
+
none: "",
|
|
14
|
+
gradient: "background: linear-gradient(135deg, oklch(40% 0.15 250), oklch(25% 0.1 300));",
|
|
15
|
+
lake: "background: url('/images/banners/video/lake-banner.jpg') center / cover;",
|
|
16
|
+
portrait: "background: url('/images/page/hero/hero-blonde.jpg') center 30% / cover;",
|
|
17
|
+
"dark-photo": "background: url('/images/page/hero/hero-dark.jpg') center / cover;",
|
|
18
|
+
stripes: `background: ${busyBackdrop};`,
|
|
19
|
+
"page-text": "background: white;",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const pageText = Array.from(
|
|
23
|
+
{ length: 6 },
|
|
24
|
+
() =>
|
|
25
|
+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. " +
|
|
26
|
+
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. " +
|
|
27
|
+
"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur."
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
/*
|
|
31
|
+
* The toast teleports to <body>, so a fill override on a story wrapper never reaches it. Inject a
|
|
32
|
+
* rule on .display-toast instead: --theme-surface-subtle then resolves against the toast's data-theme.
|
|
33
|
+
*/
|
|
34
|
+
function useMaskedFillStyle(fill: () => number) {
|
|
35
|
+
const styleEl = document.createElement("style");
|
|
36
|
+
document.head.appendChild(styleEl);
|
|
37
|
+
watchEffect(() => {
|
|
38
|
+
styleEl.textContent =
|
|
39
|
+
fill() === 80
|
|
40
|
+
? ""
|
|
41
|
+
: `.display-toast { --alert-masked-content-background: color-mix(in oklab, var(--theme-surface-subtle) ${fill()}%, transparent); }`;
|
|
42
|
+
});
|
|
43
|
+
onUnmounted(() => styleEl.remove());
|
|
44
|
+
}
|
|
45
|
+
|
|
5
46
|
interface ToastStoryArgs {
|
|
6
47
|
theme: "info" | "success" | "warning" | "error";
|
|
7
48
|
position: "top" | "bottom";
|
|
8
49
|
alignment: "left" | "center" | "right";
|
|
9
50
|
fullWidth: boolean;
|
|
51
|
+
masked: boolean;
|
|
52
|
+
maskedFill: number;
|
|
53
|
+
canvas: CanvasKey;
|
|
10
54
|
autoDismiss: boolean;
|
|
11
55
|
duration: number;
|
|
12
56
|
revealDuration: number;
|
|
@@ -42,6 +86,24 @@ export default {
|
|
|
42
86
|
description: "Whether toast spans full width",
|
|
43
87
|
table: { category: "Appearance" },
|
|
44
88
|
},
|
|
89
|
+
masked: {
|
|
90
|
+
control: { type: "boolean" },
|
|
91
|
+
description: "Render with AlertMaskedContent (SVG border, translucent fill) instead of AlertContent",
|
|
92
|
+
table: { category: "Appearance" },
|
|
93
|
+
},
|
|
94
|
+
maskedFill: {
|
|
95
|
+
control: { type: "range", min: 0, max: 100, step: 5 },
|
|
96
|
+
description:
|
|
97
|
+
"Story-only: opacity (%) of the masked fill, set via --alert-masked-content-background. 80 is the library default; lower values cost contrast",
|
|
98
|
+
if: { arg: "masked" },
|
|
99
|
+
table: { category: "Story canvas" },
|
|
100
|
+
},
|
|
101
|
+
canvas: {
|
|
102
|
+
control: { type: "select" },
|
|
103
|
+
options: ["none", "gradient", "lake", "portrait", "dark-photo", "stripes", "page-text"],
|
|
104
|
+
description: "Story-only: full-viewport backdrop behind the toast, to show what the masked variant lets through",
|
|
105
|
+
table: { category: "Story canvas" },
|
|
106
|
+
},
|
|
45
107
|
autoDismiss: {
|
|
46
108
|
control: { type: "boolean" },
|
|
47
109
|
description: "Auto dismiss toast after duration",
|
|
@@ -80,6 +142,9 @@ export default {
|
|
|
80
142
|
position: "top",
|
|
81
143
|
alignment: "right",
|
|
82
144
|
fullWidth: false,
|
|
145
|
+
masked: false,
|
|
146
|
+
maskedFill: 80,
|
|
147
|
+
canvas: "none",
|
|
83
148
|
autoDismiss: false,
|
|
84
149
|
duration: 3000,
|
|
85
150
|
revealDuration: 300,
|
|
@@ -104,9 +169,21 @@ const Template: StoryFn<ToastStoryArgs> = (args) => ({
|
|
|
104
169
|
}, 500);
|
|
105
170
|
});
|
|
106
171
|
|
|
107
|
-
|
|
172
|
+
useMaskedFillStyle(() => args.maskedFill);
|
|
173
|
+
const canvasStyle = computed(() => canvases[args.canvas]);
|
|
174
|
+
|
|
175
|
+
return { args, isActive, canvasStyle, pageText };
|
|
108
176
|
},
|
|
109
177
|
template: `
|
|
178
|
+
<div
|
|
179
|
+
v-if="args.canvas !== 'none'"
|
|
180
|
+
:style="canvasStyle + ' position: fixed; inset: 0; overflow: hidden; padding: 6rem 2rem 2rem; font-family: sans-serif; font-size: 1.6rem; line-height: 1.5; color: #222;'"
|
|
181
|
+
>
|
|
182
|
+
<template v-if="args.canvas === 'page-text'">
|
|
183
|
+
<h2 style="margin: 0 0 1rem;">Article heading behind the toast</h2>
|
|
184
|
+
<p v-for="(para, i) in pageText" :key="i" style="margin: 0 0 1rem;">{{ para }}</p>
|
|
185
|
+
</template>
|
|
186
|
+
</div>
|
|
110
187
|
<div style="position: relative; height: 200px; padding: 20px;">
|
|
111
188
|
<button
|
|
112
189
|
@click="isActive = true"
|
|
@@ -123,6 +200,7 @@ const Template: StoryFn<ToastStoryArgs> = (args) => ({
|
|
|
123
200
|
position: args.position,
|
|
124
201
|
alignment: args.alignment,
|
|
125
202
|
fullWidth: args.fullWidth,
|
|
203
|
+
masked: args.masked,
|
|
126
204
|
},
|
|
127
205
|
behavior: {
|
|
128
206
|
autoDismiss: args.autoDismiss,
|
|
@@ -220,3 +298,123 @@ StressTest.parameters = {
|
|
|
220
298
|
},
|
|
221
299
|
},
|
|
222
300
|
};
|
|
301
|
+
|
|
302
|
+
export const MaskedOverGradient = Template.bind({});
|
|
303
|
+
MaskedOverGradient.storyName = "Masked: Over Gradient";
|
|
304
|
+
MaskedOverGradient.args = {
|
|
305
|
+
masked: true,
|
|
306
|
+
canvas: "gradient",
|
|
307
|
+
title: "Masked toast",
|
|
308
|
+
description: "AlertMaskedContent: the accent border is an SVG cut-out and the fill is 80% translucent.",
|
|
309
|
+
};
|
|
310
|
+
MaskedOverGradient.parameters = {
|
|
311
|
+
docs: {
|
|
312
|
+
description: {
|
|
313
|
+
story:
|
|
314
|
+
"`appearance.masked: true` swaps AlertContent for AlertMaskedContent. Over a flat gradient the effect is subtle: " +
|
|
315
|
+
"switch `canvas` to a photo or the stripes to see the backdrop through the fill.",
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
export const MaskedOverPhoto = Template.bind({});
|
|
321
|
+
MaskedOverPhoto.storyName = "Masked: Over Photo";
|
|
322
|
+
MaskedOverPhoto.args = {
|
|
323
|
+
theme: "success",
|
|
324
|
+
masked: true,
|
|
325
|
+
canvas: "portrait",
|
|
326
|
+
title: "Booking confirmed",
|
|
327
|
+
description: "We've sent the details to your inbox.",
|
|
328
|
+
customIcon: "akar-icons:check-box",
|
|
329
|
+
};
|
|
330
|
+
MaskedOverPhoto.parameters = {
|
|
331
|
+
docs: {
|
|
332
|
+
description: {
|
|
333
|
+
story:
|
|
334
|
+
"A photo backdrop is where the masked variant earns its place: the image stays visible through the body and around the border. " +
|
|
335
|
+
"Try `lake` and `dark-photo` too, and every theme.",
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
export const MaskedVsOpaque: StoryFn<ToastStoryArgs> = (args) => ({
|
|
341
|
+
components: { StorybookComponent },
|
|
342
|
+
setup() {
|
|
343
|
+
const isActive = ref(false);
|
|
344
|
+
onMounted(() => {
|
|
345
|
+
setTimeout(() => {
|
|
346
|
+
isActive.value = true;
|
|
347
|
+
}, 500);
|
|
348
|
+
});
|
|
349
|
+
useMaskedFillStyle(() => args.maskedFill);
|
|
350
|
+
const canvasStyle = computed(() => canvases[args.canvas]);
|
|
351
|
+
const config = (masked: boolean, alignment: "left" | "right") => ({
|
|
352
|
+
appearance: { theme: args.theme, position: args.position, alignment, masked },
|
|
353
|
+
behavior: { autoDismiss: false, revealDuration: args.revealDuration },
|
|
354
|
+
content: { title: masked ? "AlertMaskedContent" : "AlertContent", description: args.description },
|
|
355
|
+
});
|
|
356
|
+
return { args, isActive, canvasStyle, config };
|
|
357
|
+
},
|
|
358
|
+
template: `
|
|
359
|
+
<div :style="canvasStyle + ' position: fixed; inset: 0;'"></div>
|
|
360
|
+
<div style="position: relative; padding: 20px;">
|
|
361
|
+
<button
|
|
362
|
+
@click="isActive = true"
|
|
363
|
+
style="padding: 8px 16px; background: #0066cc; color: white; border: none; border-radius: 4px; cursor: pointer;"
|
|
364
|
+
>
|
|
365
|
+
Trigger Toasts
|
|
366
|
+
</button>
|
|
367
|
+
</div>
|
|
368
|
+
<StorybookComponent v-model="isActive" :config="config(false, 'left')" />
|
|
369
|
+
<StorybookComponent v-model="isActive" :config="config(true, 'right')" />
|
|
370
|
+
`,
|
|
371
|
+
});
|
|
372
|
+
MaskedVsOpaque.storyName = "Masked: Side by Side with Opaque";
|
|
373
|
+
MaskedVsOpaque.args = {
|
|
374
|
+
masked: true,
|
|
375
|
+
canvas: "lake",
|
|
376
|
+
description: "Same theme, same backdrop: opaque on the left, masked on the right.",
|
|
377
|
+
};
|
|
378
|
+
MaskedVsOpaque.argTypes = {
|
|
379
|
+
masked: { table: { disable: true } },
|
|
380
|
+
alignment: { table: { disable: true } },
|
|
381
|
+
fullWidth: { table: { disable: true } },
|
|
382
|
+
title: { table: { disable: true } },
|
|
383
|
+
customIcon: { table: { disable: true } },
|
|
384
|
+
autoDismiss: { table: { disable: true } },
|
|
385
|
+
duration: { table: { disable: true } },
|
|
386
|
+
};
|
|
387
|
+
MaskedVsOpaque.parameters = {
|
|
388
|
+
docs: {
|
|
389
|
+
description: {
|
|
390
|
+
story:
|
|
391
|
+
"Two toasts over one backdrop: AlertContent (left) and AlertMaskedContent (right). Change `canvas` and `theme` to compare, " +
|
|
392
|
+
"and drag `maskedFill` to see how much backdrop the masked one lets through.",
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
export const MaskedLowContrast = Template.bind({});
|
|
398
|
+
MaskedLowContrast.storyName = "Masked: Low Contrast Risk";
|
|
399
|
+
MaskedLowContrast.args = {
|
|
400
|
+
theme: "info",
|
|
401
|
+
masked: true,
|
|
402
|
+
maskedFill: 35,
|
|
403
|
+
canvas: "page-text",
|
|
404
|
+
alignment: "center",
|
|
405
|
+
title: "Hard to read",
|
|
406
|
+
description: "Page text shows through a 35% fill and fights with the toast's own text.",
|
|
407
|
+
};
|
|
408
|
+
MaskedLowContrast.parameters = {
|
|
409
|
+
docs: {
|
|
410
|
+
description: {
|
|
411
|
+
story:
|
|
412
|
+
"What can go wrong. The default 80% fill keeps `--theme-text` readable over most backdrops, but contrast depends on what's behind the toast, " +
|
|
413
|
+
"which you don't control once it's fixed to the viewport. Here the fill is lowered to 35% over body copy, so letterforms from the page " +
|
|
414
|
+
"show through and compete with the message. Other traps to try: `stripes` or `portrait` with a low fill (busy detail behind small text), " +
|
|
415
|
+
"and the `info` theme over `gradient` (the 1px accent border nearly vanishes against a backdrop of a similar hue). " +
|
|
416
|
+
"Drag `maskedFill` back up to 80 to see the default recover. If you lower `--alert-masked-content-background`, test it over your real imagery and content, " +
|
|
417
|
+
"and prefer the opaque variant for error and warning toasts that must be read.",
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
};
|
|
@@ -198,21 +198,35 @@ describe("DisplayToast", () => {
|
|
|
198
198
|
|
|
199
199
|
// ─── Dismiss via Escape key ────────────────────────────────────────────────
|
|
200
200
|
|
|
201
|
-
it("transitions to hide class when Escape is pressed", async () => {
|
|
201
|
+
it("transitions to hide class when Escape is pressed on the toast", async () => {
|
|
202
202
|
await mountAndShow({ config: { behavior: { autoDismiss: false } } });
|
|
203
|
-
vi.advanceTimersByTime(
|
|
204
|
-
await nextTick();
|
|
203
|
+
vi.advanceTimersByTime(1);
|
|
205
204
|
toast()!.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
|
|
206
205
|
await nextTick();
|
|
207
206
|
expect(toast()!.getAttribute("data-state")).toBe("hide");
|
|
208
207
|
});
|
|
209
208
|
|
|
209
|
+
it("dismisses on Escape pressed anywhere in the document, since it never takes focus", async () => {
|
|
210
|
+
await mountAndShow({ config: { behavior: { autoDismiss: false } } });
|
|
211
|
+
document.body.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
|
|
212
|
+
await nextTick();
|
|
213
|
+
expect(toast()!.getAttribute("data-state")).toBe("hide");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("ignores an Escape another handler has already claimed", async () => {
|
|
217
|
+
await mountAndShow({ config: { behavior: { autoDismiss: false } } });
|
|
218
|
+
const event = new KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true });
|
|
219
|
+
event.preventDefault();
|
|
220
|
+
document.body.dispatchEvent(event);
|
|
221
|
+
await nextTick();
|
|
222
|
+
expect(toast()!.getAttribute("data-state")).toBe("show");
|
|
223
|
+
});
|
|
224
|
+
|
|
210
225
|
// ─── Dismiss via close button ─────────────────────────────────────────────
|
|
211
226
|
|
|
212
227
|
it("transitions to hide class when the close button is clicked", async () => {
|
|
213
228
|
await mountAndShow({ config: { behavior: { autoDismiss: false } } });
|
|
214
|
-
vi.advanceTimersByTime(
|
|
215
|
-
await nextTick();
|
|
229
|
+
vi.advanceTimersByTime(1);
|
|
216
230
|
(document.querySelector("[data-test-id='alert-dismiss']") as HTMLElement).click();
|
|
217
231
|
await nextTick();
|
|
218
232
|
expect(toast()!.getAttribute("data-state")).toBe("hide");
|
|
@@ -282,22 +296,29 @@ describe("DisplayToast", () => {
|
|
|
282
296
|
expect(toast()!.getAttribute("data-state")).toBe("hide");
|
|
283
297
|
});
|
|
284
298
|
|
|
285
|
-
it("does not
|
|
299
|
+
it("does not move focus to itself when shown", async () => {
|
|
300
|
+
await mountAndShow({ config: autoConfig });
|
|
301
|
+
vi.advanceTimersByTime(500);
|
|
302
|
+
await nextTick();
|
|
303
|
+
expect(document.activeElement).not.toBe(toast());
|
|
304
|
+
expect(toast()!.hasAttribute("data-paused")).toBe(false);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("does not pause when focused after mouse input", async () => {
|
|
286
308
|
setFocusVisible(false);
|
|
287
309
|
await mountAndShow({ config: autoConfig });
|
|
288
|
-
|
|
310
|
+
(toast() as HTMLElement).focus();
|
|
289
311
|
await nextTick();
|
|
290
|
-
expect(document.activeElement).toBe(toast());
|
|
291
312
|
expect(toast()!.hasAttribute("data-paused")).toBe(false);
|
|
292
|
-
vi.advanceTimersByTime(
|
|
313
|
+
vi.advanceTimersByTime(1000);
|
|
293
314
|
await nextTick();
|
|
294
315
|
expect(toast()!.getAttribute("data-state")).toBe("hide");
|
|
295
316
|
});
|
|
296
317
|
|
|
297
|
-
it("pauses when
|
|
318
|
+
it("pauses when a keyboard user focuses the toast, until focus leaves", async () => {
|
|
298
319
|
setFocusVisible(true);
|
|
299
320
|
await mountAndShow({ config: autoConfig });
|
|
300
|
-
|
|
321
|
+
(toast() as HTMLElement).focus();
|
|
301
322
|
await nextTick();
|
|
302
323
|
expect(toast()!.hasAttribute("data-paused")).toBe(true);
|
|
303
324
|
vi.advanceTimersByTime(5000);
|
|
@@ -305,7 +326,7 @@ describe("DisplayToast", () => {
|
|
|
305
326
|
expect(toast()!.getAttribute("data-state")).toBe("show");
|
|
306
327
|
(toast() as HTMLElement).blur();
|
|
307
328
|
await nextTick();
|
|
308
|
-
vi.advanceTimersByTime(
|
|
329
|
+
vi.advanceTimersByTime(1000);
|
|
309
330
|
await nextTick();
|
|
310
331
|
expect(toast()!.getAttribute("data-state")).toBe("hide");
|
|
311
332
|
});
|
|
@@ -208,6 +208,26 @@ describe("DisplayToastProvider", () => {
|
|
|
208
208
|
expect(items().length).toBe(0);
|
|
209
209
|
});
|
|
210
210
|
|
|
211
|
+
it("does not move focus to a toast when it appears", async () => {
|
|
212
|
+
await mountProvider(DisplayToastProvider);
|
|
213
|
+
show({ behavior: { autoDismiss: false } });
|
|
214
|
+
await nextTick();
|
|
215
|
+
await nextTick();
|
|
216
|
+
expect(document.activeElement).not.toBe(item());
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("dismisses only the newest toast when Escape is pressed outside the toasts", async () => {
|
|
220
|
+
await mountProvider(DisplayToastProvider, { props: { maxVisible: 2 } });
|
|
221
|
+
show({ content: { text: "First" }, behavior: { autoDismiss: false } });
|
|
222
|
+
show({ content: { text: "Second" }, behavior: { autoDismiss: false } });
|
|
223
|
+
await nextTick();
|
|
224
|
+
expect(items().length).toBe(2);
|
|
225
|
+
document.body.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
|
|
226
|
+
await nextTick();
|
|
227
|
+
expect(items().length).toBe(1);
|
|
228
|
+
expect(item()!.textContent).toContain("First");
|
|
229
|
+
});
|
|
230
|
+
|
|
211
231
|
// ─── Auto-dismiss timer ───────────────────────────────────────────────────
|
|
212
232
|
|
|
213
233
|
it("auto-dismisses after duration", async () => {
|
|
@@ -37,22 +37,22 @@ exports[`ServiceDetail > renders correct HTML structure 1`] = `
|
|
|
37
37
|
<h2 class="hero-text mb-20 subheading axis-horizontal">
|
|
38
38
|
<!--v-if--><span class="text-block-0 normal">The Process</span>
|
|
39
39
|
</h2>
|
|
40
|
-
<ol class="stepper-list has-connectors service-detail__process">
|
|
41
|
-
<li class="indicator-top indicator-circle" style="--_connector-top: 0px; --_connector-height: 0px;">
|
|
40
|
+
<ol role="list" class="stepper-list has-connectors service-detail__process">
|
|
41
|
+
<li class="stepper-list-item indicator-top indicator-circle" style="--_connector-left: 0px; --_connector-top: 0px; --_connector-height: 0px;">
|
|
42
42
|
<div class="stepper-list__indicator-counter"></div>
|
|
43
|
-
<div>
|
|
43
|
+
<div class="stepper-list-content">
|
|
44
44
|
<p class="page-body-normal">Consultation</p>
|
|
45
45
|
</div>
|
|
46
46
|
</li>
|
|
47
|
-
<li class="indicator-top indicator-circle" style="--_connector-top: 0px; --_connector-height: 0px;">
|
|
47
|
+
<li class="stepper-list-item indicator-top indicator-circle" style="--_connector-left: 0px; --_connector-top: 0px; --_connector-height: 0px;">
|
|
48
48
|
<div class="stepper-list__indicator-counter"></div>
|
|
49
|
-
<div>
|
|
49
|
+
<div class="stepper-list-content">
|
|
50
50
|
<p class="page-body-normal">Sectioning</p>
|
|
51
51
|
</div>
|
|
52
52
|
</li>
|
|
53
|
-
<li class="indicator-top indicator-circle">
|
|
53
|
+
<li class="stepper-list-item indicator-top indicator-circle">
|
|
54
54
|
<div class="stepper-list__indicator-counter"></div>
|
|
55
|
-
<div>
|
|
55
|
+
<div class="stepper-list-content">
|
|
56
56
|
<p class="page-body-normal">Painting</p>
|
|
57
57
|
</div>
|
|
58
58
|
</li>
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { VNode } from "vue";
|
|
2
|
+
|
|
1
3
|
export interface ISocialIcon {
|
|
2
4
|
networkName: string;
|
|
3
5
|
iconName: string;
|
|
@@ -5,4 +7,11 @@ export interface ISocialIcon {
|
|
|
5
7
|
profileId: string;
|
|
6
8
|
/** Accessible name for the link; overrides the list's linkLabelTemplate. */
|
|
7
9
|
label?: string;
|
|
10
|
+
/** Brand colour, exposed as --social-icons-list-brand-colour on this item for recipes to use. */
|
|
11
|
+
brandColour?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SocialIconsListSlots {
|
|
15
|
+
icon?(props: { item: ISocialIcon }): VNode[];
|
|
16
|
+
label?(props: { item: ISocialIcon }): VNode[];
|
|
8
17
|
}
|