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.
Files changed (45) hide show
  1. package/.claude/commands/migrate-component.md +40 -2
  2. package/.claude/component-ledger/audit.json +1 -1
  3. package/.claude/component-ledger/output.html +1 -1
  4. package/.claude/skills/components/breadcrumb.md +1 -0
  5. package/.claude/skills/components/content-docs.md +2 -0
  6. package/.claude/skills/components/display-tooltip-defined.md +3 -0
  7. package/.claude/skills/components/display-tooltip.md +1 -0
  8. package/.claude/skills/components/marquee-scroller.md +130 -0
  9. package/.claude/skills/components/navigation-items.md +1 -0
  10. package/.claude/skills/components/responsive-header.md +3 -0
  11. package/.claude/skills/components/site-header.md +4 -0
  12. package/.claude/skills/components/site-navigation.md +1 -0
  13. package/.claude/skills/components/tab-navigation.md +1 -0
  14. package/.claude/skills/index.md +1 -0
  15. package/.vscode/srcdev-component-breadcrumb.code-snippets +13 -0
  16. package/.vscode/srcdev-component-content-docs.code-snippets +18 -0
  17. package/.vscode/srcdev-component-display-tooltip-defined.code-snippets +15 -0
  18. package/.vscode/srcdev-component-display-tooltip.code-snippets +11 -0
  19. package/.vscode/srcdev-component-marquee-scroller.code-snippets +93 -0
  20. package/.vscode/srcdev-component-responsive-header.code-snippets +12 -0
  21. package/.vscode/srcdev-component-site-header.code-snippets +17 -0
  22. package/.vscode/srcdev-component-site-navigation.code-snippets +30 -0
  23. package/.vscode/srcdev-component-slider-gallery.code-snippets +38 -0
  24. package/.vscode/srcdev-component-tab-navigation.code-snippets +30 -0
  25. package/app/components/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +94 -0
  26. package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +331 -0
  27. package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +151 -0
  28. package/app/components/01.atoms/animations/marquee-scroller/tests/MarqueeScroller.spec.ts +315 -0
  29. package/app/components/01.atoms/animations/marquee-scroller/tests/__snapshots__/MarqueeScroller.spec.ts.snap +28 -0
  30. package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +8 -2
  31. package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +4 -1
  32. package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +4 -1
  33. package/app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue +16 -3
  34. package/app/components/02.molecules/display-tooltip-defined/tests/__snapshots__/DisplayTooltipDefined.spec.ts.snap +1 -1
  35. package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +4 -1
  36. package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +4 -1
  37. package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +16 -4
  38. package/app/components/03.organisms/responsive-header/NavigationItems.vue +4 -1
  39. package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +16 -3
  40. package/app/components/03.organisms/site-header/SiteHeader.vue +16 -1
  41. package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +1 -1
  42. package/app/types/components/index.ts +1 -0
  43. package/app/types/components/marquee-scroller.d.ts +10 -0
  44. package/package.json +1 -1
  45. 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="Docs navigation">
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="On this page">
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="Toggle the popover"
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="Breadcrumb">
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 :tooltip-id="tooltipId" :style-class-passthrough="styleClassPassthrough">
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="Close tool tip"
33
+ :aria-label="closeButtonAriaLabel"
30
34
  >
31
- Close
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"> Close </button>
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="Site navigation"
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="Site navigation"
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>Loading gallery...</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>SEE MORE</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="Previous image" @click.prevent="doPrevious()">
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="Next image" @click.prevent="doNext()">
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="Overflow navigation menu"
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));