srcdev-nuxt-components 9.6.10 → 9.6.11
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/rules/consumer/ids-from-useid.md +4 -2
- package/.claude/skills/component-compliance-checklist.md +9 -3
- package/.claude/skills/component-inline-action-button.md +5 -4
- package/.claude/skills/component-naming.md +2 -1
- package/.claude/skills/components/{alert-mask-core.md → alert-mask.md} +38 -23
- package/.claude/skills/components/alert-masked-content.md +3 -3
- package/.claude/skills/components/container-glow.md +1 -1
- package/.claude/skills/components/display-theme-switch.md +8 -8
- package/.claude/skills/components/input-button.md +26 -7
- package/.claude/skills/components/input-checkbox-radio.md +2 -0
- package/.claude/skills/components/input-description.md +5 -0
- package/.claude/skills/components/input-error.md +9 -2
- package/.claude/skills/components/input-label.md +8 -2
- package/.claude/skills/components/input-number.md +2 -0
- package/.claude/skills/components/input-otp.md +2 -0
- package/.claude/skills/components/input-range.md +2 -0
- package/.claude/skills/components/input-select.md +2 -0
- package/.claude/skills/components/input-text.md +2 -0
- package/.claude/skills/components/input-textarea.md +2 -0
- package/.claude/skills/components/pending-effect.md +5 -4
- package/.claude/skills/components/{triple-toggle-switch.md → segmented-control.md} +34 -19
- package/.claude/skills/components/services-card.md +1 -1
- package/.claude/skills/components/tabbed-content.md +1 -1
- package/.claude/skills/components/toggle-switch.md +2 -0
- package/.claude/skills/index.md +5 -5
- package/.claude/skills/storybook-add-story.md +3 -1
- package/.claude/skills/theming-component-token-pattern.md +24 -0
- package/.claude/skills/theming-form-geometry-tokens.md +1 -1
- package/.claude/skills/theming-partial-override.md +1 -1
- package/.vscode/srcdev-component-alert-mask.code-snippets +41 -0
- package/.vscode/{srcdev-component-triple-toggle-switch.code-snippets → srcdev-component-segmented-control.code-snippets} +6 -7
- package/app/assets/styles/setup/03.theming/_theme-slots.css +1 -1
- package/app/assets/styles/setup/04.elements/forms/03.generic-input-geometry.css +4 -0
- package/app/components/01.atoms/alert-mask/AlertMask.vue +194 -0
- package/app/components/01.atoms/alert-mask/CONSUMER-STYLING.md +57 -42
- package/app/components/01.atoms/alert-mask/stories/AlertMask.stories.ts +198 -0
- package/app/components/01.atoms/alert-mask/tests/AlertMask.spec.ts +233 -0
- package/app/components/02.molecules/alert-masked-content/AlertMaskedContent.vue +2 -2
- package/app/components/02.molecules/alert-masked-content/CONSUMER-STYLING.md +4 -4
- package/app/components/02.molecules/alert-masked-content/stories/AlertMaskedContent.stories.ts +60 -0
- package/app/components/02.molecules/alert-masked-content/tests/AlertMaskedContent.spec.ts +12 -12
- package/app/components/02.molecules/cookie-consent-banner/CONSUMER-STYLING.md +3 -3
- package/app/components/02.molecules/cookie-consent-banner/CookieConsentBanner.vue +1 -1
- package/app/components/02.molecules/cookie-consent-banner/tests/CookieConsentBanner.spec.ts +1 -1
- package/app/components/02.molecules/display-theme-switch/CONSUMER-STYLING.md +9 -9
- package/app/components/02.molecules/display-theme-switch/DisplayThemeSwitch.vue +16 -16
- package/app/components/02.molecules/display-theme-switch/stories/DisplayThemeSwitch.stories.ts +53 -1
- package/app/components/02.molecules/display-theme-switch/tests/DisplayThemeSwitch.spec.ts +8 -8
- package/app/components/02.molecules/input-copy/CONSUMER-STYLING.md +4 -4
- package/app/components/02.molecules/input-copy/InputCopy.vue +2 -2
- package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +2 -2
- package/app/components/03.organisms/google-reviews/tests/GoogleReviews.spec.ts +4 -4
- package/app/components/03.organisms/services/services-card/CONSUMER-STYLING.md +2 -2
- package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +2 -2
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +6 -6
- package/app/components/05.forms/form-errors/CONSUMER-STYLING.md +8 -0
- package/app/components/05.forms/form-errors/InputError.vue +43 -37
- package/app/components/05.forms/form-errors/stories/InputError.stories.ts +79 -1
- package/app/components/05.forms/form-errors/tests/InputError.spec.ts +29 -1
- package/app/components/05.forms/input-button/CONSUMER-STYLING.md +80 -47
- package/app/components/05.forms/input-button/InputButton.vue +52 -100
- package/app/components/05.forms/input-button/playwright/input-button.playwright.ts +1 -1
- package/app/components/05.forms/input-button/stories/InputButton.stories.ts +145 -8
- package/app/components/05.forms/input-button/tests/InputButton.spec.ts +76 -46
- package/app/components/05.forms/input-button/tests/__snapshots__/InputButton.spec.ts.snap +17 -17
- package/app/components/05.forms/input-checkbox-radio/CONSUMER-STYLING.md +9 -1
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadio.vue +8 -0
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioButton.vue +2 -2
- package/app/components/05.forms/input-description/CONSUMER-STYLING.md +14 -2
- package/app/components/05.forms/input-description/InputDescription.vue +2 -1
- package/app/components/05.forms/input-description/stories/InputDescription.stories.ts +70 -4
- package/app/components/05.forms/input-description/tests/InputDescription.spec.ts +2 -1
- package/app/components/05.forms/input-label/CONSUMER-STYLING.md +8 -0
- package/app/components/05.forms/input-label/InputLabel.vue +1 -0
- package/app/components/05.forms/input-label/stories/InputLabel.stories.ts +68 -2
- package/app/components/05.forms/input-label/tests/InputLabel.spec.ts +13 -3
- package/app/components/05.forms/input-number/CONSUMER-STYLING.md +7 -1
- package/app/components/05.forms/input-number/InputNumber.vue +4 -5
- package/app/components/05.forms/input-otp/CONSUMER-STYLING.md +7 -1
- package/app/components/05.forms/input-otp/InputOtp.vue +2 -2
- package/app/components/05.forms/input-range/InputRange.vue +1 -1
- package/app/components/05.forms/input-select/CONSUMER-STYLING.md +7 -1
- package/app/components/05.forms/input-select/InputSelect.vue +14 -5
- package/app/components/05.forms/input-text/CONSUMER-STYLING.md +7 -1
- package/app/components/05.forms/input-text/InputText.vue +4 -4
- package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +7 -1
- package/app/components/05.forms/input-textarea/InputTextarea.vue +2 -3
- package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +4 -4
- package/app/components/05.forms/pending-effect/CONSUMER-STYLING.md +7 -3
- package/app/components/05.forms/pending-effect/PendingEffect.vue +3 -3
- package/app/components/05.forms/pending-effect/tests/PendingEffect.spec.ts +1 -1
- package/app/components/05.forms/segmented-control/CONSUMER-STYLING.md +125 -0
- package/app/components/05.forms/segmented-control/SegmentedControl.vue +231 -0
- package/app/components/05.forms/segmented-control/stories/SegmentedControl.stories.ts +215 -0
- package/app/components/05.forms/segmented-control/tests/SegmentedControl.spec.ts +202 -0
- package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +7 -0
- package/app/components/05.forms/toggle-switch/ToggleSwitch.vue +2 -0
- package/app/types/components/index.ts +1 -1
- package/package.json +1 -1
- package/.vscode/srcdev-component-alert-mask-core.code-snippets +0 -31
- package/app/components/01.atoms/alert-mask/AlertMaskCore.vue +0 -166
- package/app/components/01.atoms/alert-mask/stories/AlertMaskCore.stories.ts +0 -105
- package/app/components/01.atoms/alert-mask/tests/AlertMaskCore.spec.ts +0 -342
- package/app/components/05.forms/triple-toggle-switch/CONSUMER-STYLING.md +0 -99
- package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitch.vue +0 -251
- package/app/components/05.forms/triple-toggle-switch/stories/TripleToggleSwitch.stories.ts +0 -104
- package/app/components/05.forms/triple-toggle-switch/tests/TripleToggleSwitch.spec.ts +0 -138
- /package/app/types/components/{alert-mask-core.d.ts → alert-mask.d.ts} +0 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { ref } from "vue";
|
|
2
|
+
import AlertMask from "../AlertMask.vue";
|
|
3
|
+
import CanvasSwitcher from "../../canvas-switcher/CanvasSwitcher.vue";
|
|
4
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
5
|
+
import type { MediaCanvas } from "~/types/components";
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof AlertMask> = {
|
|
8
|
+
title: "Atoms/AlertMask",
|
|
9
|
+
component: AlertMask,
|
|
10
|
+
argTypes: {
|
|
11
|
+
config: {
|
|
12
|
+
control: "object",
|
|
13
|
+
description:
|
|
14
|
+
"Border/background colour and geometry: backgroundColour, borderColour, radiusLeft, radiusRight, borderLeft, borderTop, borderRight, borderBottom",
|
|
15
|
+
table: { category: "Basic" },
|
|
16
|
+
},
|
|
17
|
+
styleClassPassthrough: {
|
|
18
|
+
table: { disable: true },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
parameters: {
|
|
22
|
+
docs: {
|
|
23
|
+
description: {
|
|
24
|
+
component:
|
|
25
|
+
"Draws a border and translucent fill around slotted content as an SVG path, sized to the measured content. Geometry (radii and per-side border widths) comes from the `config` prop; colours default from `--alert-mask-border-colour` and `--alert-mask-background-colour`, and `config` colours override them. [AlertMaskedContent](?path=/story/molecules-alertmaskedcontent--default) is the alert built on it.",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
decorators: [
|
|
30
|
+
(story, context) => ({
|
|
31
|
+
components: { story, CanvasSwitcher },
|
|
32
|
+
setup() {
|
|
33
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
|
|
34
|
+
return { canvasName };
|
|
35
|
+
},
|
|
36
|
+
template: `
|
|
37
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
38
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
39
|
+
</div>
|
|
40
|
+
<div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
|
|
41
|
+
<p style="margin: 0 0 1.6rem; font-size: 1.4rem;">
|
|
42
|
+
The alert built on this mask is
|
|
43
|
+
<a href="/?path=/story/molecules-alertmaskedcontent--default" target="_top">AlertMaskedContent</a>.
|
|
44
|
+
</p>
|
|
45
|
+
<story />
|
|
46
|
+
</div>
|
|
47
|
+
`,
|
|
48
|
+
}),
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default meta;
|
|
53
|
+
type Story = StoryObj<typeof AlertMask>;
|
|
54
|
+
|
|
55
|
+
// The mask is a translucent fill plus a cut-out border, so it needs something busy behind it to read at all.
|
|
56
|
+
const backdrop =
|
|
57
|
+
"background: url('/images/banners/video/lake-banner.jpg') center / cover; padding: 4rem 2.4rem; border-radius: 1.2rem; max-width: 60rem;";
|
|
58
|
+
const gradientBackdrop =
|
|
59
|
+
"background: repeating-linear-gradient(45deg, oklch(100% 0 0 / 0.18) 0 1.2rem, transparent 1.2rem 2.4rem), linear-gradient(120deg, oklch(70% 0.2 30), oklch(65% 0.2 200) 60%, oklch(55% 0.22 300)); padding: 4rem 2.4rem; border-radius: 1.2rem; max-width: 60rem;";
|
|
60
|
+
|
|
61
|
+
export const Default: Story = {
|
|
62
|
+
args: { config: {} },
|
|
63
|
+
render: (args) => ({
|
|
64
|
+
components: { AlertMask },
|
|
65
|
+
setup() {
|
|
66
|
+
return { args, gradientBackdrop };
|
|
67
|
+
},
|
|
68
|
+
template: `
|
|
69
|
+
<div data-theme="info" :style="gradientBackdrop">
|
|
70
|
+
<AlertMask v-bind="args">
|
|
71
|
+
<p style="margin: 0; padding: 1.6rem;">Default geometry and token colours: an 8px theme-accent border and a translucent subtle-surface fill.</p>
|
|
72
|
+
</AlertMask>
|
|
73
|
+
</div>
|
|
74
|
+
`,
|
|
75
|
+
}),
|
|
76
|
+
parameters: {
|
|
77
|
+
docs: { description: { story: "No config: default radii and borders, colours from the CSS tokens and the surrounding `data-theme`." } },
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const CustomConfig: Story = {
|
|
82
|
+
args: {
|
|
83
|
+
config: {
|
|
84
|
+
backgroundColour: "rgba(0, 0, 0, 0.3)",
|
|
85
|
+
borderColour: "var(--theme-accent)",
|
|
86
|
+
radiusLeft: 8,
|
|
87
|
+
radiusRight: 4,
|
|
88
|
+
borderLeft: 6,
|
|
89
|
+
borderTop: 1,
|
|
90
|
+
borderRight: 1,
|
|
91
|
+
borderBottom: 1,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
render: (args) => ({
|
|
95
|
+
components: { AlertMask },
|
|
96
|
+
setup() {
|
|
97
|
+
return { args, backdrop };
|
|
98
|
+
},
|
|
99
|
+
template: `
|
|
100
|
+
<div data-theme="info" :style="backdrop">
|
|
101
|
+
<AlertMask v-bind="args">
|
|
102
|
+
<p style="margin: 0; padding: 1.6rem; color: white;">Custom accent-bar config, matching AlertMaskedContent's default shape</p>
|
|
103
|
+
</AlertMask>
|
|
104
|
+
</div>
|
|
105
|
+
`,
|
|
106
|
+
}),
|
|
107
|
+
parameters: {
|
|
108
|
+
docs: {
|
|
109
|
+
description: {
|
|
110
|
+
story: "The accent-bar shape used by AlertMaskedContent: thick left radius/border, thin elsewhere.",
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const OverGradient: Story = {
|
|
117
|
+
args: {
|
|
118
|
+
config: {
|
|
119
|
+
backgroundColour: "rgba(0, 0, 0, 0.3)",
|
|
120
|
+
borderColour: "white",
|
|
121
|
+
radiusLeft: 16,
|
|
122
|
+
radiusRight: 16,
|
|
123
|
+
borderLeft: 4,
|
|
124
|
+
borderTop: 4,
|
|
125
|
+
borderRight: 4,
|
|
126
|
+
borderBottom: 4,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
render: (args) => ({
|
|
130
|
+
components: { AlertMask },
|
|
131
|
+
setup() {
|
|
132
|
+
return { args, gradientBackdrop };
|
|
133
|
+
},
|
|
134
|
+
template: `
|
|
135
|
+
<div :style="gradientBackdrop">
|
|
136
|
+
<AlertMask v-bind="args">
|
|
137
|
+
<p style="margin: 0; padding: 1.6rem; color: white;">Even border all round: the stripes show through the fill and stop at the border's inner edge.</p>
|
|
138
|
+
</AlertMask>
|
|
139
|
+
</div>
|
|
140
|
+
`,
|
|
141
|
+
}),
|
|
142
|
+
parameters: {
|
|
143
|
+
docs: {
|
|
144
|
+
description: {
|
|
145
|
+
story: "A striped gradient behind an even 4px white border, so the translucent fill and the border's cut-out are both visible.",
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const stressConfigs = [
|
|
152
|
+
{ caption: "Radii far larger than the box (500)", config: { radiusLeft: 500, radiusRight: 500 } },
|
|
153
|
+
{ caption: "Negative borders and radius (clamped to 0)", config: { borderLeft: -10, borderTop: -4, radiusLeft: -8 } },
|
|
154
|
+
{ caption: "Huge borders (40px) round a single word", config: { borderLeft: 40, borderTop: 40, borderRight: 40, borderBottom: 40 } },
|
|
155
|
+
{ caption: "Fractional values", config: { borderLeft: 2.5, borderTop: 0.5, borderRight: 0.5, borderBottom: 0.5, radiusLeft: 6.25, radiusRight: 3.75 } },
|
|
156
|
+
{ caption: "Zero borders", config: { borderLeft: 0, borderTop: 0, borderRight: 0, borderBottom: 0 } },
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
export const StressTest: Story = {
|
|
160
|
+
name: "Stress Test (Worst-Case Data)",
|
|
161
|
+
render: () => ({
|
|
162
|
+
components: { AlertMask },
|
|
163
|
+
setup() {
|
|
164
|
+
return { stressConfigs, gradientBackdrop };
|
|
165
|
+
},
|
|
166
|
+
template: `
|
|
167
|
+
<div data-theme="info" :style="gradientBackdrop" style="display: grid; gap: 2.4rem;">
|
|
168
|
+
<AlertMask :config="{ borderLeft: 6, borderTop: 1, borderRight: 1, borderBottom: 1, radiusLeft: 8, radiusRight: 4 }">
|
|
169
|
+
<p style="margin: 0; padding: 1.6rem; overflow-wrap: anywhere;">
|
|
170
|
+
📧 Datenschutzgrundverordnungskonformitätsbestätigung https://example.com/a/very/long/unbroken/url/that/should/wrap <b>not bold</b> <b>escaped</b> يرجى إدخال بريدك الإلكتروني. Wir verwenden Ihre Adresse ausschließlich für Rückfragen zu Ihrer Bestellung bei Alex Morgan in Northtown.
|
|
171
|
+
</p>
|
|
172
|
+
</AlertMask>
|
|
173
|
+
<div dir="rtl">
|
|
174
|
+
<AlertMask :config="{ borderLeft: 6, borderTop: 1, borderRight: 1, borderBottom: 1, radiusLeft: 8, radiusRight: 4 }">
|
|
175
|
+
<p style="margin: 0; padding: 1.6rem;">نص من اليمين إلى اليسار داخل القناع، مع شريط التمييز على اليسار</p>
|
|
176
|
+
</AlertMask>
|
|
177
|
+
</div>
|
|
178
|
+
<AlertMask></AlertMask>
|
|
179
|
+
<div v-for="item in stressConfigs" :key="item.caption">
|
|
180
|
+
<p style="margin: 0 0 0.8rem; font-size: 1.4rem;">{{ item.caption }}</p>
|
|
181
|
+
<AlertMask :config="item.config">
|
|
182
|
+
<p style="margin: 0; padding: 0.8rem;">x</p>
|
|
183
|
+
</AlertMask>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
`,
|
|
187
|
+
}),
|
|
188
|
+
parameters: {
|
|
189
|
+
initialCanvas: "mobileCanvas",
|
|
190
|
+
docs: {
|
|
191
|
+
story: { inline: false, height: "1100px" },
|
|
192
|
+
description: {
|
|
193
|
+
story:
|
|
194
|
+
"Long German copy with an unbroken compound word and URL, HTML-like and right-to-left text, an empty mask, then hostile config: oversized radii, negative and fractional values, huge and zero borders. Check at every canvas width: the frame always hugs its content inside the dashed outline, no path draws outside the box or inverts, the thick bar stays on the left in the right-to-left mask with the text clear of it, and the empty mask collapses to a border-only frame.",
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
};
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import { defineComponent, nextTick } from "vue";
|
|
4
|
+
import AlertMask from "../AlertMask.vue";
|
|
5
|
+
import type { AlertMaskConfig } from "~/types/components";
|
|
6
|
+
|
|
7
|
+
interface AlertMaskInstance {
|
|
8
|
+
cfg: {
|
|
9
|
+
backgroundColour: string | undefined;
|
|
10
|
+
borderColour: string | undefined;
|
|
11
|
+
radiusLeft: number;
|
|
12
|
+
radiusRight: number;
|
|
13
|
+
borderLeft: number;
|
|
14
|
+
borderTop: number;
|
|
15
|
+
borderRight: number;
|
|
16
|
+
borderBottom: number;
|
|
17
|
+
};
|
|
18
|
+
contentWidth: number;
|
|
19
|
+
contentHeight: number;
|
|
20
|
+
measured: boolean;
|
|
21
|
+
svgWidth: number;
|
|
22
|
+
svgHeight: number;
|
|
23
|
+
outerPath: string;
|
|
24
|
+
innerPath: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Vitest 4: a mock's implementation must be a regular function (not an arrow function) to stay
|
|
28
|
+
// constructible via `new`.
|
|
29
|
+
const mockResizeObserver = vi.fn(function () {
|
|
30
|
+
return { observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn() };
|
|
31
|
+
});
|
|
32
|
+
vi.stubGlobal("ResizeObserver", mockResizeObserver);
|
|
33
|
+
|
|
34
|
+
const mounted: { unmount: () => void }[] = [];
|
|
35
|
+
|
|
36
|
+
const mountMask = async (options: Parameters<typeof mountSuspended<typeof AlertMask>>[1] = {}) => {
|
|
37
|
+
const wrapper = await mountSuspended(AlertMask, options);
|
|
38
|
+
mounted.push(wrapper);
|
|
39
|
+
return wrapper;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const vmOf = (wrapper: Awaited<ReturnType<typeof mountMask>>) => wrapper.vm as unknown as AlertMaskInstance;
|
|
43
|
+
|
|
44
|
+
const setContentSize = async (vm: AlertMaskInstance, width: number, height: number) => {
|
|
45
|
+
vm.contentWidth = width;
|
|
46
|
+
vm.contentHeight = height;
|
|
47
|
+
vm.measured = true;
|
|
48
|
+
await nextTick();
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const numbersIn = (path: string) => (path.match(/-?\d+(\.\d+)?/g) ?? []).map(Number);
|
|
52
|
+
|
|
53
|
+
describe("AlertMask", () => {
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
vi.clearAllMocks();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
mounted.splice(0).forEach((wrapper) => wrapper.unmount());
|
|
60
|
+
vi.restoreAllMocks();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("renders the root, decorator and content structure", async () => {
|
|
64
|
+
const wrapper = await mountMask();
|
|
65
|
+
expect(wrapper.find(".alert-mask").exists()).toBe(true);
|
|
66
|
+
expect(wrapper.find(".alert-mask-decorator").exists()).toBe(true);
|
|
67
|
+
expect(wrapper.find(".alert-mask-border").exists()).toBe(true);
|
|
68
|
+
expect(wrapper.find(".alert-mask-background").exists()).toBe(true);
|
|
69
|
+
expect(wrapper.find(".alert-mask-content-slot").exists()).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("renders default slot content", async () => {
|
|
73
|
+
const wrapper = await mountMask({ slots: { default: "<p class='test-content'>Alert</p>" } });
|
|
74
|
+
expect(wrapper.find(".test-content").text()).toBe("Alert");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("applies passthrough classes and reacts to changes", async () => {
|
|
78
|
+
const wrapper = await mountMask({ props: { styleClassPassthrough: ["one", "two"] } });
|
|
79
|
+
const root = () => wrapper.find(".alert-mask");
|
|
80
|
+
expect(root().classes()).toEqual(expect.arrayContaining(["one", "two"]));
|
|
81
|
+
|
|
82
|
+
await wrapper.setProps({ styleClassPassthrough: "three" });
|
|
83
|
+
expect(root().classes()).toContain("three");
|
|
84
|
+
expect(root().classes()).not.toContain("one");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("uses default geometry and leaves colours to CSS tokens when no config is given", async () => {
|
|
88
|
+
const vm = vmOf(await mountMask());
|
|
89
|
+
expect(vm.cfg).toEqual({
|
|
90
|
+
backgroundColour: undefined,
|
|
91
|
+
borderColour: undefined,
|
|
92
|
+
radiusLeft: 12,
|
|
93
|
+
radiusRight: 12,
|
|
94
|
+
borderLeft: 8,
|
|
95
|
+
borderTop: 8,
|
|
96
|
+
borderRight: 8,
|
|
97
|
+
borderBottom: 8,
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("applies a full custom config", async () => {
|
|
102
|
+
const config: AlertMaskConfig = {
|
|
103
|
+
backgroundColour: "rgba(255,0,0,0.5)",
|
|
104
|
+
borderColour: "#ff0000",
|
|
105
|
+
radiusLeft: 20,
|
|
106
|
+
radiusRight: 15,
|
|
107
|
+
borderLeft: 12,
|
|
108
|
+
borderTop: 10,
|
|
109
|
+
borderRight: 8,
|
|
110
|
+
borderBottom: 6,
|
|
111
|
+
};
|
|
112
|
+
const vm = vmOf(await mountMask({ props: { config } }));
|
|
113
|
+
expect(vm.cfg).toEqual(config);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("sanitises negative and non-finite sizes", async () => {
|
|
117
|
+
const vm = vmOf(
|
|
118
|
+
await mountMask({
|
|
119
|
+
props: { config: { borderLeft: -10, borderTop: Number.NaN, radiusLeft: -4, radiusRight: Infinity } },
|
|
120
|
+
})
|
|
121
|
+
);
|
|
122
|
+
expect(vm.cfg.borderLeft).toBe(0);
|
|
123
|
+
expect(vm.cfg.borderTop).toBe(8);
|
|
124
|
+
expect(vm.cfg.radiusLeft).toBe(0);
|
|
125
|
+
expect(vm.cfg.radiusRight).toBe(12);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("only sets fill styles for colours given in config", async () => {
|
|
129
|
+
const plain = await mountMask();
|
|
130
|
+
expect(plain.find(".alert-mask-border").attributes("style")).toBeUndefined();
|
|
131
|
+
expect(plain.find(".alert-mask-background").attributes("style")).toBeUndefined();
|
|
132
|
+
|
|
133
|
+
const custom = await mountMask({
|
|
134
|
+
props: { config: { borderColour: "var(--my-border)", backgroundColour: "var(--my-bg)" } },
|
|
135
|
+
});
|
|
136
|
+
expect((custom.find(".alert-mask-border").element as unknown as SVGElement).style.fill).toBe("var(--my-border)");
|
|
137
|
+
expect((custom.find(".alert-mask-background").element as unknown as SVGElement).style.fill).toBe("var(--my-bg)");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("gives each instance its own mask id", async () => {
|
|
141
|
+
const Host = defineComponent({
|
|
142
|
+
components: { AlertMask },
|
|
143
|
+
template: "<div><AlertMask>One</AlertMask><AlertMask>Two</AlertMask></div>",
|
|
144
|
+
});
|
|
145
|
+
const wrapper = await mountSuspended(Host);
|
|
146
|
+
mounted.push(wrapper);
|
|
147
|
+
const ids = wrapper.findAll("mask").map((mask) => mask.attributes("id"));
|
|
148
|
+
expect(new Set(ids).size).toBe(2);
|
|
149
|
+
expect(wrapper.findAll(".alert-mask-border")[0]!.attributes("mask")).toBe(`url(#${ids[0]})`);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("hides the decorative SVG from assistive technology", async () => {
|
|
153
|
+
const wrapper = await mountMask();
|
|
154
|
+
expect(wrapper.find(".alert-mask-decorator").attributes("aria-hidden")).toBe("true");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("insets the content by the configured borders", async () => {
|
|
158
|
+
const wrapper = await mountMask({
|
|
159
|
+
props: { config: { borderLeft: 15, borderTop: 20, borderRight: 25, borderBottom: 30 } },
|
|
160
|
+
});
|
|
161
|
+
const style = (wrapper.find(".alert-mask-content").element as HTMLElement).style;
|
|
162
|
+
expect(style.getPropertyValue("--_inset-left")).toBe("15px");
|
|
163
|
+
expect(style.getPropertyValue("--_inset-top")).toBe("20px");
|
|
164
|
+
expect(style.getPropertyValue("--_inset-right")).toBe("25px");
|
|
165
|
+
expect(style.getPropertyValue("--_inset-bottom")).toBe("30px");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("insets the content by the default borders when config omits them", async () => {
|
|
169
|
+
const style = ((await mountMask()).find(".alert-mask-content").element as HTMLElement).style;
|
|
170
|
+
expect(style.getPropertyValue("--_inset-left")).toBe("8px");
|
|
171
|
+
expect(style.getPropertyValue("--_inset-bottom")).toBe("8px");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("observes the content and disconnects on unmount", async () => {
|
|
175
|
+
const wrapper = await mountMask({ slots: { default: "<div>Content</div>" } });
|
|
176
|
+
expect(mockResizeObserver).toHaveBeenCalled();
|
|
177
|
+
const observer = mockResizeObserver.mock.results.at(-1)?.value as { disconnect: ReturnType<typeof vi.fn> };
|
|
178
|
+
wrapper.unmount();
|
|
179
|
+
mounted.splice(mounted.indexOf(wrapper), 1);
|
|
180
|
+
expect(observer.disconnect).toHaveBeenCalled();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("draws nothing until the content is measured", async () => {
|
|
184
|
+
const vm = vmOf(await mountMask());
|
|
185
|
+
vm.measured = false;
|
|
186
|
+
await nextTick();
|
|
187
|
+
expect(vm.svgWidth).toBe(0);
|
|
188
|
+
expect(vm.outerPath).toBe("");
|
|
189
|
+
expect(vm.innerPath).toBe("");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("sizes the SVG from the content plus the borders", async () => {
|
|
193
|
+
const wrapper = await mountMask();
|
|
194
|
+
const vm = vmOf(wrapper);
|
|
195
|
+
await setContentSize(vm, 200, 100);
|
|
196
|
+
expect(vm.svgWidth).toBe(216);
|
|
197
|
+
expect(vm.svgHeight).toBe(116);
|
|
198
|
+
expect((wrapper.find(".alert-mask-decorator").element as HTMLElement).style.getPropertyValue("--_height")).toBe(
|
|
199
|
+
"116px"
|
|
200
|
+
);
|
|
201
|
+
expect(vm.outerPath).not.toBe("");
|
|
202
|
+
expect(vm.innerPath).not.toBe("");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("recomputes the size when the border config changes without a content resize", async () => {
|
|
206
|
+
const wrapper = await mountMask();
|
|
207
|
+
const vm = vmOf(wrapper);
|
|
208
|
+
await setContentSize(vm, 200, 100);
|
|
209
|
+
|
|
210
|
+
await wrapper.setProps({ config: { borderTop: 30, borderBottom: 30 } });
|
|
211
|
+
expect(vm.svgHeight).toBe(160);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("clamps radii larger than the box so the path stays inside it", async () => {
|
|
215
|
+
const wrapper = await mountMask({ props: { config: { radiusLeft: 500, radiusRight: 500 } } });
|
|
216
|
+
const vm = vmOf(wrapper);
|
|
217
|
+
await setContentSize(vm, 200, 40);
|
|
218
|
+
|
|
219
|
+
const outer = numbersIn(vm.outerPath);
|
|
220
|
+
expect(Math.min(...outer)).toBeGreaterThanOrEqual(0);
|
|
221
|
+
expect(Math.max(...outer)).toBeLessThanOrEqual(216);
|
|
222
|
+
const inner = numbersIn(vm.innerPath);
|
|
223
|
+
expect(Math.min(...inner)).toBeGreaterThanOrEqual(0);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("drops the inner cut-out when the borders fill the whole box", async () => {
|
|
227
|
+
const wrapper = await mountMask({ props: { config: { borderLeft: 100, borderRight: 100 } } });
|
|
228
|
+
const vm = vmOf(wrapper);
|
|
229
|
+
await setContentSize(vm, 0, 40);
|
|
230
|
+
expect(vm.outerPath).not.toBe("");
|
|
231
|
+
expect(vm.innerPath).toBe("");
|
|
232
|
+
});
|
|
233
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="alert-masked-content" :data-theme="theme">
|
|
3
|
-
<
|
|
3
|
+
<AlertMask :config="resolvedMaskConfig">
|
|
4
4
|
<AlertContentInner
|
|
5
5
|
:theme="theme"
|
|
6
6
|
:custom-icon="customIcon"
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
<slot name="dismissLabel"></slot>
|
|
30
30
|
</template>
|
|
31
31
|
</AlertContentInner>
|
|
32
|
-
</
|
|
32
|
+
</AlertMask>
|
|
33
33
|
</div>
|
|
34
34
|
</template>
|
|
35
35
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# AlertMaskedContent — Consumer Styling Guide
|
|
2
2
|
|
|
3
3
|
`AlertMaskedContent` is `AlertContentInner` (icon, title, body, dismiss button) wrapped in
|
|
4
|
-
`
|
|
4
|
+
`AlertMask`'s SVG mask: an accent-coloured border drawn as a cut-out, and a translucent fill,
|
|
5
5
|
so whatever sits behind the alert stays visible. It's the variant `DisplayToast`
|
|
6
6
|
(`appearance.masked`) and `DisplayPrompt` (`masked`) swap in.
|
|
7
7
|
|
|
@@ -20,7 +20,7 @@ for that instance.
|
|
|
20
20
|
|
|
21
21
|
### Mask geometry
|
|
22
22
|
|
|
23
|
-
Radius and border thickness aren't tokens: `
|
|
23
|
+
Radius and border thickness aren't tokens: `AlertMask` computes the SVG path from them in JS.
|
|
24
24
|
Set them with the `maskConfig` prop (`AlertMaskConfig`). Defaults: `radiusLeft: 8`,
|
|
25
25
|
`radiusRight: 4`, `borderLeft: 6`, `borderTop`/`borderRight`/`borderBottom: 1`.
|
|
26
26
|
|
|
@@ -31,13 +31,13 @@ set on the root (`--theme-text`, `--theme-border`, etc.).
|
|
|
31
31
|
|
|
32
32
|
### Private tokens
|
|
33
33
|
|
|
34
|
-
None of its own. `
|
|
34
|
+
None of its own. `AlertMask`'s `--_height` / `--_inset-*` are measured plumbing, not public API.
|
|
35
35
|
|
|
36
36
|
## State hooks
|
|
37
37
|
|
|
38
38
|
- `data-theme="info" | "success" | "warning" | "error"` on the root `.alert-masked-content`,
|
|
39
39
|
from the `theme` prop. Drives `--theme-accent` and the content colours.
|
|
40
|
-
- Inner classes: `.alert-mask
|
|
40
|
+
- Inner classes: `.alert-mask` > `.alert-mask-decorator` (the SVG) + `.alert-mask-content` >
|
|
41
41
|
`.alert-content-inner` > `.alert-content-icon`, `.alert-content-main` > (`.alert-content-body` >
|
|
42
42
|
`.alert-content-title`, `.alert-content-text`) + `.alert-content-actions`, then `.alert-content-dismiss`.
|
|
43
43
|
The inner content tokens (`--alert-content-gap`, `-padding`, `-text-colour`, `-icon-size`, `-title-font-size`,
|
package/app/components/02.molecules/alert-masked-content/stories/AlertMaskedContent.stories.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { ref } from "vue";
|
|
2
|
+
import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
|
|
3
|
+
import type { MediaCanvas } from "~/types/components";
|
|
1
4
|
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
2
5
|
import AlertMaskedContent from "../AlertMaskedContent.vue";
|
|
3
6
|
import AlertContent from "../../alert-content/AlertContent.vue";
|
|
@@ -211,3 +214,60 @@ export const TokenOverrides: Story = {
|
|
|
211
214
|
`,
|
|
212
215
|
}),
|
|
213
216
|
};
|
|
217
|
+
|
|
218
|
+
export const StressTest: Story = {
|
|
219
|
+
name: "Stress Test (Worst-Case Data)",
|
|
220
|
+
args: { theme: "warning", dismissible: true },
|
|
221
|
+
decorators: [
|
|
222
|
+
(story, context) => ({
|
|
223
|
+
components: { story, CanvasSwitcher },
|
|
224
|
+
setup() {
|
|
225
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "mobileCanvas");
|
|
226
|
+
return { canvasName };
|
|
227
|
+
},
|
|
228
|
+
template: `
|
|
229
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
230
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
231
|
+
</div>
|
|
232
|
+
<div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
|
|
233
|
+
<story />
|
|
234
|
+
</div>
|
|
235
|
+
`,
|
|
236
|
+
}),
|
|
237
|
+
],
|
|
238
|
+
render: (args) => ({
|
|
239
|
+
components: { AlertMaskedContent },
|
|
240
|
+
setup() {
|
|
241
|
+
return { args, wrapperStyle };
|
|
242
|
+
},
|
|
243
|
+
template: `
|
|
244
|
+
<div :data-theme="args.theme" :style="wrapperStyle" style="display: grid; gap: 1.6rem; max-width: none;">
|
|
245
|
+
<AlertMaskedContent v-bind="args">
|
|
246
|
+
<template #title>⚠️ Datenschutzgrundverordnungskonformitätsbestätigungsaufforderung <b>not bold</b></template>
|
|
247
|
+
<template #content>
|
|
248
|
+
Wir verwenden Ihre Adresse ausschließlich für Rückfragen zu Ihrer Bestellung bei Alex Morgan in Northtown.
|
|
249
|
+
https://example.com/a/very/long/unbroken/url/that/should/wrap/inside/the/mask يرجى المحاولة مرة أخرى
|
|
250
|
+
</template>
|
|
251
|
+
</AlertMaskedContent>
|
|
252
|
+
<div dir="rtl">
|
|
253
|
+
<AlertMaskedContent v-bind="args">
|
|
254
|
+
<template #title>تنبيه</template>
|
|
255
|
+
<template #content>نص من اليمين إلى اليسار؛ يبقى شريط التمييز على اليسار والنص بعيدًا عنه.</template>
|
|
256
|
+
</AlertMaskedContent>
|
|
257
|
+
</div>
|
|
258
|
+
<AlertMaskedContent v-bind="args" :mask-config="{ radiusLeft: 500, radiusRight: 500, borderLeft: -4 }">
|
|
259
|
+
<template #title>x</template>
|
|
260
|
+
</AlertMaskedContent>
|
|
261
|
+
</div>
|
|
262
|
+
`,
|
|
263
|
+
}),
|
|
264
|
+
parameters: {
|
|
265
|
+
docs: {
|
|
266
|
+
story: { inline: false, height: "900px" },
|
|
267
|
+
description: {
|
|
268
|
+
story:
|
|
269
|
+
"Long German title and body with an unbroken compound word and URL, HTML-like and right-to-left text, a right-to-left alert, and a hostile `mask-config` (oversized radii, negative border) round a one-character title. Check at every canvas width: text wraps inside the mask, the accent bar stays on the left with the text clear of it, and the hostile mask still draws a clean shape.",
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
};
|
|
@@ -38,16 +38,16 @@ describe("AlertMaskedContent", () => {
|
|
|
38
38
|
expect(wrapper.find(".alert-masked-content").attributes("data-theme")).toBe("success");
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
// ───
|
|
41
|
+
// ─── AlertMask ────────────────────────────────────────────────────────
|
|
42
42
|
|
|
43
|
-
it("renders
|
|
43
|
+
it("renders AlertMask", async () => {
|
|
44
44
|
const wrapper = await mountSuspended(AlertMaskedContent, { props: { theme: "info" } });
|
|
45
|
-
expect(wrapper.find(".alert-mask
|
|
45
|
+
expect(wrapper.find(".alert-mask").exists()).toBe(true);
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
it("renders AlertContentInner inside
|
|
48
|
+
it("renders AlertContentInner inside AlertMask", async () => {
|
|
49
49
|
const wrapper = await mountSuspended(AlertMaskedContent, { props: { theme: "info" } });
|
|
50
|
-
expect(wrapper.find(".alert-mask
|
|
50
|
+
expect(wrapper.find(".alert-mask .alert-content-inner").exists()).toBe(true);
|
|
51
51
|
});
|
|
52
52
|
|
|
53
53
|
// ─── Icon ─────────────────────────────────────────────────────────────────
|
|
@@ -131,15 +131,15 @@ describe("AlertMaskedContent", () => {
|
|
|
131
131
|
|
|
132
132
|
// ─── Default maskConfig ───────────────────────────────────────────────────
|
|
133
133
|
|
|
134
|
-
it("passes the resolved maskConfig to
|
|
134
|
+
it("passes the resolved maskConfig to AlertMask", async () => {
|
|
135
135
|
const wrapper = await mountSuspended(AlertMaskedContent, { props: { theme: "info" } });
|
|
136
136
|
const contentEl = wrapper.find(".alert-mask-content");
|
|
137
137
|
const style = (contentEl.element as HTMLElement).style;
|
|
138
|
-
// Default borderLeft: 6 → --_inset-
|
|
139
|
-
expect(style.getPropertyValue("--_inset-
|
|
140
|
-
expect(style.getPropertyValue("--_inset-
|
|
141
|
-
expect(style.getPropertyValue("--_inset-
|
|
142
|
-
expect(style.getPropertyValue("--_inset-
|
|
138
|
+
// Default borderLeft: 6 → --_inset-left: 6px
|
|
139
|
+
expect(style.getPropertyValue("--_inset-left")).toBe("6px");
|
|
140
|
+
expect(style.getPropertyValue("--_inset-top")).toBe("1px");
|
|
141
|
+
expect(style.getPropertyValue("--_inset-right")).toBe("1px");
|
|
142
|
+
expect(style.getPropertyValue("--_inset-bottom")).toBe("1px");
|
|
143
143
|
});
|
|
144
144
|
|
|
145
145
|
it("routes the default mask colours through public tokens", async () => {
|
|
@@ -168,7 +168,7 @@ describe("AlertMaskedContent", () => {
|
|
|
168
168
|
});
|
|
169
169
|
const contentEl = wrapper.find(".alert-mask-content");
|
|
170
170
|
const style = (contentEl.element as HTMLElement).style;
|
|
171
|
-
expect(style.getPropertyValue("--_inset-
|
|
171
|
+
expect(style.getPropertyValue("--_inset-left")).toBe("12px");
|
|
172
172
|
});
|
|
173
173
|
|
|
174
174
|
// ─── showIcon / actions (added 2026-09-27) ──────────────────────────────────
|
|
@@ -9,7 +9,7 @@ adds: position, motion and the two buttons.
|
|
|
9
9
|
> **Changed 2026-10-15**: the closed state class `.closed` is now `.cookie-consent-banner-closed`,
|
|
10
10
|
> and the buttons are `InputButton`s (`variant="inline"`, so the banner's tokens still style them):
|
|
11
11
|
> each button is `.cookie-consent-banner-accept.input-button` / `.cookie-consent-banner-reject.input-button`
|
|
12
|
-
> with the label in an inner `.button-text`. The button tokens below are unchanged.
|
|
12
|
+
> with the label in an inner `.input-button-text` (`.button-text` until 2026-10-17). The button tokens below are unchanged.
|
|
13
13
|
>
|
|
14
14
|
> **Changed 2026-09-27**: rebuilt on `AlertContent`. The banner's own panel tokens
|
|
15
15
|
> (`--cookie-consent-banner-padding`, `-gap`, `-border-radius`, `-border`, `-accent-border-width`,
|
|
@@ -47,7 +47,7 @@ adds: position, motion and the two buttons.
|
|
|
47
47
|
|
|
48
48
|
The gap between the buttons and their alignment (right by default) are `AlertContent`'s
|
|
49
49
|
`--alert-content-actions-gap` and `--alert-content-actions-justify`. Long labels wrap inside the
|
|
50
|
-
buttons (`InputButton`'s `.button-text`), and the buttons wrap onto their own lines when the row is full.
|
|
50
|
+
buttons (`InputButton`'s `.input-button-text`), and the buttons wrap onto their own lines when the row is full.
|
|
51
51
|
The label keeps the banner's font size and case: `InputButton`'s `--button-font-size` and
|
|
52
52
|
`--button-text-transform` don't apply here.
|
|
53
53
|
|
|
@@ -63,7 +63,7 @@ each resolving a public token once for reuse.
|
|
|
63
63
|
- Classes: `.cookie-consent-banner` > `.cookie-consent-banner-inner` (the `role="region"`) >
|
|
64
64
|
`.alert-content` (see its guide for the inner structure); the buttons are
|
|
65
65
|
`InputButton`s with `.cookie-consent-banner-reject` and `.cookie-consent-banner-accept` (plus
|
|
66
|
-
`.input-button
|
|
66
|
+
`.input-button[data-variant="inline"]`) inside `.alert-content-actions`.
|
|
67
67
|
- `data-test-id`: `cookie-consent-banner`, `cookie-consent-banner-reject`, `cookie-consent-banner-accept`,
|
|
68
68
|
plus `AlertContent`'s `alert-icon`, `alert-title`, `alert-content`, `alert-actions`.
|
|
69
69
|
|
|
@@ -118,7 +118,7 @@ describe("CookieConsentBanner", () => {
|
|
|
118
118
|
expect(button.tagName).toBe("BUTTON");
|
|
119
119
|
expect(button.getAttribute("type")).toBe("button");
|
|
120
120
|
expect(button.classList).toContain("input-button");
|
|
121
|
-
expect(button.
|
|
121
|
+
expect(button.dataset.variant).toBe("inline");
|
|
122
122
|
}
|
|
123
123
|
});
|
|
124
124
|
|