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,9 @@
|
|
|
1
|
-
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
1
|
import { ref } from "vue";
|
|
3
|
-
import
|
|
2
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
+
import InputCheckboxRadio from "../InputCheckboxRadio.vue";
|
|
4
4
|
import type { FormUiTheme, InputUiVariant } from "~/types/forms/types.forms.d";
|
|
5
5
|
|
|
6
|
-
interface
|
|
6
|
+
interface StoryArgs {
|
|
7
7
|
type: "checkbox" | "radio";
|
|
8
8
|
id: string;
|
|
9
9
|
name: string;
|
|
@@ -15,9 +15,20 @@ interface InputCheckboxRadioStoryArgs {
|
|
|
15
15
|
inputVariant: InputUiVariant;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
// Links in the canvas open in the Storybook manager (target="_top"), not inside the preview iframe.
|
|
19
|
+
const storyNoteStyle = "margin: 0 0 1.6rem; font-size: 1.4rem;";
|
|
20
|
+
const parentNote = `
|
|
21
|
+
<p style="${storyNoteStyle}">
|
|
22
|
+
This is the bare control with no label. With a text label it's the
|
|
23
|
+
<a href="/?path=/story/components-forms-input-checkbox-radio-inputcheckboxradiofield--checkbox" target="_top">InputCheckboxRadioField</a>;
|
|
24
|
+
drawn as a button or pill it's the
|
|
25
|
+
<a href="/?path=/story/components-forms-input-checkbox-radio-inputcheckboxradiobutton--option-group" target="_top">InputCheckboxRadioButton</a>.
|
|
26
|
+
</p>
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
const meta: Meta<StoryArgs> = {
|
|
19
30
|
title: "Components/Forms/Input Checkbox Radio/InputCheckboxRadio",
|
|
20
|
-
component:
|
|
31
|
+
component: InputCheckboxRadio,
|
|
21
32
|
argTypes: {
|
|
22
33
|
type: {
|
|
23
34
|
control: { type: "select" },
|
|
@@ -62,29 +73,45 @@ export default {
|
|
|
62
73
|
isButton: false,
|
|
63
74
|
inputVariant: "normal",
|
|
64
75
|
},
|
|
65
|
-
|
|
76
|
+
parameters: {
|
|
77
|
+
docs: {
|
|
78
|
+
description: {
|
|
79
|
+
component:
|
|
80
|
+
"The checkbox or radio control every form checkbox and radio is built from: a styled square or circle stacked over a hidden native input. It has no label, so pair its `id` with your own `label[for]`, or use `InputCheckboxRadioField` / `InputCheckboxRadioButton`. Slot: `checkedIcon`. No stress story here: the control renders no text, and long-label behaviour is covered by the Field and Button stress stories.",
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export default meta;
|
|
87
|
+
type Story = StoryObj<StoryArgs>;
|
|
66
88
|
|
|
67
|
-
const
|
|
68
|
-
components: {
|
|
89
|
+
const render = (args: StoryArgs) => ({
|
|
90
|
+
components: { InputCheckboxRadio },
|
|
69
91
|
setup() {
|
|
70
92
|
const modelValue = ref(false);
|
|
71
93
|
return { args, modelValue };
|
|
72
94
|
},
|
|
73
95
|
template: `
|
|
74
96
|
<div style="margin: 36px;">
|
|
75
|
-
|
|
97
|
+
${parentNote}
|
|
98
|
+
<InputCheckboxRadio v-bind="args" v-model="modelValue" />
|
|
76
99
|
</div>
|
|
77
100
|
`,
|
|
78
101
|
});
|
|
79
102
|
|
|
80
|
-
export const Checkbox =
|
|
81
|
-
Checkbox.args = { type: "checkbox" };
|
|
103
|
+
export const Checkbox: Story = { args: { type: "checkbox" }, render };
|
|
82
104
|
|
|
83
|
-
export const Radio =
|
|
84
|
-
Radio.args = { type: "radio" };
|
|
105
|
+
export const Radio: Story = { args: { type: "radio" }, render };
|
|
85
106
|
|
|
86
|
-
export const CheckboxAsDisc =
|
|
87
|
-
|
|
107
|
+
export const CheckboxAsDisc: Story = {
|
|
108
|
+
args: { type: "checkbox", isButton: true, displayAsDisc: true },
|
|
109
|
+
render,
|
|
110
|
+
parameters: { docs: { description: { story: "`displayAsDisc` rounds a checkbox, but only in button mode (`isButton`)." } } },
|
|
111
|
+
};
|
|
88
112
|
|
|
89
|
-
export const ErrorState =
|
|
90
|
-
|
|
113
|
+
export const ErrorState: Story = {
|
|
114
|
+
args: { type: "checkbox", fieldHasError: true, theme: "error" },
|
|
115
|
+
render,
|
|
116
|
+
parameters: { docs: { description: { story: "`fieldHasError` sets `aria-invalid` on the input and `data-invalid` on the root." } } },
|
|
117
|
+
};
|
package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioButton.stories.ts
CHANGED
|
@@ -1,26 +1,53 @@
|
|
|
1
|
-
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
1
|
import { defineComponent, ref } from "vue";
|
|
3
|
-
import
|
|
2
|
+
import type { PropType } from "vue";
|
|
3
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
4
|
+
import InputCheckboxRadioButton from "../InputCheckboxRadioButton.vue";
|
|
5
|
+
import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
|
|
6
|
+
import type { OptionsLayout } from "~/types/forms/types.forms.d";
|
|
7
|
+
import type { MediaCanvas } from "~/types/components";
|
|
4
8
|
|
|
5
|
-
interface
|
|
9
|
+
interface StoryArgs {
|
|
6
10
|
type: "checkbox" | "radio";
|
|
7
11
|
label: string;
|
|
8
12
|
isPill: boolean;
|
|
9
13
|
fieldHasError: boolean;
|
|
10
14
|
displayAsDisc: boolean;
|
|
11
15
|
direction: "row" | "row-reverse";
|
|
16
|
+
optionsLayout: OptionsLayout;
|
|
17
|
+
options: string[];
|
|
12
18
|
}
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
// Links in the canvas open in the Storybook manager (target="_top"), not inside the preview iframe.
|
|
21
|
+
const storyNoteStyle = "margin: 0 0 1.6rem; font-size: 1.4rem;";
|
|
22
|
+
const parentNote = `
|
|
23
|
+
<p style="${storyNoteStyle}">
|
|
24
|
+
The group components draw each option with this when <code>isButton</code> is on, and add the
|
|
25
|
+
fieldset, legend, description and error: see the
|
|
26
|
+
<a href="/?path=/story/components-forms-input-checkbox-multiplecheckboxes--buttons" target="_top">MultipleCheckboxes</a>
|
|
27
|
+
and
|
|
28
|
+
<a href="/?path=/story/components-forms-input-radio-multipleradiobuttons--buttons" target="_top">MultipleRadiobuttons</a>
|
|
29
|
+
Buttons stories.
|
|
30
|
+
</p>
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
const serviceOptions = ["Balayage", "Highlights", "Half Head Highlights", "Full Colour", "Lowlights", "Toner & Gloss"];
|
|
34
|
+
|
|
35
|
+
const layoutStyles: Record<OptionsLayout, string> = {
|
|
36
|
+
"equal-widths": "display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr)); gap: 0.8rem;",
|
|
37
|
+
inline: "display: flex; flex-wrap: wrap; gap: 0.8rem;",
|
|
38
|
+
block: "display: flex; flex-direction: column; gap: 0.8rem;",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const meta: Meta<StoryArgs> = {
|
|
15
42
|
title: "Components/Forms/Input Checkbox Radio/InputCheckboxRadioButton",
|
|
16
|
-
component:
|
|
43
|
+
component: InputCheckboxRadioButton,
|
|
17
44
|
argTypes: {
|
|
18
45
|
type: {
|
|
19
46
|
control: { type: "select" },
|
|
20
47
|
options: ["checkbox", "radio"],
|
|
21
48
|
table: { category: "Basic" },
|
|
22
49
|
},
|
|
23
|
-
label: { control: "text", table: { category: "Basic" } },
|
|
50
|
+
label: { control: "text", description: "Single-option stories only", table: { category: "Basic" } },
|
|
24
51
|
isPill: { control: "boolean", table: { category: "Styling" } },
|
|
25
52
|
fieldHasError: { control: "boolean", table: { category: "States" } },
|
|
26
53
|
displayAsDisc: { control: "boolean", table: { category: "Styling" } },
|
|
@@ -30,6 +57,18 @@ export default {
|
|
|
30
57
|
description: "row-reverse puts the control on the right",
|
|
31
58
|
table: { category: "Styling" },
|
|
32
59
|
},
|
|
60
|
+
optionsLayout: {
|
|
61
|
+
control: { type: "select" },
|
|
62
|
+
options: ["equal-widths", "inline", "block"],
|
|
63
|
+
description:
|
|
64
|
+
"Rendered as `data-options-layout`. In the group stories it also lays out the story's wrapper the way `MultipleCheckboxes` does",
|
|
65
|
+
table: { category: "Styling" },
|
|
66
|
+
},
|
|
67
|
+
options: {
|
|
68
|
+
control: "object",
|
|
69
|
+
description: "Story-only: the option labels in the group stories",
|
|
70
|
+
table: { category: "Story data" },
|
|
71
|
+
},
|
|
33
72
|
},
|
|
34
73
|
args: {
|
|
35
74
|
type: "checkbox",
|
|
@@ -38,93 +77,121 @@ export default {
|
|
|
38
77
|
fieldHasError: false,
|
|
39
78
|
displayAsDisc: false,
|
|
40
79
|
direction: "row",
|
|
80
|
+
optionsLayout: "equal-widths",
|
|
81
|
+
options: serviceOptions,
|
|
41
82
|
},
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
83
|
+
decorators: [
|
|
84
|
+
(story, context) => ({
|
|
85
|
+
components: { story, CanvasSwitcher },
|
|
86
|
+
setup() {
|
|
87
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
|
|
88
|
+
return { canvasName };
|
|
89
|
+
},
|
|
90
|
+
template: `
|
|
91
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
92
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
93
|
+
</div>
|
|
94
|
+
<div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
|
|
95
|
+
<story />
|
|
96
|
+
</div>
|
|
97
|
+
`,
|
|
98
|
+
}),
|
|
99
|
+
],
|
|
100
|
+
parameters: {
|
|
101
|
+
layout: "fullscreen",
|
|
102
|
+
docs: {
|
|
103
|
+
description: {
|
|
104
|
+
component:
|
|
105
|
+
"One checkbox or radio option drawn as a button or pill: a `<label>` holding the control, a centred label and a decorator icon. `id` is optional and generated with `useId()` when omitted. Props: `type`, `name`, `label`, `trueValue`, `multipleOptions`, `isPill`, `direction`, `displayAsDisc`, `optionsLayout`, `itemIcon`. Slots: `labelContent`, `checkedIcon`, `itemIcon`. Usually used through the `isButton` mode of `MultipleCheckboxes` and `MultipleRadiobuttons`: see the [MultipleCheckboxes Buttons](?path=/story/components-forms-input-checkbox-multiplecheckboxes--buttons) story.",
|
|
106
|
+
},
|
|
107
|
+
},
|
|
49
108
|
},
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
name="single"
|
|
55
|
-
:type="args.type"
|
|
56
|
-
:label="args.label"
|
|
57
|
-
:is-pill="args.isPill"
|
|
58
|
-
:field-has-error="args.fieldHasError"
|
|
59
|
-
:display-as-disc="args.displayAsDisc"
|
|
60
|
-
:direction="args.direction"
|
|
61
|
-
v-model="modelValue"
|
|
62
|
-
/>
|
|
63
|
-
</div>
|
|
64
|
-
`,
|
|
65
|
-
});
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export default meta;
|
|
112
|
+
type Story = StoryObj<StoryArgs>;
|
|
66
113
|
|
|
67
|
-
export const Default =
|
|
114
|
+
export const Default: Story = {
|
|
115
|
+
render: (args: StoryArgs) => ({
|
|
116
|
+
components: { InputCheckboxRadioButton },
|
|
117
|
+
setup() {
|
|
118
|
+
const modelValue = ref(false);
|
|
119
|
+
return { args, modelValue };
|
|
120
|
+
},
|
|
121
|
+
template: `
|
|
122
|
+
${parentNote}
|
|
123
|
+
<div style="max-width: 260px;">
|
|
124
|
+
<InputCheckboxRadioButton
|
|
125
|
+
name="single"
|
|
126
|
+
:type="args.type"
|
|
127
|
+
:label="args.label"
|
|
128
|
+
:is-pill="args.isPill"
|
|
129
|
+
:field-has-error="args.fieldHasError"
|
|
130
|
+
:display-as-disc="args.displayAsDisc"
|
|
131
|
+
:direction="args.direction"
|
|
132
|
+
:options-layout="args.optionsLayout"
|
|
133
|
+
v-model="modelValue"
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
136
|
+
`,
|
|
137
|
+
}),
|
|
138
|
+
argTypes: { options: { table: { disable: true } } },
|
|
139
|
+
};
|
|
68
140
|
|
|
69
|
-
// "Services of interest" pattern — the actual multi-option checkbox-pill layout this component
|
|
70
|
-
// is normally used in (as opposed to Default's single isolated control).
|
|
71
141
|
// Selection state lives in a child keyed on type, so switching checkbox/radio remounts it with
|
|
72
142
|
// the right empty value (an array for checkboxes, a string for radios). A watch on args.type
|
|
73
143
|
// isn't reliable here: the template sees the new arg, but the watcher doesn't fire.
|
|
74
|
-
const
|
|
75
|
-
components: {
|
|
144
|
+
const OptionGroupDemo = defineComponent({
|
|
145
|
+
components: { InputCheckboxRadioButton },
|
|
76
146
|
props: {
|
|
77
|
-
type: { type: String as
|
|
147
|
+
type: { type: String as PropType<"checkbox" | "radio">, required: true },
|
|
148
|
+
options: { type: Array as PropType<string[]>, required: true },
|
|
149
|
+
optionsLayout: { type: String as PropType<OptionsLayout>, required: true },
|
|
78
150
|
isPill: Boolean,
|
|
79
151
|
fieldHasError: Boolean,
|
|
80
152
|
displayAsDisc: Boolean,
|
|
81
|
-
direction: { type: String as
|
|
153
|
+
direction: { type: String as PropType<"row" | "row-reverse">, default: "row" },
|
|
82
154
|
},
|
|
83
155
|
setup(props) {
|
|
84
|
-
const services = [
|
|
85
|
-
"Balayage",
|
|
86
|
-
"Highlights",
|
|
87
|
-
"Half Head Highlights",
|
|
88
|
-
"Full Colour",
|
|
89
|
-
"Lowlights",
|
|
90
|
-
"Toner & Gloss",
|
|
91
|
-
];
|
|
92
156
|
const selected = ref<string[] | string>(props.type === "checkbox" ? [] : "");
|
|
93
|
-
return {
|
|
157
|
+
return { selected, layoutStyles };
|
|
94
158
|
},
|
|
95
159
|
template: `
|
|
96
160
|
<div>
|
|
97
|
-
<div style="
|
|
98
|
-
<
|
|
99
|
-
v-for="
|
|
100
|
-
:key="
|
|
101
|
-
:id="'service-' + service"
|
|
161
|
+
<div :style="layoutStyles[optionsLayout]">
|
|
162
|
+
<InputCheckboxRadioButton
|
|
163
|
+
v-for="(option, index) in options"
|
|
164
|
+
:key="index"
|
|
102
165
|
name="services"
|
|
103
166
|
:type="type"
|
|
104
|
-
:label="
|
|
105
|
-
:true-value="
|
|
167
|
+
:label="option"
|
|
168
|
+
:true-value="option"
|
|
106
169
|
:multiple-options="type === 'checkbox'"
|
|
107
170
|
:is-pill="isPill"
|
|
108
171
|
:field-has-error="fieldHasError"
|
|
109
172
|
:display-as-disc="displayAsDisc"
|
|
110
173
|
:direction="direction"
|
|
174
|
+
:options-layout="optionsLayout"
|
|
111
175
|
v-model="selected"
|
|
112
176
|
/>
|
|
113
177
|
</div>
|
|
114
|
-
<p style="margin:
|
|
178
|
+
<p style="margin-block-start: 1.6rem; font-family: monospace; overflow-wrap: anywhere;">selected: {{ JSON.stringify(selected) }}</p>
|
|
115
179
|
</div>
|
|
116
180
|
`,
|
|
117
181
|
});
|
|
118
182
|
|
|
119
|
-
const
|
|
120
|
-
components: {
|
|
183
|
+
const renderGroup = (args: StoryArgs) => ({
|
|
184
|
+
components: { OptionGroupDemo },
|
|
121
185
|
setup() {
|
|
122
186
|
return { args };
|
|
123
187
|
},
|
|
124
188
|
template: `
|
|
125
|
-
|
|
189
|
+
${parentNote}
|
|
190
|
+
<OptionGroupDemo
|
|
126
191
|
:key="args.type"
|
|
127
192
|
:type="args.type"
|
|
193
|
+
:options="args.options"
|
|
194
|
+
:options-layout="args.optionsLayout"
|
|
128
195
|
:is-pill="args.isPill"
|
|
129
196
|
:field-has-error="args.fieldHasError"
|
|
130
197
|
:display-as-disc="args.displayAsDisc"
|
|
@@ -133,5 +200,44 @@ const OptionGroupTemplate: StoryFn<InputCheckboxRadioButtonStoryArgs> = (args) =
|
|
|
133
200
|
`,
|
|
134
201
|
});
|
|
135
202
|
|
|
136
|
-
export const OptionGroup =
|
|
137
|
-
|
|
203
|
+
export const OptionGroup: Story = {
|
|
204
|
+
render: renderGroup,
|
|
205
|
+
argTypes: { label: { table: { disable: true } } },
|
|
206
|
+
parameters: {
|
|
207
|
+
docs: {
|
|
208
|
+
description: {
|
|
209
|
+
story: "A group of options sharing one `name` and model, the way the group components render them. Switch `type` and `optionsLayout`.",
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
export const StressTest: Story = {
|
|
216
|
+
name: "Stress Test (Worst-Case Data)",
|
|
217
|
+
args: {
|
|
218
|
+
optionsLayout: "inline",
|
|
219
|
+
isPill: true,
|
|
220
|
+
options: [
|
|
221
|
+
"A",
|
|
222
|
+
"",
|
|
223
|
+
"Haarverlängerungsberatungstermindienstleistungspaket",
|
|
224
|
+
"https://example.com/a/very/long/unbroken/url/that/should/wrap/somewhere",
|
|
225
|
+
"<b>Not bold</b> & <i>not italic</i>",
|
|
226
|
+
"💇♀️ Colour + cut 🌈",
|
|
227
|
+
"قص الشعر وتصفيفه",
|
|
228
|
+
"A conditioning treatment with a scalp massage, a hot towel and a blow-dry finish",
|
|
229
|
+
...Array.from({ length: 16 }, (_, index) => `Option ${index + 9}`),
|
|
230
|
+
],
|
|
231
|
+
},
|
|
232
|
+
render: renderGroup,
|
|
233
|
+
argTypes: { label: { table: { disable: true } } },
|
|
234
|
+
parameters: {
|
|
235
|
+
initialCanvas: "mobileCanvas",
|
|
236
|
+
docs: {
|
|
237
|
+
description: {
|
|
238
|
+
story:
|
|
239
|
+
"24 pill options with an empty label, a single character, a long unbroken German word and URL, HTML-like text, emoji, right-to-left text and a long sentence, laid out inline. Check at every canvas width: no option pokes out of the dashed outline (long labels wrap inside their pill instead), the control and decorator icon keep their size, short labels stay on one line, and markup renders as text. Switch `optionsLayout`, `direction` and `type`.",
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
};
|
package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioField.stories.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
1
|
import { computed, ref } from "vue";
|
|
3
|
-
import
|
|
2
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
+
import InputCheckboxRadioField from "../InputCheckboxRadioField.vue";
|
|
4
|
+
import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
|
|
4
5
|
import type { FormUiTheme, InputUiVariant } from "~/types/forms/types.forms.d";
|
|
6
|
+
import type { MediaCanvas } from "~/types/components";
|
|
5
7
|
|
|
6
|
-
interface
|
|
8
|
+
interface StoryArgs {
|
|
7
9
|
type: "checkbox" | "radio";
|
|
8
10
|
name: string;
|
|
9
11
|
label: string;
|
|
@@ -15,9 +17,22 @@ interface InputCheckboxRadioFieldStoryArgs {
|
|
|
15
17
|
useLabelSlot: boolean;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
// Links in the canvas open in the Storybook manager (target="_top"), not inside the preview iframe.
|
|
21
|
+
const storyNoteStyle = "margin: 0 0 1.6rem; font-size: 1.4rem;";
|
|
22
|
+
const parentNote = `
|
|
23
|
+
<p style="${storyNoteStyle}">
|
|
24
|
+
Wrapped in a fieldset with a legend, description and error message, this is the
|
|
25
|
+
<a href="/?path=/story/components-forms-input-checkbox-singlecheckbox--default" target="_top">SingleCheckbox</a>;
|
|
26
|
+
a data-driven group of them is
|
|
27
|
+
<a href="/?path=/story/components-forms-input-checkbox-multiplecheckboxes--default" target="_top">MultipleCheckboxes</a>
|
|
28
|
+
or
|
|
29
|
+
<a href="/?path=/story/components-forms-input-radio-multipleradiobuttons--default" target="_top">MultipleRadiobuttons</a>.
|
|
30
|
+
</p>
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
const meta: Meta<StoryArgs> = {
|
|
19
34
|
title: "Components/Forms/Input Checkbox Radio/InputCheckboxRadioField",
|
|
20
|
-
component:
|
|
35
|
+
component: InputCheckboxRadioField,
|
|
21
36
|
argTypes: {
|
|
22
37
|
type: { control: { type: "select" }, options: ["checkbox", "radio"], table: { category: "Basic" } },
|
|
23
38
|
name: { control: "text", table: { category: "Basic" } },
|
|
@@ -25,7 +40,7 @@ export default {
|
|
|
25
40
|
required: { control: "boolean", table: { category: "Basic" } },
|
|
26
41
|
useLabelSlot: {
|
|
27
42
|
control: "boolean",
|
|
28
|
-
description: "
|
|
43
|
+
description: "Story-only: use the labelContent slot (rich label with a link) instead of the label prop",
|
|
29
44
|
table: { category: "Slots" },
|
|
30
45
|
},
|
|
31
46
|
fieldHasError: { control: "boolean", description: "Rendered as data-invalid", table: { category: "States" } },
|
|
@@ -41,8 +56,9 @@ export default {
|
|
|
41
56
|
},
|
|
42
57
|
labelColor: {
|
|
43
58
|
control: "color",
|
|
44
|
-
description:
|
|
45
|
-
|
|
59
|
+
description:
|
|
60
|
+
"**Story control, not a prop.** Sets `--input-checkbox-label-color` on the story wrapper. In your CSS: `.my-form { --input-checkbox-label-color: #333; }`",
|
|
61
|
+
table: { category: "CSS tokens (story only, set in your CSS)" },
|
|
46
62
|
},
|
|
47
63
|
},
|
|
48
64
|
args: {
|
|
@@ -56,10 +72,39 @@ export default {
|
|
|
56
72
|
labelColor: "",
|
|
57
73
|
useLabelSlot: false,
|
|
58
74
|
},
|
|
59
|
-
|
|
75
|
+
decorators: [
|
|
76
|
+
(story, context) => ({
|
|
77
|
+
components: { story, CanvasSwitcher },
|
|
78
|
+
setup() {
|
|
79
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
|
|
80
|
+
return { canvasName };
|
|
81
|
+
},
|
|
82
|
+
template: `
|
|
83
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
84
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
85
|
+
</div>
|
|
86
|
+
<div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
|
|
87
|
+
<story />
|
|
88
|
+
</div>
|
|
89
|
+
`,
|
|
90
|
+
}),
|
|
91
|
+
],
|
|
92
|
+
parameters: {
|
|
93
|
+
layout: "fullscreen",
|
|
94
|
+
docs: {
|
|
95
|
+
description: {
|
|
96
|
+
component:
|
|
97
|
+
"A `<label>` holding the checkbox or radio control and its text, so the whole row is clickable. It generates its own id with `useId()`. Props: `type`, `name`, `label`, `trueValue`/`falseValue`, `multipleOptions` (array model for a checkbox group), `required`, `fieldHasError`. Slots: `labelContent` (rich label), `checkedIcon`. Usually used through `SingleCheckbox`, `MultipleCheckboxes` or `MultipleRadiobuttons`: see the [SingleCheckbox](?path=/story/components-forms-input-checkbox-singlecheckbox--default) story.",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
};
|
|
60
102
|
|
|
61
|
-
|
|
62
|
-
|
|
103
|
+
export default meta;
|
|
104
|
+
type Story = StoryObj<StoryArgs>;
|
|
105
|
+
|
|
106
|
+
const render = (args: StoryArgs) => ({
|
|
107
|
+
components: { InputCheckboxRadioField },
|
|
63
108
|
setup() {
|
|
64
109
|
const modelValue = ref<boolean | string>(false);
|
|
65
110
|
const componentArgs = computed(() => {
|
|
@@ -70,24 +115,52 @@ const Template: StoryFn<InputCheckboxRadioFieldStoryArgs> = (args) => ({
|
|
|
70
115
|
return { args, componentArgs, tokenStyles, modelValue };
|
|
71
116
|
},
|
|
72
117
|
template: `
|
|
73
|
-
<div
|
|
74
|
-
|
|
118
|
+
<div :style="tokenStyles">
|
|
119
|
+
${parentNote}
|
|
120
|
+
<InputCheckboxRadioField v-bind="componentArgs" v-model="modelValue">
|
|
75
121
|
<template v-if="args.useLabelSlot" #labelContent>
|
|
76
122
|
I agree to the <a href="#terms">terms and conditions</a>
|
|
77
123
|
</template>
|
|
78
|
-
</
|
|
124
|
+
</InputCheckboxRadioField>
|
|
79
125
|
<p style="margin-top: 2rem; font-family: monospace;">modelValue: {{ JSON.stringify(modelValue) }}</p>
|
|
80
126
|
</div>
|
|
81
127
|
`,
|
|
82
128
|
});
|
|
83
129
|
|
|
84
|
-
export const Checkbox =
|
|
130
|
+
export const Checkbox: Story = { render };
|
|
131
|
+
|
|
132
|
+
export const RichLabel: Story = {
|
|
133
|
+
args: { useLabelSlot: true },
|
|
134
|
+
render,
|
|
135
|
+
parameters: { docs: { description: { story: "The `labelContent` slot replaces the `label` prop, here with a link." } } },
|
|
136
|
+
};
|
|
85
137
|
|
|
86
|
-
export const
|
|
87
|
-
|
|
138
|
+
export const Underlined: Story = {
|
|
139
|
+
args: { inputVariant: "underlined" },
|
|
140
|
+
render,
|
|
141
|
+
parameters: { docs: { description: { story: "`inputVariant=\"underlined\"` squares off the checkbox." } } },
|
|
142
|
+
};
|
|
88
143
|
|
|
89
|
-
export const
|
|
90
|
-
|
|
144
|
+
export const ErrorState: Story = {
|
|
145
|
+
args: { fieldHasError: true, theme: "error" },
|
|
146
|
+
render,
|
|
147
|
+
parameters: { docs: { description: { story: "`fieldHasError` sets `data-invalid` on the label and `aria-invalid` on the input." } } },
|
|
148
|
+
};
|
|
91
149
|
|
|
92
|
-
export const
|
|
93
|
-
|
|
150
|
+
export const StressTest: Story = {
|
|
151
|
+
name: "Stress Test (Worst-Case Data)",
|
|
152
|
+
args: {
|
|
153
|
+
label:
|
|
154
|
+
"💇♀️ Datenschutzgrundverordnungseinwilligungsbestätigungserklärung https://example.com/privacy/policy/with/a/very/long/unbroken/path <b>not bold</b> قص الشعر",
|
|
155
|
+
},
|
|
156
|
+
render,
|
|
157
|
+
parameters: {
|
|
158
|
+
initialCanvas: "mobileCanvas",
|
|
159
|
+
docs: {
|
|
160
|
+
description: {
|
|
161
|
+
story:
|
|
162
|
+
"A label with emoji at the start, a long unbroken German word and URL, HTML-like text and right-to-left text. Check at every canvas width: the label wraps inside the dashed outline, the control keeps its size instead of being squashed, markup renders as text, and clicking anywhere on the label toggles the control. Also try an empty label and a single character, and switch `type` to `radio`.",
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
1
|
+
import { afterEach, describe, it, expect, vi } from "vitest";
|
|
2
|
+
import type { VueWrapper } from "@vue/test-utils";
|
|
2
3
|
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
4
|
import InputCheckboxRadio from "../InputCheckboxRadio.vue";
|
|
4
5
|
|
|
@@ -11,12 +12,22 @@ const defaultProps = {
|
|
|
11
12
|
modelValue: "unchecked",
|
|
12
13
|
};
|
|
13
14
|
|
|
14
|
-
const
|
|
15
|
-
|
|
15
|
+
const mounted: VueWrapper[] = [];
|
|
16
|
+
|
|
17
|
+
const createWrapper = async (props = {}, slots = {}) => {
|
|
18
|
+
const wrapper = await mountSuspended(InputCheckboxRadio, {
|
|
16
19
|
attachTo: document.body,
|
|
17
20
|
props: { ...defaultProps, ...props },
|
|
18
21
|
slots,
|
|
19
22
|
});
|
|
23
|
+
mounted.push(wrapper);
|
|
24
|
+
return wrapper;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
mounted.splice(0).forEach((wrapper) => wrapper.unmount());
|
|
29
|
+
vi.restoreAllMocks();
|
|
30
|
+
});
|
|
20
31
|
|
|
21
32
|
const inputOf = (wrapper: Awaited<ReturnType<typeof createWrapper>>) => wrapper.find("input").element as HTMLInputElement;
|
|
22
33
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
1
|
+
import { afterEach, describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { defineComponent } from "vue";
|
|
3
|
+
import type { VueWrapper } from "@vue/test-utils";
|
|
2
4
|
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
5
|
import InputCheckboxRadioButton from "../InputCheckboxRadioButton.vue";
|
|
4
6
|
|
|
@@ -12,12 +14,22 @@ const defaultProps = {
|
|
|
12
14
|
multipleOptions: true,
|
|
13
15
|
};
|
|
14
16
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
+
const mounted: VueWrapper[] = [];
|
|
18
|
+
|
|
19
|
+
const createWrapper = async (props = {}, slots = {}) => {
|
|
20
|
+
const wrapper = await mountSuspended(InputCheckboxRadioButton, {
|
|
17
21
|
attachTo: document.body,
|
|
18
22
|
props: { ...defaultProps, ...props },
|
|
19
23
|
slots,
|
|
20
24
|
});
|
|
25
|
+
mounted.push(wrapper);
|
|
26
|
+
return wrapper;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
mounted.splice(0).forEach((wrapper) => wrapper.unmount());
|
|
31
|
+
vi.restoreAllMocks();
|
|
32
|
+
});
|
|
21
33
|
|
|
22
34
|
describe("InputCheckboxRadioButton", () => {
|
|
23
35
|
it("renders a label linked to the inner control by id", async () => {
|
|
@@ -27,6 +39,29 @@ describe("InputCheckboxRadioButton", () => {
|
|
|
27
39
|
expect(wrapper.find("input").attributes("id")).toBe("service-balayage");
|
|
28
40
|
});
|
|
29
41
|
|
|
42
|
+
it("generates a space-free id linking label and control when no id is passed", async () => {
|
|
43
|
+
const wrapper = await createWrapper({ id: undefined });
|
|
44
|
+
const id = wrapper.find("input").attributes("id");
|
|
45
|
+
|
|
46
|
+
expect(id).toBeTruthy();
|
|
47
|
+
expect(id).not.toMatch(/\s/);
|
|
48
|
+
expect(wrapper.find("label.input-checkbox-radio-button").attributes("for")).toBe(id);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("generates a different id for each instance", async () => {
|
|
52
|
+
const Host = defineComponent({
|
|
53
|
+
components: { InputCheckboxRadioButton },
|
|
54
|
+
setup: () => ({ props: { ...defaultProps, id: undefined } }),
|
|
55
|
+
template: `<div><InputCheckboxRadioButton v-bind="props" /><InputCheckboxRadioButton v-bind="props" /></div>`,
|
|
56
|
+
});
|
|
57
|
+
const host = await mountSuspended(Host);
|
|
58
|
+
mounted.push(host);
|
|
59
|
+
const ids = host.findAll("input").map((input) => input.attributes("id"));
|
|
60
|
+
|
|
61
|
+
expect(ids).toHaveLength(2);
|
|
62
|
+
expect(ids[0]).not.toBe(ids[1]);
|
|
63
|
+
});
|
|
64
|
+
|
|
30
65
|
it("renders the label prop, or the labelContent slot in its place", async () => {
|
|
31
66
|
const plain = await createWrapper();
|
|
32
67
|
expect(plain.find(".input-checkbox-radio-button-label").text()).toBe("Balayage");
|