srcdev-nuxt-components 9.5.7 → 9.6.1
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/skills/component-compliance-checklist.md +8 -2
- package/.claude/skills/component-dynamic-slots.md +3 -3
- package/.claude/skills/component-export-types.md +10 -16
- package/.claude/skills/component-naming.md +1 -1
- package/.claude/skills/components/alert-content.md +9 -1
- package/.claude/skills/components/breadcrumb.md +5 -0
- package/.claude/skills/components/container-glow.md +21 -8
- package/.claude/skills/components/display-avatar.md +13 -7
- package/.claude/skills/components/display-chip.md +37 -24
- package/.claude/skills/components/display-pill.md +58 -8
- package/.claude/skills/components/display-prompt.md +8 -5
- package/.claude/skills/components/display-toast.md +21 -4
- package/.claude/skills/components/glowing-border.md +20 -2
- package/.claude/skills/components/input-button.md +3 -0
- package/.claude/skills/components/responsive-header.md +59 -0
- package/.claude/skills/components/select-menu.md +54 -5
- package/.claude/skills/components/service-detail.md +32 -0
- package/.claude/skills/components/service-summary.md +8 -0
- package/.claude/skills/components/services-card-grid.md +5 -3
- package/.claude/skills/components/services-card.md +22 -12
- package/.claude/skills/components/site-header.md +2 -2
- package/.claude/skills/components/slider-gallery.md +10 -4
- package/.claude/skills/components/tabbed-content.md +300 -0
- package/.claude/skills/composable-anchor-scroll.md +16 -15
- package/.claude/skills/index.md +4 -5
- package/.claude/skills/storybook-add-story.md +53 -0
- package/.claude/skills/theming-status-tokens.md +60 -0
- package/.vscode/srcdev-component-display-avatar.code-snippets +2 -1
- package/.vscode/srcdev-component-display-chip.code-snippets +3 -2
- package/.vscode/srcdev-component-display-pill.code-snippets +10 -0
- package/.vscode/srcdev-component-responsive-header.code-snippets +16 -0
- package/.vscode/srcdev-component-select-menu.code-snippets +28 -0
- package/.vscode/srcdev-component-service-detail.code-snippets +1 -0
- package/.vscode/srcdev-component-service-summary.code-snippets +3 -1
- package/.vscode/srcdev-component-services-card.code-snippets +38 -36
- package/.vscode/srcdev-component-services-grids.code-snippets +2 -1
- package/.vscode/{srcdev-component-tabs-core.code-snippets → srcdev-component-tabbed-content.code-snippets} +35 -16
- package/app/assets/styles/setup/03.theming/_status.css +24 -0
- package/app/assets/styles/setup/03.theming/index.css +1 -0
- package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +39 -35
- package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +25 -15
- package/app/components/01.atoms/animations/container-glow/stories/ContainerGlow.stories.ts +130 -24
- package/app/components/01.atoms/animations/container-glow/tests/ContainerGlow.spec.ts +45 -0
- package/app/components/01.atoms/animations/container-glow/tests/__snapshots__/ContainerGlow.spec.ts.snap +2 -2
- package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +76 -19
- package/app/components/01.atoms/animations/glowing-border/GlowingBorder.vue +12 -10
- package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +201 -42
- package/app/components/01.atoms/animations/glowing-border/tests/GlowingBorder.spec.ts +15 -4
- package/app/components/01.atoms/animations/glowing-border/tests/__snapshots__/GlowingBorder.spec.ts.snap +1 -1
- package/app/components/01.atoms/display-pill/CONSUMER-STYLING.md +78 -9
- package/app/components/01.atoms/display-pill/DisplayPill.vue +59 -23
- package/app/components/01.atoms/display-pill/stories/DisplayPill.stories.ts +240 -16
- package/app/components/01.atoms/display-pill/tests/DisplayPill.spec.ts +43 -14
- package/app/components/01.atoms/display-pill/tests/__snapshots__/DisplayPill.spec.ts.snap +3 -3
- package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +16 -4
- package/app/components/01.atoms/navigation/breadcrumb/CONSUMER-STYLING.md +28 -5
- package/app/components/01.atoms/navigation/breadcrumb/stories/Breadcrumb.stories.ts +94 -0
- package/app/components/01.atoms/navigation/breadcrumb/tests/Breadcrumb.spec.ts +27 -0
- package/app/components/01.atoms/tabbed-content/CONSUMER-STYLING.md +201 -0
- package/app/components/01.atoms/tabbed-content/TabbedContent.vue +750 -0
- package/app/components/01.atoms/tabbed-content/stories/TabbedContent.stories.ts +279 -0
- package/app/components/01.atoms/tabbed-content/tests/TabbedContent.spec.ts +816 -0
- package/app/components/02.molecules/alert-content/AlertContent.vue +11 -2
- package/app/components/02.molecules/alert-content/AlertContentInner.vue +19 -0
- package/app/components/02.molecules/alert-content/CONSUMER-STYLING.md +17 -4
- package/app/components/02.molecules/alert-content/stories/AlertContent.stories.ts +200 -17
- package/app/components/02.molecules/alert-content/tests/AlertContent.spec.ts +33 -0
- package/app/components/02.molecules/display-avatar/CONSUMER-STYLING.md +7 -5
- package/app/components/02.molecules/display-avatar/DisplayAvatar.vue +6 -2
- package/app/components/02.molecules/display-avatar/stories/DisplayAvatar.stories.ts +24 -3
- package/app/components/02.molecules/display-avatar/tests/DisplayAvatar.spec.ts +19 -0
- package/app/components/02.molecules/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap +2 -1
- package/app/components/02.molecules/display-chip/CONSUMER-STYLING.md +52 -19
- package/app/components/02.molecules/display-chip/DisplayChip.vue +72 -54
- package/app/components/02.molecules/display-chip/stories/DisplayChip.stories.ts +103 -6
- package/app/components/02.molecules/display-chip/tests/DisplayChip.spec.ts +98 -29
- package/app/components/02.molecules/display-chip/tests/__snapshots__/DisplayChip.spec.ts.snap +8 -4
- package/app/components/02.molecules/prompt/CONSUMER-STYLING.md +9 -1
- package/app/components/02.molecules/prompt/DisplayPrompt.vue +2 -2
- package/app/components/02.molecules/prompt/stories/DisplayPrompt.stories.ts +208 -71
- package/app/components/02.molecules/prompt/tests/DisplayPrompt.spec.ts +19 -6
- package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +66 -8
- package/app/components/02.molecules/select-menu/SelectMenu.vue +141 -18
- package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +232 -2
- package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +151 -0
- package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +6 -2
- package/app/components/02.molecules/toast/CONSUMER-STYLING.md +91 -27
- package/app/components/02.molecules/toast/DisplayToast.vue +109 -50
- package/app/components/02.molecules/toast/DisplayToastProvider.vue +109 -45
- package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +23 -0
- package/app/components/02.molecules/toast/stories/DisplayToastProvider.stories.ts +73 -0
- package/app/components/02.molecules/toast/tests/DisplayToast.spec.ts +132 -18
- package/app/components/02.molecules/toast/tests/DisplayToastProvider.spec.ts +105 -39
- package/app/components/03.organisms/image-galleries/slider-gallery/CONSUMER-STYLING.md +19 -1
- package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +65 -7
- package/app/components/03.organisms/image-galleries/slider-gallery/stories/SliderGallery.stories.ts +201 -63
- package/app/components/03.organisms/image-galleries/slider-gallery/tests/SliderGallery.spec.ts +41 -2
- package/app/components/03.organisms/image-galleries/slider-gallery/tests/__snapshots__/SliderGallery.spec.ts.snap +6 -6
- package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +42 -11
- package/app/components/03.organisms/responsive-header/NavigationItems.vue +60 -16
- package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +86 -24
- package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +52 -0
- package/app/components/03.organisms/responsive-header/tests/NavigationItems.spec.ts +20 -0
- package/app/components/03.organisms/responsive-header/tests/ResponsiveHeader.spec.ts +40 -0
- package/app/components/03.organisms/services/service-detail/CONSUMER-STYLING.md +92 -41
- package/app/components/03.organisms/services/service-detail/ServiceDetail.vue +198 -73
- package/app/components/03.organisms/services/service-detail/stories/ServiceDetail.stories.ts +157 -9
- package/app/components/03.organisms/services/service-detail/tests/ServiceDetail.spec.ts +58 -0
- package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +5 -4
- package/app/components/03.organisms/services/service-summary/CONSUMER-STYLING.md +30 -0
- package/app/components/03.organisms/services/service-summary/ServiceSummary.vue +32 -8
- package/app/components/03.organisms/services/service-summary/stories/ServiceSummary.stories.ts +147 -0
- package/app/components/03.organisms/services/service-summary/tests/ServiceSummary.spec.ts +26 -0
- package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +2 -2
- package/app/components/03.organisms/services/services-card/CONSUMER-STYLING.md +104 -63
- package/app/components/03.organisms/services/services-card/ServicesCard.vue +92 -66
- package/app/components/03.organisms/services/services-card/stories/ServicesCard.stories.ts +194 -27
- package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +34 -8
- package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +8 -8
- package/app/components/03.organisms/services/services-grids/CONSUMER-STYLING.md +9 -0
- package/app/components/03.organisms/services/services-grids/ServicesCardGrid.vue +2 -1
- package/app/components/03.organisms/services/services-grids/stories/ServiceSummaryGrid.stories.ts +46 -6
- package/app/components/03.organisms/services/services-grids/stories/ServicesCardGrid.stories.ts +89 -11
- package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +8 -0
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +6 -6
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +24 -24
- package/app/components/05.forms/input-button/CONSUMER-STYLING.md +8 -0
- package/app/components/05.forms/input-button/InputButton.vue +3 -1
- package/app/composables/tests/useAnchorScroll.spec.ts +8 -0
- package/app/composables/useAnchorScroll.ts +1 -1
- package/app/composables/useTabs.ts +148 -206
- package/app/types/components/display-chip.d.ts +8 -4
- package/app/types/components/display-toast.d.ts +2 -0
- package/app/types/components/index.ts +0 -1
- package/app/types/components/select-menu.d.ts +2 -0
- package/app/utils/pausableTimeout.ts +41 -0
- package/nuxt.config.ts +1 -1
- package/package.json +1 -1
- package/.claude/skills/components/site-navigation.md +0 -145
- package/.claude/skills/components/tab-navigation.md +0 -272
- package/.claude/skills/components/tabs-core.md +0 -187
- package/.vscode/srcdev-component-site-navigation.code-snippets +0 -38
- package/.vscode/srcdev-component-tab-navigation.code-snippets +0 -30
- package/app/components/01.atoms/navigation/tabs/CONSUMER-STYLING.md +0 -120
- package/app/components/01.atoms/navigation/tabs/TabsCore.vue +0 -272
- package/app/components/01.atoms/navigation/tabs/stories/TabsCore.stories.ts +0 -199
- package/app/components/01.atoms/navigation/tabs/tests/TabsCore.spec.ts +0 -274
- package/app/components/02.molecules/navigation/site-navigation/CONSUMER-STYLING.md +0 -109
- package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +0 -751
- package/app/components/02.molecules/navigation/site-navigation/stories/SiteNavigation.stories.ts +0 -356
- package/app/components/02.molecules/navigation/site-navigation/tests/SiteNavigation.spec.ts +0 -367
- package/app/components/02.molecules/navigation/site-navigation/tests/__snapshots__/SiteNavigation.spec.ts.snap +0 -30
- package/app/components/02.molecules/navigation/tab-navigation/CONSUMER-STYLING.md +0 -150
- package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +0 -518
- package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts +0 -340
- package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +0 -339
- package/app/components/02.molecules/navigation/tab-navigation/tests/__snapshots__/TabNavigation.spec.ts.snap +0 -33
- package/app/composables/useNavCollapse.ts +0 -86
- package/app/types/components/nav-item.d.ts +0 -11
|
@@ -37,7 +37,7 @@ describe("DisplayChip", () => {
|
|
|
37
37
|
|
|
38
38
|
it("renders correct HTML structure (square + styleClassPassthrough)", async () => {
|
|
39
39
|
const wrapper = await mountSuspended(DisplayChip, {
|
|
40
|
-
props: { shape: "square", styleClassPassthrough: ["
|
|
40
|
+
props: { shape: "square", status: "online", styleClassPassthrough: ["featured"] },
|
|
41
41
|
});
|
|
42
42
|
expect(wrapper.html()).toMatchSnapshot();
|
|
43
43
|
});
|
|
@@ -63,15 +63,41 @@ describe("DisplayChip", () => {
|
|
|
63
63
|
|
|
64
64
|
// ─── Shape ────────────────────────────────────────────────────────────────
|
|
65
65
|
|
|
66
|
-
it("
|
|
66
|
+
it("sets data-shape='circle' by default", async () => {
|
|
67
67
|
const wrapper = await mountSuspended(DisplayChip);
|
|
68
|
-
expect(wrapper.
|
|
68
|
+
expect(wrapper.attributes("data-shape")).toBe("circle");
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
-
it("
|
|
71
|
+
it("sets data-shape='square' when shape='square'", async () => {
|
|
72
72
|
const wrapper = await mountSuspended(DisplayChip, { props: { shape: "square" } });
|
|
73
|
-
expect(wrapper.
|
|
74
|
-
|
|
73
|
+
expect(wrapper.attributes("data-shape")).toBe("square");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("does not put shape or status on the root as classes", async () => {
|
|
77
|
+
const wrapper = await mountSuspended(DisplayChip, { props: { shape: "square", status: "online" } });
|
|
78
|
+
expect(wrapper.classes()).toEqual(["display-chip"]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ─── Status ───────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
it("sets data-status='offline' by default", async () => {
|
|
84
|
+
const wrapper = await mountSuspended(DisplayChip);
|
|
85
|
+
expect(wrapper.attributes("data-status")).toBe("offline");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it.each(["online", "idle", "dnd"] as const)("sets data-status='%s' from the status prop", async (status) => {
|
|
89
|
+
const wrapper = await mountSuspended(DisplayChip, { props: { status } });
|
|
90
|
+
expect(wrapper.attributes("data-status")).toBe(status);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("renders statusLabel as screen-reader text", async () => {
|
|
94
|
+
const wrapper = await mountSuspended(DisplayChip, { props: { status: "online", statusLabel: "Online" } });
|
|
95
|
+
expect(wrapper.find(".sr-only").text()).toBe("Online");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("renders no screen-reader text without statusLabel", async () => {
|
|
99
|
+
const wrapper = await mountSuspended(DisplayChip);
|
|
100
|
+
expect(wrapper.find(".sr-only").exists()).toBe(false);
|
|
75
101
|
});
|
|
76
102
|
|
|
77
103
|
// ─── CSS custom properties ────────────────────────────────────────────────
|
|
@@ -89,39 +115,81 @@ describe("DisplayChip", () => {
|
|
|
89
115
|
expect(style.getPropertyValue("--_chip-angle")).toBe("45deg");
|
|
90
116
|
});
|
|
91
117
|
|
|
92
|
-
it("
|
|
118
|
+
it("falls back to the public tokens when no config is provided", async () => {
|
|
93
119
|
const wrapper = await mountSuspended(DisplayChip);
|
|
94
120
|
const style = (wrapper.element as HTMLElement).style;
|
|
95
|
-
expect(style.getPropertyValue("--_chip-size")).toBe("
|
|
96
|
-
expect(style.getPropertyValue("--_chip-mask-width")).toBe("
|
|
97
|
-
expect(style.getPropertyValue("--_chip-offset")).toBe("
|
|
98
|
-
expect(style.getPropertyValue("--_chip-angle")).toBe("90deg");
|
|
121
|
+
expect(style.getPropertyValue("--_chip-size")).toBe("var(--display-chip-size, 1.2rem)");
|
|
122
|
+
expect(style.getPropertyValue("--_chip-mask-width")).toBe("var(--display-chip-mask-width, 0.4rem)");
|
|
123
|
+
expect(style.getPropertyValue("--_chip-offset")).toBe("var(--display-chip-offset, 0rem)");
|
|
124
|
+
expect(style.getPropertyValue("--_chip-angle")).toBe("var(--display-chip-angle, 90deg)");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("falls back per field when config is partial", async () => {
|
|
128
|
+
const wrapper = await mountSuspended(DisplayChip, { props: { config: { label: "5", angle: "45deg" } } });
|
|
129
|
+
const style = (wrapper.element as HTMLElement).style;
|
|
130
|
+
expect(style.getPropertyValue("--_chip-size")).toBe("var(--display-chip-size, 1.2rem)");
|
|
131
|
+
expect(style.getPropertyValue("--_chip-angle")).toBe("45deg");
|
|
99
132
|
});
|
|
100
133
|
|
|
101
134
|
// ─── Label ────────────────────────────────────────────────────────────────
|
|
102
135
|
|
|
103
|
-
it("does not render .chip-label when config has no label", async () => {
|
|
136
|
+
it("does not render .display-chip-label when config has no label", async () => {
|
|
104
137
|
const wrapper = await mountSuspended(DisplayChip);
|
|
105
|
-
expect(wrapper.find(".chip-label").exists()).toBe(false);
|
|
138
|
+
expect(wrapper.find(".display-chip-label").exists()).toBe(false);
|
|
106
139
|
});
|
|
107
140
|
|
|
108
|
-
it("renders .chip-label when config.label is set", async () => {
|
|
141
|
+
it("renders .display-chip-label when config.label is set", async () => {
|
|
109
142
|
const wrapper = await mountSuspended(DisplayChip, {
|
|
110
143
|
props: { config: { size: "12px", maskWidth: "4px", offset: "0px", angle: "90deg", label: "5" } },
|
|
111
144
|
});
|
|
112
|
-
expect(wrapper.find(".chip-label").exists()).toBe(true);
|
|
113
|
-
expect(wrapper.find(".chip-label").text()).toBe("5");
|
|
145
|
+
expect(wrapper.find(".display-chip-label").exists()).toBe(true);
|
|
146
|
+
expect(wrapper.find(".display-chip-label").text()).toBe("5");
|
|
114
147
|
});
|
|
115
148
|
|
|
116
149
|
it.each([
|
|
117
|
-
["A", "
|
|
118
|
-
["+2", "
|
|
119
|
-
["DND", "
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
});
|
|
124
|
-
|
|
150
|
+
["A", "1"],
|
|
151
|
+
["+2", "2"],
|
|
152
|
+
["DND", "3"],
|
|
153
|
+
["👨👩👧👦", "1"],
|
|
154
|
+
["999+", "3"],
|
|
155
|
+
])("sets data-length for label '%s'", async (label, expected) => {
|
|
156
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
157
|
+
const wrapper = await mountSuspended(DisplayChip, { props: { config: { label } } });
|
|
158
|
+
expect(wrapper.find(".display-chip-label").attributes("data-length")).toBe(expected);
|
|
159
|
+
warnSpy.mockRestore();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("keeps a multi-codepoint emoji whole instead of splitting it", async () => {
|
|
163
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
164
|
+
const wrapper = await mountSuspended(DisplayChip, { props: { config: { label: "👨👩👧👦" } } });
|
|
165
|
+
expect(wrapper.find(".display-chip-label").text()).toBe("👨👩👧👦");
|
|
166
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
167
|
+
warnSpy.mockRestore();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("truncates by grapheme, not UTF-16 code unit", async () => {
|
|
171
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
172
|
+
const wrapper = await mountSuspended(DisplayChip, { props: { config: { label: "🇬🇧🇺🇸🇫🇷🇩🇪" } } });
|
|
173
|
+
expect(wrapper.find(".display-chip-label").text()).toBe("🇬🇧🇺🇸🇫🇷");
|
|
174
|
+
warnSpy.mockRestore();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("does not render a label for a whitespace-only label", async () => {
|
|
178
|
+
const wrapper = await mountSuspended(DisplayChip, { props: { config: { label: " " } } });
|
|
179
|
+
expect(wrapper.find(".display-chip-label").exists()).toBe(false);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("renders an HTML-like label as text", async () => {
|
|
183
|
+
const wrapper = await mountSuspended(DisplayChip, { props: { config: { label: "<b>" } } });
|
|
184
|
+
expect(wrapper.find(".display-chip-label b").exists()).toBe(false);
|
|
185
|
+
expect(wrapper.find(".display-chip-label").text()).toBe("<b>");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("updates the label when config changes after mount", async () => {
|
|
189
|
+
const wrapper = await mountSuspended(DisplayChip, { props: { config: { label: "1" } } });
|
|
190
|
+
await wrapper.setProps({ config: { label: "42" } });
|
|
191
|
+
expect(wrapper.find(".display-chip-label").text()).toBe("42");
|
|
192
|
+
expect(wrapper.find(".display-chip-label").attributes("data-length")).toBe("2");
|
|
125
193
|
});
|
|
126
194
|
|
|
127
195
|
it("truncates label to 3 characters when label exceeds maximum length", async () => {
|
|
@@ -129,7 +197,7 @@ describe("DisplayChip", () => {
|
|
|
129
197
|
const wrapper = await mountSuspended(DisplayChip, {
|
|
130
198
|
props: { config: { size: "12px", maskWidth: "4px", offset: "0px", angle: "90deg", label: "ABCD" } },
|
|
131
199
|
});
|
|
132
|
-
expect(wrapper.find(".chip-label").text()).toBe("ABC");
|
|
200
|
+
expect(wrapper.find(".display-chip-label").text()).toBe("ABC");
|
|
133
201
|
warnSpy.mockRestore();
|
|
134
202
|
});
|
|
135
203
|
|
|
@@ -144,16 +212,17 @@ describe("DisplayChip", () => {
|
|
|
144
212
|
|
|
145
213
|
// ─── Icon ─────────────────────────────────────────────────────────────────
|
|
146
214
|
|
|
147
|
-
it("does not render .chip-icon when config has no icon", async () => {
|
|
215
|
+
it("does not render .display-chip-icon when config has no icon", async () => {
|
|
148
216
|
const wrapper = await mountSuspended(DisplayChip);
|
|
149
|
-
expect(wrapper.find(".chip-icon").exists()).toBe(false);
|
|
217
|
+
expect(wrapper.find(".display-chip-icon").exists()).toBe(false);
|
|
150
218
|
});
|
|
151
219
|
|
|
152
|
-
it("renders .chip-icon when config.icon is set", async () => {
|
|
220
|
+
it("renders .display-chip-icon when config.icon is set", async () => {
|
|
153
221
|
const wrapper = await mountSuspended(DisplayChip, {
|
|
154
222
|
props: { config: { size: "12px", maskWidth: "4px", offset: "0px", angle: "90deg", icon: "mdi:check" } },
|
|
155
223
|
});
|
|
156
|
-
expect(wrapper.find(".chip-icon").exists()).toBe(true);
|
|
224
|
+
expect(wrapper.find(".display-chip-icon").exists()).toBe(true);
|
|
225
|
+
expect(wrapper.find(".display-chip-icon").attributes("aria-hidden")).toBe("true");
|
|
157
226
|
});
|
|
158
227
|
|
|
159
228
|
// ─── Slot ─────────────────────────────────────────────────────────────────
|
package/app/components/02.molecules/display-chip/tests/__snapshots__/DisplayChip.spec.ts.snap
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
2
|
|
|
3
|
-
exports[`DisplayChip > renders correct HTML structure (default) 1`] = `"<span class="display-chip circle" style="--_chip-size:
|
|
3
|
+
exports[`DisplayChip > renders correct HTML structure (default) 1`] = `"<span class="display-chip" data-shape="circle" data-status="offline" style="--_chip-size: var(--display-chip-size, 1.2rem); --_chip-mask-width: var(--display-chip-mask-width, 0.4rem); --_chip-offset: var(--display-chip-offset, 0rem); --_chip-angle: var(--display-chip-angle, 90deg);"><!--v-if--><!--v-if--><!--v-if--></span>"`;
|
|
4
4
|
|
|
5
|
-
exports[`DisplayChip > renders correct HTML structure (square + styleClassPassthrough) 1`] = `"<span class="display-chip square online" style="--_chip-size:
|
|
5
|
+
exports[`DisplayChip > renders correct HTML structure (square + styleClassPassthrough) 1`] = `"<span class="display-chip featured" data-shape="square" data-status="online" style="--_chip-size: var(--display-chip-size, 1.2rem); --_chip-mask-width: var(--display-chip-mask-width, 0.4rem); --_chip-offset: var(--display-chip-offset, 0rem); --_chip-angle: var(--display-chip-angle, 90deg);"><!--v-if--><!--v-if--><!--v-if--></span>"`;
|
|
6
6
|
|
|
7
7
|
exports[`DisplayChip > renders correct HTML structure (with icon) 1`] = `
|
|
8
|
-
"<span class="display-chip circle" style="--_chip-size: 12px; --_chip-mask-width: 4px; --_chip-offset: 0px; --_chip-angle: 90deg;"><span class="iconify i-mdi:check chip-icon" aria-hidden="true"></span>
|
|
8
|
+
"<span class="display-chip" data-shape="circle" data-status="offline" style="--_chip-size: 12px; --_chip-mask-width: 4px; --_chip-offset: 0px; --_chip-angle: 90deg;"><span class="iconify i-mdi:check display-chip-icon" aria-hidden="true"></span>
|
|
9
|
+
<!--v-if-->
|
|
9
10
|
<!--v-if--></span>"
|
|
10
11
|
`;
|
|
11
12
|
|
|
12
|
-
exports[`DisplayChip > renders correct HTML structure (with label) 1`] = `
|
|
13
|
+
exports[`DisplayChip > renders correct HTML structure (with label) 1`] = `
|
|
14
|
+
"<span class="display-chip" data-shape="circle" data-status="offline" style="--_chip-size: 12px; --_chip-mask-width: 4px; --_chip-offset: 0px; --_chip-angle: 90deg;"><!--v-if--><span class="display-chip-label" data-length="1">5</span>
|
|
15
|
+
<!--v-if--></span>"
|
|
16
|
+
`;
|
|
@@ -11,6 +11,10 @@ by `AlertContent`, or `AlertMaskedContent` with `masked`, and styled by their to
|
|
|
11
11
|
`02.molecules/alert-content/CONSUMER-STYLING.md` and `02.molecules/alert-masked-content/CONSUMER-STYLING.md`.
|
|
12
12
|
The palette comes from the `data-theme` attribute on `.display-prompt-wrapper` (`--theme-*` tokens).
|
|
13
13
|
|
|
14
|
+
To shorten long titles or messages, use AlertContent's line-clamp tokens, which inherit through the
|
|
15
|
+
prompt: `--alert-content-title-line-clamp` and `--alert-content-text-line-clamp` (default `none`,
|
|
16
|
+
`1` is single-line ellipsis).
|
|
17
|
+
|
|
14
18
|
> Changed 2026-09-28: `--display-prompt-transition-duration` is new (the transition was a hardcoded
|
|
15
19
|
> `all 200ms`), and the transition now respects reduced motion.
|
|
16
20
|
|
|
@@ -22,9 +26,13 @@ No private `--_` tokens.
|
|
|
22
26
|
|
|
23
27
|
| Hook | Element | When |
|
|
24
28
|
|---|---|---|
|
|
25
|
-
|
|
|
29
|
+
| `[data-state="open\|closed"]` | `.display-prompt` (root) | `closed` once dismissed without a parent `v-model`; the root is then also `inert` |
|
|
26
30
|
| `[data-theme="info\|success\|warning\|error"]` | `.display-prompt-wrapper` | The resolved `theme` |
|
|
27
31
|
|
|
32
|
+
> Changed 2026-10-08: the bare `.closed` class is now `data-state="closed"` (and `data-state="open"` before
|
|
33
|
+
> dismissal), so a consumer's own `.closed` utility class can no longer match the prompt. Replace
|
|
34
|
+
> `.display-prompt.closed` selectors with `.display-prompt[data-state="closed"]`.
|
|
35
|
+
|
|
28
36
|
> Changed 2026-09-28: root class renamed from `.display-prompt-core`. A closed prompt is now `inert`
|
|
29
37
|
> (its dismiss button used to stay in the tab order while invisible), and the root is only focusable
|
|
30
38
|
> (`tabindex="-1"`) when `use-auto-focus` is on.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<div
|
|
3
3
|
ref="promptElementRef"
|
|
4
4
|
class="display-prompt"
|
|
5
|
-
:
|
|
5
|
+
:data-state="componentOpen ? 'open' : 'closed'"
|
|
6
6
|
:data-test-id="`display-prompt-${resolved.theme}`"
|
|
7
7
|
:tabindex="resolved.useAutoFocus ? -1 : undefined"
|
|
8
8
|
:inert="!componentOpen || undefined"
|
|
@@ -114,7 +114,7 @@ onMounted(() => {
|
|
|
114
114
|
transition: none;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
&[data-state="closed"] {
|
|
118
118
|
grid-template-rows: 0fr;
|
|
119
119
|
opacity: 0;
|
|
120
120
|
pointer-events: none;
|
|
@@ -1,11 +1,33 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { computed, ref } from "vue";
|
|
2
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
3
|
import StorybookComponent from "../DisplayPrompt.vue";
|
|
4
|
+
import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
|
|
5
|
+
import type { MediaCanvas } from "~/types/components";
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
type LineClamp = "none" | "1" | "2" | "3" | "4";
|
|
8
|
+
|
|
9
|
+
// Story-only args: drive AlertContent's CSS tokens, not DisplayPrompt props. See their argTypes entries.
|
|
10
|
+
type StoryArgs = InstanceType<typeof StorybookComponent>["$props"] & {
|
|
11
|
+
titleLineClamp: LineClamp;
|
|
12
|
+
textLineClamp: LineClamp;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const lineClampArgType = (token: string, example: string) => ({
|
|
16
|
+
control: "select" as const,
|
|
17
|
+
options: ["none", "1", "2", "3", "4"] satisfies LineClamp[],
|
|
18
|
+
description:
|
|
19
|
+
`**Story control, not a prop.** Sets the \`${token}\` CSS token on a wrapper so you can try it here. ` +
|
|
20
|
+
`To use it in an app, set the token in your own CSS, e.g. \`.site-notice { ${token}: ${example}; }\`. ` +
|
|
21
|
+
"`1` is single-line ellipsis, `none` (the default) shows everything. Don't clamp messages people need to read in full.",
|
|
22
|
+
table: { category: "CSS tokens (story only, set in your CSS)", defaultValue: { summary: "none" } },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const meta: Meta<StoryArgs> = {
|
|
6
26
|
title: "Molecules/DisplayPrompt",
|
|
7
27
|
component: StorybookComponent,
|
|
8
28
|
argTypes: {
|
|
29
|
+
titleLineClamp: lineClampArgType("--alert-content-title-line-clamp", "1"),
|
|
30
|
+
textLineClamp: lineClampArgType("--alert-content-text-line-clamp", "3"),
|
|
9
31
|
theme: {
|
|
10
32
|
control: { type: "inline-radio" },
|
|
11
33
|
options: ["info", "success", "warning", "error"],
|
|
@@ -46,84 +68,199 @@ export default {
|
|
|
46
68
|
masked: false,
|
|
47
69
|
closeLabel: "Close this prompt",
|
|
48
70
|
styleClassPassthrough: [],
|
|
71
|
+
titleLineClamp: "none",
|
|
72
|
+
textLineClamp: "none",
|
|
49
73
|
},
|
|
50
|
-
}
|
|
74
|
+
};
|
|
51
75
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
setup() {
|
|
55
|
-
const model = ref(false);
|
|
56
|
-
return { args, model };
|
|
57
|
-
},
|
|
58
|
-
template: `
|
|
59
|
-
<div style="padding: 2rem; max-width: 640px;">
|
|
60
|
-
<StorybookComponent
|
|
61
|
-
v-model="model"
|
|
62
|
-
:theme="args.theme"
|
|
63
|
-
:dismissible="args.dismissible"
|
|
64
|
-
:use-auto-focus="args.useAutoFocus"
|
|
65
|
-
:masked="args.masked"
|
|
66
|
-
:close-label="args.closeLabel"
|
|
67
|
-
:style-class-passthrough="args.styleClassPassthrough"
|
|
68
|
-
>
|
|
69
|
-
<template #title>Prompt title</template>
|
|
70
|
-
<template #content>This is the prompt content. It can contain any information you want to communicate to the user.</template>
|
|
71
|
-
</StorybookComponent>
|
|
72
|
-
</div>
|
|
73
|
-
`,
|
|
74
|
-
});
|
|
76
|
+
export default meta;
|
|
77
|
+
type Story = StoryObj<StoryArgs>;
|
|
75
78
|
|
|
76
|
-
|
|
79
|
+
function useStorySetup(args: StoryArgs) {
|
|
80
|
+
const tokenStyles = computed(() => ({
|
|
81
|
+
"--alert-content-title-line-clamp": args.titleLineClamp,
|
|
82
|
+
"--alert-content-text-line-clamp": args.textLineClamp,
|
|
83
|
+
}));
|
|
84
|
+
return { args, tokenStyles };
|
|
85
|
+
}
|
|
77
86
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
87
|
+
const Template: Story = {
|
|
88
|
+
render: (args) => ({
|
|
89
|
+
components: { StorybookComponent },
|
|
90
|
+
setup() {
|
|
91
|
+
const model = ref(false);
|
|
92
|
+
return { ...useStorySetup(args), model };
|
|
93
|
+
},
|
|
94
|
+
template: `
|
|
95
|
+
<div :style="tokenStyles" style="padding: 2rem; max-width: 640px;">
|
|
96
|
+
<StorybookComponent
|
|
97
|
+
v-model="model"
|
|
98
|
+
:theme="args.theme"
|
|
99
|
+
:dismissible="args.dismissible"
|
|
100
|
+
:use-auto-focus="args.useAutoFocus"
|
|
101
|
+
:masked="args.masked"
|
|
102
|
+
:close-label="args.closeLabel"
|
|
103
|
+
:style-class-passthrough="args.styleClassPassthrough"
|
|
104
|
+
>
|
|
105
|
+
<template #title>Prompt title</template>
|
|
106
|
+
<template #content>This is the prompt content. It can contain any information you want to communicate to the user.</template>
|
|
107
|
+
</StorybookComponent>
|
|
108
|
+
</div>
|
|
109
|
+
`,
|
|
110
|
+
}),
|
|
82
111
|
};
|
|
83
112
|
|
|
84
|
-
export const
|
|
85
|
-
SuccessTheme.args = {
|
|
86
|
-
theme: "success",
|
|
87
|
-
dismissible: true,
|
|
88
|
-
};
|
|
113
|
+
export const Default: Story = { ...Template };
|
|
89
114
|
|
|
90
|
-
export const
|
|
91
|
-
WarningTheme.args = {
|
|
92
|
-
theme: "warning",
|
|
93
|
-
dismissible: true,
|
|
94
|
-
};
|
|
115
|
+
export const InfoTheme: Story = { ...Template, args: { theme: "info", dismissible: true } };
|
|
95
116
|
|
|
96
|
-
export const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
};
|
|
117
|
+
export const SuccessTheme: Story = { ...Template, args: { theme: "success", dismissible: true } };
|
|
118
|
+
|
|
119
|
+
export const WarningTheme: Story = { ...Template, args: { theme: "warning", dismissible: true } };
|
|
120
|
+
|
|
121
|
+
export const ErrorTheme: Story = { ...Template, args: { theme: "error", dismissible: true } };
|
|
122
|
+
|
|
123
|
+
export const Dismissible: Story = { ...Template, args: { theme: "info", dismissible: true } };
|
|
101
124
|
|
|
102
|
-
export const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
125
|
+
export const AllThemesDismissible: Story = {
|
|
126
|
+
render: () => ({
|
|
127
|
+
components: { StorybookComponent },
|
|
128
|
+
setup() {
|
|
129
|
+
return { themes: ["info", "success", "warning", "error"] as const };
|
|
130
|
+
},
|
|
131
|
+
template: `
|
|
132
|
+
<div style="padding: 2rem; display: flex; flex-direction: column; gap: 1.6rem; max-width: 640px;">
|
|
133
|
+
<StorybookComponent
|
|
134
|
+
v-for="theme in themes"
|
|
135
|
+
:key="theme"
|
|
136
|
+
:theme="theme"
|
|
137
|
+
:dismissible="true"
|
|
138
|
+
>
|
|
139
|
+
<template #title>{{ theme.charAt(0).toUpperCase() + theme.slice(1) }} prompt</template>
|
|
140
|
+
<template #content>This is the {{ theme }} variant of the prompt component.</template>
|
|
141
|
+
</StorybookComponent>
|
|
142
|
+
</div>
|
|
143
|
+
`,
|
|
144
|
+
}),
|
|
145
|
+
// Fixed showcase: hardcoded values, so Controls would do nothing here.
|
|
146
|
+
parameters: { controls: { disable: true } },
|
|
106
147
|
};
|
|
107
148
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
149
|
+
const stressPrompts = [
|
|
150
|
+
{
|
|
151
|
+
key: "german",
|
|
152
|
+
theme: "error",
|
|
153
|
+
title: "Donaudampfschifffahrtselektrizitätenhauptbetriebswerkbauunterbeamtengesellschaft",
|
|
154
|
+
content:
|
|
155
|
+
"Ihre Sitzung läuft in Kürze ab. Bitte speichern Sie alle ungespeicherten Änderungen, bevor Sie fortfahren, sonst gehen sie verloren. https://example.com/konto/sitzung/a-very-long-path-segment-without-any-spaces-at-all-in-it",
|
|
156
|
+
closeLabel: "Diese Benachrichtigung schließen und nicht erneut anzeigen",
|
|
112
157
|
},
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
158
|
+
{
|
|
159
|
+
key: "emoji-html",
|
|
160
|
+
theme: "warning",
|
|
161
|
+
title: "🚨🔥 Heads up <script>alert('xss')</script>",
|
|
162
|
+
content: "<b>Not bold</b> & <img src=x onerror=alert(1)> should all render as plain text. 👍🏽👨👩👧👦",
|
|
163
|
+
decoratorIcon: "🚨",
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
key: "rtl",
|
|
167
|
+
theme: "info",
|
|
168
|
+
dir: "rtl",
|
|
169
|
+
title: "تنبيه مهم بخصوص حسابك",
|
|
170
|
+
content: "تم تحديث شروط الخدمة. يرجى مراجعة التغييرات قبل المتابعة لاستخدام الموقع.",
|
|
171
|
+
closeLabel: "إغلاق",
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
key: "masked",
|
|
175
|
+
theme: "success",
|
|
176
|
+
masked: true,
|
|
177
|
+
title: "Gespeichert: Kundendienstmitarbeiterkontaktaufnahmeformular",
|
|
178
|
+
content: "Masked variant over a gradient, with an unbroken German compound in the title.",
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
key: "title-only",
|
|
182
|
+
theme: "success",
|
|
183
|
+
title: "Ok",
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
key: "empty-title",
|
|
187
|
+
theme: "info",
|
|
188
|
+
title: "",
|
|
189
|
+
content: "An empty title slot, a custom close icon and nothing else.",
|
|
190
|
+
customCloseIcon: true,
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
key: "content-only",
|
|
194
|
+
theme: "warning",
|
|
195
|
+
content: "x",
|
|
196
|
+
},
|
|
197
|
+
];
|
|
127
198
|
|
|
128
|
-
|
|
129
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Stress test — hostile content across several prompts, inside the CanvasSwitcher so it can be
|
|
201
|
+
* checked at every width.
|
|
202
|
+
*/
|
|
203
|
+
export const StressTest: Story = {
|
|
204
|
+
name: "Stress Test (Worst-Case Data)",
|
|
205
|
+
args: { titleLineClamp: "none", textLineClamp: "none" },
|
|
206
|
+
decorators: [
|
|
207
|
+
(story, context) => ({
|
|
208
|
+
components: { story, CanvasSwitcher },
|
|
209
|
+
setup() {
|
|
210
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "mobileCanvas");
|
|
211
|
+
return { canvasName };
|
|
212
|
+
},
|
|
213
|
+
template: `
|
|
214
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
215
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
216
|
+
</div>
|
|
217
|
+
<div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
|
|
218
|
+
<story />
|
|
219
|
+
</div>
|
|
220
|
+
`,
|
|
221
|
+
}),
|
|
222
|
+
],
|
|
223
|
+
render: (args) => ({
|
|
224
|
+
components: { StorybookComponent },
|
|
225
|
+
setup() {
|
|
226
|
+
return { ...useStorySetup(args), stressPrompts };
|
|
227
|
+
},
|
|
228
|
+
template: `
|
|
229
|
+
<div :style="tokenStyles" style="display: grid; gap: 1.6rem;">
|
|
230
|
+
<div
|
|
231
|
+
v-for="prompt in stressPrompts"
|
|
232
|
+
:key="prompt.key"
|
|
233
|
+
:dir="prompt.dir"
|
|
234
|
+
:style="prompt.masked ? 'padding: 1.6rem; background: linear-gradient(135deg, oklch(40% 0.15 250), oklch(25% 0.1 300));' : undefined"
|
|
235
|
+
>
|
|
236
|
+
<StorybookComponent
|
|
237
|
+
:theme="prompt.theme"
|
|
238
|
+
:masked="prompt.masked ?? false"
|
|
239
|
+
:close-label="prompt.closeLabel"
|
|
240
|
+
dismissible
|
|
241
|
+
>
|
|
242
|
+
<template v-if="prompt.decoratorIcon" #customDecoratorIcon>{{ prompt.decoratorIcon }}</template>
|
|
243
|
+
<template v-if="prompt.title !== undefined" #title>{{ prompt.title }}</template>
|
|
244
|
+
<template v-if="prompt.content" #content>{{ prompt.content }}</template>
|
|
245
|
+
<template v-if="prompt.customCloseIcon" #customCloseIcon>✕</template>
|
|
246
|
+
</StorybookComponent>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
`,
|
|
250
|
+
}),
|
|
251
|
+
parameters: {
|
|
252
|
+
layout: "fullscreen",
|
|
253
|
+
initialCanvas: "mobileCanvas",
|
|
254
|
+
docs: {
|
|
255
|
+
description: {
|
|
256
|
+
story:
|
|
257
|
+
"Hostile content: an unbroken German compound as the title, long German copy ending in a long URL, a long German close label, " +
|
|
258
|
+
"emoji at the start and as the decorator icon, HTML-like text (must render as text), right-to-left Arabic, the masked variant over a gradient, " +
|
|
259
|
+
"a two-character title, an empty title slot with a custom close icon, and a one-character message with no title. " +
|
|
260
|
+
"Check at every CanvasSwitcher width that nothing overflows or gets clipped, unbroken text wraps, the icon and dismiss button keep their size, " +
|
|
261
|
+
"the RTL prompt mirrors, and dismissing any prompt collapses it to zero height (the story's own grid gap stays)." +
|
|
262
|
+
"The two line-clamp controls are AlertContent CSS tokens (story only), not props: try `1` to see single-line ellipsis.",
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
};
|
|
@@ -33,9 +33,9 @@ describe("DisplayPrompt", () => {
|
|
|
33
33
|
expect(root(w).exists()).toBe(true);
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
it("is
|
|
36
|
+
it("is open by default (data-state=open)", async () => {
|
|
37
37
|
const w = await mountSuspended(DisplayPrompt);
|
|
38
|
-
expect(root(w).
|
|
38
|
+
expect(root(w).attributes("data-state")).toBe("open");
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
it("is not in the tab order by default", async () => {
|
|
@@ -104,6 +104,19 @@ describe("DisplayPrompt", () => {
|
|
|
104
104
|
expect(w.find("[data-test-id='alert-content']").text()).toContain("Detailed explanation here");
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
+
it("does not render the title element when the title slot is not provided", async () => {
|
|
108
|
+
const w = await mountSuspended(DisplayPrompt, { slots: { content: "x" } });
|
|
109
|
+
expect(w.find("[data-test-id='alert-title']").exists()).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("renders HTML-like slot text as plain text", async () => {
|
|
113
|
+
const w = await mountSuspended(DisplayPrompt, {
|
|
114
|
+
slots: { title: () => "<script>alert('xss')</script>" },
|
|
115
|
+
});
|
|
116
|
+
expect(w.find("[data-test-id='alert-title'] script").exists()).toBe(false);
|
|
117
|
+
expect(w.find("[data-test-id='alert-title']").text()).toContain("<script>");
|
|
118
|
+
});
|
|
119
|
+
|
|
107
120
|
it("does not render content element when content slot is empty", async () => {
|
|
108
121
|
const w = await mountSuspended(DisplayPrompt);
|
|
109
122
|
expect(w.find("[data-test-id='alert-content']").exists()).toBe(false);
|
|
@@ -123,11 +136,11 @@ describe("DisplayPrompt", () => {
|
|
|
123
136
|
|
|
124
137
|
// ─── Dismiss behaviour (no parent model) ─────────────────────────────────
|
|
125
138
|
|
|
126
|
-
it("
|
|
139
|
+
it("sets data-state=closed after the dismiss button is clicked", async () => {
|
|
127
140
|
const w = await mountSuspended(DisplayPrompt, { props: { dismissible: true } });
|
|
128
141
|
await w.find("[data-test-id='alert-dismiss']").trigger("click");
|
|
129
142
|
await nextTick();
|
|
130
|
-
expect(root(w).
|
|
143
|
+
expect(root(w).attributes("data-state")).toBe("closed");
|
|
131
144
|
});
|
|
132
145
|
|
|
133
146
|
it("makes the closed prompt inert so its controls leave the tab order", async () => {
|
|
@@ -168,14 +181,14 @@ describe("DisplayPrompt", () => {
|
|
|
168
181
|
expect(emitted![0]).toEqual([false]);
|
|
169
182
|
});
|
|
170
183
|
|
|
171
|
-
it("
|
|
184
|
+
it("stays data-state=open when dismissed via parent model", async () => {
|
|
172
185
|
const w = await mountSuspended(DisplayPrompt, {
|
|
173
186
|
props: { dismissible: true, modelValue: true },
|
|
174
187
|
});
|
|
175
188
|
await w.find("[data-test-id='alert-dismiss']").trigger("click");
|
|
176
189
|
await nextTick();
|
|
177
190
|
// componentOpen is unchanged — parent controls visibility via modelValue
|
|
178
|
-
expect(root(w).
|
|
191
|
+
expect(root(w).attributes("data-state")).toBe("open");
|
|
179
192
|
});
|
|
180
193
|
|
|
181
194
|
// ─── styleClassPassthrough ────────────────────────────────────────────────
|