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,274 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import TabsCore from "../TabsCore.vue";
|
|
4
|
+
|
|
5
|
+
class MockResizeObserver {
|
|
6
|
+
observe = vi.fn();
|
|
7
|
+
unobserve = vi.fn();
|
|
8
|
+
disconnect = vi.fn();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const slots = {
|
|
12
|
+
"tab-0-trigger": "Tab One",
|
|
13
|
+
"tab-0-content": "<p class='panel-0'>Content one</p>",
|
|
14
|
+
"tab-1-trigger": "Tab Two",
|
|
15
|
+
"tab-1-content": "<p class='panel-1'>Content two</p>",
|
|
16
|
+
"tab-2-trigger": "Tab Three",
|
|
17
|
+
"tab-2-content": "<p class='panel-2'>Content three</p>",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const fiveSlots = {
|
|
21
|
+
...slots,
|
|
22
|
+
"tab-3-trigger": "Tab Four",
|
|
23
|
+
"tab-3-content": "<p class='panel-3'>Content four</p>",
|
|
24
|
+
"tab-4-trigger": "Tab Five",
|
|
25
|
+
"tab-4-content": "<p class='panel-4'>Content five</p>",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
describe("TabsCore", () => {
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
vi.stubGlobal("ResizeObserver", MockResizeObserver);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// ─── Mount ───────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
it("mounts without error", async () => {
|
|
36
|
+
const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3 }, slots });
|
|
37
|
+
expect(wrapper.vm).toBeTruthy();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("renders one trigger and one panel per itemCount", async () => {
|
|
41
|
+
const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3 }, slots });
|
|
42
|
+
expect(wrapper.findAll(".tabs-list-item")).toHaveLength(3);
|
|
43
|
+
expect(wrapper.findAll(".tab-content")).toHaveLength(3);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ─── Accessibility structure ─────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
it("renders a tablist with the default aria-label", async () => {
|
|
49
|
+
const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3 }, slots });
|
|
50
|
+
const tablist = wrapper.find("[role='tablist']");
|
|
51
|
+
expect(tablist.exists()).toBe(true);
|
|
52
|
+
expect(tablist.attributes("aria-label")).toBe("Tabs");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("uses a custom ariaLabel when provided", async () => {
|
|
56
|
+
const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3, ariaLabel: "Settings" }, slots });
|
|
57
|
+
expect(wrapper.find("[role='tablist']").attributes("aria-label")).toBe("Settings");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("renders each trigger as a type=button with role=tab", async () => {
|
|
61
|
+
const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3 }, slots });
|
|
62
|
+
const triggers = wrapper.findAll(".tabs-list-item");
|
|
63
|
+
triggers.forEach((trigger) => {
|
|
64
|
+
expect(trigger.attributes("type")).toBe("button");
|
|
65
|
+
expect(trigger.attributes("role")).toBe("tab");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("renders each panel as role=tabpanel labelled by its trigger", async () => {
|
|
70
|
+
const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3 }, slots });
|
|
71
|
+
const panels = wrapper.findAll(".tab-content");
|
|
72
|
+
panels.forEach((panel, index) => {
|
|
73
|
+
expect(panel.attributes("role")).toBe("tabpanel");
|
|
74
|
+
expect(panel.attributes("aria-labelledby")).toBe(`tab-${index}-trigger`);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ─── Active tab state ────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
it("marks the first tab active and its panel visible/unhidden on mount", async () => {
|
|
81
|
+
const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3 }, slots });
|
|
82
|
+
const triggers = wrapper.findAll(".tabs-list-item");
|
|
83
|
+
const panels = wrapper.findAll(".tab-content");
|
|
84
|
+
|
|
85
|
+
expect(triggers[0]!.attributes("aria-selected")).toBe("true");
|
|
86
|
+
expect(triggers[0]!.attributes("tabindex")).toBe("0");
|
|
87
|
+
expect(triggers[1]!.attributes("aria-selected")).toBe("false");
|
|
88
|
+
expect(triggers[1]!.attributes("tabindex")).toBe("-1");
|
|
89
|
+
|
|
90
|
+
expect(panels[0]!.attributes("aria-hidden")).toBe("false");
|
|
91
|
+
expect((panels[0]!.element as HTMLElement).style.display).toBe("block");
|
|
92
|
+
expect(panels[1]!.attributes("aria-hidden")).toBe("true");
|
|
93
|
+
expect((panels[1]!.element as HTMLElement).style.display).toBe("none");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("activates a tab on click and updates aria-selected, tabindex, and panel visibility", async () => {
|
|
97
|
+
const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3 }, slots });
|
|
98
|
+
const triggers = wrapper.findAll(".tabs-list-item");
|
|
99
|
+
const panels = wrapper.findAll(".tab-content");
|
|
100
|
+
|
|
101
|
+
await triggers[1]!.trigger("click");
|
|
102
|
+
|
|
103
|
+
expect(triggers[0]!.attributes("aria-selected")).toBe("false");
|
|
104
|
+
expect(triggers[0]!.attributes("tabindex")).toBe("-1");
|
|
105
|
+
expect(triggers[1]!.attributes("aria-selected")).toBe("true");
|
|
106
|
+
expect(triggers[1]!.attributes("tabindex")).toBe("0");
|
|
107
|
+
|
|
108
|
+
expect(panels[0]!.attributes("aria-hidden")).toBe("true");
|
|
109
|
+
expect(panels[1]!.attributes("aria-hidden")).toBe("false");
|
|
110
|
+
expect((panels[1]!.element as HTMLElement).style.display).toBe("block");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ─── Keyboard navigation ─────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
it("moves focus and activates the next tab on ArrowRight (axis x)", async () => {
|
|
116
|
+
const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3 }, slots });
|
|
117
|
+
const triggers = wrapper.findAll(".tabs-list-item");
|
|
118
|
+
|
|
119
|
+
await triggers[0]!.trigger("keydown", { key: "ArrowRight" });
|
|
120
|
+
|
|
121
|
+
expect(triggers[1]!.attributes("aria-selected")).toBe("true");
|
|
122
|
+
expect(triggers[1]!.attributes("tabindex")).toBe("0");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("wraps to the first tab on ArrowRight from the last tab", async () => {
|
|
126
|
+
const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3 }, slots });
|
|
127
|
+
const triggers = wrapper.findAll(".tabs-list-item");
|
|
128
|
+
|
|
129
|
+
await triggers[2]!.trigger("click");
|
|
130
|
+
await triggers[2]!.trigger("keydown", { key: "ArrowRight" });
|
|
131
|
+
|
|
132
|
+
expect(triggers[0]!.attributes("aria-selected")).toBe("true");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("moves focus to the previous tab on ArrowLeft (axis x)", async () => {
|
|
136
|
+
const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3 }, slots });
|
|
137
|
+
const triggers = wrapper.findAll(".tabs-list-item");
|
|
138
|
+
|
|
139
|
+
await triggers[1]!.trigger("click");
|
|
140
|
+
await triggers[1]!.trigger("keydown", { key: "ArrowLeft" });
|
|
141
|
+
|
|
142
|
+
expect(triggers[0]!.attributes("aria-selected")).toBe("true");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("uses ArrowUp/ArrowDown instead of ArrowLeft/ArrowRight when axis is y", async () => {
|
|
146
|
+
const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3, axis: "y" }, slots });
|
|
147
|
+
const triggers = wrapper.findAll(".tabs-list-item");
|
|
148
|
+
|
|
149
|
+
await triggers[0]!.trigger("keydown", { key: "ArrowRight" });
|
|
150
|
+
expect(triggers[0]!.attributes("aria-selected")).toBe("true");
|
|
151
|
+
|
|
152
|
+
await triggers[0]!.trigger("keydown", { key: "ArrowDown" });
|
|
153
|
+
expect(triggers[1]!.attributes("aria-selected")).toBe("true");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("jumps to the last tab on End and the first tab on Home", async () => {
|
|
157
|
+
const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3 }, slots });
|
|
158
|
+
const triggers = wrapper.findAll(".tabs-list-item");
|
|
159
|
+
|
|
160
|
+
await triggers[0]!.trigger("keydown", { key: "End" });
|
|
161
|
+
expect(triggers[2]!.attributes("aria-selected")).toBe("true");
|
|
162
|
+
|
|
163
|
+
await triggers[2]!.trigger("keydown", { key: "Home" });
|
|
164
|
+
expect(triggers[0]!.attributes("aria-selected")).toBe("true");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// ─── trackHover / trackActive / trackIndicator ──────────────────────────
|
|
168
|
+
|
|
169
|
+
it("renders all three indicator decorators by default", async () => {
|
|
170
|
+
const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3 }, slots });
|
|
171
|
+
expect(wrapper.find(".nav__hovered").exists()).toBe(true);
|
|
172
|
+
expect(wrapper.find(".nav__active").exists()).toBe(true);
|
|
173
|
+
expect(wrapper.find(".nav__active-indicator").exists()).toBe(true);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("omits the hover decorator when trackHover is false", async () => {
|
|
177
|
+
const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3, trackHover: false }, slots });
|
|
178
|
+
expect(wrapper.find(".nav__hovered").exists()).toBe(false);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("omits the active decorator when trackActive is false", async () => {
|
|
182
|
+
const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3, trackActive: false }, slots });
|
|
183
|
+
expect(wrapper.find(".nav__active").exists()).toBe(false);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("omits the underline indicator when trackIndicator is false", async () => {
|
|
187
|
+
const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3, trackIndicator: false }, slots });
|
|
188
|
+
expect(wrapper.find(".nav__active-indicator").exists()).toBe(false);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// ─── Hover indicator movement ────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
it("moves the hover indicator position on mouseenter", async () => {
|
|
194
|
+
const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3 }, slots });
|
|
195
|
+
const tabsList = wrapper.find(".tabs-list").element as HTMLElement;
|
|
196
|
+
const triggers = wrapper.findAll(".tabs-list-item");
|
|
197
|
+
|
|
198
|
+
expect(tabsList.style.getPropertyValue("--_width-hovered")).toBe("");
|
|
199
|
+
|
|
200
|
+
await triggers[1]!.trigger("mouseenter");
|
|
201
|
+
|
|
202
|
+
expect(tabsList.style.getPropertyValue("--_width-hovered")).not.toBe("");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("resets the hover indicator to the active tab on mouseleave", async () => {
|
|
206
|
+
const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3 }, slots });
|
|
207
|
+
const tabsList = wrapper.find(".tabs-list");
|
|
208
|
+
const triggers = wrapper.findAll(".tabs-list-item");
|
|
209
|
+
|
|
210
|
+
await triggers[1]!.trigger("mouseenter");
|
|
211
|
+
await tabsList.trigger("mouseleave");
|
|
212
|
+
|
|
213
|
+
// Should not throw, and the hover indicator settles back onto the active (first) tab.
|
|
214
|
+
expect((tabsList.element as HTMLElement).style.getPropertyValue("--_x-hovered")).toBe("0px");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("clears a pending settle timeout when hovering a second tab before the first settles", async () => {
|
|
218
|
+
const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 3 }, slots });
|
|
219
|
+
const triggers = wrapper.findAll(".tabs-list-item");
|
|
220
|
+
const clearTimeoutSpy = vi.spyOn(global, "clearTimeout");
|
|
221
|
+
|
|
222
|
+
await triggers[1]!.trigger("mouseenter");
|
|
223
|
+
await triggers[2]!.trigger("mouseenter");
|
|
224
|
+
|
|
225
|
+
expect(clearTimeoutSpy).toHaveBeenCalled();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// ─── Regression: more than 3 tabs ────────────────────────────────────────
|
|
229
|
+
// The original hand-rolled version of this component had an indicator-positioning issue
|
|
230
|
+
// that only showed up with itemCount > 3 — these lock in correct behaviour at 5.
|
|
231
|
+
|
|
232
|
+
it("activates a distant tab (index 0 -> index 4) directly via click", async () => {
|
|
233
|
+
const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 5 }, slots: fiveSlots });
|
|
234
|
+
const triggers = wrapper.findAll(".tabs-list-item");
|
|
235
|
+
|
|
236
|
+
await triggers[4]!.trigger("click");
|
|
237
|
+
|
|
238
|
+
expect(triggers[0]!.attributes("aria-selected")).toBe("false");
|
|
239
|
+
expect(triggers[4]!.attributes("aria-selected")).toBe("true");
|
|
240
|
+
expect(triggers[4]!.attributes("tabindex")).toBe("0");
|
|
241
|
+
expect(wrapper.findAll(".tab-content")[4]!.attributes("aria-hidden")).toBe("false");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("does not recolour tabs spanned by a distant activation jump (regression: intermediate labels going invisible)", async () => {
|
|
245
|
+
// The old implementation force-applied a "transitioning" class to every tab spanned by a
|
|
246
|
+
// jump, forcing it to the active-indicator's text colour for the whole transition — even
|
|
247
|
+
// though the sliding indicator only visually reaches each spanned tab partway through, and
|
|
248
|
+
// for a forward jump doesn't reach the middle tabs until late in the transition. That made
|
|
249
|
+
// intermediate labels flash a colour matching neither their own background nor (yet) the
|
|
250
|
+
// indicator's, going invisible. Spanned tabs should only ever carry aria-selected/hover
|
|
251
|
+
// state of their own — never a class forcing them to look active.
|
|
252
|
+
const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 5 }, slots: fiveSlots });
|
|
253
|
+
const triggers = wrapper.findAll(".tabs-list-item");
|
|
254
|
+
|
|
255
|
+
// Active tab starts at index 0, so activating index 4 spans every tab in between.
|
|
256
|
+
await triggers[4]!.trigger("click");
|
|
257
|
+
|
|
258
|
+
[1, 2, 3].forEach((index) => {
|
|
259
|
+
expect(triggers[index]!.attributes("aria-selected")).toBe("false");
|
|
260
|
+
expect(triggers[index]!.classes()).not.toContain("transitioning");
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("wraps correctly with End then ArrowRight across 5 tabs", async () => {
|
|
265
|
+
const wrapper = await mountSuspended(TabsCore, { props: { itemCount: 5 }, slots: fiveSlots });
|
|
266
|
+
const triggers = wrapper.findAll(".tabs-list-item");
|
|
267
|
+
|
|
268
|
+
await triggers[0]!.trigger("keydown", { key: "End" });
|
|
269
|
+
expect(triggers[4]!.attributes("aria-selected")).toBe("true");
|
|
270
|
+
|
|
271
|
+
await triggers[4]!.trigger("keydown", { key: "ArrowRight" });
|
|
272
|
+
expect(triggers[0]!.attributes("aria-selected")).toBe("true");
|
|
273
|
+
});
|
|
274
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# PopOver — Consumer Styling Guide
|
|
2
|
+
|
|
3
|
+
## Public token API
|
|
4
|
+
|
|
5
|
+
| Token | Default | Controls |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `--pop-over-gap` | `1rem` | Space between the trigger and the popover panel |
|
|
8
|
+
| `--pop-over-trigger-outline-width` | `0.1rem` | Trigger focus outline width |
|
|
9
|
+
| `--pop-over-trigger-outline-colour-hover` | `var(--theme-ring)` | Trigger outline colour on focus-visible |
|
|
10
|
+
| `--pop-over-width` | `30rem` | Popover panel width |
|
|
11
|
+
| `--pop-over-border-width` | `0.1rem` | Popover panel border width |
|
|
12
|
+
| `--pop-over-border-colour` | `light-dark(var(--slate-02), var(--slate-06))` | Popover panel border colour |
|
|
13
|
+
| `--pop-over-border-radius` | `0.8rem` | Popover panel corner radius |
|
|
14
|
+
| `--pop-over-background-colour` | `light-dark(var(--slate-00), var(--slate-07))` | Popover panel background |
|
|
15
|
+
| `--pop-over-text-colour` | `light-dark(var(--slate-09), var(--slate-01))` | Popover panel text colour |
|
|
16
|
+
| `--pop-over-shadow` | `light-dark(0 0.4rem 1.6rem rgba(0,0,0,.12), 0 0.4rem 1.6rem rgba(0,0,0,.5))` | Popover panel elevation shadow |
|
|
17
|
+
| `--pop-over-transition-duration` | `200ms` | Open/close fade duration |
|
|
18
|
+
| `--pop-over-content-padding` | `1.6rem` | Padding around the `content` slot |
|
|
19
|
+
| `--pop-over-content-padding-block-start` | `3.2rem` | Extra top padding so content clears the close button |
|
|
20
|
+
| `--pop-over-close-button-offset` | `0.8rem` | Distance of the close button from the panel's top/right edges |
|
|
21
|
+
| `--pop-over-close-button-size` | `2.4rem` | Close button box width/height |
|
|
22
|
+
| `--pop-over-close-button-border-radius` | `50%` | Close button corner radius |
|
|
23
|
+
| `--pop-over-close-button-colour` | `light-dark(var(--slate-06), var(--slate-03))` | Close button icon colour |
|
|
24
|
+
| `--pop-over-close-button-icon-size` | `1.4rem` | Close button icon glyph size |
|
|
25
|
+
| `--pop-over-close-button-outline-width` | `0.1rem` | Close button focus/hover outline width |
|
|
26
|
+
| `--pop-over-close-button-surface-hover` | `light-dark(var(--slate-01), var(--slate-06))` | Close button background on hover/focus-visible |
|
|
27
|
+
| `--pop-over-close-button-outline-colour-hover` | `var(--theme-ring)` | Close button outline colour on hover/focus-visible |
|
|
28
|
+
|
|
29
|
+
```css
|
|
30
|
+
.my-page {
|
|
31
|
+
--pop-over-background-colour: #1a1a1a;
|
|
32
|
+
--pop-over-text-colour: white;
|
|
33
|
+
--pop-over-width: 24rem;
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Or scope to a single instance via `styleClassPassthrough`:
|
|
38
|
+
|
|
39
|
+
```vue
|
|
40
|
+
<PopOver style-class-passthrough="promo-pop-over">...</PopOver>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Notes
|
|
44
|
+
|
|
45
|
+
- Built on the native Popover API (`popover`/`popovertarget`) and CSS anchor-positioning
|
|
46
|
+
(`anchor-name`/`position-anchor`/`anchor()`) — check current browser support before relying on
|
|
47
|
+
it where broad support is a hard requirement.
|
|
48
|
+
- `placement` (`"top" | "right" | "bottom" | "left"`, default `"right"`) picks which side of the
|
|
49
|
+
trigger the panel opens on; each side gets a `position-try-fallbacks` so the browser flips it
|
|
50
|
+
automatically if it would overflow the viewport.
|
|
51
|
+
- Unlike `DisplayTooltip` (a fixed icon trigger for inline help text) or `ActionMenu` (menu
|
|
52
|
+
semantics with arrow-key navigation), `PopOver` is a generic disclosure: both `trigger` and
|
|
53
|
+
`content` slots are entirely consumer-supplied, and it always renders its own visible close
|
|
54
|
+
button rather than relying on click-outside/Escape alone.
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<ClientOnly>
|
|
3
|
+
<div class="pop-over" :class="[elementClasses]" :style="{ '--_anchor-name': anchorName }" :data-placement="placement">
|
|
4
|
+
<button
|
|
5
|
+
ref="triggerRef"
|
|
6
|
+
:popovertarget="popoverId"
|
|
7
|
+
popovertargetaction="toggle"
|
|
8
|
+
type="button"
|
|
9
|
+
class="pop-over-trigger"
|
|
10
|
+
:aria-label="triggerAriaLabel || undefined"
|
|
11
|
+
>
|
|
12
|
+
<slot name="trigger"></slot>
|
|
13
|
+
</button>
|
|
14
|
+
|
|
15
|
+
<div :id="popoverId" ref="popoverRef" popover class="pop-over-popover" :aria-label="popoverAriaLabel || undefined" @toggle="handleToggle">
|
|
16
|
+
<button
|
|
17
|
+
ref="closeButtonRef"
|
|
18
|
+
:popovertarget="popoverId"
|
|
19
|
+
popovertargetaction="hide"
|
|
20
|
+
type="button"
|
|
21
|
+
class="pop-over-close-button"
|
|
22
|
+
:aria-label="closeButtonAriaLabel"
|
|
23
|
+
>
|
|
24
|
+
<Icon name="lucide:x" class="pop-over-close-button-icon" aria-hidden="true" />
|
|
25
|
+
</button>
|
|
26
|
+
<div class="pop-over-content">
|
|
27
|
+
<slot name="content"></slot>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</ClientOnly>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<script setup lang="ts">
|
|
35
|
+
interface Props {
|
|
36
|
+
placement?: "top" | "right" | "bottom" | "left";
|
|
37
|
+
/** aria-label on the trigger button — set this when the trigger slot is icon-only. */
|
|
38
|
+
triggerAriaLabel?: string;
|
|
39
|
+
/** aria-label on the popover content region — set this when the content slot has no visible heading. */
|
|
40
|
+
popoverAriaLabel?: string;
|
|
41
|
+
/** aria-label on the close button — override for localisation. */
|
|
42
|
+
closeButtonAriaLabel?: string;
|
|
43
|
+
styleClassPassthrough?: string | string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
47
|
+
placement: "right",
|
|
48
|
+
triggerAriaLabel: "",
|
|
49
|
+
popoverAriaLabel: "",
|
|
50
|
+
closeButtonAriaLabel: "Close",
|
|
51
|
+
styleClassPassthrough: () => [],
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const id = useId();
|
|
55
|
+
const popoverId = `pop-over-${id}`;
|
|
56
|
+
const anchorName = `--pop-over-anchor-${id}`;
|
|
57
|
+
|
|
58
|
+
const triggerRef = ref<HTMLButtonElement | null>(null);
|
|
59
|
+
const popoverRef = ref<HTMLDivElement | null>(null);
|
|
60
|
+
const closeButtonRef = ref<HTMLButtonElement | null>(null);
|
|
61
|
+
|
|
62
|
+
/** Move focus into the popover on open; the Popover API restores focus to the trigger on close. */
|
|
63
|
+
const handleToggle = (event: Event) => {
|
|
64
|
+
const toggleEvent = event as ToggleEvent;
|
|
65
|
+
if (toggleEvent.newState === "open") {
|
|
66
|
+
closeButtonRef.value?.focus();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
71
|
+
|
|
72
|
+
watch(
|
|
73
|
+
() => props.styleClassPassthrough,
|
|
74
|
+
() => {
|
|
75
|
+
resetElementClasses(props.styleClassPassthrough);
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<style lang="css">
|
|
81
|
+
@layer components {
|
|
82
|
+
.pop-over {
|
|
83
|
+
display: inline-block;
|
|
84
|
+
position: relative;
|
|
85
|
+
|
|
86
|
+
.pop-over-trigger {
|
|
87
|
+
all: unset;
|
|
88
|
+
cursor: pointer;
|
|
89
|
+
anchor-name: var(--_anchor-name);
|
|
90
|
+
outline: var(--pop-over-trigger-outline-width, 0.1rem) solid transparent;
|
|
91
|
+
|
|
92
|
+
&:focus-visible {
|
|
93
|
+
outline-color: var(--pop-over-trigger-outline-colour-hover, var(--theme-ring));
|
|
94
|
+
outline-offset: 0.2rem;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.pop-over-popover {
|
|
99
|
+
display: none;
|
|
100
|
+
position: absolute;
|
|
101
|
+
position-anchor: var(--_anchor-name);
|
|
102
|
+
margin: 0;
|
|
103
|
+
inset: auto;
|
|
104
|
+
border: var(--pop-over-border-width, 0.1rem) solid
|
|
105
|
+
var(--pop-over-border-colour, light-dark(var(--slate-02), var(--slate-06)));
|
|
106
|
+
border-radius: var(--pop-over-border-radius, 0.8rem);
|
|
107
|
+
background-color: var(--pop-over-background-colour, light-dark(var(--slate-00), var(--slate-07)));
|
|
108
|
+
color: var(--pop-over-text-colour, light-dark(var(--slate-09), var(--slate-01)));
|
|
109
|
+
box-shadow: var(
|
|
110
|
+
--pop-over-shadow,
|
|
111
|
+
light-dark(0 0.4rem 1.6rem rgba(0, 0, 0, 0.12), 0 0.4rem 1.6rem rgba(0, 0, 0, 0.5))
|
|
112
|
+
);
|
|
113
|
+
width: var(--pop-over-width, 30rem);
|
|
114
|
+
|
|
115
|
+
opacity: 0;
|
|
116
|
+
transition:
|
|
117
|
+
opacity var(--pop-over-transition-duration, 200ms),
|
|
118
|
+
display var(--pop-over-transition-duration, 200ms),
|
|
119
|
+
overlay var(--pop-over-transition-duration, 200ms);
|
|
120
|
+
transition-behavior: allow-discrete;
|
|
121
|
+
|
|
122
|
+
&:popover-open {
|
|
123
|
+
display: block;
|
|
124
|
+
opacity: 1;
|
|
125
|
+
|
|
126
|
+
@starting-style {
|
|
127
|
+
opacity: 0;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.pop-over-close-button {
|
|
132
|
+
all: unset;
|
|
133
|
+
cursor: pointer;
|
|
134
|
+
position: absolute;
|
|
135
|
+
top: var(--pop-over-close-button-offset, 0.8rem);
|
|
136
|
+
right: var(--pop-over-close-button-offset, 0.8rem);
|
|
137
|
+
display: grid;
|
|
138
|
+
place-items: center;
|
|
139
|
+
width: var(--pop-over-close-button-size, 2.4rem);
|
|
140
|
+
height: var(--pop-over-close-button-size, 2.4rem);
|
|
141
|
+
border-radius: var(--pop-over-close-button-border-radius, 50%);
|
|
142
|
+
color: var(--pop-over-close-button-colour, light-dark(var(--slate-06), var(--slate-03)));
|
|
143
|
+
outline: var(--pop-over-close-button-outline-width, 0.1rem) solid transparent;
|
|
144
|
+
|
|
145
|
+
&:hover,
|
|
146
|
+
&:focus-visible {
|
|
147
|
+
background-color: var(--pop-over-close-button-surface-hover, light-dark(var(--slate-01), var(--slate-06)));
|
|
148
|
+
outline-color: var(--pop-over-close-button-outline-colour-hover, var(--theme-ring));
|
|
149
|
+
outline-offset: 0.1rem;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.pop-over-close-button-icon {
|
|
153
|
+
width: var(--pop-over-close-button-icon-size, 1.4rem);
|
|
154
|
+
height: var(--pop-over-close-button-icon-size, 1.4rem);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.pop-over-content {
|
|
159
|
+
padding: var(--pop-over-content-padding, 1.6rem);
|
|
160
|
+
padding-top: var(--pop-over-content-padding-block-start, 3.2rem);
|
|
161
|
+
|
|
162
|
+
/* Trim the slotted content's own default UA margins so this padding is the only
|
|
163
|
+
space above/below it — interior spacing between multiple children is untouched. */
|
|
164
|
+
> :first-child {
|
|
165
|
+
margin-block-start: 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
> :last-child {
|
|
169
|
+
margin-block-end: 0;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
&[data-placement="right"] .pop-over-popover {
|
|
175
|
+
top: anchor(top);
|
|
176
|
+
left: calc(anchor(right) + var(--pop-over-gap, 1rem));
|
|
177
|
+
position-try-fallbacks: flip-inline;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
&[data-placement="left"] .pop-over-popover {
|
|
181
|
+
top: anchor(top);
|
|
182
|
+
right: calc(anchor(left) + var(--pop-over-gap, 1rem));
|
|
183
|
+
left: auto;
|
|
184
|
+
position-try-fallbacks: flip-inline;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
&[data-placement="bottom"] .pop-over-popover {
|
|
188
|
+
top: calc(anchor(bottom) + var(--pop-over-gap, 1rem));
|
|
189
|
+
left: anchor(left);
|
|
190
|
+
position-try-fallbacks: flip-block;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
&[data-placement="top"] .pop-over-popover {
|
|
194
|
+
bottom: calc(anchor(top) + var(--pop-over-gap, 1rem));
|
|
195
|
+
top: auto;
|
|
196
|
+
left: anchor(left);
|
|
197
|
+
position-try-fallbacks: flip-block;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
</style>
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import PopOver from "../PopOver.vue";
|
|
2
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof PopOver> = {
|
|
5
|
+
title: "Atoms/PopOver",
|
|
6
|
+
component: PopOver,
|
|
7
|
+
argTypes: {
|
|
8
|
+
placement: {
|
|
9
|
+
control: "select",
|
|
10
|
+
options: ["top", "right", "bottom", "left"],
|
|
11
|
+
description: "Which side of the trigger the popover panel opens on.",
|
|
12
|
+
table: { category: "Basic" },
|
|
13
|
+
},
|
|
14
|
+
triggerAriaLabel: {
|
|
15
|
+
control: "text",
|
|
16
|
+
description: "aria-label on the trigger button — set this when the trigger slot is icon-only.",
|
|
17
|
+
table: { category: "Accessibility" },
|
|
18
|
+
},
|
|
19
|
+
popoverAriaLabel: {
|
|
20
|
+
control: "text",
|
|
21
|
+
description: "aria-label on the popover content region — set this when the content slot has no visible heading.",
|
|
22
|
+
table: { category: "Accessibility" },
|
|
23
|
+
},
|
|
24
|
+
closeButtonAriaLabel: {
|
|
25
|
+
control: "text",
|
|
26
|
+
description: "aria-label on the close button — override for localisation.",
|
|
27
|
+
table: { category: "Accessibility" },
|
|
28
|
+
},
|
|
29
|
+
styleClassPassthrough: { table: { disable: true } },
|
|
30
|
+
},
|
|
31
|
+
args: {
|
|
32
|
+
placement: "right",
|
|
33
|
+
closeButtonAriaLabel: "Close",
|
|
34
|
+
},
|
|
35
|
+
parameters: {
|
|
36
|
+
docs: {
|
|
37
|
+
description: {
|
|
38
|
+
component:
|
|
39
|
+
"A generic anchor-positioned disclosure panel: fully consumer-supplied trigger and content slots, built on the native Popover API and CSS anchor-positioning. See DisplayTooltip for a fixed icon-trigger variant, or ActionMenu for one with menu semantics.",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export default meta;
|
|
46
|
+
type Story = StoryObj<typeof PopOver>;
|
|
47
|
+
|
|
48
|
+
export const Default: Story = {
|
|
49
|
+
render: (args) => ({
|
|
50
|
+
components: { PopOver },
|
|
51
|
+
setup() {
|
|
52
|
+
return { args };
|
|
53
|
+
},
|
|
54
|
+
template: `
|
|
55
|
+
<div style="padding: 8rem;">
|
|
56
|
+
<PopOver v-bind="args">
|
|
57
|
+
<template #trigger>
|
|
58
|
+
<span>Open popover</span>
|
|
59
|
+
</template>
|
|
60
|
+
<template #content>
|
|
61
|
+
<p>This is some popover content.</p>
|
|
62
|
+
</template>
|
|
63
|
+
</PopOver>
|
|
64
|
+
</div>
|
|
65
|
+
`,
|
|
66
|
+
}),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const Placements: Story = {
|
|
70
|
+
render: (args) => ({
|
|
71
|
+
components: { PopOver },
|
|
72
|
+
setup() {
|
|
73
|
+
return { args };
|
|
74
|
+
},
|
|
75
|
+
template: `
|
|
76
|
+
<div style="display: flex; gap: 4rem; padding: 10rem;">
|
|
77
|
+
<PopOver v-bind="args" placement="top">
|
|
78
|
+
<template #trigger><span>Top</span></template>
|
|
79
|
+
<template #content><p>Opens above the trigger.</p></template>
|
|
80
|
+
</PopOver>
|
|
81
|
+
<PopOver v-bind="args" placement="right">
|
|
82
|
+
<template #trigger><span>Right</span></template>
|
|
83
|
+
<template #content><p>Opens to the right of the trigger.</p></template>
|
|
84
|
+
</PopOver>
|
|
85
|
+
<PopOver v-bind="args" placement="bottom">
|
|
86
|
+
<template #trigger><span>Bottom</span></template>
|
|
87
|
+
<template #content><p>Opens below the trigger.</p></template>
|
|
88
|
+
</PopOver>
|
|
89
|
+
<PopOver v-bind="args" placement="left">
|
|
90
|
+
<template #trigger><span>Left</span></template>
|
|
91
|
+
<template #content><p>Opens to the left of the trigger.</p></template>
|
|
92
|
+
</PopOver>
|
|
93
|
+
</div>
|
|
94
|
+
`,
|
|
95
|
+
}),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const IconTrigger: Story = {
|
|
99
|
+
render: (args) => ({
|
|
100
|
+
components: { PopOver },
|
|
101
|
+
setup() {
|
|
102
|
+
return { args };
|
|
103
|
+
},
|
|
104
|
+
template: `
|
|
105
|
+
<div style="padding: 8rem;">
|
|
106
|
+
<PopOver v-bind="args" trigger-aria-label="Show filters" popover-aria-label="Filter options">
|
|
107
|
+
<template #trigger>
|
|
108
|
+
<Icon name="lucide:filter" aria-hidden="true" />
|
|
109
|
+
</template>
|
|
110
|
+
<template #content>
|
|
111
|
+
<p>Filter controls would go here.</p>
|
|
112
|
+
</template>
|
|
113
|
+
</PopOver>
|
|
114
|
+
</div>
|
|
115
|
+
`,
|
|
116
|
+
}),
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const CustomColours: Story = {
|
|
120
|
+
render: (args) => ({
|
|
121
|
+
components: { PopOver },
|
|
122
|
+
setup() {
|
|
123
|
+
return { args };
|
|
124
|
+
},
|
|
125
|
+
template: `
|
|
126
|
+
<div style="padding: 8rem;">
|
|
127
|
+
<PopOver
|
|
128
|
+
v-bind="args"
|
|
129
|
+
style="--pop-over-background-colour: #1a1a1a; --pop-over-text-colour: white; --pop-over-border-colour: transparent;"
|
|
130
|
+
>
|
|
131
|
+
<template #trigger>
|
|
132
|
+
<span>Open popover</span>
|
|
133
|
+
</template>
|
|
134
|
+
<template #content>
|
|
135
|
+
<p>Overriding the public tokens via inline custom properties.</p>
|
|
136
|
+
</template>
|
|
137
|
+
</PopOver>
|
|
138
|
+
</div>
|
|
139
|
+
`,
|
|
140
|
+
}),
|
|
141
|
+
};
|