srcdev-nuxt-components 9.6.5 → 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.
Files changed (26) hide show
  1. package/.claude/rules/consumer/a11y-no-focus-steal.md +14 -0
  2. package/.claude/skills/component-compliance-checklist.md +5 -1
  3. package/.claude/skills/components/display-toast.md +6 -3
  4. package/.claude/skills/components/social-icons-list.md +32 -5
  5. package/.claude/skills/css-dynamic-border-radius.md +114 -0
  6. package/.claude/skills/index.md +2 -1
  7. package/.claude/skills/storybook-add-story.md +6 -0
  8. package/.vscode/srcdev-component-display-toast.code-snippets +6 -8
  9. package/.vscode/srcdev-component-social-icons-list.code-snippets +14 -0
  10. package/.vscode/srcdev-utility-dynamic-border-radius.code-snippets +31 -0
  11. package/app/assets/styles/setup/06.utility-classes/_dynamic-border-radius.css +27 -0
  12. package/app/assets/styles/setup/06.utility-classes/index.css +1 -0
  13. package/app/components/02.molecules/social-icons-list/CONSUMER-STYLING.md +97 -8
  14. package/app/components/02.molecules/social-icons-list/SocialIconsList.vue +80 -14
  15. package/app/components/02.molecules/social-icons-list/stories/SocialIconsList.stories.ts +230 -2
  16. package/app/components/02.molecules/social-icons-list/stories/social-icons-list-recipes.css +108 -0
  17. package/app/components/02.molecules/social-icons-list/tests/SocialIconsList.spec.ts +123 -52
  18. package/app/components/02.molecules/social-icons-list/tests/__snapshots__/SocialIconsList.spec.ts.snap +10 -4
  19. package/app/components/02.molecules/toast/CONSUMER-STYLING.md +9 -3
  20. package/app/components/02.molecules/toast/DisplayToast.vue +10 -10
  21. package/app/components/02.molecules/toast/DisplayToastProvider.vue +12 -4
  22. package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +200 -2
  23. package/app/components/02.molecules/toast/tests/DisplayToast.spec.ts +33 -12
  24. package/app/components/02.molecules/toast/tests/DisplayToastProvider.spec.ts +20 -0
  25. package/app/types/components/social-icons-list.d.ts +9 -0
  26. package/package.json +1 -1
@@ -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(100); // fire the focus setTimeout
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(100);
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 pause when the toast takes focus after mouse input", async () => {
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
- vi.advanceTimersByTime(100); // the toast focuses itself
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(900);
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 the toast takes focus after keyboard input, until focus leaves", async () => {
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
- vi.advanceTimersByTime(100);
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(900);
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 () => {
@@ -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
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "srcdev-nuxt-components",
3
3
  "type": "module",
4
- "version": "9.6.5",
4
+ "version": "9.6.6",
5
5
  "main": "nuxt.config.ts",
6
6
  "types": "types.d.ts",
7
7
  "license": "MIT",