srcdev-nuxt-components 9.4.5 → 9.4.7
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 +74 -5
- package/.claude/component-ledger/audit.json +1 -1
- package/.claude/component-ledger/build.mjs +27 -0
- package/.claude/component-ledger/output.html +8 -3
- package/.claude/component-ledger/template.html +7 -2
- package/.claude/skills/components/breadcrumb.md +1 -0
- package/.claude/skills/components/column-flow-grid.md +93 -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/entry-animation.md +88 -0
- package/.claude/skills/components/input-copy.md +2 -0
- package/.claude/skills/components/input-text-core.md +186 -0
- package/.claude/skills/components/marquee-scroller.md +130 -0
- package/.claude/skills/components/masonry-grid.md +153 -0
- package/.claude/skills/components/navigation-items.md +1 -0
- package/.claude/skills/components/pop-over.md +100 -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 +7 -1
- package/.vscode/srcdev-component-breadcrumb.code-snippets +13 -0
- package/.vscode/srcdev-component-column-flow-grid.code-snippets +39 -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-entry-animation.code-snippets +29 -0
- package/.vscode/srcdev-component-input-text.code-snippets +107 -0
- package/.vscode/srcdev-component-marquee-scroller.code-snippets +93 -0
- package/.vscode/srcdev-component-masonry-grid.code-snippets +51 -0
- package/.vscode/srcdev-component-pop-over.code-snippets +44 -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/entry/EntryAnimation.vue +7 -1
- package/app/components/01.atoms/animations/entry/stories/EntryAnimation.stories.ts +47 -0
- package/app/components/01.atoms/animations/entry/tests/EntryAnimation.spec.ts +57 -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/canvas-switcher/stories/CanvasSwitcher.stories.ts +9 -10
- package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +8 -2
- package/app/components/01.atoms/content-wrappers/docs-pages/stories/ContentDocs.stories.ts +12 -13
- package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +4 -1
- package/app/components/01.atoms/grids/column-flow-grid/CONSUMER-STYLING.md +33 -0
- package/app/components/01.atoms/grids/column-flow-grid/ColumnFlowGrid.vue +55 -0
- package/app/components/01.atoms/grids/column-flow-grid/stories/ColumnFlowGrid.stories.ts +178 -0
- package/app/components/01.atoms/grids/column-flow-grid/tests/ColumnFlowGrid.spec.ts +75 -0
- package/app/components/01.atoms/grids/masonry-grid/CONSUMER-STYLING.md +35 -0
- package/app/components/01.atoms/grids/masonry-grid/MasonryGrid.vue +178 -0
- package/app/components/01.atoms/grids/masonry-grid/stories/MasonryGrid.stories.ts +199 -0
- package/app/components/01.atoms/grids/masonry-grid/tests/MasonryGrid.spec.ts +158 -0
- package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +4 -1
- package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +54 -0
- package/app/components/01.atoms/pop-over/PopOver.vue +201 -0
- package/app/components/01.atoms/pop-over/stories/PopOver.stories.ts +141 -0
- package/app/components/01.atoms/pop-over/tests/PopOver.spec.ts +195 -0
- package/app/components/01.atoms/pop-over/tests/__snapshots__/PopOver.spec.ts.snap +11 -0
- package/app/components/02.molecules/action-menu/stories/ActionMenu.stories.ts +8 -2
- 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/input-copy/InputCopy.vue +14 -0
- package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +15 -0
- package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +41 -0
- 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/components/05.forms/input-select/InputSelectCore.vue +1 -1
- package/app/components/05.forms/input-text/InputTextCore.vue +6 -0
- package/app/components/05.forms/input-text/stories/InputPasswordWithLabel.stories.ts +49 -20
- package/app/components/05.forms/input-text/stories/InputTextAsNumberWithLabel.stories.ts +41 -22
- package/app/components/05.forms/input-text/stories/InputTextCore.stories.ts +48 -22
- package/app/components/05.forms/input-text/stories/InputTextWithLabel.stories.ts +43 -17
- package/app/components/05.forms/input-text/tests/InputPasswordWithLabel.spec.ts +56 -0
- package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +61 -0
- package/app/components/05.forms/input-text/tests/InputTextCore.spec.ts +60 -0
- package/app/components/05.forms/input-text/tests/InputTextWithLabel.spec.ts +46 -0
- package/app/components/05.forms/input-text/variants/InputPasswordWithLabel.vue +7 -1
- package/app/components/05.forms/input-text/variants/InputTextAsNumberWithLabel.vue +8 -2
- package/app/components/05.forms/input-text/variants/InputTextWithLabel.vue +6 -0
- package/app/components/05.forms/input-textarea/stories/InputTextareaCore.stories.ts +20 -20
- package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +32 -26
- package/app/types/components/index.ts +1 -0
- package/app/types/components/marquee-scroller.d.ts +10 -0
- package/app/types/forms/types.forms.d.ts +1 -1
- package/package.json +1 -1
- package/app/components/marquee-scroller/MarqueeScroller.vue +0 -289
- package/app/components/masonry-grid/MasonryGrid.vue +0 -68
- package/app/components/masonry-grid-ordered/MasonryGridOrdered.vue +0 -163
- package/app/components/masonry-grid-ordered/MasonryGridOrderedGridExperiment.vue +0 -259
- package/app/components/masonry-grid-ordered/stories/MasonryGridOrdered.stories.ts +0 -354
- package/app/components/masonry-grid-sorted/MasonryGridSorted.vue +0 -120
- package/app/components/pop-over/PopOver.vue +0 -90
- package/app/layouts/default.vue +0 -308
- package/app/layouts/site-navigation-demo.vue +0 -188
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import PopOver from "../PopOver.vue";
|
|
4
|
+
|
|
5
|
+
interface PopOverInstance {
|
|
6
|
+
popoverId: string;
|
|
7
|
+
anchorName: string;
|
|
8
|
+
handleToggle: (event: Event) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const createWrapper = async (props: Record<string, unknown> = {}, slots: Record<string, string> = {}) => {
|
|
12
|
+
return mountSuspended(PopOver, { props, slots });
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
describe("PopOver", () => {
|
|
16
|
+
let wrapper: Awaited<ReturnType<typeof createWrapper>>;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
// Popover API is not implemented in jsdom — define stubs so we can test
|
|
20
|
+
Object.defineProperty(HTMLElement.prototype, "hidePopover", {
|
|
21
|
+
value: vi.fn(),
|
|
22
|
+
writable: true,
|
|
23
|
+
configurable: true,
|
|
24
|
+
});
|
|
25
|
+
Object.defineProperty(HTMLElement.prototype, "showPopover", {
|
|
26
|
+
value: vi.fn(),
|
|
27
|
+
writable: true,
|
|
28
|
+
configurable: true,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
wrapper?.unmount();
|
|
34
|
+
vi.restoreAllMocks();
|
|
35
|
+
delete (HTMLElement.prototype as unknown as Record<string, unknown>)["hidePopover"];
|
|
36
|
+
delete (HTMLElement.prototype as unknown as Record<string, unknown>)["showPopover"];
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("mounts without error", async () => {
|
|
40
|
+
wrapper = await createWrapper();
|
|
41
|
+
expect(wrapper.vm).toBeTruthy();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("renders correct HTML structure", async () => {
|
|
45
|
+
wrapper = await createWrapper(
|
|
46
|
+
{},
|
|
47
|
+
{ trigger: "<span>Open</span>", content: "<p>Details</p>" }
|
|
48
|
+
);
|
|
49
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("Trigger", () => {
|
|
53
|
+
it("renders the trigger slot", async () => {
|
|
54
|
+
wrapper = await createWrapper({}, { trigger: '<span class="slot-trigger">Open</span>' });
|
|
55
|
+
expect(wrapper.find(".slot-trigger").text()).toBe("Open");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("trigger button has type='button'", async () => {
|
|
59
|
+
wrapper = await createWrapper();
|
|
60
|
+
expect(wrapper.find(".pop-over-trigger").attributes("type")).toBe("button");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("does not set aria-label on the trigger by default", async () => {
|
|
64
|
+
wrapper = await createWrapper();
|
|
65
|
+
expect(wrapper.find(".pop-over-trigger").attributes("aria-label")).toBeUndefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("sets aria-label on the trigger when triggerAriaLabel is given", async () => {
|
|
69
|
+
wrapper = await createWrapper({ triggerAriaLabel: "Show filters" });
|
|
70
|
+
expect(wrapper.find(".pop-over-trigger").attributes("aria-label")).toBe("Show filters");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("trigger button popovertarget matches the popover id", async () => {
|
|
74
|
+
wrapper = await createWrapper();
|
|
75
|
+
const vm = wrapper.vm as unknown as PopOverInstance;
|
|
76
|
+
expect(wrapper.find(".pop-over-trigger").attributes("popovertarget")).toBe(vm.popoverId);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("trigger button has popovertargetaction='toggle'", async () => {
|
|
80
|
+
wrapper = await createWrapper();
|
|
81
|
+
expect(wrapper.find(".pop-over-trigger").attributes("popovertargetaction")).toBe("toggle");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("Popover panel", () => {
|
|
86
|
+
it("renders the content slot", async () => {
|
|
87
|
+
wrapper = await createWrapper({}, { content: '<p class="slot-content">Details</p>' });
|
|
88
|
+
expect(wrapper.find(".pop-over-popover .slot-content").text()).toBe("Details");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("has the popover attribute", async () => {
|
|
92
|
+
wrapper = await createWrapper();
|
|
93
|
+
expect(wrapper.find(".pop-over-popover").attributes("popover")).toBeDefined();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("does not set aria-label by default", async () => {
|
|
97
|
+
wrapper = await createWrapper();
|
|
98
|
+
expect(wrapper.find(".pop-over-popover").attributes("aria-label")).toBeUndefined();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("sets aria-label when popoverAriaLabel is given", async () => {
|
|
102
|
+
wrapper = await createWrapper({ popoverAriaLabel: "Filter options" });
|
|
103
|
+
expect(wrapper.find(".pop-over-popover").attributes("aria-label")).toBe("Filter options");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("sets data-placement on the root element", async () => {
|
|
107
|
+
wrapper = await createWrapper({ placement: "top" });
|
|
108
|
+
expect(wrapper.find(".pop-over").attributes("data-placement")).toBe("top");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("defaults placement to right", async () => {
|
|
112
|
+
wrapper = await createWrapper();
|
|
113
|
+
expect(wrapper.find(".pop-over").attributes("data-placement")).toBe("right");
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("Close button", () => {
|
|
118
|
+
it("has a default aria-label", async () => {
|
|
119
|
+
wrapper = await createWrapper();
|
|
120
|
+
expect(wrapper.find(".pop-over-close-button").attributes("aria-label")).toBe("Close");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("uses a custom closeButtonAriaLabel", async () => {
|
|
124
|
+
wrapper = await createWrapper({ closeButtonAriaLabel: "Dismiss" });
|
|
125
|
+
expect(wrapper.find(".pop-over-close-button").attributes("aria-label")).toBe("Dismiss");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("has popovertargetaction='hide' pointing at the popover", async () => {
|
|
129
|
+
wrapper = await createWrapper();
|
|
130
|
+
const vm = wrapper.vm as unknown as PopOverInstance;
|
|
131
|
+
const closeButton = wrapper.find(".pop-over-close-button");
|
|
132
|
+
expect(closeButton.attributes("popovertargetaction")).toBe("hide");
|
|
133
|
+
expect(closeButton.attributes("popovertarget")).toBe(vm.popoverId);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("handleToggle focus management", () => {
|
|
138
|
+
it("focuses the close button when the popover opens", async () => {
|
|
139
|
+
wrapper = await createWrapper();
|
|
140
|
+
const closeButtonEl = wrapper.find(".pop-over-close-button").element as HTMLElement;
|
|
141
|
+
const focusSpy = vi.spyOn(closeButtonEl, "focus");
|
|
142
|
+
|
|
143
|
+
const vm = wrapper.vm as unknown as PopOverInstance;
|
|
144
|
+
vm.handleToggle(Object.assign(new Event("toggle"), { newState: "open" }));
|
|
145
|
+
|
|
146
|
+
expect(focusSpy).toHaveBeenCalledOnce();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("does not move focus when the popover closes", async () => {
|
|
150
|
+
wrapper = await createWrapper();
|
|
151
|
+
const closeButtonEl = wrapper.find(".pop-over-close-button").element as HTMLElement;
|
|
152
|
+
const focusSpy = vi.spyOn(closeButtonEl, "focus");
|
|
153
|
+
|
|
154
|
+
const vm = wrapper.vm as unknown as PopOverInstance;
|
|
155
|
+
vm.handleToggle(Object.assign(new Event("toggle"), { newState: "closed" }));
|
|
156
|
+
|
|
157
|
+
expect(focusSpy).not.toHaveBeenCalled();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("Anchor name", () => {
|
|
162
|
+
it("sets a unique --_anchor-name on the root element style", async () => {
|
|
163
|
+
wrapper = await createWrapper();
|
|
164
|
+
const style = wrapper.find(".pop-over").attributes("style") ?? "";
|
|
165
|
+
expect(style).toContain("--_anchor-name: --pop-over-anchor-");
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("styleClassPassthrough", () => {
|
|
170
|
+
it("applies a string class to the root element", async () => {
|
|
171
|
+
wrapper = await createWrapper({ styleClassPassthrough: "custom-pop-over" });
|
|
172
|
+
expect(wrapper.find(".pop-over").classes()).toContain("custom-pop-over");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("applies an array of classes to the root element", async () => {
|
|
176
|
+
wrapper = await createWrapper({ styleClassPassthrough: ["pop-over-a", "pop-over-b"] });
|
|
177
|
+
expect(wrapper.find(".pop-over").classes()).toContain("pop-over-a");
|
|
178
|
+
expect(wrapper.find(".pop-over").classes()).toContain("pop-over-b");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("root element always has the 'pop-over' class", async () => {
|
|
182
|
+
wrapper = await createWrapper({ styleClassPassthrough: "extra-class" });
|
|
183
|
+
expect(wrapper.find(".pop-over").classes()).toContain("pop-over");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("resets classes when styleClassPassthrough prop changes", async () => {
|
|
187
|
+
wrapper = await createWrapper({ styleClassPassthrough: "initial-class" });
|
|
188
|
+
expect(wrapper.find(".pop-over").classes()).toContain("initial-class");
|
|
189
|
+
|
|
190
|
+
await wrapper.setProps({ styleClassPassthrough: "updated-class" });
|
|
191
|
+
expect(wrapper.find(".pop-over").classes()).not.toContain("initial-class");
|
|
192
|
+
expect(wrapper.find(".pop-over").classes()).toContain("updated-class");
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`PopOver > renders correct HTML structure 1`] = `
|
|
4
|
+
"<div class="pop-over" style="--_anchor-name: --pop-over-anchor-v-0-0;" data-placement="right"><button popovertarget="pop-over-v-0-0" popovertargetaction="toggle" type="button" class="pop-over-trigger"><span>Open</span></button>
|
|
5
|
+
<div id="pop-over-v-0-0" popover="" class="pop-over-popover"><button popovertarget="pop-over-v-0-0" popovertargetaction="hide" type="button" class="pop-over-close-button" aria-label="Close"><span class="iconify i-lucide:x pop-over-close-button-icon" aria-hidden="true"></span></button>
|
|
6
|
+
<div class="pop-over-content">
|
|
7
|
+
<p>Details</p>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
</div>"
|
|
11
|
+
`;
|
|
@@ -74,10 +74,16 @@ export const Default: Story = {
|
|
|
74
74
|
render: (args) => ({
|
|
75
75
|
components: { ActionMenu, ActionMenuItemCore },
|
|
76
76
|
setup() {
|
|
77
|
-
|
|
77
|
+
// `args` is Storybook's own reactive object — read from it directly (`args.x`) inside
|
|
78
|
+
// these computeds rather than destructuring it into local variables at setup-time, which
|
|
79
|
+
// would snapshot the values once and stop reflecting later Controls-panel changes.
|
|
80
|
+
const componentArgs = computed(() => {
|
|
81
|
+
const { itemCount, ...rest } = args;
|
|
82
|
+
return rest;
|
|
83
|
+
});
|
|
78
84
|
const lastAction = ref<string | null>(null);
|
|
79
85
|
const items = computed(() =>
|
|
80
|
-
actionItems.slice(0, itemCount ?? 5).map((item, i) => ({
|
|
86
|
+
actionItems.slice(0, args.itemCount ?? 5).map((item, i) => ({
|
|
81
87
|
...item,
|
|
82
88
|
slotName: `item-${i}`,
|
|
83
89
|
}))
|
|
@@ -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>
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
<div class="input-copy" :class="elementClasses">
|
|
3
3
|
<div class="input-copy__wrapper">
|
|
4
4
|
<input
|
|
5
|
+
:id="resolvedId"
|
|
5
6
|
type="text"
|
|
6
7
|
class="input-copy__input"
|
|
8
|
+
:name="name || resolvedId"
|
|
7
9
|
:value="value"
|
|
8
10
|
:aria-label="ariaLabel || label"
|
|
9
11
|
readonly
|
|
@@ -26,6 +28,9 @@
|
|
|
26
28
|
<script setup lang="ts">
|
|
27
29
|
interface Props {
|
|
28
30
|
value: string;
|
|
31
|
+
/** Defaults to an auto-generated id — pass one explicitly only if something needs to target this input. */
|
|
32
|
+
id?: string;
|
|
33
|
+
name?: string;
|
|
29
34
|
label?: string;
|
|
30
35
|
ariaLabel?: string;
|
|
31
36
|
description?: string;
|
|
@@ -37,6 +42,8 @@ interface Props {
|
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
const props = withDefaults(defineProps<Props>(), {
|
|
45
|
+
id: undefined,
|
|
46
|
+
name: undefined,
|
|
40
47
|
label: undefined,
|
|
41
48
|
ariaLabel: undefined,
|
|
42
49
|
description: undefined,
|
|
@@ -47,6 +54,13 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
47
54
|
styleClassPassthrough: () => [],
|
|
48
55
|
});
|
|
49
56
|
|
|
57
|
+
// Readonly text inputs still need an id/name to satisfy "a form field element should have an
|
|
58
|
+
// id or name attribute" — auto-generated so every existing call site (none of which pass one)
|
|
59
|
+
// gets a valid, unique id for free. Named resolvedId, not id, so it can't be confused with (or
|
|
60
|
+
// silently shadow) the `id` prop it falls back to.
|
|
61
|
+
const autoId = useId();
|
|
62
|
+
const resolvedId = computed(() => props.id ?? autoId);
|
|
63
|
+
|
|
50
64
|
const emit = defineEmits<{
|
|
51
65
|
copy: [value: string];
|
|
52
66
|
copied: [value: string];
|
|
@@ -7,6 +7,11 @@ const meta: Meta<typeof InputCopy> = {
|
|
|
7
7
|
tags: ["autodocs"],
|
|
8
8
|
argTypes: {
|
|
9
9
|
value: { control: "text" },
|
|
10
|
+
id: {
|
|
11
|
+
control: "text",
|
|
12
|
+
description: "Defaults to an auto-generated id — pass one only if something needs to target this input.",
|
|
13
|
+
},
|
|
14
|
+
name: { control: "text", description: "Defaults to the resolved id if not provided." },
|
|
10
15
|
label: { control: "text" },
|
|
11
16
|
description: { control: "text" },
|
|
12
17
|
buttonText: { control: "text" },
|
|
@@ -101,6 +106,16 @@ export const NoDescription: Story = {
|
|
|
101
106
|
},
|
|
102
107
|
};
|
|
103
108
|
|
|
109
|
+
export const WithExplicitIdAndName: Story = {
|
|
110
|
+
args: {
|
|
111
|
+
value: "sk_live_abc123defgh456xyz789uvw",
|
|
112
|
+
id: "checkout-license-key",
|
|
113
|
+
name: "licenseKey",
|
|
114
|
+
label: "License key",
|
|
115
|
+
description: "Demonstrates passing explicit id/name instead of relying on the auto-generated one.",
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
104
119
|
export const LongValue: Story = {
|
|
105
120
|
args: {
|
|
106
121
|
value: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.POdHyP_DQkT3WTZhu9qLWSwVe8ZlHfHEPBOzzVI3vFA",
|
|
@@ -178,4 +178,45 @@ describe("InputCopy", () => {
|
|
|
178
178
|
expect(wrapper.classes()).toContain("custom-class");
|
|
179
179
|
expect(wrapper.classes()).toContain("another-class");
|
|
180
180
|
});
|
|
181
|
+
|
|
182
|
+
it("auto-generates an id and mirrors it as the name when neither is provided", () => {
|
|
183
|
+
const wrapper = mount(InputCopy, {
|
|
184
|
+
props: {
|
|
185
|
+
value: "test-key",
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const input = wrapper.find("input");
|
|
190
|
+
const id = input.attributes("id");
|
|
191
|
+
|
|
192
|
+
expect(id).toBeTruthy();
|
|
193
|
+
expect(input.attributes("name")).toBe(id);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("uses the provided id prop instead of auto-generating one", () => {
|
|
197
|
+
const wrapper = mount(InputCopy, {
|
|
198
|
+
props: {
|
|
199
|
+
value: "test-key",
|
|
200
|
+
id: "license-key-input",
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const input = wrapper.find("input");
|
|
205
|
+
expect(input.attributes("id")).toBe("license-key-input");
|
|
206
|
+
expect(input.attributes("name")).toBe("license-key-input");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("uses the provided name prop instead of falling back to the id", () => {
|
|
210
|
+
const wrapper = mount(InputCopy, {
|
|
211
|
+
props: {
|
|
212
|
+
value: "test-key",
|
|
213
|
+
id: "license-key-input",
|
|
214
|
+
name: "licenseKey",
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const input = wrapper.find("input");
|
|
219
|
+
expect(input.attributes("id")).toBe("license-key-input");
|
|
220
|
+
expect(input.attributes("name")).toBe("licenseKey");
|
|
221
|
+
});
|
|
181
222
|
});
|
|
@@ -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));
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
ref="mainNav"
|
|
10
10
|
class="main-navigation"
|
|
11
11
|
:class="{ 'is-animated': isAnimated }"
|
|
12
|
-
aria-label="
|
|
12
|
+
:aria-label="mainNavAriaLabel"
|
|
13
13
|
@mouseleave="hoveredItemKey = null"
|
|
14
14
|
@focusout="handleNavFocusout"
|
|
15
15
|
>
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
<div aria-hidden="true" class="nav-indicator-hovered"></div>
|
|
79
79
|
<div aria-hidden="true" class="nav-indicator-active"></div>
|
|
80
80
|
</nav>
|
|
81
|
-
<nav ref="secondaryNav" class="secondary-navigation" aria-label="
|
|
81
|
+
<nav ref="secondaryNav" class="secondary-navigation" :aria-label="secondaryNavAriaLabel">
|
|
82
82
|
<details
|
|
83
83
|
ref="overflowDetails"
|
|
84
84
|
class="overflow-details"
|
|
@@ -100,7 +100,11 @@
|
|
|
100
100
|
/>
|
|
101
101
|
</summary>
|
|
102
102
|
<div class="overflow-details-nav" role="menu">
|
|
103
|
-
<NavigationItems
|
|
103
|
+
<NavigationItems
|
|
104
|
+
:main-navigation-state="mainNavigationState"
|
|
105
|
+
:panel-variant="panelVariant"
|
|
106
|
+
:aria-label="overflowMenuAriaLabel"
|
|
107
|
+
/>
|
|
104
108
|
</div>
|
|
105
109
|
</details>
|
|
106
110
|
<slot v-if="slots.secondaryNavigation" name="secondaryNavigation"></slot>
|
|
@@ -126,6 +130,12 @@ interface Props {
|
|
|
126
130
|
styleClassPassthrough?: string | string[];
|
|
127
131
|
allowExpandOnGesture?: boolean;
|
|
128
132
|
panelVariant?: "modern" | "classic";
|
|
133
|
+
/** aria-label on the primary nav landmark — override for localisation. */
|
|
134
|
+
mainNavAriaLabel?: string;
|
|
135
|
+
/** aria-label on the secondary (overflow) nav landmark — override for localisation. */
|
|
136
|
+
secondaryNavAriaLabel?: string;
|
|
137
|
+
/** aria-label on the overflow menu itself (forwarded to NavigationItems) — override for localisation. */
|
|
138
|
+
overflowMenuAriaLabel?: string;
|
|
129
139
|
}
|
|
130
140
|
|
|
131
141
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -142,6 +152,9 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
142
152
|
// Forwarded to NavigationItems' overflow submenu panels — see its own default for why
|
|
143
153
|
// "classic" is the safe default (CLAUDE.md pitfall #19).
|
|
144
154
|
panelVariant: "classic",
|
|
155
|
+
mainNavAriaLabel: "Main navigation",
|
|
156
|
+
secondaryNavAriaLabel: "Secondary navigation",
|
|
157
|
+
overflowMenuAriaLabel: "Overflow navigation menu",
|
|
145
158
|
});
|
|
146
159
|
|
|
147
160
|
const collapseNavigationBelowWidth = computed(
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<PageRow tag="div" :variant="pageRowVariant" :style-class-passthrough="styleClassPassthrough">
|
|
3
3
|
<template #default>
|
|
4
4
|
<header class="site-header">
|
|
5
|
-
<nav class="home-navigation" aria-label="
|
|
5
|
+
<nav class="home-navigation" :aria-label="homeNavAriaLabel">
|
|
6
6
|
<SkipLinks>
|
|
7
7
|
<template #homeLink>
|
|
8
8
|
<slot name="branding"></slot>
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
:collapse-at-main-nav-intersection="collapseAtMainNavIntersection"
|
|
18
18
|
:allow-expand-on-gesture="allowExpandOnGesture"
|
|
19
19
|
:style-class-passthrough="navStyleClassPassthrough"
|
|
20
|
+
:main-nav-aria-label="mainNavAriaLabel"
|
|
21
|
+
:secondary-nav-aria-label="secondaryNavAriaLabel"
|
|
22
|
+
:overflow-menu-aria-label="overflowMenuAriaLabel"
|
|
20
23
|
>
|
|
21
24
|
<template v-if="slots.secondaryNavigation" #secondaryNavigation>
|
|
22
25
|
<slot name="secondaryNavigation"></slot>
|
|
@@ -43,6 +46,14 @@ interface Props {
|
|
|
43
46
|
pageRowVariant?: "full" | "popout" | "content" | "inset-content";
|
|
44
47
|
styleClassPassthrough?: string | string[];
|
|
45
48
|
navStyleClassPassthrough?: string | string[];
|
|
49
|
+
/** aria-label on the home/branding nav landmark — override for localisation. */
|
|
50
|
+
homeNavAriaLabel?: string;
|
|
51
|
+
/** Forwarded to ResponsiveHeader's mainNavAriaLabel — override for localisation. */
|
|
52
|
+
mainNavAriaLabel?: string;
|
|
53
|
+
/** Forwarded to ResponsiveHeader's secondaryNavAriaLabel — override for localisation. */
|
|
54
|
+
secondaryNavAriaLabel?: string;
|
|
55
|
+
/** Forwarded to ResponsiveHeader's overflowMenuAriaLabel — override for localisation. */
|
|
56
|
+
overflowMenuAriaLabel?: string;
|
|
46
57
|
}
|
|
47
58
|
|
|
48
59
|
withDefaults(defineProps<Props>(), {
|
|
@@ -58,6 +69,10 @@ withDefaults(defineProps<Props>(), {
|
|
|
58
69
|
pageRowVariant: "content",
|
|
59
70
|
styleClassPassthrough: () => [],
|
|
60
71
|
navStyleClassPassthrough: () => [],
|
|
72
|
+
homeNavAriaLabel: "Home Navigation",
|
|
73
|
+
mainNavAriaLabel: "Main navigation",
|
|
74
|
+
secondaryNavAriaLabel: "Secondary navigation",
|
|
75
|
+
overflowMenuAriaLabel: "Overflow navigation menu",
|
|
61
76
|
});
|
|
62
77
|
|
|
63
78
|
const slots = useSlots();
|
|
@@ -9,7 +9,7 @@ exports[`SiteHeader > renders correct HTML structure with default props 1`] = `
|
|
|
9
9
|
<nav class="skip-links-nav" aria-label="Skip navigation"><a href="#main-content" class="skip-link">Skip to main content</a><a href="#footer-content" class="skip-link">Skip to footer</a></nav>
|
|
10
10
|
</div>
|
|
11
11
|
</nav>
|
|
12
|
-
<div class="responsive-header-stub"></div>
|
|
12
|
+
<div class="responsive-header-stub" main-nav-aria-label="Main navigation" secondary-nav-aria-label="Secondary navigation" overflow-menu-aria-label="Overflow navigation menu"></div>
|
|
13
13
|
</header>
|
|
14
14
|
</div>"
|
|
15
15
|
`;
|