srcdev-nuxt-components 9.4.5 → 9.4.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/commands/migrate-component.md +40 -2
- package/.claude/component-ledger/audit.json +1 -1
- package/.claude/component-ledger/output.html +1 -1
- package/.claude/skills/components/breadcrumb.md +1 -0
- package/.claude/skills/components/content-docs.md +2 -0
- package/.claude/skills/components/display-tooltip-defined.md +3 -0
- package/.claude/skills/components/display-tooltip.md +1 -0
- package/.claude/skills/components/marquee-scroller.md +130 -0
- package/.claude/skills/components/navigation-items.md +1 -0
- package/.claude/skills/components/responsive-header.md +3 -0
- package/.claude/skills/components/site-header.md +4 -0
- package/.claude/skills/components/site-navigation.md +1 -0
- package/.claude/skills/components/tab-navigation.md +1 -0
- package/.claude/skills/index.md +1 -0
- package/.vscode/srcdev-component-breadcrumb.code-snippets +13 -0
- package/.vscode/srcdev-component-content-docs.code-snippets +18 -0
- package/.vscode/srcdev-component-display-tooltip-defined.code-snippets +15 -0
- package/.vscode/srcdev-component-display-tooltip.code-snippets +11 -0
- package/.vscode/srcdev-component-marquee-scroller.code-snippets +93 -0
- package/.vscode/srcdev-component-responsive-header.code-snippets +12 -0
- package/.vscode/srcdev-component-site-header.code-snippets +17 -0
- package/.vscode/srcdev-component-site-navigation.code-snippets +30 -0
- package/.vscode/srcdev-component-slider-gallery.code-snippets +38 -0
- package/.vscode/srcdev-component-tab-navigation.code-snippets +30 -0
- package/app/components/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +94 -0
- package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +331 -0
- package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +151 -0
- package/app/components/01.atoms/animations/marquee-scroller/tests/MarqueeScroller.spec.ts +315 -0
- package/app/components/01.atoms/animations/marquee-scroller/tests/__snapshots__/MarqueeScroller.spec.ts.snap +28 -0
- package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +8 -2
- package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +4 -1
- package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +4 -1
- package/app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue +16 -3
- package/app/components/02.molecules/display-tooltip-defined/tests/__snapshots__/DisplayTooltipDefined.spec.ts.snap +1 -1
- package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +4 -1
- package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +4 -1
- package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +16 -4
- package/app/components/03.organisms/responsive-header/NavigationItems.vue +4 -1
- package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +16 -3
- package/app/components/03.organisms/site-header/SiteHeader.vue +16 -1
- package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +1 -1
- package/app/types/components/index.ts +1 -0
- package/app/types/components/marquee-scroller.d.ts +10 -0
- package/package.json +1 -1
- package/app/components/marquee-scroller/MarqueeScroller.vue +0 -289
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import { nextTick } from "vue";
|
|
4
|
+
import MarqueeScroller from "../MarqueeScroller.vue";
|
|
5
|
+
|
|
6
|
+
const stubMatchMedia = (prefersReducedMotion: boolean) => {
|
|
7
|
+
vi.stubGlobal(
|
|
8
|
+
"matchMedia",
|
|
9
|
+
vi.fn().mockReturnValue({
|
|
10
|
+
matches: prefersReducedMotion,
|
|
11
|
+
addEventListener: vi.fn(),
|
|
12
|
+
})
|
|
13
|
+
);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// jsdom reports 0 for offsetWidth/scrollWidth — stub them per-element by class so
|
|
17
|
+
// updateRepeatCount() sees realistic container/content dimensions.
|
|
18
|
+
const originalOffsetWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetWidth");
|
|
19
|
+
const originalScrollWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "scrollWidth");
|
|
20
|
+
|
|
21
|
+
const stubDimensions = ({ containerWidth, groupScrollWidth }: { containerWidth: number; groupScrollWidth: number }) => {
|
|
22
|
+
Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
|
|
23
|
+
configurable: true,
|
|
24
|
+
get() {
|
|
25
|
+
return this.classList.contains("marquee-scroller") ? containerWidth : 0;
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
Object.defineProperty(HTMLElement.prototype, "scrollWidth", {
|
|
29
|
+
configurable: true,
|
|
30
|
+
get() {
|
|
31
|
+
return this.classList.contains("marquee-group") ? groupScrollWidth : 0;
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const restoreDimensions = () => {
|
|
37
|
+
if (originalOffsetWidth) Object.defineProperty(HTMLElement.prototype, "offsetWidth", originalOffsetWidth);
|
|
38
|
+
if (originalScrollWidth) Object.defineProperty(HTMLElement.prototype, "scrollWidth", originalScrollWidth);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const marqueeData = [
|
|
42
|
+
{ id: 1, content: "logo-a" },
|
|
43
|
+
{ id: 2, content: "logo-b" },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
describe("MarqueeScroller", () => {
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
stubMatchMedia(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
restoreDimensions();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ─── Mount ───────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
it("mounts without error", async () => {
|
|
58
|
+
const wrapper = await mountSuspended(MarqueeScroller);
|
|
59
|
+
expect(wrapper.vm).toBeTruthy();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("renders after mount (displayComponent flips true in onMounted)", async () => {
|
|
63
|
+
const wrapper = await mountSuspended(MarqueeScroller);
|
|
64
|
+
expect(wrapper.find(".marquee-scroller").exists()).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ─── Snapshots ───────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
it("renders correct HTML structure (default props)", async () => {
|
|
70
|
+
const wrapper = await mountSuspended(MarqueeScroller);
|
|
71
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("renders correct HTML structure (all props set)", async () => {
|
|
75
|
+
const wrapper = await mountSuspended(MarqueeScroller, {
|
|
76
|
+
props: {
|
|
77
|
+
animationRuntime: "20s",
|
|
78
|
+
reverse: true,
|
|
79
|
+
marqueeData,
|
|
80
|
+
itemConfig: { width: "80px", height: "80px", gap: "24px" },
|
|
81
|
+
ariaLabel: "Client logos",
|
|
82
|
+
ariaDescription: "Custom instructions",
|
|
83
|
+
showControls: true,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ─── marqueeData / slots ─────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
it("renders each marqueeData item twice (main track + duplicate for looping)", async () => {
|
|
92
|
+
const wrapper = await mountSuspended(MarqueeScroller, {
|
|
93
|
+
props: { marqueeData },
|
|
94
|
+
slots: {
|
|
95
|
+
"1": "<span class='logo-a'>A</span>",
|
|
96
|
+
"2": "<span class='logo-b'>B</span>",
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
expect(wrapper.findAll(".logo-a")).toHaveLength(2);
|
|
100
|
+
expect(wrapper.findAll(".logo-b")).toHaveLength(2);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("renders no items when marqueeData is empty", async () => {
|
|
104
|
+
const wrapper = await mountSuspended(MarqueeScroller);
|
|
105
|
+
expect(wrapper.findAll(".item")).toHaveLength(0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ─── reverse ─────────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
it("does not apply the reverse class by default", async () => {
|
|
111
|
+
const wrapper = await mountSuspended(MarqueeScroller);
|
|
112
|
+
expect(wrapper.classes()).not.toContain("reverse");
|
|
113
|
+
});
|
|
114
|
+
|
|
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");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ─── Accessibility attributes ────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
it("defaults aria-label to 'Scrolling content'", async () => {
|
|
125
|
+
const wrapper = await mountSuspended(MarqueeScroller);
|
|
126
|
+
expect(wrapper.attributes("aria-label")).toBe("Scrolling content");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("uses a custom ariaLabel when provided", async () => {
|
|
130
|
+
const wrapper = await mountSuspended(MarqueeScroller, {
|
|
131
|
+
props: { ariaLabel: "Client logos" },
|
|
132
|
+
});
|
|
133
|
+
expect(wrapper.attributes("aria-label")).toBe("Client logos");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
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");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
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");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("sets aria-live to off while playing", async () => {
|
|
149
|
+
const wrapper = await mountSuspended(MarqueeScroller);
|
|
150
|
+
expect(wrapper.attributes("aria-live")).toBe("off");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ─── Controls ────────────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
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);
|
|
158
|
+
});
|
|
159
|
+
|
|
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);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
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");
|
|
173
|
+
expect(wrapper.attributes("aria-live")).toBe("polite");
|
|
174
|
+
expect(wrapper.find(".control-btn").attributes("aria-label")).toBe("Play animation");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// ─── Control button icon / label customisation ──────────────────────────
|
|
178
|
+
|
|
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");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
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");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("renders a custom playIcon/pauseIcon", async () => {
|
|
195
|
+
const wrapper = await mountSuspended(MarqueeScroller, {
|
|
196
|
+
props: { showControls: true, playIcon: "mdi:play-circle", pauseIcon: "mdi:pause-circle" },
|
|
197
|
+
});
|
|
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");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("uses custom playLabel/pauseLabel for the control button's aria-label", async () => {
|
|
204
|
+
const wrapper = await mountSuspended(MarqueeScroller, {
|
|
205
|
+
props: { showControls: true, playLabel: "Reproduire", pauseLabel: "Suspendre" },
|
|
206
|
+
});
|
|
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");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("replaces the toggle icon via the toggle-icon slot", async () => {
|
|
213
|
+
const wrapper = await mountSuspended(MarqueeScroller, {
|
|
214
|
+
props: { showControls: true },
|
|
215
|
+
slots: { "toggle-icon": "<span class='custom-icon'>custom</span>" },
|
|
216
|
+
});
|
|
217
|
+
expect(wrapper.find(".custom-icon").exists()).toBe(true);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("passes isPaused to the toggle-icon slot", async () => {
|
|
221
|
+
const wrapper = await mountSuspended(MarqueeScroller, {
|
|
222
|
+
props: { showControls: true },
|
|
223
|
+
slots: { "toggle-icon": "<template #toggle-icon=\"{ isPaused }\"><span class='state'>{{ isPaused }}</span></template>" },
|
|
224
|
+
});
|
|
225
|
+
expect(wrapper.find(".state").text()).toBe("false");
|
|
226
|
+
await wrapper.find(".control-btn").trigger("click");
|
|
227
|
+
expect(wrapper.find(".state").text()).toBe("true");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// ─── Keyboard interaction ────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
it("toggles pause on spacebar keydown", async () => {
|
|
233
|
+
const wrapper = await mountSuspended(MarqueeScroller);
|
|
234
|
+
await wrapper.trigger("keydown", { key: " " });
|
|
235
|
+
expect(wrapper.classes()).toContain("paused");
|
|
236
|
+
await wrapper.trigger("keydown", { key: " " });
|
|
237
|
+
expect(wrapper.classes()).not.toContain("paused");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("does not react to arrow keys (no manual-stepping behaviour is implemented)", async () => {
|
|
241
|
+
const wrapper = await mountSuspended(MarqueeScroller);
|
|
242
|
+
await wrapper.trigger("keydown", { key: "ArrowLeft" });
|
|
243
|
+
await wrapper.trigger("keydown", { key: "ArrowRight" });
|
|
244
|
+
expect(wrapper.classes()).not.toContain("paused");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// ─── Focus / blur ────────────────────────────────────────────────────────
|
|
248
|
+
|
|
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");
|
|
253
|
+
});
|
|
254
|
+
|
|
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");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("does not pause on focus when respectReducedMotion is false", async () => {
|
|
263
|
+
const wrapper = await mountSuspended(MarqueeScroller, {
|
|
264
|
+
props: { respectReducedMotion: false },
|
|
265
|
+
});
|
|
266
|
+
await wrapper.trigger("focus");
|
|
267
|
+
expect(wrapper.classes()).not.toContain("paused");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// ─── Reduced motion ──────────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
it("auto-pauses and applies reduced-motion class when the user prefers reduced motion", async () => {
|
|
273
|
+
stubMatchMedia(true);
|
|
274
|
+
const wrapper = await mountSuspended(MarqueeScroller);
|
|
275
|
+
expect(wrapper.classes()).toContain("paused");
|
|
276
|
+
expect(wrapper.classes()).toContain("reduced-motion");
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("does not check matchMedia when respectReducedMotion is false", async () => {
|
|
280
|
+
stubMatchMedia(false);
|
|
281
|
+
const matchMediaSpy = vi.fn(() => ({ matches: false, addEventListener: vi.fn() }));
|
|
282
|
+
vi.stubGlobal("matchMedia", matchMediaSpy);
|
|
283
|
+
await mountSuspended(MarqueeScroller, {
|
|
284
|
+
props: { respectReducedMotion: false },
|
|
285
|
+
});
|
|
286
|
+
expect(matchMediaSpy).not.toHaveBeenCalled();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// ─── Repeating marqueeData to fill a wide container ─────────────────────
|
|
290
|
+
|
|
291
|
+
it("repeats marqueeData enough times so one group's width covers a wide container", async () => {
|
|
292
|
+
stubDimensions({ containerWidth: 500, groupScrollWidth: 100 });
|
|
293
|
+
const wrapper = await mountSuspended(MarqueeScroller, {
|
|
294
|
+
props: { marqueeData },
|
|
295
|
+
slots: { "1": "<span class='logo-a'></span>", "2": "<span class='logo-b'></span>" },
|
|
296
|
+
});
|
|
297
|
+
await nextTick();
|
|
298
|
+
await nextTick();
|
|
299
|
+
// needs ceil(500/100) = 5 copies of the 2-item data per group
|
|
300
|
+
expect(wrapper.findAll(".logo-a")).toHaveLength(10);
|
|
301
|
+
expect(wrapper.findAll(".logo-b")).toHaveLength(10);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("does not add extra copies when a single copy already covers the container", async () => {
|
|
305
|
+
stubDimensions({ containerWidth: 50, groupScrollWidth: 200 });
|
|
306
|
+
const wrapper = await mountSuspended(MarqueeScroller, {
|
|
307
|
+
props: { marqueeData },
|
|
308
|
+
slots: { "1": "<span class='logo-a'></span>", "2": "<span class='logo-b'></span>" },
|
|
309
|
+
});
|
|
310
|
+
await nextTick();
|
|
311
|
+
await nextTick();
|
|
312
|
+
expect(wrapper.findAll(".logo-a")).toHaveLength(2);
|
|
313
|
+
expect(wrapper.findAll(".logo-b")).toHaveLength(2);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
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>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="marquee-group" aria-hidden="true">
|
|
12
|
+
<div class="item"></div>
|
|
13
|
+
<div class="item"></div>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
</div>"
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
exports[`MarqueeScroller > renders correct HTML structure (default props) 1`] = `
|
|
20
|
+
"<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>
|
|
22
|
+
<!--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>
|
|
26
|
+
</div>
|
|
27
|
+
</div>"
|
|
28
|
+
`;
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
<h3 class="docs-nav-heading">{{ docsNavLabel }}</h3>
|
|
18
18
|
</template>
|
|
19
19
|
<template #content>
|
|
20
|
-
<nav class="docs-nav-list" aria-label="
|
|
20
|
+
<nav class="docs-nav-list" :aria-label="docsNavAriaLabel">
|
|
21
21
|
<ul>
|
|
22
22
|
<li v-for="item in docsNavItems" :key="item.to">
|
|
23
23
|
<component
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
<h3 class="docs-page-nav-heading">{{ docsPageNavLabel }}</h3>
|
|
54
54
|
</template>
|
|
55
55
|
<template #content>
|
|
56
|
-
<nav class="docs-page-nav-list" aria-label="
|
|
56
|
+
<nav class="docs-page-nav-list" :aria-label="docsPageNavAriaLabel">
|
|
57
57
|
<ul>
|
|
58
58
|
<li v-for="item in docsPageNavItems" :key="item.to">
|
|
59
59
|
<component
|
|
@@ -91,6 +91,10 @@ interface Props {
|
|
|
91
91
|
docsPageNavItems?: DocsNavItem[];
|
|
92
92
|
docsNavLabel?: string;
|
|
93
93
|
docsPageNavLabel?: string;
|
|
94
|
+
/** aria-label on the docs-nav <nav> landmark — override for localisation. */
|
|
95
|
+
docsNavAriaLabel?: string;
|
|
96
|
+
/** aria-label on the page-nav <nav> landmark — override for localisation. */
|
|
97
|
+
docsPageNavAriaLabel?: string;
|
|
94
98
|
panelVariant?: "modern" | "classic";
|
|
95
99
|
styleClassPassthrough?: string | string[];
|
|
96
100
|
}
|
|
@@ -100,6 +104,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
100
104
|
docsPageNavItems: () => [],
|
|
101
105
|
docsNavLabel: "Navigation",
|
|
102
106
|
docsPageNavLabel: "On this page",
|
|
107
|
+
docsNavAriaLabel: "Docs navigation",
|
|
108
|
+
docsPageNavAriaLabel: "On this page",
|
|
103
109
|
// Defaults to "classic" — the docs/admin nav toggle relies on contentIsOnTop on mobile, and
|
|
104
110
|
// ExpandingPanel's ::details-content-based positioning silently fails on WebKit (Safari/iOS),
|
|
105
111
|
// making the nav unreachable there. See AccordianCore's own variant prop and CLAUDE.md
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
popovertargetaction="toggle"
|
|
8
8
|
class="display-tooltip-trigger-button"
|
|
9
9
|
:class="{ hide: hideTrigger }"
|
|
10
|
-
aria-label="
|
|
10
|
+
:aria-label="triggerAriaLabel"
|
|
11
11
|
>
|
|
12
12
|
<Icon name="fa7-solid:circle-question" class="display-tooltip-trigger-icon" aria-hidden="true" />
|
|
13
13
|
</button>
|
|
@@ -24,12 +24,15 @@
|
|
|
24
24
|
interface Props {
|
|
25
25
|
tooltipId?: string;
|
|
26
26
|
hideTrigger?: boolean;
|
|
27
|
+
/** aria-label on the trigger button — override for localisation. */
|
|
28
|
+
triggerAriaLabel?: string;
|
|
27
29
|
styleClassPassthrough?: string | string[];
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
const props = withDefaults(defineProps<Props>(), {
|
|
31
33
|
tooltipId: "",
|
|
32
34
|
hideTrigger: false,
|
|
35
|
+
triggerAriaLabel: "Toggle the popover",
|
|
33
36
|
styleClassPassthrough: () => [],
|
|
34
37
|
});
|
|
35
38
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<nav class="breadcrumb" :class="[elementClasses]" aria-label="
|
|
2
|
+
<nav class="breadcrumb" :class="[elementClasses]" :aria-label="ariaLabel">
|
|
3
3
|
<ol class="breadcrumb__list">
|
|
4
4
|
<li v-for="(item, index) in items" :key="`${item.label}-${index}`" class="breadcrumb__item">
|
|
5
5
|
<NuxtLink v-if="item.to" :to="item.to" class="breadcrumb__link">{{ item.label }}</NuxtLink>
|
|
@@ -18,11 +18,14 @@ import type { BreadcrumbItem } from "~/types/components/breadcrumb";
|
|
|
18
18
|
interface Props {
|
|
19
19
|
items: BreadcrumbItem[];
|
|
20
20
|
separator?: string;
|
|
21
|
+
/** aria-label on the nav landmark — override for localisation. */
|
|
22
|
+
ariaLabel?: string;
|
|
21
23
|
styleClassPassthrough?: string | string[];
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
const props = withDefaults(defineProps<Props>(), {
|
|
25
27
|
separator: "/",
|
|
28
|
+
ariaLabel: "Breadcrumb",
|
|
26
29
|
styleClassPassthrough: () => [],
|
|
27
30
|
});
|
|
28
31
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<DisplayTooltip
|
|
2
|
+
<DisplayTooltip
|
|
3
|
+
:tooltip-id="tooltipId"
|
|
4
|
+
:trigger-aria-label="triggerAriaLabel"
|
|
5
|
+
:style-class-passthrough="styleClassPassthrough"
|
|
6
|
+
>
|
|
3
7
|
<template v-if="$slots.triggerContent" #triggerContent>
|
|
4
8
|
<slot name="triggerContent"></slot>
|
|
5
9
|
</template>
|
|
@@ -26,9 +30,9 @@
|
|
|
26
30
|
:popovertarget="tooltipId"
|
|
27
31
|
popovertargetaction="hide"
|
|
28
32
|
class="display-tooltip-close-button"
|
|
29
|
-
aria-label="
|
|
33
|
+
:aria-label="closeButtonAriaLabel"
|
|
30
34
|
>
|
|
31
|
-
|
|
35
|
+
{{ closeButtonText }}
|
|
32
36
|
</button>
|
|
33
37
|
</div>
|
|
34
38
|
</template>
|
|
@@ -41,12 +45,21 @@ import type { TooltipContentText } from "~/types/components";
|
|
|
41
45
|
interface Props {
|
|
42
46
|
tooltipId?: string;
|
|
43
47
|
contentText?: TooltipContentText;
|
|
48
|
+
/** aria-label on the trigger button — override for localisation. */
|
|
49
|
+
triggerAriaLabel?: string;
|
|
50
|
+
/** Visible text on the close button — override for localisation. */
|
|
51
|
+
closeButtonText?: string;
|
|
52
|
+
/** aria-label on the close button — override for localisation. */
|
|
53
|
+
closeButtonAriaLabel?: string;
|
|
44
54
|
styleClassPassthrough?: string | string[];
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
const props = withDefaults(defineProps<Props>(), {
|
|
48
58
|
tooltipId: "",
|
|
49
59
|
contentText: () => ({}),
|
|
60
|
+
triggerAriaLabel: "Toggle the popover",
|
|
61
|
+
closeButtonText: "Close",
|
|
62
|
+
closeButtonAriaLabel: "Close tool tip",
|
|
50
63
|
styleClassPassthrough: () => [],
|
|
51
64
|
});
|
|
52
65
|
|
|
@@ -9,7 +9,7 @@ exports[`DisplayTooltipDefined > renders correct HTML structure 1`] = `
|
|
|
9
9
|
<div class="display-tooltip-popover-content">
|
|
10
10
|
<div class="popover-content-defined">
|
|
11
11
|
<h4 class="tooltip-title subtitle-sm">Title</h4>
|
|
12
|
-
<p class="tooltip-body body-sm">Body copy</p><span class="tooltip-action input-value">Learn more</span><button popovertarget="nuxt-tooltip-fixed" popovertargetaction="hide" class="display-tooltip-close-button" aria-label="Close tool tip">
|
|
12
|
+
<p class="tooltip-body body-sm">Body copy</p><span class="tooltip-action input-value">Learn more</span><button popovertarget="nuxt-tooltip-fixed" popovertargetaction="hide" class="display-tooltip-close-button" aria-label="Close tool tip">Close</button>
|
|
13
13
|
</div>
|
|
14
14
|
</div>
|
|
15
15
|
</div>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
`site-navigation--${navAlign}`,
|
|
8
8
|
{ 'is-collapsed': isCollapsed, 'is-loaded': isLoaded, 'menu-open': isMenuOpen, 'is-animated': isAnimated },
|
|
9
9
|
]"
|
|
10
|
-
aria-label="
|
|
10
|
+
:aria-label="ariaLabel"
|
|
11
11
|
>
|
|
12
12
|
<ul
|
|
13
13
|
v-if="!isCollapsed || !isLoaded"
|
|
@@ -106,11 +106,14 @@ interface Props {
|
|
|
106
106
|
navItemData: NavItemData;
|
|
107
107
|
navAlign?: "left" | "center" | "right";
|
|
108
108
|
styleClassPassthrough?: string | string[];
|
|
109
|
+
/** aria-label on the nav landmark — override for localisation. */
|
|
110
|
+
ariaLabel?: string;
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
const props = withDefaults(defineProps<Props>(), {
|
|
112
114
|
navAlign: "left",
|
|
113
115
|
styleClassPassthrough: () => [],
|
|
116
|
+
ariaLabel: "Site navigation",
|
|
114
117
|
});
|
|
115
118
|
|
|
116
119
|
// ─── Animation gate — prevents indicator from transitioning on first paint ───
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
`tab-navigation--${navAlign}`,
|
|
8
8
|
{ 'is-collapsed': isCollapsed, 'is-loaded': isLoaded, 'menu-open': isMenuOpen, 'is-animated': isAnimated },
|
|
9
9
|
]"
|
|
10
|
-
aria-label="
|
|
10
|
+
:aria-label="ariaLabel"
|
|
11
11
|
>
|
|
12
12
|
<ul v-if="!isCollapsed || !isLoaded" ref="navListRef" class="tab-nav-list" @mouseleave="hoveredItemHref = null">
|
|
13
13
|
<li
|
|
@@ -131,12 +131,15 @@ interface Props {
|
|
|
131
131
|
navAlign?: "left" | "center" | "right";
|
|
132
132
|
styleClassPassthrough?: string | string[];
|
|
133
133
|
anchorScrollOffset?: number | (() => number);
|
|
134
|
+
/** aria-label on the nav landmark — override for localisation. */
|
|
135
|
+
ariaLabel?: string;
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
const props = withDefaults(defineProps<Props>(), {
|
|
137
139
|
navAlign: "left",
|
|
138
140
|
styleClassPassthrough: () => [],
|
|
139
141
|
anchorScrollOffset: undefined,
|
|
142
|
+
ariaLabel: "Site navigation",
|
|
140
143
|
});
|
|
141
144
|
|
|
142
145
|
const { navRef, navListRef, isCollapsed, isLoaded, isMenuOpen, isActiveItem, toggleMenu, closeMenu } =
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<div ref="sliderGalleryWrapper" class="slider-gallery" :class="[elementClasses]">
|
|
3
3
|
<div class="loading-state" :class="[{ galleryLoaded: !galleryLoaded }]">
|
|
4
4
|
<div class="loading-spinner"></div>
|
|
5
|
-
<p>
|
|
5
|
+
<p>{{ loadingText }}</p>
|
|
6
6
|
</div>
|
|
7
7
|
|
|
8
8
|
<div v-if="showGallery" class="gallery-content" :class="[{ galleryLoaded: !galleryLoaded }]">
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
{{ item.description }}
|
|
18
18
|
</div>
|
|
19
19
|
<div class="buttons" :class="item.textBrightness">
|
|
20
|
-
<button>
|
|
20
|
+
<button>{{ seeMoreText }}</button>
|
|
21
21
|
</div>
|
|
22
22
|
</div>
|
|
23
23
|
</div>
|
|
@@ -40,10 +40,10 @@
|
|
|
40
40
|
</div>
|
|
41
41
|
|
|
42
42
|
<div class="arrows">
|
|
43
|
-
<button id="prev" ref="prevDom" aria-label="
|
|
43
|
+
<button id="prev" ref="prevDom" :aria-label="prevAriaLabel" @click.prevent="doPrevious()">
|
|
44
44
|
<Icon name="ic:outline-keyboard-arrow-left" class="arrows-icon" />
|
|
45
45
|
</button>
|
|
46
|
-
<button id="next" ref="nextDom" aria-label="
|
|
46
|
+
<button id="next" ref="nextDom" :aria-label="nextAriaLabel" @click.prevent="doNext()">
|
|
47
47
|
<Icon name="ic:outline-keyboard-arrow-right" class="arrows-icon" />
|
|
48
48
|
</button>
|
|
49
49
|
</div>
|
|
@@ -60,6 +60,14 @@ interface Props {
|
|
|
60
60
|
autoRun?: boolean;
|
|
61
61
|
autoRunInterval?: number;
|
|
62
62
|
animationDuration?: number;
|
|
63
|
+
/** Loading-state copy — override for localisation. */
|
|
64
|
+
loadingText?: string;
|
|
65
|
+
/** Per-slide call-to-action button copy — override for localisation. */
|
|
66
|
+
seeMoreText?: string;
|
|
67
|
+
/** aria-label on the previous-image button — override for localisation. */
|
|
68
|
+
prevAriaLabel?: string;
|
|
69
|
+
/** aria-label on the next-image button — override for localisation. */
|
|
70
|
+
nextAriaLabel?: string;
|
|
63
71
|
styleClassPassthrough?: string | string[];
|
|
64
72
|
}
|
|
65
73
|
|
|
@@ -67,6 +75,10 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
67
75
|
autoRun: true,
|
|
68
76
|
autoRunInterval: 7000,
|
|
69
77
|
animationDuration: 3000,
|
|
78
|
+
loadingText: "Loading gallery...",
|
|
79
|
+
seeMoreText: "SEE MORE",
|
|
80
|
+
prevAriaLabel: "Previous image",
|
|
81
|
+
nextAriaLabel: "Next image",
|
|
70
82
|
styleClassPassthrough: () => [],
|
|
71
83
|
});
|
|
72
84
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
class="overflow-navigation-wrapper"
|
|
4
4
|
:class="[elementClasses, { 'is-panel-animating': isPanelAnimating }]"
|
|
5
5
|
role="menu"
|
|
6
|
-
aria-label="
|
|
6
|
+
:aria-label="ariaLabel"
|
|
7
7
|
@mouseleave="
|
|
8
8
|
hoveredItemKey = null;
|
|
9
9
|
hoveredChildKey = null;
|
|
@@ -115,6 +115,8 @@ interface Props {
|
|
|
115
115
|
mainNavigationState?: ResponsiveHeaderState;
|
|
116
116
|
panelVariant?: "modern" | "classic";
|
|
117
117
|
styleClassPassthrough?: string | string[];
|
|
118
|
+
/** aria-label on the overflow menu — override for localisation. */
|
|
119
|
+
ariaLabel?: string;
|
|
118
120
|
}
|
|
119
121
|
|
|
120
122
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -124,6 +126,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
124
126
|
// "modern" is opt-in.
|
|
125
127
|
panelVariant: "classic",
|
|
126
128
|
styleClassPassthrough: () => [],
|
|
129
|
+
ariaLabel: "Overflow navigation menu",
|
|
127
130
|
});
|
|
128
131
|
|
|
129
132
|
const panelComponent = computed(() => (props.panelVariant === "modern" ? ExpandingPanel : ExpandingPanelClassic));
|