srcdev-nuxt-components 9.6.11 → 9.6.12

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.
@@ -1,16 +1,13 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
2
  import { mountSuspended } from "@nuxt/test-utils/runtime";
3
3
  import { nextTick } from "vue";
4
+ import type { VueWrapper } from "@vue/test-utils";
4
5
  import MarqueeScroller from "../MarqueeScroller.vue";
5
6
 
6
7
  const stubMatchMedia = (prefersReducedMotion: boolean) => {
7
- vi.stubGlobal(
8
- "matchMedia",
9
- vi.fn().mockReturnValue({
10
- matches: prefersReducedMotion,
11
- addEventListener: vi.fn(),
12
- })
13
- );
8
+ const query = { matches: prefersReducedMotion, addEventListener: vi.fn(), removeEventListener: vi.fn() };
9
+ vi.stubGlobal("matchMedia", vi.fn().mockReturnValue(query));
10
+ return query;
14
11
  };
15
12
 
16
13
  // jsdom reports 0 for offsetWidth/scrollWidth — stub them per-element by class so
@@ -28,7 +25,7 @@ const stubDimensions = ({ containerWidth, groupScrollWidth }: { containerWidth:
28
25
  Object.defineProperty(HTMLElement.prototype, "scrollWidth", {
29
26
  configurable: true,
30
27
  get() {
31
- return this.classList.contains("marquee-group") ? groupScrollWidth : 0;
28
+ return this.classList.contains("marquee-scroller-group") ? groupScrollWidth : 0;
32
29
  },
33
30
  });
34
31
  };
@@ -43,36 +40,44 @@ const marqueeData = [
43
40
  { id: 2, content: "logo-b" },
44
41
  ];
45
42
 
43
+ const isPaused = (wrapper: VueWrapper) => wrapper.attributes("data-paused") !== undefined;
44
+ const control = (wrapper: VueWrapper) => wrapper.find(".marquee-scroller-control");
45
+
46
46
  describe("MarqueeScroller", () => {
47
+ let wrapper: VueWrapper | undefined;
48
+
47
49
  beforeEach(() => {
48
50
  stubMatchMedia(false);
49
51
  });
50
52
 
51
53
  afterEach(() => {
54
+ wrapper?.unmount();
55
+ wrapper = undefined;
52
56
  restoreDimensions();
57
+ vi.restoreAllMocks();
53
58
  });
54
59
 
55
60
  // ─── Mount ───────────────────────────────────────────────────────────────
56
61
 
57
62
  it("mounts without error", async () => {
58
- const wrapper = await mountSuspended(MarqueeScroller);
63
+ wrapper = await mountSuspended(MarqueeScroller);
59
64
  expect(wrapper.vm).toBeTruthy();
60
65
  });
61
66
 
62
67
  it("renders after mount (displayComponent flips true in onMounted)", async () => {
63
- const wrapper = await mountSuspended(MarqueeScroller);
68
+ wrapper = await mountSuspended(MarqueeScroller);
64
69
  expect(wrapper.find(".marquee-scroller").exists()).toBe(true);
65
70
  });
66
71
 
67
72
  // ─── Snapshots ───────────────────────────────────────────────────────────
68
73
 
69
74
  it("renders correct HTML structure (default props)", async () => {
70
- const wrapper = await mountSuspended(MarqueeScroller);
75
+ wrapper = await mountSuspended(MarqueeScroller);
71
76
  expect(wrapper.html()).toMatchSnapshot();
72
77
  });
73
78
 
74
79
  it("renders correct HTML structure (all props set)", async () => {
75
- const wrapper = await mountSuspended(MarqueeScroller, {
80
+ wrapper = await mountSuspended(MarqueeScroller, {
76
81
  props: {
77
82
  animationRuntime: "20s",
78
83
  reverse: true,
@@ -89,7 +94,7 @@ describe("MarqueeScroller", () => {
89
94
  // ─── marqueeData / slots ─────────────────────────────────────────────────
90
95
 
91
96
  it("renders each marqueeData item twice (main track + duplicate for looping)", async () => {
92
- const wrapper = await mountSuspended(MarqueeScroller, {
97
+ wrapper = await mountSuspended(MarqueeScroller, {
93
98
  props: { marqueeData },
94
99
  slots: {
95
100
  "1": "<span class='logo-a'>A</span>",
@@ -101,116 +106,135 @@ describe("MarqueeScroller", () => {
101
106
  });
102
107
 
103
108
  it("renders no items when marqueeData is empty", async () => {
104
- const wrapper = await mountSuspended(MarqueeScroller);
105
- expect(wrapper.findAll(".item")).toHaveLength(0);
109
+ wrapper = await mountSuspended(MarqueeScroller);
110
+ expect(wrapper.findAll(".marquee-scroller-item")).toHaveLength(0);
111
+ });
112
+
113
+ it("makes the duplicate group inert and aria-hidden so its copies can't be focused", async () => {
114
+ wrapper = await mountSuspended(MarqueeScroller, { props: { marqueeData } });
115
+ const groups = wrapper.findAll(".marquee-scroller-group");
116
+ expect(groups[0]!.attributes("inert")).toBeUndefined();
117
+ expect(groups[1]!.attributes("inert")).toBeDefined();
118
+ expect(groups[1]!.attributes("aria-hidden")).toBe("true");
119
+ });
120
+
121
+ it("uses only component-prefixed structural class names", async () => {
122
+ wrapper = await mountSuspended(MarqueeScroller, { props: { marqueeData, showControls: true } });
123
+ for (const generic of [".item", ".control-btn", ".marquee-track", ".marquee-group"]) {
124
+ expect(wrapper.find(generic).exists()).toBe(false);
125
+ }
106
126
  });
107
127
 
108
128
  // ─── reverse ─────────────────────────────────────────────────────────────
109
129
 
110
- it("does not apply the reverse class by default", async () => {
111
- const wrapper = await mountSuspended(MarqueeScroller);
112
- expect(wrapper.classes()).not.toContain("reverse");
130
+ it("does not set data-reverse by default", async () => {
131
+ wrapper = await mountSuspended(MarqueeScroller);
132
+ expect(wrapper.attributes("data-reverse")).toBeUndefined();
113
133
  });
114
134
 
115
- it("applies the reverse class when reverse is true", async () => {
116
- const wrapper = await mountSuspended(MarqueeScroller, {
117
- props: { reverse: true },
118
- });
119
- expect(wrapper.classes()).toContain("reverse");
135
+ it("sets data-reverse when reverse is true, with no bare state classes", async () => {
136
+ wrapper = await mountSuspended(MarqueeScroller, { props: { reverse: true } });
137
+ expect(wrapper.attributes("data-reverse")).toBe("");
138
+ expect(wrapper.classes()).not.toContain("reverse");
120
139
  });
121
140
 
122
141
  // ─── Accessibility attributes ────────────────────────────────────────────
123
142
 
124
143
  it("defaults aria-label to 'Scrolling content'", async () => {
125
- const wrapper = await mountSuspended(MarqueeScroller);
144
+ wrapper = await mountSuspended(MarqueeScroller);
126
145
  expect(wrapper.attributes("aria-label")).toBe("Scrolling content");
127
146
  });
128
147
 
129
148
  it("uses a custom ariaLabel when provided", async () => {
130
- const wrapper = await mountSuspended(MarqueeScroller, {
131
- props: { ariaLabel: "Client logos" },
132
- });
149
+ wrapper = await mountSuspended(MarqueeScroller, { props: { ariaLabel: "Client logos" } });
133
150
  expect(wrapper.attributes("aria-label")).toBe("Client logos");
134
151
  });
135
152
 
136
153
  it("uses default screen-reader instructions when ariaDescription is not set", async () => {
137
- const wrapper = await mountSuspended(MarqueeScroller);
138
- expect(wrapper.find(".sr-only").text()).toContain("Use spacebar to pause or play");
154
+ wrapper = await mountSuspended(MarqueeScroller);
155
+ expect(wrapper.find(".marquee-scroller-description").text()).toContain("Use spacebar to pause or play");
139
156
  });
140
157
 
141
158
  it("uses a custom ariaDescription when provided", async () => {
142
- const wrapper = await mountSuspended(MarqueeScroller, {
143
- props: { ariaDescription: "Custom instructions" },
144
- });
145
- expect(wrapper.find(".sr-only").text()).toBe("Custom instructions");
159
+ wrapper = await mountSuspended(MarqueeScroller, { props: { ariaDescription: "Custom instructions" } });
160
+ expect(wrapper.find(".marquee-scroller-description").text()).toBe("Custom instructions");
146
161
  });
147
162
 
148
- it("sets aria-live to off while playing", async () => {
149
- const wrapper = await mountSuspended(MarqueeScroller);
163
+ it("sets aria-live to off and hides the moving track while playing", async () => {
164
+ wrapper = await mountSuspended(MarqueeScroller);
150
165
  expect(wrapper.attributes("aria-live")).toBe("off");
166
+ expect(wrapper.find(".marquee-scroller-track").attributes("aria-hidden")).toBe("true");
167
+ });
168
+
169
+ it("exposes the track (no aria-hidden attribute) once paused", async () => {
170
+ wrapper = await mountSuspended(MarqueeScroller);
171
+ await wrapper.trigger("keydown", { key: " " });
172
+ expect(wrapper.find(".marquee-scroller-track").attributes("aria-hidden")).toBeUndefined();
151
173
  });
152
174
 
153
175
  // ─── Controls ────────────────────────────────────────────────────────────
154
176
 
155
177
  it("does not render the control button by default", async () => {
156
- const wrapper = await mountSuspended(MarqueeScroller);
157
- expect(wrapper.find(".control-btn").exists()).toBe(false);
178
+ wrapper = await mountSuspended(MarqueeScroller);
179
+ expect(control(wrapper).exists()).toBe(false);
158
180
  });
159
181
 
160
- it("renders the control button when showControls is true", async () => {
161
- const wrapper = await mountSuspended(MarqueeScroller, {
162
- props: { showControls: true },
163
- });
164
- expect(wrapper.find(".control-btn").exists()).toBe(true);
182
+ it("renders the control as an InputButton when showControls is true", async () => {
183
+ wrapper = await mountSuspended(MarqueeScroller, { props: { showControls: true } });
184
+ expect(control(wrapper).exists()).toBe(true);
185
+ expect(control(wrapper).classes()).toContain("input-button");
186
+ expect(control(wrapper).attributes("type")).toBe("button");
165
187
  });
166
188
 
167
189
  it("toggles paused state and aria-live when the control button is clicked", async () => {
168
- const wrapper = await mountSuspended(MarqueeScroller, {
169
- props: { showControls: true },
170
- });
171
- await wrapper.find(".control-btn").trigger("click");
172
- expect(wrapper.classes()).toContain("paused");
190
+ wrapper = await mountSuspended(MarqueeScroller, { props: { showControls: true } });
191
+ await control(wrapper).trigger("click");
192
+ expect(isPaused(wrapper)).toBe(true);
173
193
  expect(wrapper.attributes("aria-live")).toBe("polite");
174
- expect(wrapper.find(".control-btn").attributes("aria-label")).toBe("Play animation");
194
+ expect(control(wrapper).find(".input-button-text").text()).toBe("Play animation");
195
+ });
196
+
197
+ it("does not auto-pause when focus lands on the control button, so one click pauses", async () => {
198
+ wrapper = await mountSuspended(MarqueeScroller, { props: { showControls: true } });
199
+ await control(wrapper).trigger("focusin");
200
+ expect(isPaused(wrapper)).toBe(false);
201
+ await control(wrapper).trigger("click");
202
+ expect(isPaused(wrapper)).toBe(true);
175
203
  });
176
204
 
177
205
  // ─── Control button icon / label customisation ──────────────────────────
178
206
 
179
- it("renders the default play icon while playing", async () => {
180
- const wrapper = await mountSuspended(MarqueeScroller, {
181
- props: { showControls: true },
182
- });
183
- expect(wrapper.find(".control-btn").html()).toContain("mdi:pause");
207
+ it("renders the default pause icon while playing", async () => {
208
+ wrapper = await mountSuspended(MarqueeScroller, { props: { showControls: true } });
209
+ expect(control(wrapper).html()).toContain("mdi:pause");
184
210
  });
185
211
 
186
212
  it("renders the default play icon once paused", async () => {
187
- const wrapper = await mountSuspended(MarqueeScroller, {
188
- props: { showControls: true },
189
- });
190
- await wrapper.find(".control-btn").trigger("click");
191
- expect(wrapper.find(".control-btn").html()).toContain("mdi:play");
213
+ wrapper = await mountSuspended(MarqueeScroller, { props: { showControls: true } });
214
+ await control(wrapper).trigger("click");
215
+ expect(control(wrapper).html()).toContain("mdi:play");
192
216
  });
193
217
 
194
218
  it("renders a custom playIcon/pauseIcon", async () => {
195
- const wrapper = await mountSuspended(MarqueeScroller, {
219
+ wrapper = await mountSuspended(MarqueeScroller, {
196
220
  props: { showControls: true, playIcon: "mdi:play-circle", pauseIcon: "mdi:pause-circle" },
197
221
  });
198
- expect(wrapper.find(".control-btn").html()).toContain("mdi:pause-circle");
199
- await wrapper.find(".control-btn").trigger("click");
200
- expect(wrapper.find(".control-btn").html()).toContain("mdi:play-circle");
222
+ expect(control(wrapper).html()).toContain("mdi:pause-circle");
223
+ await control(wrapper).trigger("click");
224
+ expect(control(wrapper).html()).toContain("mdi:play-circle");
201
225
  });
202
226
 
203
- it("uses custom playLabel/pauseLabel for the control button's aria-label", async () => {
204
- const wrapper = await mountSuspended(MarqueeScroller, {
227
+ it("uses custom playLabel/pauseLabel as the control button's accessible text", async () => {
228
+ wrapper = await mountSuspended(MarqueeScroller, {
205
229
  props: { showControls: true, playLabel: "Reproduire", pauseLabel: "Suspendre" },
206
230
  });
207
- expect(wrapper.find(".control-btn").attributes("aria-label")).toBe("Suspendre");
208
- await wrapper.find(".control-btn").trigger("click");
209
- expect(wrapper.find(".control-btn").attributes("aria-label")).toBe("Reproduire");
231
+ expect(control(wrapper).find(".input-button-text").text()).toBe("Suspendre");
232
+ await control(wrapper).trigger("click");
233
+ expect(control(wrapper).find(".input-button-text").text()).toBe("Reproduire");
210
234
  });
211
235
 
212
236
  it("replaces the toggle icon via the toggle-icon slot", async () => {
213
- const wrapper = await mountSuspended(MarqueeScroller, {
237
+ wrapper = await mountSuspended(MarqueeScroller, {
214
238
  props: { showControls: true },
215
239
  slots: { "toggle-icon": "<span class='custom-icon'>custom</span>" },
216
240
  });
@@ -218,79 +242,132 @@ describe("MarqueeScroller", () => {
218
242
  });
219
243
 
220
244
  it("passes isPaused to the toggle-icon slot", async () => {
221
- const wrapper = await mountSuspended(MarqueeScroller, {
245
+ wrapper = await mountSuspended(MarqueeScroller, {
222
246
  props: { showControls: true },
223
247
  slots: { "toggle-icon": "<template #toggle-icon=\"{ isPaused }\"><span class='state'>{{ isPaused }}</span></template>" },
224
248
  });
225
249
  expect(wrapper.find(".state").text()).toBe("false");
226
- await wrapper.find(".control-btn").trigger("click");
250
+ await control(wrapper).trigger("click");
227
251
  expect(wrapper.find(".state").text()).toBe("true");
228
252
  });
229
253
 
230
254
  // ─── Keyboard interaction ────────────────────────────────────────────────
231
255
 
232
- it("toggles pause on spacebar keydown", async () => {
233
- const wrapper = await mountSuspended(MarqueeScroller);
256
+ it("toggles pause on spacebar keydown on the region", async () => {
257
+ wrapper = await mountSuspended(MarqueeScroller);
234
258
  await wrapper.trigger("keydown", { key: " " });
235
- expect(wrapper.classes()).toContain("paused");
259
+ expect(isPaused(wrapper)).toBe(true);
236
260
  await wrapper.trigger("keydown", { key: " " });
237
- expect(wrapper.classes()).not.toContain("paused");
261
+ expect(isPaused(wrapper)).toBe(false);
262
+ });
263
+
264
+ it("ignores spacebar bubbling from inside the region (the control button handles its own)", async () => {
265
+ wrapper = await mountSuspended(MarqueeScroller, { props: { showControls: true } });
266
+ await control(wrapper).trigger("keydown", { key: " " });
267
+ expect(isPaused(wrapper)).toBe(false);
238
268
  });
239
269
 
240
270
  it("does not react to arrow keys (no manual-stepping behaviour is implemented)", async () => {
241
- const wrapper = await mountSuspended(MarqueeScroller);
271
+ wrapper = await mountSuspended(MarqueeScroller);
242
272
  await wrapper.trigger("keydown", { key: "ArrowLeft" });
243
273
  await wrapper.trigger("keydown", { key: "ArrowRight" });
244
- expect(wrapper.classes()).not.toContain("paused");
274
+ expect(isPaused(wrapper)).toBe(false);
245
275
  });
246
276
 
247
- // ─── Focus / blur ────────────────────────────────────────────────────────
277
+ // ─── Focus ───────────────────────────────────────────────────────────────
248
278
 
249
- it("pauses on focus when respectReducedMotion is true (default)", async () => {
250
- const wrapper = await mountSuspended(MarqueeScroller);
251
- await wrapper.trigger("focus");
252
- expect(wrapper.classes()).toContain("paused");
279
+ it("pauses when focus enters the region", async () => {
280
+ wrapper = await mountSuspended(MarqueeScroller);
281
+ await wrapper.trigger("focusin");
282
+ expect(isPaused(wrapper)).toBe(true);
253
283
  });
254
284
 
255
- it("resumes on blur when the user does not prefer reduced motion", async () => {
256
- const wrapper = await mountSuspended(MarqueeScroller);
257
- await wrapper.trigger("focus");
258
- await wrapper.trigger("blur");
259
- expect(wrapper.classes()).not.toContain("paused");
285
+ it("pauses on focus even when respectReducedMotion is false", async () => {
286
+ wrapper = await mountSuspended(MarqueeScroller, { props: { respectReducedMotion: false } });
287
+ await wrapper.trigger("focusin");
288
+ expect(isPaused(wrapper)).toBe(true);
260
289
  });
261
290
 
262
- it("does not pause on focus when respectReducedMotion is false", async () => {
263
- const wrapper = await mountSuspended(MarqueeScroller, {
264
- props: { respectReducedMotion: false },
291
+ it("pauses and exposes the track when focus lands on content inside an item", async () => {
292
+ wrapper = await mountSuspended(MarqueeScroller, {
293
+ props: { marqueeData },
294
+ slots: { "1": "<a class='logo-link' href='#a'>A</a>", "2": "<span>B</span>" },
265
295
  });
266
- await wrapper.trigger("focus");
267
- expect(wrapper.classes()).not.toContain("paused");
296
+ await wrapper.find(".logo-link").trigger("focusin");
297
+ expect(isPaused(wrapper)).toBe(true);
298
+ expect(wrapper.find(".marquee-scroller-track").attributes("aria-hidden")).toBeUndefined();
299
+ });
300
+
301
+ it("resumes when focus leaves the region if focus caused the pause", async () => {
302
+ wrapper = await mountSuspended(MarqueeScroller);
303
+ await wrapper.trigger("focusin");
304
+ await wrapper.trigger("focusout", { relatedTarget: null });
305
+ expect(isPaused(wrapper)).toBe(false);
306
+ });
307
+
308
+ it("stays paused while focus moves between elements inside the region", async () => {
309
+ wrapper = await mountSuspended(MarqueeScroller, { props: { showControls: true } });
310
+ await wrapper.trigger("focusin");
311
+ await wrapper.trigger("focusout", { relatedTarget: control(wrapper).element });
312
+ expect(isPaused(wrapper)).toBe(true);
313
+ });
314
+
315
+ it("keeps a pause the user chose after focus leaves", async () => {
316
+ wrapper = await mountSuspended(MarqueeScroller, { props: { showControls: true } });
317
+ await control(wrapper).trigger("click");
318
+ await control(wrapper).trigger("focusout", { relatedTarget: null });
319
+ expect(isPaused(wrapper)).toBe(true);
268
320
  });
269
321
 
270
322
  // ─── Reduced motion ──────────────────────────────────────────────────────
271
323
 
272
- it("auto-pauses and applies reduced-motion class when the user prefers reduced motion", async () => {
324
+ it("auto-pauses and sets data-reduced-motion when the user prefers reduced motion", async () => {
273
325
  stubMatchMedia(true);
274
- const wrapper = await mountSuspended(MarqueeScroller);
275
- expect(wrapper.classes()).toContain("paused");
276
- expect(wrapper.classes()).toContain("reduced-motion");
326
+ wrapper = await mountSuspended(MarqueeScroller);
327
+ expect(isPaused(wrapper)).toBe(true);
328
+ expect(wrapper.attributes("data-reduced-motion")).toBe("");
329
+ });
330
+
331
+ it("stays paused after focus leaves when the user prefers reduced motion", async () => {
332
+ stubMatchMedia(true);
333
+ wrapper = await mountSuspended(MarqueeScroller);
334
+ await wrapper.trigger("focusin");
335
+ await wrapper.trigger("focusout", { relatedTarget: null });
336
+ expect(isPaused(wrapper)).toBe(true);
277
337
  });
278
338
 
279
339
  it("does not check matchMedia when respectReducedMotion is false", async () => {
280
- stubMatchMedia(false);
281
- const matchMediaSpy = vi.fn(() => ({ matches: false, addEventListener: vi.fn() }));
340
+ const matchMediaSpy = vi.fn(() => ({ matches: false, addEventListener: vi.fn(), removeEventListener: vi.fn() }));
282
341
  vi.stubGlobal("matchMedia", matchMediaSpy);
283
- await mountSuspended(MarqueeScroller, {
284
- props: { respectReducedMotion: false },
285
- });
342
+ wrapper = await mountSuspended(MarqueeScroller, { props: { respectReducedMotion: false } });
286
343
  expect(matchMediaSpy).not.toHaveBeenCalled();
287
344
  });
288
345
 
346
+ it("removes its reduced-motion listener on unmount", async () => {
347
+ const query = stubMatchMedia(false);
348
+ wrapper = await mountSuspended(MarqueeScroller);
349
+ const handler = query.addEventListener.mock.calls[0]![1];
350
+ wrapper.unmount();
351
+ wrapper = undefined;
352
+ expect(query.removeEventListener).toHaveBeenCalledWith("change", handler);
353
+ });
354
+
355
+ // ─── styleClassPassthrough ───────────────────────────────────────────────
356
+
357
+ it("applies styleClassPassthrough to the root and updates on change", async () => {
358
+ wrapper = await mountSuspended(MarqueeScroller, { props: { styleClassPassthrough: ["one"] } });
359
+ expect(wrapper.classes()).toContain("one");
360
+ await wrapper.setProps({ styleClassPassthrough: "two three" });
361
+ await nextTick();
362
+ expect(wrapper.classes()).toEqual(expect.arrayContaining(["marquee-scroller", "two", "three"]));
363
+ expect(wrapper.classes()).not.toContain("one");
364
+ });
365
+
289
366
  // ─── Repeating marqueeData to fill a wide container ─────────────────────
290
367
 
291
368
  it("repeats marqueeData enough times so one group's width covers a wide container", async () => {
292
369
  stubDimensions({ containerWidth: 500, groupScrollWidth: 100 });
293
- const wrapper = await mountSuspended(MarqueeScroller, {
370
+ wrapper = await mountSuspended(MarqueeScroller, {
294
371
  props: { marqueeData },
295
372
  slots: { "1": "<span class='logo-a'></span>", "2": "<span class='logo-b'></span>" },
296
373
  });
@@ -303,7 +380,7 @@ describe("MarqueeScroller", () => {
303
380
 
304
381
  it("does not add extra copies when a single copy already covers the container", async () => {
305
382
  stubDimensions({ containerWidth: 50, groupScrollWidth: 200 });
306
- const wrapper = await mountSuspended(MarqueeScroller, {
383
+ wrapper = await mountSuspended(MarqueeScroller, {
307
384
  props: { marqueeData },
308
385
  slots: { "1": "<span class='logo-a'></span>", "2": "<span class='logo-b'></span>" },
309
386
  });
@@ -312,4 +389,15 @@ describe("MarqueeScroller", () => {
312
389
  expect(wrapper.findAll(".logo-a")).toHaveLength(2);
313
390
  expect(wrapper.findAll(".logo-b")).toHaveLength(2);
314
391
  });
392
+
393
+ it("caps the repeat count at 50 copies for tiny items in a huge container", async () => {
394
+ stubDimensions({ containerWidth: 100000, groupScrollWidth: 2 });
395
+ wrapper = await mountSuspended(MarqueeScroller, {
396
+ props: { marqueeData },
397
+ slots: { "1": "<span class='logo-a'></span>", "2": "<span class='logo-b'></span>" },
398
+ });
399
+ await nextTick();
400
+ await nextTick();
401
+ expect(wrapper.findAll(".logo-a")).toHaveLength(100);
402
+ });
315
403
  });
@@ -1,16 +1,20 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
3
  exports[`MarqueeScroller > renders correct HTML structure (all props set) 1`] = `
4
- "<div class="marquee-scroller reverse" role="region" aria-label="Client logos" aria-live="off" tabindex="0">
5
- <div class="sr-only">Custom instructions</div><button class="control-btn" aria-label="Pause animation" type="button"><span class="iconify i-mdi:pause" aria-hidden="true"></span></button>
6
- <div class="marquee-track" aria-hidden="true">
7
- <div class="marquee-group">
8
- <div class="item"></div>
9
- <div class="item"></div>
4
+ "<div class="marquee-scroller" data-reverse="" role="region" aria-label="Client logos" aria-live="off" tabindex="0">
5
+ <div class="marquee-scroller-description">Custom instructions</div><button type="button" data-testid="input-button" data-theme="default" data-variant="inline" data-icon-only="" class="input-button marquee-scroller-control">
6
+ <!--v-if-->
7
+ <!--v-if--><span class="input-button-text sr-only">Pause animation</span>
8
+ <!--v-if--><span class="input-button-icon" data-slot="icon-only"><span class="iconify i-mdi:pause" aria-hidden="true"></span></span>
9
+ </button>
10
+ <div class="marquee-scroller-track" aria-hidden="true">
11
+ <div class="marquee-scroller-group">
12
+ <div class="marquee-scroller-item"></div>
13
+ <div class="marquee-scroller-item"></div>
10
14
  </div>
11
- <div class="marquee-group" aria-hidden="true">
12
- <div class="item"></div>
13
- <div class="item"></div>
15
+ <div class="marquee-scroller-group" aria-hidden="true" inert="">
16
+ <div class="marquee-scroller-item"></div>
17
+ <div class="marquee-scroller-item"></div>
14
18
  </div>
15
19
  </div>
16
20
  </div>"
@@ -18,11 +22,11 @@ exports[`MarqueeScroller > renders correct HTML structure (all props set) 1`] =
18
22
 
19
23
  exports[`MarqueeScroller > renders correct HTML structure (default props) 1`] = `
20
24
  "<div class="marquee-scroller" role="region" aria-label="Scrolling content" aria-live="off" tabindex="0">
21
- <div class="sr-only">Use spacebar to pause or play the animation.</div>
25
+ <div class="marquee-scroller-description">Use spacebar to pause or play the animation.</div>
22
26
  <!--v-if-->
23
- <div class="marquee-track" aria-hidden="true">
24
- <div class="marquee-group"></div>
25
- <div class="marquee-group" aria-hidden="true"></div>
27
+ <div class="marquee-scroller-track" aria-hidden="true">
28
+ <div class="marquee-scroller-group"></div>
29
+ <div class="marquee-scroller-group" aria-hidden="true" inert=""></div>
26
30
  </div>
27
31
  </div>"
28
32
  `;
@@ -89,3 +89,10 @@ Or scoped by a page/section wrapper class:
89
89
 
90
90
  `:style-class-passthrough` (string or string array) adds classes to the root `.form-field` element.
91
91
 
92
+ ---
93
+
94
+ ## Notes
95
+
96
+ - FormField sets no `overflow-wrap` or `min-inline-size` on its slot content. The `*Field`
97
+ components wrap their own long copy; plain text you slot in directly needs its own wrapping rule.
98
+