srcdev-nuxt-components 9.6.6 → 9.6.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/rules/consumer/ids-from-useid.md +14 -0
- package/.claude/skills/component-compliance-checklist.md +6 -0
- package/.claude/skills/component-naming.md +2 -2
- package/.claude/skills/components/input-checkbox-radio.md +19 -3
- package/.claude/skills/components/input-checkbox.md +14 -3
- package/.claude/skills/components/input-description.md +1 -1
- package/.claude/skills/components/input-error.md +1 -1
- package/.claude/skills/components/input-label.md +2 -2
- package/.claude/skills/components/input-number.md +44 -13
- package/.claude/skills/components/input-radio.md +15 -1
- package/.claude/skills/components/{input-range-core.md → input-range.md} +53 -18
- package/.claude/skills/components/input-textarea-core.md +4 -4
- package/.claude/skills/components/page-hero-highlights.md +22 -18
- package/.claude/skills/index.md +6 -6
- package/.claude/skills/testing-add-unit-test.md +17 -0
- package/.vscode/srcdev-component-input-checkbox-radio.code-snippets +0 -1
- package/.vscode/srcdev-component-input-range.code-snippets +38 -18
- package/.vscode/srcdev-component-page-hero-highlights.code-snippets +2 -1
- package/app/components/04.templates/page-hero-highlights/CONSUMER-STYLING.md +32 -12
- package/app/components/04.templates/page-hero-highlights/PageHeroHighlights.vue +56 -47
- package/app/components/04.templates/page-hero-highlights/stories/PageHeroHighlights.stories.ts +158 -307
- package/app/components/04.templates/page-hero-highlights/stories/PageHeroHighlightsHeader.stories.ts +10 -0
- package/app/components/04.templates/page-hero-highlights/tests/PageHeroHighlights.spec.ts +49 -27
- package/app/components/04.templates/page-hero-highlights/tests/PageHeroHighlightsHeader.spec.ts +42 -7
- package/app/components/04.templates/page-hero-highlights/tests/__snapshots__/PageHeroHighlights.spec.ts.snap +12 -10
- package/app/components/05.forms/input-checkbox/CONSUMER-STYLING.md +10 -0
- package/app/components/05.forms/input-checkbox/MultipleCheckboxes.vue +18 -4
- package/app/components/05.forms/input-checkbox/SingleCheckbox.vue +14 -2
- package/app/components/05.forms/input-checkbox/stories/MultipleCheckboxes.stories.ts +146 -37
- package/app/components/05.forms/input-checkbox/stories/SingleCheckbox.stories.ts +107 -30
- package/app/components/05.forms/input-checkbox/tests/MultipleCheckboxes.spec.ts +38 -4
- package/app/components/05.forms/input-checkbox/tests/SingleCheckbox.spec.ts +5 -0
- package/app/components/05.forms/input-checkbox-radio/CONSUMER-STYLING.md +10 -3
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadio.vue +1 -0
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioButton.vue +11 -10
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioField.vue +3 -0
- package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadio.stories.ts +44 -17
- package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioButton.stories.ts +164 -58
- package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioField.stories.ts +94 -21
- package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadio.spec.ts +14 -3
- package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadioButton.spec.ts +38 -3
- package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadioField.spec.ts +14 -3
- package/app/components/05.forms/input-description/CONSUMER-STYLING.md +2 -2
- package/app/components/05.forms/input-label/CONSUMER-STYLING.md +2 -2
- package/app/components/05.forms/input-number/CONSUMER-STYLING.md +42 -2
- package/app/components/05.forms/input-number/InputNumber.vue +37 -25
- package/app/components/05.forms/input-number/InputNumberField.vue +29 -16
- package/app/components/05.forms/input-number/stories/InputNumber.stories.ts +76 -25
- package/app/components/05.forms/input-number/stories/InputNumberField.stories.ts +116 -50
- package/app/components/05.forms/input-number/tests/InputNumber.spec.ts +49 -13
- package/app/components/05.forms/input-number/tests/InputNumberField.spec.ts +59 -5
- package/app/components/05.forms/input-radio/CONSUMER-STYLING.md +11 -0
- package/app/components/05.forms/input-radio/MultipleRadiobuttons.vue +14 -3
- package/app/components/05.forms/input-radio/stories/MultipleRadiobuttons.stories.ts +138 -38
- package/app/components/05.forms/input-radio/tests/MultipleRadioButtons.spec.ts +50 -3
- package/app/components/05.forms/input-range/CONSUMER-STYLING.md +68 -26
- package/app/components/05.forms/input-range/InputRange.vue +247 -0
- package/app/components/05.forms/input-range/{InputRangeDefault.vue → InputRangeField.vue} +41 -18
- package/app/components/05.forms/input-range/stories/InputRange.stories.ts +187 -0
- package/app/components/05.forms/input-range/stories/InputRangeField.stories.ts +196 -0
- package/app/components/05.forms/input-range/tests/{InputRangeCore.spec.ts → InputRange.spec.ts} +39 -15
- package/app/components/05.forms/input-range/tests/{InputRangeDefault.spec.ts → InputRangeField.spec.ts} +71 -7
- package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +5 -5
- package/package.json +1 -1
- package/app/components/05.forms/input-range/InputRangeCore.vue +0 -169
- package/app/components/05.forms/input-range/stories/InputRangeCore.stories.ts +0 -126
- package/app/components/05.forms/input-range/stories/InputRangeDefault.stories.ts +0 -108
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
1
|
+
import { computed, ref } from "vue";
|
|
2
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
+
import MultipleCheckboxes from "../MultipleCheckboxes.vue";
|
|
4
|
+
import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
|
|
4
5
|
import type { IFormMultipleOptions, InputUiVariant, OptionsLayout } from "~/types/forms/types.forms.d";
|
|
6
|
+
import type { MediaCanvas } from "~/types/components";
|
|
5
7
|
|
|
6
|
-
interface
|
|
8
|
+
interface StoryArgs {
|
|
7
9
|
name: string;
|
|
8
10
|
legend: string;
|
|
9
11
|
errorMessage: string;
|
|
@@ -15,11 +17,21 @@ interface MultipleCheckboxesStoryArgs {
|
|
|
15
17
|
direction: "row" | "row-reverse";
|
|
16
18
|
displayAsDisc: boolean;
|
|
17
19
|
inputVariant: InputUiVariant;
|
|
20
|
+
descriptionText: string;
|
|
21
|
+
options: IFormMultipleOptions["data"];
|
|
18
22
|
}
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
const serviceOptions: IFormMultipleOptions["data"] = [
|
|
25
|
+
{ id: "1", name: "services", value: "cut", label: "Cut" },
|
|
26
|
+
{ id: "2", name: "services", value: "colour", label: "Colour" },
|
|
27
|
+
{ id: "3", name: "services", value: "balayage", label: "Balayage" },
|
|
28
|
+
{ id: "4", name: "services", value: "styling", label: "Styling" },
|
|
29
|
+
{ id: "5", name: "services", value: "treatment", label: "Conditioning treatment" },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const meta: Meta<StoryArgs> = {
|
|
21
33
|
title: "Components/Forms/Input Checkbox/MultipleCheckboxes",
|
|
22
|
-
component:
|
|
34
|
+
component: MultipleCheckboxes,
|
|
23
35
|
argTypes: {
|
|
24
36
|
name: { control: "text", table: { category: "Basic" } },
|
|
25
37
|
legend: { control: "text", table: { category: "Basic" } },
|
|
@@ -45,6 +57,16 @@ export default {
|
|
|
45
57
|
options: ["normal", "outlined", "underlined"],
|
|
46
58
|
table: { category: "Styling" },
|
|
47
59
|
},
|
|
60
|
+
descriptionText: {
|
|
61
|
+
control: "text",
|
|
62
|
+
description: "Story-only: fills the descriptionText slot",
|
|
63
|
+
table: { category: "Slots" },
|
|
64
|
+
},
|
|
65
|
+
options: {
|
|
66
|
+
control: "object",
|
|
67
|
+
description: "Story-only: the `data` array of the `fieldData` model",
|
|
68
|
+
table: { category: "Model" },
|
|
69
|
+
},
|
|
48
70
|
},
|
|
49
71
|
args: {
|
|
50
72
|
name: "services",
|
|
@@ -58,47 +80,134 @@ export default {
|
|
|
58
80
|
direction: "row",
|
|
59
81
|
displayAsDisc: false,
|
|
60
82
|
inputVariant: "normal",
|
|
83
|
+
descriptionText: "Select all that apply.",
|
|
84
|
+
options: serviceOptions,
|
|
85
|
+
},
|
|
86
|
+
decorators: [
|
|
87
|
+
(story, context) => ({
|
|
88
|
+
components: { story, CanvasSwitcher },
|
|
89
|
+
setup() {
|
|
90
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
|
|
91
|
+
return { canvasName };
|
|
92
|
+
},
|
|
93
|
+
template: `
|
|
94
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
95
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
96
|
+
</div>
|
|
97
|
+
<div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
|
|
98
|
+
<story />
|
|
99
|
+
</div>
|
|
100
|
+
`,
|
|
101
|
+
}),
|
|
102
|
+
],
|
|
103
|
+
parameters: {
|
|
104
|
+
layout: "fullscreen",
|
|
105
|
+
docs: {
|
|
106
|
+
description: {
|
|
107
|
+
component:
|
|
108
|
+
"A fieldset of checkboxes built from the `fieldData` model, bound to an array `v-model`. `isButton` swaps the labelled checkboxes for button tiles (`isPill` rounds them), `optionsLayout` lays them out as equal-width columns, an inline wrap or a block column, and `errorMessage` with `fieldHasError` shows the error strip. Slots: `descriptionText`, `descriptionHtml`, `checkedIcon`, `itemIcon`.",
|
|
109
|
+
},
|
|
110
|
+
},
|
|
61
111
|
},
|
|
62
|
-
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export default meta;
|
|
115
|
+
type Story = StoryObj<StoryArgs>;
|
|
63
116
|
|
|
64
|
-
const
|
|
65
|
-
components: {
|
|
117
|
+
const render = (args: StoryArgs) => ({
|
|
118
|
+
components: { MultipleCheckboxes },
|
|
66
119
|
setup() {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
{ id: "1", name: "services", value: "cut", label: "Cut" },
|
|
71
|
-
{ id: "2", name: "services", value: "colour", label: "Colour" },
|
|
72
|
-
{ id: "3", name: "services", value: "balayage", label: "Balayage" },
|
|
73
|
-
{ id: "4", name: "services", value: "styling", label: "Styling" },
|
|
74
|
-
{ id: "5", name: "services", value: "treatment", label: "Conditioning treatment" },
|
|
75
|
-
],
|
|
76
|
-
total: 5,
|
|
77
|
-
skip: 0,
|
|
78
|
-
limit: 10,
|
|
120
|
+
const componentArgs = computed(() => {
|
|
121
|
+
const { descriptionText: _descriptionText, options: _options, ...rest } = args;
|
|
122
|
+
return rest;
|
|
79
123
|
});
|
|
80
|
-
|
|
124
|
+
const fieldData = computed<IFormMultipleOptions>(() => ({
|
|
125
|
+
data: args.options,
|
|
126
|
+
total: args.options.length,
|
|
127
|
+
skip: 0,
|
|
128
|
+
limit: args.options.length,
|
|
129
|
+
}));
|
|
130
|
+
const modelValue = ref<string[]>([]);
|
|
131
|
+
return { args, componentArgs, fieldData, modelValue };
|
|
81
132
|
},
|
|
82
133
|
template: `
|
|
83
|
-
<
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
<p style="margin-top: 2rem; font-family: monospace;">modelValue: {{ JSON.stringify(modelValue) }}</p>
|
|
88
|
-
</div>
|
|
134
|
+
<MultipleCheckboxes v-bind="componentArgs" v-model="modelValue" :field-data="fieldData">
|
|
135
|
+
<template v-if="args.descriptionText" #descriptionText>{{ args.descriptionText }}</template>
|
|
136
|
+
</MultipleCheckboxes>
|
|
137
|
+
<p style="margin-block-start: 2rem; font-family: monospace; overflow-wrap: anywhere;">modelValue: {{ JSON.stringify(modelValue) }}</p>
|
|
89
138
|
`,
|
|
90
139
|
});
|
|
91
140
|
|
|
92
|
-
export const Default =
|
|
141
|
+
export const Default: Story = { render };
|
|
93
142
|
|
|
94
|
-
export const Buttons =
|
|
95
|
-
|
|
143
|
+
export const Buttons: Story = {
|
|
144
|
+
args: { isButton: true, optionsLayout: "inline" },
|
|
145
|
+
render,
|
|
146
|
+
parameters: { docs: { description: { story: "Button tiles in an inline wrap, each with the `itemIcon` decorator." } } },
|
|
147
|
+
};
|
|
96
148
|
|
|
97
|
-
export const Pills =
|
|
98
|
-
|
|
149
|
+
export const Pills: Story = {
|
|
150
|
+
args: { isButton: true, isPill: true, optionsLayout: "inline" },
|
|
151
|
+
render,
|
|
152
|
+
parameters: { docs: { description: { story: "Button tiles with `isPill` for fully rounded ends." } } },
|
|
153
|
+
};
|
|
99
154
|
|
|
100
|
-
export const Block =
|
|
101
|
-
|
|
155
|
+
export const Block: Story = {
|
|
156
|
+
args: { optionsLayout: "block" },
|
|
157
|
+
render,
|
|
158
|
+
parameters: { docs: { description: { story: "`optionsLayout=\"block\"` stacks the options in one column." } } },
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export const ErrorState: Story = {
|
|
162
|
+
args: { fieldHasError: true },
|
|
163
|
+
render,
|
|
164
|
+
parameters: {
|
|
165
|
+
docs: { description: { story: "`fieldHasError` shows the error message and links it to every checkbox with `aria-describedby`." } },
|
|
166
|
+
},
|
|
167
|
+
};
|
|
102
168
|
|
|
103
|
-
export const
|
|
104
|
-
|
|
169
|
+
export const StressTest: Story = {
|
|
170
|
+
name: "Stress Test (Worst-Case Data)",
|
|
171
|
+
args: {
|
|
172
|
+
name: "dienstleistungen auswahl",
|
|
173
|
+
legend:
|
|
174
|
+
"🎨 Gewünschte Dienstleistungsauswahlmöglichkeitenbeschreibungsüberschrift https://example.com/a/very/long/unbroken/url/that/should/wrap/somewhere <script>alert(1)</script>",
|
|
175
|
+
descriptionText:
|
|
176
|
+
"اختر الخدمات Wählen Sie alle zutreffenden Dienstleistungen aus, die Sie bei Ihrem nächsten Besuch in Anspruch nehmen möchten. <b>not bold</b>",
|
|
177
|
+
errorMessage: "BittewählenSiemindestenseineDienstleistungausbevorSiedasFormularabsenden",
|
|
178
|
+
fieldHasError: true,
|
|
179
|
+
required: true,
|
|
180
|
+
options: [
|
|
181
|
+
{ id: "1", name: "s", value: "a", label: "A" },
|
|
182
|
+
{ id: "2", name: "s", value: "", label: "" },
|
|
183
|
+
{ id: "3", name: "s", value: "long", label: "Haarverlängerungsberatungstermindienstleistungspaket" },
|
|
184
|
+
{ id: "4", name: "s", value: "url", label: "https://example.com/a/very/long/unbroken/url/that/should/wrap/somewhere" },
|
|
185
|
+
{ id: "5", name: "s", value: "has spaces <b>", label: "<b>Not bold</b> & <i>not italic</i>" },
|
|
186
|
+
{ id: "6", name: "s", value: "emoji", label: "💇♀️ Colour + cut 🌈" },
|
|
187
|
+
{ id: "7", name: "s", value: "rtl", label: "قص الشعر وتصفيفه" },
|
|
188
|
+
{
|
|
189
|
+
id: "8",
|
|
190
|
+
name: "s",
|
|
191
|
+
value: "sentence",
|
|
192
|
+
label: "A conditioning treatment with a scalp massage, a hot towel and a blow-dry finish, booked as one appointment",
|
|
193
|
+
},
|
|
194
|
+
...Array.from({ length: 22 }, (_, index) => ({
|
|
195
|
+
id: `extra-${index}`,
|
|
196
|
+
name: "s",
|
|
197
|
+
value: `extra-${index}`,
|
|
198
|
+
label: `Option ${index + 9}`,
|
|
199
|
+
})),
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
render,
|
|
203
|
+
parameters: {
|
|
204
|
+
initialCanvas: "mobileCanvas",
|
|
205
|
+
docs: {
|
|
206
|
+
story: { inline: false, height: "900px" },
|
|
207
|
+
description: {
|
|
208
|
+
story:
|
|
209
|
+
"30 options including an empty label, a single character, long unbroken German and URL labels, emoji, right-to-left text, HTML-like text and a value with spaces. Check at every canvas width: the legend, description, labels and error wrap inside the dashed outline, markup renders as text, the equal-width columns never get wider than the container, and clicking any label (including the odd ones) toggles its own checkbox. Switch `optionsLayout` to `inline` and `block`, and turn on `isButton`..",
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
1
|
+
import { computed, ref } from "vue";
|
|
2
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
+
import SingleCheckbox from "../SingleCheckbox.vue";
|
|
4
|
+
import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
|
|
4
5
|
import type { InputUiVariant } from "~/types/forms/types.forms.d";
|
|
6
|
+
import type { MediaCanvas } from "~/types/components";
|
|
5
7
|
|
|
6
|
-
interface
|
|
8
|
+
interface StoryArgs {
|
|
7
9
|
name: string;
|
|
8
10
|
legend: string;
|
|
9
11
|
label: string;
|
|
@@ -11,11 +13,12 @@ interface SingleCheckboxStoryArgs {
|
|
|
11
13
|
required: boolean;
|
|
12
14
|
fieldHasError: boolean;
|
|
13
15
|
inputVariant: InputUiVariant;
|
|
16
|
+
descriptionText: string;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
const meta: Meta<StoryArgs> = {
|
|
17
20
|
title: "Components/Forms/Input Checkbox/SingleCheckbox",
|
|
18
|
-
component:
|
|
21
|
+
component: SingleCheckbox,
|
|
19
22
|
argTypes: {
|
|
20
23
|
name: { control: "text", table: { category: "Basic" } },
|
|
21
24
|
legend: { control: "text", table: { category: "Basic" } },
|
|
@@ -28,6 +31,11 @@ export default {
|
|
|
28
31
|
options: ["normal", "outlined", "underlined"],
|
|
29
32
|
table: { category: "Styling" },
|
|
30
33
|
},
|
|
34
|
+
descriptionText: {
|
|
35
|
+
control: "text",
|
|
36
|
+
description: "Story-only: fills the descriptionText slot",
|
|
37
|
+
table: { category: "Slots" },
|
|
38
|
+
},
|
|
31
39
|
},
|
|
32
40
|
args: {
|
|
33
41
|
name: "terms",
|
|
@@ -37,39 +45,108 @@ export default {
|
|
|
37
45
|
required: true,
|
|
38
46
|
fieldHasError: false,
|
|
39
47
|
inputVariant: "normal",
|
|
48
|
+
descriptionText: "",
|
|
49
|
+
},
|
|
50
|
+
decorators: [
|
|
51
|
+
(story, context) => ({
|
|
52
|
+
components: { story, CanvasSwitcher },
|
|
53
|
+
setup() {
|
|
54
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
|
|
55
|
+
return { canvasName };
|
|
56
|
+
},
|
|
57
|
+
template: `
|
|
58
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
59
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
60
|
+
</div>
|
|
61
|
+
<div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
|
|
62
|
+
<story />
|
|
63
|
+
</div>
|
|
64
|
+
`,
|
|
65
|
+
}),
|
|
66
|
+
],
|
|
67
|
+
parameters: {
|
|
68
|
+
layout: "fullscreen",
|
|
69
|
+
docs: {
|
|
70
|
+
description: {
|
|
71
|
+
component:
|
|
72
|
+
"A fieldset holding one checkbox, for consent or opt-in fields. `v-model` toggles between `trueValue` and `falseValue` (default `true`/`false`); `required` puts native `required` on the checkbox. Slots: `labelContent` (rich label, e.g. a link), `descriptionText`, `descriptionHtml`, `checkedIcon`.",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
40
75
|
},
|
|
41
|
-
}
|
|
76
|
+
};
|
|
42
77
|
|
|
43
|
-
|
|
44
|
-
|
|
78
|
+
export default meta;
|
|
79
|
+
type Story = StoryObj<StoryArgs>;
|
|
80
|
+
|
|
81
|
+
const render = (args: StoryArgs) => ({
|
|
82
|
+
components: { SingleCheckbox },
|
|
45
83
|
setup() {
|
|
84
|
+
const componentArgs = computed(() => {
|
|
85
|
+
const { descriptionText: _descriptionText, ...rest } = args;
|
|
86
|
+
return rest;
|
|
87
|
+
});
|
|
46
88
|
const modelValue = ref(false);
|
|
47
|
-
return { args, modelValue };
|
|
89
|
+
return { args, componentArgs, modelValue };
|
|
48
90
|
},
|
|
49
91
|
template: `
|
|
50
|
-
<
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
</
|
|
92
|
+
<SingleCheckbox v-bind="componentArgs" v-model="modelValue">
|
|
93
|
+
<template v-if="args.descriptionText" #descriptionText>{{ args.descriptionText }}</template>
|
|
94
|
+
</SingleCheckbox>
|
|
95
|
+
<p style="margin-block-start: 2rem; font-family: monospace;">modelValue: {{ modelValue }}</p>
|
|
54
96
|
`,
|
|
55
97
|
});
|
|
56
98
|
|
|
57
|
-
export const Default =
|
|
58
|
-
|
|
59
|
-
export const ErrorState = Template.bind({});
|
|
60
|
-
ErrorState.args = { fieldHasError: true };
|
|
99
|
+
export const Default: Story = { render };
|
|
61
100
|
|
|
62
|
-
export const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
101
|
+
export const ErrorState: Story = {
|
|
102
|
+
args: { fieldHasError: true },
|
|
103
|
+
render,
|
|
104
|
+
parameters: {
|
|
105
|
+
docs: { description: { story: "`fieldHasError` shows the error message and links it to the checkbox with `aria-describedby`." } },
|
|
67
106
|
},
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const RichLabel: Story = {
|
|
110
|
+
render: (args: StoryArgs) => ({
|
|
111
|
+
components: { SingleCheckbox },
|
|
112
|
+
setup() {
|
|
113
|
+
const componentArgs = computed(() => {
|
|
114
|
+
const { descriptionText: _descriptionText, ...rest } = args;
|
|
115
|
+
return rest;
|
|
116
|
+
});
|
|
117
|
+
const modelValue = ref(false);
|
|
118
|
+
return { componentArgs, modelValue };
|
|
119
|
+
},
|
|
120
|
+
template: `
|
|
121
|
+
<SingleCheckbox v-bind="componentArgs" v-model="modelValue">
|
|
71
122
|
<template #labelContent>I agree to the <a href="#">terms and conditions</a></template>
|
|
72
|
-
</
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
123
|
+
</SingleCheckbox>
|
|
124
|
+
`,
|
|
125
|
+
}),
|
|
126
|
+
parameters: { docs: { description: { story: "The `labelContent` slot replaces the `label` prop, here with a link." } } },
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const StressTest: Story = {
|
|
130
|
+
name: "Stress Test (Worst-Case Data)",
|
|
131
|
+
args: {
|
|
132
|
+
name: "marketing einwilligung",
|
|
133
|
+
legend:
|
|
134
|
+
"📨 Einwilligungserklärungzurdatenschutzkonformenverarbeitungpersonenbezogenerdaten https://example.com/a/very/long/unbroken/url <script>alert(1)</script>",
|
|
135
|
+
label:
|
|
136
|
+
"Ich stimme zu: Datenschutzgrundverordnungseinwilligungsbestätigung https://example.com/privacy/policy/with/a/long/path <b>not bold</b> قص",
|
|
137
|
+
descriptionText:
|
|
138
|
+
"سياسة الخصوصية Sie können Ihre Einwilligung jederzeit widerrufen, indem Sie uns eine kurze Nachricht schicken. 🙂",
|
|
139
|
+
errorMessage: "BitteakzeptierenSiedieDatenschutzbestimmungenbevorSiefortfahren",
|
|
140
|
+
fieldHasError: true,
|
|
141
|
+
},
|
|
142
|
+
render,
|
|
143
|
+
parameters: {
|
|
144
|
+
initialCanvas: "mobileCanvas",
|
|
145
|
+
docs: {
|
|
146
|
+
description: {
|
|
147
|
+
story:
|
|
148
|
+
"Long unbroken German and URL text in the legend, label and error, emoji, right-to-left text and HTML-like text. Check at every canvas width: everything wraps inside the dashed outline, markup renders as text, and clicking anywhere on the label toggles the checkbox. Also try an empty label and a single character.",
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// https://nuxt.com/docs/getting-started/testing#unit-testing
|
|
2
|
+
import { defineComponent } from "vue";
|
|
2
3
|
import type { VueWrapper } from "@vue/test-utils";
|
|
3
4
|
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
4
5
|
import type { FormUiTheme, OptionsLayout } from "~/types/forms/types.forms";
|
|
@@ -25,6 +26,11 @@ const initialSlots = {
|
|
|
25
26
|
};
|
|
26
27
|
|
|
27
28
|
let wrapper: VueWrapper<InstanceType<typeof ComponentUnderTest>>;
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
wrapper?.unmount();
|
|
32
|
+
vi.restoreAllMocks();
|
|
33
|
+
});
|
|
28
34
|
const wrapperFactory = (propsData = {}, slotsData = {}) => {
|
|
29
35
|
const mockPropsData = { ...initialPropsData, ...propsData };
|
|
30
36
|
const mockSlotsData = { ...initialSlots, ...slotsData };
|
|
@@ -84,10 +90,38 @@ describe("MultipleCheckboxes Component", () => {
|
|
|
84
90
|
});
|
|
85
91
|
});
|
|
86
92
|
|
|
87
|
-
it("gives each checkbox
|
|
88
|
-
wrapper = await wrapperFactory();
|
|
89
|
-
const
|
|
90
|
-
expect(
|
|
93
|
+
it("gives each button checkbox a unique, space-free id linked to its label", async () => {
|
|
94
|
+
wrapper = await wrapperFactory({ isButton: true });
|
|
95
|
+
const ids = wrapper.findAll('input[type="checkbox"]').map((checkbox) => checkbox.attributes("id")!);
|
|
96
|
+
expect(new Set(ids).size).toBe(tagsData.data.length);
|
|
97
|
+
ids.forEach((id) => expect(id).not.toMatch(/\s/));
|
|
98
|
+
ids.forEach((id) => expect(wrapper.find(`label[for="${id}"]`).exists()).toBe(true));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("keeps option ids unique across two groups sharing a name and values", async () => {
|
|
102
|
+
const Host = defineComponent({
|
|
103
|
+
components: { ComponentUnderTest },
|
|
104
|
+
setup: () => ({ props: initialPropsData }),
|
|
105
|
+
template: `
|
|
106
|
+
<div>
|
|
107
|
+
<ComponentUnderTest v-bind="props" :model-value="[]" />
|
|
108
|
+
<ComponentUnderTest v-bind="props" :model-value="[]" />
|
|
109
|
+
</div>
|
|
110
|
+
`,
|
|
111
|
+
});
|
|
112
|
+
const host = await mountSuspended(Host);
|
|
113
|
+
const ids = host.findAll('input[type="checkbox"]').map((checkbox) => checkbox.attributes("id"));
|
|
114
|
+
expect(ids.length).toBe(tagsData.data.length * 2);
|
|
115
|
+
expect(new Set(ids).size).toBe(ids.length);
|
|
116
|
+
host.unmount();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("builds ids that don't depend on option values containing spaces", async () => {
|
|
120
|
+
wrapper = await wrapperFactory({
|
|
121
|
+
isButton: true,
|
|
122
|
+
fieldData: { ...tagsData, data: [{ id: "x", name: "tags", value: "has spaces <b>", label: "Spaces" }] },
|
|
123
|
+
});
|
|
124
|
+
expect(wrapper.find('input[type="checkbox"]').attributes("id")).not.toMatch(/\s/);
|
|
91
125
|
});
|
|
92
126
|
|
|
93
127
|
it("adds each clicked value to the model", async () => {
|
|
@@ -12,6 +12,11 @@ const initialPropsData = {
|
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
let wrapper: VueWrapper<InstanceType<typeof ComponentUnderTest>>;
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
wrapper?.unmount();
|
|
18
|
+
vi.restoreAllMocks();
|
|
19
|
+
});
|
|
15
20
|
const wrapperFactory = (propsData = {}, slotsData = {}) =>
|
|
16
21
|
mountSuspended(ComponentUnderTest, {
|
|
17
22
|
attachTo: document.body,
|
|
@@ -67,8 +67,7 @@ Also reads `--input-checkbox-decorator-icon-size` (decorator icon `font-size`, `
|
|
|
67
67
|
`--input-font-size` (label).
|
|
68
68
|
|
|
69
69
|
Private (not public API): `--_transition-duration` (the control's resolved duration, shared by two
|
|
70
|
-
transitions)
|
|
71
|
-
`--_white-space` (the button label's wrapping, swapped by `data-options-layout="inline"`).
|
|
70
|
+
transitions) and `--_icon-size` (the button shrinks the inner control's checked icon through it).
|
|
72
71
|
|
|
73
72
|
> **Changed 2026-09-27**:
|
|
74
73
|
>
|
|
@@ -80,6 +79,14 @@ transitions), `--_icon-size` (the button shrinks the inner control's checked ico
|
|
|
80
79
|
> so both tokens take effect for the first time: the `+` renders at `2rem` and the checked icon
|
|
81
80
|
> inside a button at `--input-checked-icon-size` minus `0.8rem`.
|
|
82
81
|
> - `--input-checkbox-label-color` is new (was unstyled, so `inherit`).
|
|
82
|
+
>
|
|
83
|
+
> **Changed 2026-10-16**: long labels wrap instead of overflowing. Both labels
|
|
84
|
+
> (`.input-checkbox-radio-field-label`, `.input-checkbox-radio-button-label`) and their roots set
|
|
85
|
+
> `min-inline-size: 0`, the labels set `overflow-wrap: anywhere`, and the control and decorator
|
|
86
|
+
> icon no longer shrink (`flex-shrink: 0`). The button label used to be `nowrap` under
|
|
87
|
+
> `data-options-layout="inline"` (private `--_white-space`, now removed), so an unbroken label
|
|
88
|
+
> overflowed the row. In a wrapping row a short label still sits on one line; if you relied on
|
|
89
|
+
> `nowrap`, set `white-space: nowrap` on `.input-checkbox-radio-button-label` yourself.
|
|
83
90
|
|
|
84
91
|
---
|
|
85
92
|
|
|
@@ -95,7 +102,7 @@ transitions), `--_icon-size` (the button shrinks the inner control's checked ico
|
|
|
95
102
|
| `data-invalid` | all three roots | The field has an error |
|
|
96
103
|
| `data-pill` | `.input-checkbox-radio-button` | `is-pill` |
|
|
97
104
|
| `data-direction="row" \| "row-reverse"` | `.input-checkbox-radio-button` | Always, mirrors `direction` |
|
|
98
|
-
| `data-options-layout` | `.input-checkbox-radio-button` | Always, mirrors `options-layout` (
|
|
105
|
+
| `data-options-layout` | `.input-checkbox-radio-button` | Always, mirrors `options-layout` (a hook only: it no longer changes wrapping) |
|
|
99
106
|
|
|
100
107
|
Inner elements: `.input-checkbox-radio-icon-slot` (holds the `checkedIcon` slot or the default
|
|
101
108
|
`.input-checkbox-radio-icon`), `.input-checkbox-radio-input` (the hidden native input),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<label
|
|
3
|
-
:for="
|
|
3
|
+
:for="inputId"
|
|
4
4
|
class="input-checkbox-radio-button"
|
|
5
5
|
:class="elementClasses"
|
|
6
6
|
:data-theme="theme"
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
:data-pill="isPill ? '' : undefined"
|
|
11
11
|
>
|
|
12
12
|
<InputCheckboxRadio
|
|
13
|
-
:id
|
|
13
|
+
:id="inputId"
|
|
14
14
|
v-model="modelValue"
|
|
15
15
|
:is-button="true"
|
|
16
16
|
:type
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
import type { FormUiTheme, InputUiVariant, OptionsLayout } from "~/types/forms/types.forms";
|
|
45
45
|
|
|
46
46
|
interface Props {
|
|
47
|
-
id
|
|
47
|
+
id?: string;
|
|
48
48
|
type: "checkbox" | "radio";
|
|
49
49
|
name: string;
|
|
50
50
|
label?: string;
|
|
@@ -65,6 +65,7 @@ interface Props {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
const props = withDefaults(defineProps<Props>(), {
|
|
68
|
+
id: undefined,
|
|
68
69
|
label: "",
|
|
69
70
|
required: false,
|
|
70
71
|
theme: "default",
|
|
@@ -84,6 +85,9 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
84
85
|
|
|
85
86
|
const modelValue = defineModel<(string | number | boolean)[] | string | number | boolean | undefined>({ required: true });
|
|
86
87
|
|
|
88
|
+
const generatedId = useId();
|
|
89
|
+
const inputId = computed(() => props.id || generatedId);
|
|
90
|
+
|
|
87
91
|
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
88
92
|
|
|
89
93
|
watch(
|
|
@@ -97,9 +101,8 @@ watch(
|
|
|
97
101
|
<style lang="css">
|
|
98
102
|
@layer components {
|
|
99
103
|
.input-checkbox-radio-button {
|
|
100
|
-
--_white-space: wrap;
|
|
101
|
-
|
|
102
104
|
display: flex;
|
|
105
|
+
min-inline-size: 0;
|
|
103
106
|
align-items: center;
|
|
104
107
|
justify-content: space-between;
|
|
105
108
|
gap: var(--input-checkbox-button-gap, 1rem);
|
|
@@ -122,10 +125,6 @@ watch(
|
|
|
122
125
|
border-radius: var(--input-checkbox-button-border-radius-pill, 100vw);
|
|
123
126
|
}
|
|
124
127
|
|
|
125
|
-
&[data-options-layout="inline"] {
|
|
126
|
-
--_white-space: nowrap;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
128
|
&:hover {
|
|
130
129
|
background-color: var(--input-checkbox-button-surface-hover, var(--theme-input-surface-hover));
|
|
131
130
|
outline-color: var(--input-checkbox-button-ring-hover, var(--theme-ring));
|
|
@@ -155,12 +154,14 @@ watch(
|
|
|
155
154
|
text-align: center;
|
|
156
155
|
padding-block: var(--input-checkbox-button-label-padding-block, 0.8rem);
|
|
157
156
|
padding-inline: var(--input-checkbox-button-label-padding-inline, 0.8rem);
|
|
158
|
-
|
|
157
|
+
min-inline-size: 0;
|
|
158
|
+
overflow-wrap: anywhere;
|
|
159
159
|
cursor: pointer;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
.input-checkbox-radio-button-icon {
|
|
163
163
|
display: flex;
|
|
164
|
+
flex-shrink: 0;
|
|
164
165
|
align-items: center;
|
|
165
166
|
justify-content: center;
|
|
166
167
|
color: var(--input-checkbox-button-icon-color, var(--theme-text));
|
|
@@ -85,11 +85,14 @@ watch(
|
|
|
85
85
|
gap: var(--input-checked-icon-gap);
|
|
86
86
|
min-height: var(--input-min-height);
|
|
87
87
|
user-select: none;
|
|
88
|
+
min-inline-size: 0;
|
|
88
89
|
|
|
89
90
|
.input-checkbox-radio-field-label {
|
|
90
91
|
display: flex;
|
|
91
92
|
flex: 1;
|
|
92
93
|
align-items: center;
|
|
94
|
+
min-inline-size: 0;
|
|
95
|
+
overflow-wrap: anywhere;
|
|
93
96
|
color: var(--input-checkbox-label-color, inherit);
|
|
94
97
|
padding-block: var(--input-checkbox-label-padding-block);
|
|
95
98
|
padding-inline: var(--input-checkbox-label-padding-inline);
|