srcdev-nuxt-components 9.4.6 → 9.4.8
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 +34 -3
- 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/component-dynamic-slots.md +1 -1
- package/.claude/skills/components/column-flow-grid.md +93 -0
- package/.claude/skills/components/entry-animation.md +88 -0
- package/.claude/skills/components/header-block.md +92 -0
- package/.claude/skills/components/input-copy.md +2 -0
- package/.claude/skills/components/input-text-core.md +186 -0
- package/.claude/skills/components/masonry-grid.md +153 -0
- package/.claude/skills/components/pop-over.md +100 -0
- package/.claude/skills/components/rotating-carousel-image.md +101 -0
- package/.claude/skills/components/skip-links.md +82 -0
- package/.claude/skills/components/tabs-core.md +187 -0
- package/.claude/skills/index.md +10 -1
- package/.vscode/srcdev-component-column-flow-grid.code-snippets +39 -0
- package/.vscode/srcdev-component-entry-animation.code-snippets +29 -0
- package/.vscode/srcdev-component-header-block.code-snippets +41 -0
- package/.vscode/srcdev-component-input-text.code-snippets +107 -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-rotating-carousel-image.code-snippets +74 -0
- package/.vscode/srcdev-component-skip-links.code-snippets +55 -0
- package/.vscode/srcdev-component-tabs-core.code-snippets +78 -0
- package/app/assets/styles/setup/05.typography/02.utility-classes/_font-classes-page-heading.css +2 -1
- 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/rotating-carousel-image/CONSUMER-STYLING.md +54 -0
- package/app/components/01.atoms/animations/rotating-carousel-image/RotatingCarouselImage.vue +315 -0
- package/app/components/01.atoms/animations/rotating-carousel-image/stories/RotatingCarouselImage.stories.ts +156 -0
- package/app/components/01.atoms/animations/rotating-carousel-image/tests/RotatingCarouselImage.spec.ts +230 -0
- package/app/components/01.atoms/animations/rotating-carousel-image/tests/__snapshots__/RotatingCarouselImage.spec.ts.snap +19 -0
- package/app/components/01.atoms/canvas-switcher/stories/CanvasSwitcher.stories.ts +9 -10
- package/app/components/01.atoms/content-wrappers/docs-pages/stories/ContentDocs.stories.ts +12 -13
- 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/skip-links/CONSUMER-STYLING.md +31 -0
- package/app/components/01.atoms/navigation/skip-links/SkipLinks.vue +92 -0
- package/app/components/01.atoms/navigation/skip-links/stories/SkipLinks.stories.ts +113 -0
- package/app/components/01.atoms/navigation/skip-links/tests/SkipLinks.spec.ts +71 -0
- package/app/components/01.atoms/navigation/skip-links/tests/__snapshots__/SkipLinks.spec.ts.snap +15 -0
- package/app/components/01.atoms/navigation/tabs/CONSUMER-STYLING.md +99 -0
- package/app/components/01.atoms/navigation/tabs/TabsCore.vue +272 -0
- package/app/components/01.atoms/navigation/tabs/stories/TabsCore.stories.ts +199 -0
- package/app/components/01.atoms/navigation/tabs/tests/TabsCore.spec.ts +274 -0
- 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/01.atoms/text-blocks/header-block/CONSUMER-STYLING.md +43 -0
- package/app/components/01.atoms/text-blocks/header-block/HeaderBlock.vue +50 -0
- package/app/components/01.atoms/text-blocks/header-block/stories/HeaderBlock.stories.ts +111 -0
- package/app/components/01.atoms/text-blocks/header-block/tests/HeaderBlock.spec.ts +119 -0
- package/app/components/01.atoms/text-blocks/header-block/tests/__snapshots__/HeaderBlock.spec.ts.snap +5 -0
- package/app/components/02.molecules/action-menu/stories/ActionMenu.stories.ts +8 -2
- 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/03.organisms/site-header/SiteHeader.vue +1 -1
- package/app/components/03.organisms/site-header/tests/SiteHeader.spec.ts +1 -1
- package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +3 -3
- 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/composables/useTabs.ts +225 -207
- package/app/types/components/index.ts +2 -0
- package/app/types/components/rotating-carousel-image.d.ts +4 -0
- package/app/types/components/skip-links.d.ts +4 -0
- package/app/types/forms/types.forms.d.ts +1 -1
- package/package.json +1 -1
- package/app/components/01.atoms/grids/data-grid/tests/__snapshots__/DataGrid.spec.ts.snap +0 -11
- 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/components/rotating-carousel/RotatingCarouselImage.vue +0 -216
- package/app/components/skip-links/SkipLinks.vue +0 -60
- package/app/components/tabs/TabsCore.vue +0 -306
- package/app/components/typography/HeaderBlock.vue +0 -35
- package/app/layouts/default.vue +0 -308
- package/app/layouts/site-navigation-demo.vue +0 -188
- /package/app/components/01.atoms/grids/{data-grid → auto-grid}/AutoGrid.vue +0 -0
- /package/app/components/01.atoms/grids/{data-grid → auto-grid}/stories/AutoGrid.stories.ts +0 -0
- /package/app/components/01.atoms/grids/{data-grid → auto-grid}/tests/AutoGrid.spec.ts +0 -0
- /package/app/components/01.atoms/grids/{data-grid → auto-grid}/tests/__snapshots__/AutoGrid.spec.ts.snap +0 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
+
import MasonryGrid from "../MasonryGrid.vue";
|
|
3
|
+
|
|
4
|
+
interface MasonryGridArgs {
|
|
5
|
+
tag: "div" | "section" | "article" | "main";
|
|
6
|
+
itemMinWidth: number;
|
|
7
|
+
gap: number;
|
|
8
|
+
fixedWidth: boolean;
|
|
9
|
+
justify: "left" | "center" | "right";
|
|
10
|
+
styleClassPassthrough: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const meta: Meta<MasonryGridArgs> = {
|
|
14
|
+
title: "Atoms/Grids/MasonryGrid",
|
|
15
|
+
component: MasonryGrid,
|
|
16
|
+
argTypes: {
|
|
17
|
+
tag: {
|
|
18
|
+
control: { type: "select" },
|
|
19
|
+
options: ["div", "section", "article", "main"],
|
|
20
|
+
description: "HTML tag to render as",
|
|
21
|
+
table: { category: "Props" },
|
|
22
|
+
},
|
|
23
|
+
itemMinWidth: {
|
|
24
|
+
control: { type: "select" },
|
|
25
|
+
options: [160, 200, 240, 300, 360, 450],
|
|
26
|
+
description: "Minimum tile width in pixels — also the fixed tile width when fixedWidth is set",
|
|
27
|
+
table: { category: "Props" },
|
|
28
|
+
},
|
|
29
|
+
gap: {
|
|
30
|
+
control: { type: "select" },
|
|
31
|
+
options: [0, 4, 8, 12, 16, 24, 32],
|
|
32
|
+
description: "Gap between tiles in pixels",
|
|
33
|
+
table: { category: "Props" },
|
|
34
|
+
},
|
|
35
|
+
fixedWidth: {
|
|
36
|
+
control: { type: "boolean" },
|
|
37
|
+
description: "Keep every tile at exactly itemMinWidth instead of stretching to fill each column",
|
|
38
|
+
table: { category: "Props" },
|
|
39
|
+
},
|
|
40
|
+
justify: {
|
|
41
|
+
control: { type: "select" },
|
|
42
|
+
options: ["left", "center", "right"],
|
|
43
|
+
description: "How the block of tiles aligns within the wrapper — only visible when fixedWidth is set",
|
|
44
|
+
table: { category: "Props" },
|
|
45
|
+
},
|
|
46
|
+
styleClassPassthrough: {
|
|
47
|
+
table: { disable: true },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
args: {
|
|
51
|
+
tag: "div",
|
|
52
|
+
itemMinWidth: 300,
|
|
53
|
+
gap: 12,
|
|
54
|
+
fixedWidth: false,
|
|
55
|
+
justify: "left",
|
|
56
|
+
styleClassPassthrough: [],
|
|
57
|
+
},
|
|
58
|
+
parameters: {
|
|
59
|
+
docs: {
|
|
60
|
+
description: {
|
|
61
|
+
component:
|
|
62
|
+
"A real masonry layout: each item is measured and placed into whichever column is " +
|
|
63
|
+
"currently shortest (a greedy bin-pack), not CSS `columns`' column-fill. Renders " +
|
|
64
|
+
"whatever slots the consumer passes, in natural DOM/reading order — no count/data prop " +
|
|
65
|
+
"needed, and no reordering trick required (unlike a `columns`-based masonry) since " +
|
|
66
|
+
"placement already follows the order items were authored in. Resize-reactive: items " +
|
|
67
|
+
"animate into their new position (respecting `prefers-reduced-motion`) as the container " +
|
|
68
|
+
"resizes. See `ColumnFlowGrid` for the lighter, non-measuring CSS `columns` alternative.",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export default meta;
|
|
75
|
+
|
|
76
|
+
const itemStyle =
|
|
77
|
+
"padding: 1.6rem; background: white; border-radius: 0.6rem; box-shadow: 0 2px 8px rgba(0,0,0,0.08);";
|
|
78
|
+
const badgeStyle =
|
|
79
|
+
"display: inline-flex; align-items: center; justify-content: center; width: 2.4rem; height: 2.4rem; " +
|
|
80
|
+
"margin-bottom: 0.8rem; border-radius: 50%; background: #111827; color: white; font-weight: 700; " +
|
|
81
|
+
"font-size: 1.3rem; font-family: monospace;";
|
|
82
|
+
|
|
83
|
+
// Each item shows its DOM/reading order as a numbered badge. Unlike a reading-order effect based
|
|
84
|
+
// on rearranging DOM order to match a visual grid, here the number is simply the authoring order —
|
|
85
|
+
// tab/screen-reader order always reads 1, 2, 3... regardless of which column an item's measured
|
|
86
|
+
// height ends up placing it in visually.
|
|
87
|
+
const paragraphPool = [
|
|
88
|
+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio praesent libero sed cursus ante.",
|
|
89
|
+
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
|
|
90
|
+
"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
|
|
91
|
+
"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
const itemBadge = (n: number) => `<span style="${badgeStyle}">${n}</span><br />`;
|
|
95
|
+
|
|
96
|
+
interface DemoItem {
|
|
97
|
+
slotName: string;
|
|
98
|
+
index: number;
|
|
99
|
+
bodyHtml: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Built once per mount (not on every render) so a Controls-panel change doesn't reshuffle the
|
|
103
|
+
// demo content underneath it. Random paragraph counts (1-4) give genuinely uneven heights —
|
|
104
|
+
// necessary to actually see the shortest-column packing at work, unlike uniform-height tiles.
|
|
105
|
+
function buildDemoItems(count: number): DemoItem[] {
|
|
106
|
+
return Array.from({ length: count }, (_, i) => {
|
|
107
|
+
const paragraphCount = Math.floor(Math.random() * 4) + 1; // 1-4
|
|
108
|
+
const bodyHtml = Array.from(
|
|
109
|
+
{ length: paragraphCount },
|
|
110
|
+
() => `<p style="margin: 0 0 0.8rem;">${paragraphPool[Math.floor(Math.random() * paragraphPool.length)]}</p>`
|
|
111
|
+
).join("");
|
|
112
|
+
return { slotName: `item-${i + 1}`, index: i + 1, bodyHtml };
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const Template: StoryFn<MasonryGridArgs> = (args) => ({
|
|
117
|
+
components: { MasonryGrid },
|
|
118
|
+
setup() {
|
|
119
|
+
const items = buildDemoItems(12);
|
|
120
|
+
return { args, items };
|
|
121
|
+
},
|
|
122
|
+
template: `
|
|
123
|
+
<div style="padding: 3.2rem; background: #f9fafb;">
|
|
124
|
+
<MasonryGrid v-bind="args">
|
|
125
|
+
<template v-for="item in items" :key="item.slotName" #[item.slotName]>
|
|
126
|
+
<div style="${itemStyle}">
|
|
127
|
+
<span style="${badgeStyle}">{{ item.index }}</span><br />
|
|
128
|
+
<div v-html="item.bodyHtml"></div>
|
|
129
|
+
</div>
|
|
130
|
+
</template>
|
|
131
|
+
</MasonryGrid>
|
|
132
|
+
</div>
|
|
133
|
+
`,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
export const Default = Template.bind({});
|
|
137
|
+
Default.parameters = {
|
|
138
|
+
docs: {
|
|
139
|
+
description: {
|
|
140
|
+
story:
|
|
141
|
+
"12 numbered items with 1-4 random paragraphs each, flowing into whichever column is " +
|
|
142
|
+
"currently shortest. Resize the preview panel — items animate into their new positions, " +
|
|
143
|
+
"and DOM/tab order always stays 1, 2, 3... regardless of which column each ends up in.",
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export const NarrowColumns = Template.bind({});
|
|
149
|
+
NarrowColumns.args = { itemMinWidth: 160, gap: 8 };
|
|
150
|
+
NarrowColumns.parameters = {
|
|
151
|
+
docs: {
|
|
152
|
+
description: { story: "A smaller `itemMinWidth` fits more, narrower columns." },
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export const WideColumns = Template.bind({});
|
|
157
|
+
WideColumns.args = { itemMinWidth: 450, gap: 16 };
|
|
158
|
+
WideColumns.parameters = {
|
|
159
|
+
docs: {
|
|
160
|
+
description: { story: "A larger `itemMinWidth` fits fewer, wider columns." },
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export const FixedWidthCentered = Template.bind({});
|
|
165
|
+
FixedWidthCentered.storyName = "Fixed Width, Centered";
|
|
166
|
+
FixedWidthCentered.args = { fixedWidth: true, justify: "center", itemMinWidth: 240 };
|
|
167
|
+
FixedWidthCentered.parameters = {
|
|
168
|
+
docs: {
|
|
169
|
+
description: {
|
|
170
|
+
story:
|
|
171
|
+
"`fixedWidth` keeps every tile at exactly `itemMinWidth` instead of stretching to fill " +
|
|
172
|
+
"each column — `justify` then controls how the resulting (narrower) block of tiles " +
|
|
173
|
+
"aligns within the available width.",
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export const TwoItems: StoryFn<MasonryGridArgs> = (args) => ({
|
|
179
|
+
components: { MasonryGrid },
|
|
180
|
+
setup() {
|
|
181
|
+
return { args };
|
|
182
|
+
},
|
|
183
|
+
template: `
|
|
184
|
+
<div style="padding: 3.2rem; background: #f9fafb;">
|
|
185
|
+
<MasonryGrid v-bind="args">
|
|
186
|
+
<template #item-1>
|
|
187
|
+
<div style="${itemStyle}">${itemBadge(1)}Only two items provided</div>
|
|
188
|
+
</template>
|
|
189
|
+
<template #item-2>
|
|
190
|
+
<div style="${itemStyle}">
|
|
191
|
+
${itemBadge(2)}No count prop to keep in sync — the grid just renders whichever
|
|
192
|
+
slots you give it.
|
|
193
|
+
</div>
|
|
194
|
+
</template>
|
|
195
|
+
</MasonryGrid>
|
|
196
|
+
</div>
|
|
197
|
+
`,
|
|
198
|
+
});
|
|
199
|
+
TwoItems.storyName = "Two Items (no count prop needed)";
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import { nextTick } from "vue";
|
|
4
|
+
import MasonryGrid from "../MasonryGrid.vue";
|
|
5
|
+
|
|
6
|
+
// useElementSize/useResizeObserver (from @vueuse/core) require ResizeObserver, which jsdom
|
|
7
|
+
// doesn't implement. Mock it and capture the callback so tests can simulate a real resize event —
|
|
8
|
+
// jsdom also never actually measures layout (offsetWidth/offsetHeight stay 0), so this is the only
|
|
9
|
+
// way to feed the component a non-zero measured width.
|
|
10
|
+
// Vitest 4: a mock's implementation must be a regular function (not an arrow function)
|
|
11
|
+
// to remain constructible via `new`.
|
|
12
|
+
let resizeCallback: ResizeObserverCallback | undefined;
|
|
13
|
+
const mockResizeObserver = vi.fn(function (callback: ResizeObserverCallback) {
|
|
14
|
+
resizeCallback = callback;
|
|
15
|
+
return {
|
|
16
|
+
observe: vi.fn(),
|
|
17
|
+
unobserve: vi.fn(),
|
|
18
|
+
disconnect: vi.fn(),
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
resizeCallback = undefined;
|
|
24
|
+
vi.stubGlobal("ResizeObserver", mockResizeObserver);
|
|
25
|
+
// ⚠️ Do NOT call vi.unstubAllGlobals() in afterEach — it removes global stubs from
|
|
26
|
+
// vitest.setup.ts ($fetch, etc.)
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function simulateResizeTo(width: number) {
|
|
30
|
+
resizeCallback?.(
|
|
31
|
+
[{ contentBoxSize: [{ inlineSize: width, blockSize: 0 }], contentRect: { width } } as unknown as ResizeObserverEntry],
|
|
32
|
+
{} as ResizeObserver
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function stubOffsetHeight(el: Element, height: number) {
|
|
37
|
+
Object.defineProperty(el, "offsetHeight", { value: height, configurable: true });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("MasonryGrid", () => {
|
|
41
|
+
it("mounts without error", async () => {
|
|
42
|
+
const wrapper = await mountSuspended(MasonryGrid);
|
|
43
|
+
expect(wrapper.vm).toBeTruthy();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("renders as div by default", async () => {
|
|
47
|
+
const wrapper = await mountSuspended(MasonryGrid);
|
|
48
|
+
expect(wrapper.element.tagName).toBe("DIV");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("renders a different tag when provided", async () => {
|
|
52
|
+
const wrapper = await mountSuspended(MasonryGrid, {
|
|
53
|
+
props: { tag: "section" },
|
|
54
|
+
});
|
|
55
|
+
expect(wrapper.element.tagName).toBe("SECTION");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("renders slot content in natural authoring order", async () => {
|
|
59
|
+
const wrapper = await mountSuspended(MasonryGrid, {
|
|
60
|
+
slots: {
|
|
61
|
+
"item-1": "<div>Item 1</div>",
|
|
62
|
+
"item-2": "<div>Item 2</div>",
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
const items = wrapper.findAll(".masonry-grid-item");
|
|
66
|
+
expect(items.map((item) => item.text())).toEqual(["Item 1", "Item 2"]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("renders no items when no slots are provided", async () => {
|
|
70
|
+
const wrapper = await mountSuspended(MasonryGrid);
|
|
71
|
+
expect(wrapper.findAll(".masonry-grid-item")).toHaveLength(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("does not absolutely position items when only one column fits (single-column fallback)", async () => {
|
|
75
|
+
const wrapper = await mountSuspended(MasonryGrid, {
|
|
76
|
+
slots: { "item-1": "<div>Item 1</div>", "item-2": "<div>Item 2</div>" },
|
|
77
|
+
});
|
|
78
|
+
await nextTick();
|
|
79
|
+
await nextTick();
|
|
80
|
+
expect(wrapper.find(".masonry-grid-wrapper").classes()).not.toContain("multiple-cols");
|
|
81
|
+
const item = wrapper.find(".masonry-grid-item").element as HTMLElement;
|
|
82
|
+
expect(item.style.getPropertyValue("--_position")).toBe("");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("packs items into whichever column is currently shortest", async () => {
|
|
86
|
+
const wrapper = await mountSuspended(MasonryGrid, {
|
|
87
|
+
// itemMinWidth (300, default) + gap (12, default) = 312 -> floor(700/312) = 2 columns
|
|
88
|
+
slots: {
|
|
89
|
+
"item-1": "<div>Item 1</div>",
|
|
90
|
+
"item-2": "<div>Item 2</div>",
|
|
91
|
+
"item-3": "<div>Item 3</div>",
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
const items = wrapper.findAll(".masonry-grid-item");
|
|
95
|
+
stubOffsetHeight(items[0]!.element, 100);
|
|
96
|
+
stubOffsetHeight(items[1]!.element, 50);
|
|
97
|
+
stubOffsetHeight(items[2]!.element, 80);
|
|
98
|
+
|
|
99
|
+
simulateResizeTo(700);
|
|
100
|
+
await nextTick();
|
|
101
|
+
await nextTick(); // updateGrid is itself wrapped in an extra nextTick()
|
|
102
|
+
|
|
103
|
+
// col heights start [0, 0]. item-1 (h=100) -> shortest is col 0 -> col becomes 112.
|
|
104
|
+
// item-2 (h=50) -> shortest is now col 1 (0 < 112) -> col becomes 62.
|
|
105
|
+
// item-3 (h=80) -> shortest is still col 1 (62 < 112) -> placed below item-2.
|
|
106
|
+
// itemWidth (stretch) = floor((700 - 1*12) / 2) = 344; column steps are itemWidth+gap = 356px.
|
|
107
|
+
const el1 = items[0]!.element as HTMLElement;
|
|
108
|
+
const el2 = items[1]!.element as HTMLElement;
|
|
109
|
+
const el3 = items[2]!.element as HTMLElement;
|
|
110
|
+
expect(el1.style.getPropertyValue("--_position-top")).toBe("0px");
|
|
111
|
+
expect(el1.style.getPropertyValue("--_position-left")).toBe("0px");
|
|
112
|
+
expect(el2.style.getPropertyValue("--_position-top")).toBe("0px");
|
|
113
|
+
expect(el2.style.getPropertyValue("--_position-left")).toBe("356px");
|
|
114
|
+
expect(el3.style.getPropertyValue("--_position-top")).toBe("62px");
|
|
115
|
+
expect(el3.style.getPropertyValue("--_position-left")).toBe("356px");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("uses a fixed item width when fixedWidth is set, ignoring stretch sizing", async () => {
|
|
119
|
+
const wrapper = await mountSuspended(MasonryGrid, {
|
|
120
|
+
props: { fixedWidth: true, itemMinWidth: 200 },
|
|
121
|
+
slots: { "item-1": "<div>Item 1</div>", "item-2": "<div>Item 2</div>" },
|
|
122
|
+
});
|
|
123
|
+
simulateResizeTo(700); // (200 + 12) -> floor(700/212) = 3 columns
|
|
124
|
+
await nextTick();
|
|
125
|
+
await nextTick();
|
|
126
|
+
const item = wrapper.find(".masonry-grid-item").element as HTMLElement;
|
|
127
|
+
expect(item.style.getPropertyValue("--_element-width")).toBe("200px");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("spaces fixed-width columns by exactly itemWidth + gap, and centers the block via justify", async () => {
|
|
131
|
+
const wrapper = await mountSuspended(MasonryGrid, {
|
|
132
|
+
props: { fixedWidth: true, itemMinWidth: 200, gap: 20, justify: "center" },
|
|
133
|
+
slots: {
|
|
134
|
+
"item-1": "<div>Item 1</div>",
|
|
135
|
+
"item-2": "<div>Item 2</div>",
|
|
136
|
+
"item-3": "<div>Item 3</div>",
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
simulateResizeTo(700); // (200 + 20) -> floor(700/220) = 3 columns
|
|
140
|
+
await nextTick();
|
|
141
|
+
await nextTick();
|
|
142
|
+
|
|
143
|
+
// 3 columns * 200px + 2 gaps * 20px = 640px total content width, centered in 700px
|
|
144
|
+
// available -> 30px leftover on each side.
|
|
145
|
+
const items = wrapper.findAll(".masonry-grid-item");
|
|
146
|
+
const [el1, el2, el3] = items.map((item) => item.element as HTMLElement);
|
|
147
|
+
expect(el1!.style.getPropertyValue("--_position-left")).toBe("30px");
|
|
148
|
+
expect(el2!.style.getPropertyValue("--_position-left")).toBe("250px");
|
|
149
|
+
expect(el3!.style.getPropertyValue("--_position-left")).toBe("470px");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("applies styleClassPassthrough", async () => {
|
|
153
|
+
const wrapper = await mountSuspended(MasonryGrid, {
|
|
154
|
+
props: { styleClassPassthrough: "custom-class" },
|
|
155
|
+
});
|
|
156
|
+
expect(wrapper.classes()).toContain("custom-class");
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# SkipLinks — Consumer Styling Guide
|
|
2
|
+
|
|
3
|
+
## Public token API
|
|
4
|
+
|
|
5
|
+
| Token | Default | Controls |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `--skip-links-z-index` | `1000` | Stacking context of the revealed skip-nav panel |
|
|
8
|
+
| `--skip-links-gap` | `0.2rem` | Gap between stacked skip links |
|
|
9
|
+
| `--skip-links-background-colour` | `black` | Skip-nav panel background |
|
|
10
|
+
| `--skip-links-border-colour` | `white` | Skip-nav panel border colour |
|
|
11
|
+
| `--skip-links-border-width` | `1px` | Skip-nav panel border width |
|
|
12
|
+
| `--skip-links-padding` | `0.2rem` | Skip-nav panel padding |
|
|
13
|
+
| `--skip-links-text-colour` | `white` | Skip link text colour |
|
|
14
|
+
| `--skip-links-link-padding-block` | `0.8rem` | Skip link vertical padding |
|
|
15
|
+
| `--skip-links-link-padding-inline` | `1.2rem` | Skip link horizontal padding |
|
|
16
|
+
| `--skip-links-transition-duration` | `0.3s` | Duration of the reveal transition when a link is focused |
|
|
17
|
+
| `--skip-links-focus-outline-width` | `2px` | Focus outline width on a skip link |
|
|
18
|
+
| `--skip-links-focus-outline-colour` | `white` | Focus outline colour on a skip link |
|
|
19
|
+
|
|
20
|
+
```css
|
|
21
|
+
.my-page {
|
|
22
|
+
--skip-links-background-colour: #111;
|
|
23
|
+
--skip-links-border-colour: #ffd700;
|
|
24
|
+
--skip-links-focus-outline-colour: #ffd700;
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Links are prop-driven
|
|
29
|
+
|
|
30
|
+
The set of skip links is provided via the `links` prop (`{ href, label }[]`), not slots — see the
|
|
31
|
+
skill doc for the default value and localisation notes.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="skip-links" :class="[elementClasses]">
|
|
3
|
+
<div v-if="$slots.homeLink" class="skip-links__home">
|
|
4
|
+
<slot name="homeLink"></slot>
|
|
5
|
+
</div>
|
|
6
|
+
<nav class="skip-links__nav" :aria-label="ariaLabel">
|
|
7
|
+
<a v-for="link in links" :key="link.href" :href="link.href" class="skip-links__link">{{ link.label }}</a>
|
|
8
|
+
</nav>
|
|
9
|
+
</div>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script lang="ts" setup>
|
|
13
|
+
import type { SkipLink } from "~/types/components"
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
links?: SkipLink[]
|
|
17
|
+
/** aria-label on the nav landmark — override for localisation. */
|
|
18
|
+
ariaLabel?: string
|
|
19
|
+
styleClassPassthrough?: string | string[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
23
|
+
links: () => [
|
|
24
|
+
{ href: "#main-content", label: "Skip to main content" },
|
|
25
|
+
{ href: "#footer-content", label: "Skip to footer" },
|
|
26
|
+
],
|
|
27
|
+
ariaLabel: "Skip navigation",
|
|
28
|
+
styleClassPassthrough: () => [],
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough)
|
|
32
|
+
|
|
33
|
+
watch(
|
|
34
|
+
() => props.styleClassPassthrough,
|
|
35
|
+
() => {
|
|
36
|
+
resetElementClasses(props.styleClassPassthrough)
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<style lang="css">
|
|
42
|
+
@layer components {
|
|
43
|
+
.skip-links {
|
|
44
|
+
position: relative;
|
|
45
|
+
|
|
46
|
+
.skip-links__nav {
|
|
47
|
+
position: absolute;
|
|
48
|
+
top: 100%;
|
|
49
|
+
left: 0;
|
|
50
|
+
z-index: var(--skip-links-z-index, 1000);
|
|
51
|
+
|
|
52
|
+
display: flex;
|
|
53
|
+
flex-direction: column;
|
|
54
|
+
gap: var(--skip-links-gap, 0.2rem);
|
|
55
|
+
background-color: var(--skip-links-background-colour, black);
|
|
56
|
+
border: var(--skip-links-border-width, 1px) solid var(--skip-links-border-colour, white);
|
|
57
|
+
padding: var(--skip-links-padding, 0.2rem);
|
|
58
|
+
margin: 0;
|
|
59
|
+
|
|
60
|
+
/* Hidden off-screen via transform — does NOT break keyboard focusability */
|
|
61
|
+
opacity: 0;
|
|
62
|
+
transform: translateY(-100%);
|
|
63
|
+
transition:
|
|
64
|
+
transform var(--skip-links-transition-duration, 0.3s) ease-in-out,
|
|
65
|
+
opacity var(--skip-links-transition-duration, 0.3s) ease-in-out;
|
|
66
|
+
|
|
67
|
+
&:focus-within {
|
|
68
|
+
opacity: 1;
|
|
69
|
+
transform: translateY(0);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.skip-links__link {
|
|
74
|
+
display: block;
|
|
75
|
+
color: var(--skip-links-text-colour, white);
|
|
76
|
+
text-decoration: none;
|
|
77
|
+
padding: var(--skip-links-link-padding-block, 0.8rem) var(--skip-links-link-padding-inline, 1.2rem);
|
|
78
|
+
text-wrap: nowrap;
|
|
79
|
+
|
|
80
|
+
&:focus-visible {
|
|
81
|
+
outline: var(--skip-links-focus-outline-width, 2px) solid var(--skip-links-focus-outline-colour, white);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@media (prefers-reduced-motion: reduce) {
|
|
87
|
+
.skip-links .skip-links__nav {
|
|
88
|
+
transition: none;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
</style>
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import SkipLinks from "../SkipLinks.vue";
|
|
2
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
+
import type { SkipLink } from "~/types/components";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof SkipLinks> = {
|
|
6
|
+
title: "Atoms/Navigation/SkipLinks",
|
|
7
|
+
component: SkipLinks,
|
|
8
|
+
argTypes: {
|
|
9
|
+
links: {
|
|
10
|
+
control: "object",
|
|
11
|
+
description: "Ordered list of { href, label } skip targets",
|
|
12
|
+
},
|
|
13
|
+
ariaLabel: {
|
|
14
|
+
control: "text",
|
|
15
|
+
description: "aria-label on the nav landmark — override for localisation",
|
|
16
|
+
},
|
|
17
|
+
styleClassPassthrough: {
|
|
18
|
+
control: "object",
|
|
19
|
+
description: "Additional CSS classes applied to the root element",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
args: {
|
|
23
|
+
ariaLabel: "Skip navigation",
|
|
24
|
+
styleClassPassthrough: [],
|
|
25
|
+
},
|
|
26
|
+
parameters: {
|
|
27
|
+
docs: {
|
|
28
|
+
description: {
|
|
29
|
+
component:
|
|
30
|
+
"An accessibility utility that renders a set of visually-hidden links revealed only on keyboard focus, letting keyboard/screen-reader users jump past repeated navigation straight to main content, the footer, or any other in-page target. The links are placed first in the DOM — consumers should place `<SkipLinks>` at the very top of their page layout.",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default meta;
|
|
37
|
+
type Story = StoryObj<typeof SkipLinks>;
|
|
38
|
+
|
|
39
|
+
// ─── Stories ──────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
export const Default: Story = {
|
|
42
|
+
render: (args) => ({
|
|
43
|
+
components: { SkipLinks },
|
|
44
|
+
setup() {
|
|
45
|
+
return { args };
|
|
46
|
+
},
|
|
47
|
+
template: `
|
|
48
|
+
<div>
|
|
49
|
+
<p style="padding: 2rem; text-align: center;">
|
|
50
|
+
The skip links below are visually hidden until focused. Click into this preview, then
|
|
51
|
+
press <kbd>Tab</kbd> to reveal them.
|
|
52
|
+
</p>
|
|
53
|
+
<SkipLinks v-bind="args" />
|
|
54
|
+
</div>
|
|
55
|
+
`,
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const customLinks: SkipLink[] = [
|
|
60
|
+
{ href: "#site-navigation", label: "Skip to navigation" },
|
|
61
|
+
{ href: "#main-content", label: "Skip to main content" },
|
|
62
|
+
{ href: "#footer-content", label: "Skip to footer" },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
export const CustomLinks: Story = {
|
|
66
|
+
name: "Custom Links",
|
|
67
|
+
args: { links: customLinks },
|
|
68
|
+
render: (args) => ({
|
|
69
|
+
components: { SkipLinks },
|
|
70
|
+
setup() {
|
|
71
|
+
return { args };
|
|
72
|
+
},
|
|
73
|
+
template: `
|
|
74
|
+
<div>
|
|
75
|
+
<p style="padding: 2rem; text-align: center;">
|
|
76
|
+
Press <kbd>Tab</kbd> after clicking into this preview to reveal three custom skip links.
|
|
77
|
+
</p>
|
|
78
|
+
<SkipLinks v-bind="args" />
|
|
79
|
+
</div>
|
|
80
|
+
`,
|
|
81
|
+
}),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const AlwaysVisible: Story = {
|
|
85
|
+
name: "Always Visible (for review)",
|
|
86
|
+
parameters: {
|
|
87
|
+
docs: {
|
|
88
|
+
description: {
|
|
89
|
+
story: "Forces the skip-nav panel open via `:focus-within` emulation so it can be inspected without tabbing — for visual review only, not representative of real usage.",
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
render: (args) => ({
|
|
94
|
+
components: { SkipLinks },
|
|
95
|
+
setup() {
|
|
96
|
+
return { args };
|
|
97
|
+
},
|
|
98
|
+
template: `
|
|
99
|
+
<div>
|
|
100
|
+
<style>
|
|
101
|
+
.always-visible-demo .skip-links__nav {
|
|
102
|
+
opacity: 1 !important;
|
|
103
|
+
transform: translateY(0) !important;
|
|
104
|
+
position: static !important;
|
|
105
|
+
}
|
|
106
|
+
</style>
|
|
107
|
+
<div class="always-visible-demo">
|
|
108
|
+
<SkipLinks v-bind="args" />
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
`,
|
|
112
|
+
}),
|
|
113
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import SkipLinks from "../SkipLinks.vue";
|
|
4
|
+
import type { SkipLink } from "~/types/components";
|
|
5
|
+
|
|
6
|
+
describe("SkipLinks", () => {
|
|
7
|
+
it("mounts without error", async () => {
|
|
8
|
+
const wrapper = await mountSuspended(SkipLinks);
|
|
9
|
+
expect(wrapper.vm).toBeTruthy();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("renders correct HTML structure (default props)", async () => {
|
|
13
|
+
const wrapper = await mountSuspended(SkipLinks);
|
|
14
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("renders correct HTML structure (all props set)", async () => {
|
|
18
|
+
const wrapper = await mountSuspended(SkipLinks, {
|
|
19
|
+
props: {
|
|
20
|
+
links: [{ href: "#content", label: "Skip to content" }],
|
|
21
|
+
ariaLabel: "Custom skip nav",
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("renders the default main-content and footer links", async () => {
|
|
28
|
+
const wrapper = await mountSuspended(SkipLinks);
|
|
29
|
+
const links = wrapper.findAll(".skip-links__link");
|
|
30
|
+
expect(links).toHaveLength(2);
|
|
31
|
+
expect(links[0]!.attributes("href")).toBe("#main-content");
|
|
32
|
+
expect(links[0]!.text()).toBe("Skip to main content");
|
|
33
|
+
expect(links[1]!.attributes("href")).toBe("#footer-content");
|
|
34
|
+
expect(links[1]!.text()).toBe("Skip to footer");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("renders custom links when the links prop is provided", async () => {
|
|
38
|
+
const customLinks: SkipLink[] = [
|
|
39
|
+
{ href: "#nav", label: "Skip to navigation" },
|
|
40
|
+
{ href: "#search", label: "Skip to search" },
|
|
41
|
+
{ href: "#content", label: "Skip to content" },
|
|
42
|
+
];
|
|
43
|
+
const wrapper = await mountSuspended(SkipLinks, { props: { links: customLinks } });
|
|
44
|
+
const links = wrapper.findAll(".skip-links__link");
|
|
45
|
+
expect(links).toHaveLength(3);
|
|
46
|
+
expect(links.map((link) => link.text())).toEqual(["Skip to navigation", "Skip to search", "Skip to content"]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("defaults the nav aria-label to 'Skip navigation'", async () => {
|
|
50
|
+
const wrapper = await mountSuspended(SkipLinks);
|
|
51
|
+
expect(wrapper.find(".skip-links__nav").attributes("aria-label")).toBe("Skip navigation");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("uses a custom ariaLabel when provided", async () => {
|
|
55
|
+
const wrapper = await mountSuspended(SkipLinks, { props: { ariaLabel: "Custom skip nav" } });
|
|
56
|
+
expect(wrapper.find(".skip-links__nav").attributes("aria-label")).toBe("Custom skip nav");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("does not render the home-link slot wrapper when the slot is unused", async () => {
|
|
60
|
+
const wrapper = await mountSuspended(SkipLinks);
|
|
61
|
+
expect(wrapper.find(".skip-links__home").exists()).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("renders the homeLink slot content when provided", async () => {
|
|
65
|
+
const wrapper = await mountSuspended(SkipLinks, {
|
|
66
|
+
slots: { homeLink: "<a href='/' class='logo-link'>Home</a>" },
|
|
67
|
+
});
|
|
68
|
+
expect(wrapper.find(".skip-links__home").exists()).toBe(true);
|
|
69
|
+
expect(wrapper.find(".logo-link").exists()).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
});
|
package/app/components/01.atoms/navigation/skip-links/tests/__snapshots__/SkipLinks.spec.ts.snap
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`SkipLinks > renders correct HTML structure (all props set) 1`] = `
|
|
4
|
+
"<div class="skip-links">
|
|
5
|
+
<!--v-if-->
|
|
6
|
+
<nav class="skip-links__nav" aria-label="Custom skip nav"><a href="#content" class="skip-links__link">Skip to content</a></nav>
|
|
7
|
+
</div>"
|
|
8
|
+
`;
|
|
9
|
+
|
|
10
|
+
exports[`SkipLinks > renders correct HTML structure (default props) 1`] = `
|
|
11
|
+
"<div class="skip-links">
|
|
12
|
+
<!--v-if-->
|
|
13
|
+
<nav class="skip-links__nav" aria-label="Skip navigation"><a href="#main-content" class="skip-links__link">Skip to main content</a><a href="#footer-content" class="skip-links__link">Skip to footer</a></nav>
|
|
14
|
+
</div>"
|
|
15
|
+
`;
|