srcdev-nuxt-components 9.6.7 → 9.6.9
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 +18 -1
- package/.claude/skills/component-inline-action-button.md +1 -1
- package/.claude/skills/component-local-style-override.md +1 -1
- package/.claude/skills/component-naming.md +13 -13
- package/.claude/skills/components/container-glow.md +2 -2
- package/.claude/skills/components/form-field.md +2 -2
- package/.claude/skills/components/form-wrapper.md +1 -1
- package/.claude/skills/components/glowing-border.md +1 -1
- 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 +7 -7
- package/.claude/skills/components/input-error.md +3 -3
- package/.claude/skills/components/input-label.md +7 -8
- package/.claude/skills/components/input-number.md +60 -29
- 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-select.md +153 -0
- package/.claude/skills/components/input-text.md +220 -0
- package/.claude/skills/components/input-textarea.md +147 -0
- package/.claude/skills/components/select-menu.md +4 -4
- package/.claude/skills/components/toggle-switch.md +143 -0
- package/.claude/skills/composable-zod-validation.md +1 -1
- package/.claude/skills/icon-sets.md +1 -1
- package/.claude/skills/icon-sizing.md +1 -1
- package/.claude/skills/index.md +10 -10
- package/.claude/skills/storybook-add-story.md +1 -1
- package/.claude/skills/testing-add-unit-test.md +17 -0
- package/.claude/skills/theming-component-token-pattern.md +8 -8
- package/.claude/skills/theming-form-geometry-tokens.md +1 -1
- 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-input-select.code-snippets +20 -14
- package/.vscode/srcdev-component-input-text.code-snippets +46 -40
- package/.vscode/srcdev-component-input-textarea.code-snippets +19 -14
- package/.vscode/srcdev-component-toggle-switch.code-snippets +19 -29
- package/app/assets/styles/setup/04.elements/forms/00.element-defaults.css +2 -2
- package/app/assets/styles/setup/04.elements/forms/06.button-geometry.css +1 -1
- package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +2 -2
- package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +4 -4
- package/app/components/02.molecules/progress-tracker/stories/ProgressTracker.stories.ts +4 -4
- package/app/components/05.forms/form-errors/CONSUMER-STYLING.md +1 -1
- package/app/components/05.forms/form-field/CONSUMER-STYLING.md +2 -2
- package/app/components/05.forms/input-button/stories/InputButton.stories.ts +3 -3
- 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 +7 -7
- package/app/components/05.forms/input-label/CONSUMER-STYLING.md +6 -6
- package/app/components/05.forms/input-number/CONSUMER-STYLING.md +50 -6
- package/app/components/05.forms/input-number/InputNumber.vue +39 -27
- 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/input-select/CONSUMER-STYLING.md +80 -10
- package/app/components/05.forms/input-select/InputSelect.vue +198 -0
- package/app/components/05.forms/input-select/InputSelectField.vue +130 -0
- package/app/components/05.forms/input-select/stories/InputSelect.stories.ts +184 -0
- package/app/components/05.forms/input-select/stories/InputSelectField.stories.ts +187 -0
- package/app/components/05.forms/input-select/tests/{InputSelectCore.spec.ts → InputSelect.spec.ts} +75 -12
- package/app/components/05.forms/input-select/tests/InputSelectField.spec.ts +121 -0
- package/app/components/05.forms/input-text/CONSUMER-STYLING.md +85 -15
- package/app/components/05.forms/input-text/{InputPasswordWithLabel.vue → InputPasswordField.vue} +16 -19
- package/app/components/05.forms/input-text/{InputTextCore.vue → InputText.vue} +42 -78
- package/app/components/05.forms/input-text/{InputTextAsNumberWithLabel.vue → InputTextAsNumberField.vue} +66 -62
- package/app/components/05.forms/input-text/InputTextField.vue +168 -0
- package/app/components/05.forms/input-text/stories/InputPasswordField.stories.ts +163 -0
- package/app/components/05.forms/input-text/stories/InputText.stories.ts +194 -0
- package/app/components/05.forms/input-text/stories/InputTextAsNumberField.stories.ts +182 -0
- package/app/components/05.forms/input-text/stories/InputTextField.stories.ts +206 -0
- package/app/components/05.forms/input-text/tests/{InputPasswordWithLabel.spec.ts → InputPasswordField.spec.ts} +42 -3
- package/app/components/05.forms/input-text/tests/InputText.spec.ts +166 -0
- package/app/components/05.forms/input-text/tests/InputTextAsNumberField.spec.ts +135 -0
- package/app/components/05.forms/input-text/tests/InputTextField.spec.ts +130 -0
- package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +94 -12
- package/app/components/05.forms/input-textarea/InputTextarea.vue +165 -0
- package/app/components/05.forms/input-textarea/InputTextareaField.vue +159 -0
- package/app/components/05.forms/input-textarea/stories/InputTextarea.stories.ts +198 -0
- package/app/components/05.forms/input-textarea/stories/InputTextareaField.stories.ts +161 -0
- package/app/components/05.forms/input-textarea/tests/InputTextarea.spec.ts +149 -0
- package/app/components/05.forms/input-textarea/tests/InputTextareaField.spec.ts +122 -0
- package/app/components/05.forms/patterns/stories/ContactForm.stories.ts +13 -13
- package/app/components/05.forms/patterns/stories/LoginForm.stories.ts +6 -6
- package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +34 -36
- package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +71 -50
- package/app/components/05.forms/toggle-switch/{ToggleSwitchCore.vue → ToggleSwitch.vue} +58 -73
- package/app/components/05.forms/toggle-switch/{ToggleSwitchWithLabel.vue → ToggleSwitchField.vue} +65 -19
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitch.stories.ts +148 -0
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchField.stories.ts +176 -0
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitch.spec.ts +145 -0
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchField.spec.ts +155 -0
- package/package.json +1 -1
- package/.claude/skills/components/input-select-core.md +0 -151
- package/.claude/skills/components/input-text-core.md +0 -204
- package/.claude/skills/components/input-textarea-core.md +0 -154
- package/.claude/skills/components/toggle-switch-core.md +0 -169
- 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
- package/app/components/05.forms/input-select/InputSelectCore.vue +0 -188
- package/app/components/05.forms/input-select/InputSelectWithLabel.vue +0 -118
- package/app/components/05.forms/input-select/stories/InputSelectCore.stories.ts +0 -69
- package/app/components/05.forms/input-select/stories/InputSelectWithLabel.stories.ts +0 -115
- package/app/components/05.forms/input-select/tests/InputSelectWithLabel.spec.ts +0 -87
- package/app/components/05.forms/input-text/InputTextWithLabel.vue +0 -154
- package/app/components/05.forms/input-text/stories/InputPasswordWithLabel.stories.ts +0 -368
- package/app/components/05.forms/input-text/stories/InputTextAsNumberWithLabel.stories.ts +0 -480
- package/app/components/05.forms/input-text/stories/InputTextCore.stories.ts +0 -267
- package/app/components/05.forms/input-text/stories/InputTextWithLabel.stories.ts +0 -319
- package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +0 -75
- package/app/components/05.forms/input-text/tests/InputTextCore.spec.ts +0 -60
- package/app/components/05.forms/input-text/tests/InputTextWithLabel.spec.ts +0 -71
- package/app/components/05.forms/input-textarea/InputTextareaCore.vue +0 -159
- package/app/components/05.forms/input-textarea/InputTextareaWithLabel.vue +0 -136
- package/app/components/05.forms/input-textarea/stories/InputTextareaCore.stories.ts +0 -247
- package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +0 -458
- package/app/components/05.forms/input-textarea/tests/InputTextareaCore.spec.ts +0 -119
- package/app/components/05.forms/input-textarea/tests/InputTextareaWithLabel.spec.ts +0 -93
- package/app/components/05.forms/toggle-switch/ToggleSwitchWithLabelInline.vue +0 -81
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchCore.stories.ts +0 -251
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabel.stories.ts +0 -251
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabelInline.stories.ts +0 -283
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchCore.spec.ts +0 -101
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchWithLabel.spec.ts +0 -133
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchWithLabelInline.spec.ts +0 -66
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { computed, ref } from "vue";
|
|
2
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
+
import InputSelect from "../InputSelect.vue";
|
|
4
|
+
import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
|
|
5
|
+
import type { FormUiTheme, IFormMultipleOptions, IOptionsConfig, InputUiVariant } from "~/types/forms/types.forms.d";
|
|
6
|
+
import type { MediaCanvas } from "~/types/components";
|
|
7
|
+
|
|
8
|
+
interface StoryArgs {
|
|
9
|
+
modelValue: string | number;
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
placeholder: string;
|
|
13
|
+
title: string;
|
|
14
|
+
required: boolean;
|
|
15
|
+
fieldHasError: boolean;
|
|
16
|
+
theme: FormUiTheme;
|
|
17
|
+
inputVariant: InputUiVariant;
|
|
18
|
+
options: IOptionsConfig[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const colourOptions: IOptionsConfig[] = [
|
|
22
|
+
{ id: "1", name: "red", value: "red", label: "Red" },
|
|
23
|
+
{ id: "2", name: "blue", value: "blue", label: "Blue" },
|
|
24
|
+
{ id: "3", name: "green", value: "green", label: "Green" },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const meta: Meta<StoryArgs> = {
|
|
28
|
+
title: "Components/Forms/Input Select/InputSelect",
|
|
29
|
+
component: InputSelect,
|
|
30
|
+
argTypes: {
|
|
31
|
+
modelValue: { control: "text", table: { category: "Model" } },
|
|
32
|
+
id: { control: "text", table: { category: "Basic" } },
|
|
33
|
+
name: { control: "text", table: { category: "Basic" } },
|
|
34
|
+
placeholder: { control: "text", table: { category: "Basic" } },
|
|
35
|
+
title: { control: "text", description: "Native tooltip; pass translated copy", table: { category: "Accessibility" } },
|
|
36
|
+
required: { control: "boolean", table: { category: "Basic" } },
|
|
37
|
+
fieldHasError: { control: "boolean", table: { category: "States" } },
|
|
38
|
+
theme: {
|
|
39
|
+
control: { type: "select" },
|
|
40
|
+
options: ["default", "success", "error", "warning"],
|
|
41
|
+
table: { category: "Styling" },
|
|
42
|
+
},
|
|
43
|
+
inputVariant: {
|
|
44
|
+
control: { type: "select" },
|
|
45
|
+
options: ["normal", "outlined", "underlined"],
|
|
46
|
+
table: { category: "Styling" },
|
|
47
|
+
},
|
|
48
|
+
options: {
|
|
49
|
+
control: "object",
|
|
50
|
+
description: "Story-only: the `data` array of `v-model:field-data`",
|
|
51
|
+
table: { category: "Model" },
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
args: {
|
|
55
|
+
modelValue: "",
|
|
56
|
+
id: "select-default",
|
|
57
|
+
name: "selectDefault",
|
|
58
|
+
placeholder: "Choose a colour",
|
|
59
|
+
title: "Please select an option",
|
|
60
|
+
required: false,
|
|
61
|
+
fieldHasError: false,
|
|
62
|
+
theme: "default",
|
|
63
|
+
inputVariant: "normal",
|
|
64
|
+
options: colourOptions,
|
|
65
|
+
},
|
|
66
|
+
decorators: [
|
|
67
|
+
(story, context) => ({
|
|
68
|
+
components: { story, CanvasSwitcher },
|
|
69
|
+
setup() {
|
|
70
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
|
|
71
|
+
return { canvasName };
|
|
72
|
+
},
|
|
73
|
+
template: `
|
|
74
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
75
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
76
|
+
</div>
|
|
77
|
+
<div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
|
|
78
|
+
<story />
|
|
79
|
+
</div>
|
|
80
|
+
`,
|
|
81
|
+
}),
|
|
82
|
+
],
|
|
83
|
+
parameters: {
|
|
84
|
+
layout: "fullscreen",
|
|
85
|
+
docs: {
|
|
86
|
+
description: {
|
|
87
|
+
component:
|
|
88
|
+
"Bare native `<select>` for the `05.forms` select family: no label, description or error (use `InputSelectField` for those). Options come from `v-model:field-data` (`{ data: IOptionsConfig[] }`), each with an optional decorative `icon`. `placeholder` adds a disabled first option. Where the browser supports `appearance: base-select` the open picker is styled too; elsewhere the native dropdown shows. State is exposed as `data-input-variant`, `data-invalid`, `data-dirty` and `data-active` on `.input-select-wrapper`.",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export default meta;
|
|
95
|
+
type Story = StoryObj<StoryArgs>;
|
|
96
|
+
|
|
97
|
+
const render = (args: StoryArgs) => ({
|
|
98
|
+
components: { InputSelect },
|
|
99
|
+
setup() {
|
|
100
|
+
const fieldData = computed<IFormMultipleOptions>(() => ({
|
|
101
|
+
data: args.options,
|
|
102
|
+
total: args.options.length,
|
|
103
|
+
skip: 0,
|
|
104
|
+
limit: args.options.length,
|
|
105
|
+
}));
|
|
106
|
+
const componentArgs = computed(() => {
|
|
107
|
+
const { modelValue: _modelValue, options: _options, ...rest } = args;
|
|
108
|
+
return rest;
|
|
109
|
+
});
|
|
110
|
+
return { args, componentArgs, fieldData };
|
|
111
|
+
},
|
|
112
|
+
template: `
|
|
113
|
+
<InputSelect v-model="args.modelValue" :field-data="fieldData" v-bind="componentArgs" />
|
|
114
|
+
<p style="margin-block-start: 1.6rem; font-family: monospace;">modelValue: {{ JSON.stringify(args.modelValue) }}</p>
|
|
115
|
+
`,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
export const Default: Story = { render };
|
|
119
|
+
|
|
120
|
+
export const WithIcons: Story = {
|
|
121
|
+
args: {
|
|
122
|
+
options: [
|
|
123
|
+
{ id: "1", name: "email", value: "email", label: "Email", icon: "mdi:email-outline" },
|
|
124
|
+
{ id: "2", name: "phone", value: "phone", label: "Phone", icon: "mdi:phone-outline" },
|
|
125
|
+
{ id: "3", name: "post", value: "post", label: "Post", icon: "mdi:mailbox-outline" },
|
|
126
|
+
],
|
|
127
|
+
placeholder: "Contact preference",
|
|
128
|
+
},
|
|
129
|
+
render,
|
|
130
|
+
parameters: {
|
|
131
|
+
docs: {
|
|
132
|
+
description: {
|
|
133
|
+
story: "Options with an `icon`. The icon only shows in browsers that support `appearance: base-select`; the native dropdown shows the label alone.",
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export const ErrorState: Story = { args: { fieldHasError: true, theme: "error" }, render };
|
|
140
|
+
|
|
141
|
+
export const Underlined: Story = { args: { inputVariant: "underlined" }, render };
|
|
142
|
+
|
|
143
|
+
export const StressTest: Story = {
|
|
144
|
+
name: "Stress Test (Worst-Case Data)",
|
|
145
|
+
args: {
|
|
146
|
+
modelValue: "0",
|
|
147
|
+
placeholder: "Bitte wählen Sie die gewünschte Zustellungsart für Ihre Bestellung aus der folgenden Liste aus",
|
|
148
|
+
title: "Bitteeineoptionauswählenundanschließendbestätigen",
|
|
149
|
+
fieldHasError: true,
|
|
150
|
+
theme: "error",
|
|
151
|
+
options: [
|
|
152
|
+
{ id: "0", name: "zero", value: "0", label: "0 (falsy-looking value)" },
|
|
153
|
+
{
|
|
154
|
+
id: "1",
|
|
155
|
+
name: "long",
|
|
156
|
+
value: "long",
|
|
157
|
+
label: "Expresszustellungmitsendungsverfolgungundversicherungbiszueinemwarenwertvonzehntausendeuro",
|
|
158
|
+
},
|
|
159
|
+
{ id: "2", name: "url", value: "url", label: "https://example.com/a/very/long/unbroken/url/that/never/wraps/anywhere" },
|
|
160
|
+
{ id: "3", name: "emoji", value: "emoji", label: "🚚 Delivery 📦", icon: "mdi:truck-outline" },
|
|
161
|
+
{ id: "4", name: "html", value: "html", label: "<b>not bold</b> <script>alert(1)</script>" },
|
|
162
|
+
{ id: "5", name: "rtl", value: "rtl", label: "التوصيل السريع إلى باب المنزل" },
|
|
163
|
+
{ id: "6", name: "one", value: "x", label: "x" },
|
|
164
|
+
{ id: "7", name: "empty", value: "empty", label: "" },
|
|
165
|
+
...Array.from({ length: 30 }, (_, index) => ({
|
|
166
|
+
id: `n${index}`,
|
|
167
|
+
name: `n${index}`,
|
|
168
|
+
value: `n${index}`,
|
|
169
|
+
label: `Option ${index + 1}`,
|
|
170
|
+
})),
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
render,
|
|
174
|
+
parameters: {
|
|
175
|
+
initialCanvas: "mobileCanvas",
|
|
176
|
+
docs: {
|
|
177
|
+
story: { inline: false, height: "520px" },
|
|
178
|
+
description: {
|
|
179
|
+
story:
|
|
180
|
+
"38 options: a `0` value (selected on load, not the placeholder), long unbroken German and URL labels, emoji, HTML-like text (must render as text), right-to-left text, a single character and an empty label, plus a very long placeholder. Check at every canvas width: the closed select stays inside the dashed outline, and in a `base-select` browser the open picker's long labels wrap instead of running off screen. The empty label renders as a blank row; that's the data, not a bug.",
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { computed, ref } from "vue";
|
|
2
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
+
import InputSelectField from "../InputSelectField.vue";
|
|
4
|
+
import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
|
|
5
|
+
import type { FormUiTheme, InputUiVariant, IFormMultipleOptions, IOptionsConfig } from "~/types/forms/types.forms.d";
|
|
6
|
+
import type { MediaCanvas } from "~/types/components";
|
|
7
|
+
|
|
8
|
+
interface StoryArgs {
|
|
9
|
+
modelValue: string | number;
|
|
10
|
+
name: string;
|
|
11
|
+
label: string;
|
|
12
|
+
placeholder: string;
|
|
13
|
+
title: string;
|
|
14
|
+
errorMessage: string;
|
|
15
|
+
fieldHasError: boolean;
|
|
16
|
+
required: boolean;
|
|
17
|
+
theme: FormUiTheme;
|
|
18
|
+
inputVariant: InputUiVariant;
|
|
19
|
+
styleClassPassthrough: string[];
|
|
20
|
+
descriptionText: string;
|
|
21
|
+
options: IOptionsConfig[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const colourOptions: IOptionsConfig[] = [
|
|
25
|
+
{ id: "1", name: "red", value: "red", label: "Red" },
|
|
26
|
+
{ id: "2", name: "blue", value: "blue", label: "Blue" },
|
|
27
|
+
{ id: "3", name: "green", value: "green", label: "Green" },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const meta: Meta<StoryArgs> = {
|
|
31
|
+
title: "Components/Forms/Input Select/InputSelectField",
|
|
32
|
+
component: InputSelectField,
|
|
33
|
+
argTypes: {
|
|
34
|
+
modelValue: { control: "text", table: { category: "Model" } },
|
|
35
|
+
name: { control: "text", table: { category: "Basic" } },
|
|
36
|
+
label: { control: "text", table: { category: "Basic" } },
|
|
37
|
+
placeholder: { control: "text", table: { category: "Basic" } },
|
|
38
|
+
title: { control: "text", description: "Native tooltip; pass translated copy", table: { category: "Accessibility" } },
|
|
39
|
+
errorMessage: { control: "text", table: { category: "Validation" } },
|
|
40
|
+
fieldHasError: { control: "boolean", table: { category: "Validation" } },
|
|
41
|
+
required: { control: "boolean", table: { category: "Validation" } },
|
|
42
|
+
theme: {
|
|
43
|
+
control: { type: "select" },
|
|
44
|
+
options: ["default", "success", "error", "warning"],
|
|
45
|
+
table: { category: "Styling" },
|
|
46
|
+
},
|
|
47
|
+
inputVariant: {
|
|
48
|
+
control: { type: "select" },
|
|
49
|
+
options: ["normal", "outlined", "underlined"],
|
|
50
|
+
table: { category: "Styling" },
|
|
51
|
+
},
|
|
52
|
+
styleClassPassthrough: { control: "object", table: { category: "Styling" } },
|
|
53
|
+
descriptionText: {
|
|
54
|
+
control: "text",
|
|
55
|
+
description: "Story-only: fills the descriptionText slot",
|
|
56
|
+
table: { category: "Slots" },
|
|
57
|
+
},
|
|
58
|
+
options: {
|
|
59
|
+
control: "object",
|
|
60
|
+
description: "Story-only: the `data` array of `v-model:field-data`",
|
|
61
|
+
table: { category: "Model" },
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
args: {
|
|
65
|
+
modelValue: "",
|
|
66
|
+
name: "colour",
|
|
67
|
+
label: "Favourite colour",
|
|
68
|
+
placeholder: "Choose a colour",
|
|
69
|
+
title: "Please select an option",
|
|
70
|
+
errorMessage: "",
|
|
71
|
+
fieldHasError: false,
|
|
72
|
+
required: false,
|
|
73
|
+
theme: "default",
|
|
74
|
+
inputVariant: "normal",
|
|
75
|
+
styleClassPassthrough: [],
|
|
76
|
+
descriptionText: "",
|
|
77
|
+
options: colourOptions,
|
|
78
|
+
},
|
|
79
|
+
decorators: [
|
|
80
|
+
(story, context) => ({
|
|
81
|
+
components: { story, CanvasSwitcher },
|
|
82
|
+
setup() {
|
|
83
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
|
|
84
|
+
return { canvasName };
|
|
85
|
+
},
|
|
86
|
+
template: `
|
|
87
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
88
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
89
|
+
</div>
|
|
90
|
+
<div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
|
|
91
|
+
<story />
|
|
92
|
+
</div>
|
|
93
|
+
`,
|
|
94
|
+
}),
|
|
95
|
+
],
|
|
96
|
+
parameters: {
|
|
97
|
+
layout: "fullscreen",
|
|
98
|
+
docs: {
|
|
99
|
+
description: {
|
|
100
|
+
component:
|
|
101
|
+
"Labelled select field: `InputLabel`, an optional description (`descriptionText`/`descriptionHtml` slots, moved after the control for the `outlined` variant), `InputSelect` and an error strip linked through `aria-describedby`. `field-has-error` switches the control to the `error` theme. `style-class-passthrough` lands on the `.input-select-field` root only.",
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export default meta;
|
|
108
|
+
type Story = StoryObj<StoryArgs>;
|
|
109
|
+
|
|
110
|
+
const render = (args: StoryArgs) => ({
|
|
111
|
+
components: { InputSelectField },
|
|
112
|
+
setup() {
|
|
113
|
+
const fieldData = computed<IFormMultipleOptions>(() => ({
|
|
114
|
+
data: args.options,
|
|
115
|
+
total: args.options.length,
|
|
116
|
+
skip: 0,
|
|
117
|
+
limit: args.options.length,
|
|
118
|
+
}));
|
|
119
|
+
const componentArgs = computed(() => {
|
|
120
|
+
const { modelValue: _modelValue, descriptionText: _descriptionText, options: _options, ...rest } = args;
|
|
121
|
+
return rest;
|
|
122
|
+
});
|
|
123
|
+
return { args, componentArgs, fieldData };
|
|
124
|
+
},
|
|
125
|
+
template: `
|
|
126
|
+
<InputSelectField v-model="args.modelValue" :field-data="fieldData" v-bind="componentArgs">
|
|
127
|
+
<template v-if="args.descriptionText" #descriptionText>{{ args.descriptionText }}</template>
|
|
128
|
+
</InputSelectField>
|
|
129
|
+
<p style="margin-block-start: 1.6rem; font-family: monospace;">modelValue: {{ JSON.stringify(args.modelValue) }}</p>
|
|
130
|
+
`,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
export const Default: Story = { render };
|
|
134
|
+
|
|
135
|
+
export const WithError: Story = {
|
|
136
|
+
args: { fieldHasError: true, errorMessage: "Please choose a colour" },
|
|
137
|
+
render,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export const Required: Story = { args: { required: true }, render };
|
|
141
|
+
|
|
142
|
+
export const WithDescription: Story = { args: { descriptionText: "Pick the colour you like best" }, render };
|
|
143
|
+
|
|
144
|
+
export const Underlined: Story = { args: { inputVariant: "underlined" }, render };
|
|
145
|
+
|
|
146
|
+
export const Outlined: Story = {
|
|
147
|
+
args: { inputVariant: "outlined", descriptionText: "Pick the colour you like best" },
|
|
148
|
+
render,
|
|
149
|
+
parameters: { docs: { description: { story: "The `outlined` variant renders the description after the control and error." } } },
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export const StressTest: Story = {
|
|
153
|
+
name: "Stress Test (Worst-Case Data)",
|
|
154
|
+
args: {
|
|
155
|
+
modelValue: "long",
|
|
156
|
+
label:
|
|
157
|
+
"🎨 Bevorzugtefarbefürdiegesamtegestaltungderbenutzeroberfläche https://example.com/a/very/long/unbroken/url <b>not bold</b> اللون",
|
|
158
|
+
descriptionText: "يرجى اختيار لون Wählen Sie bitte die Farbe, die Ihnen für die Oberfläche am besten gefällt.",
|
|
159
|
+
placeholder: "Bitte wählen Sie eine Farbe aus der folgenden sehr langen Liste aus",
|
|
160
|
+
errorMessage: "DieausgewähltefarbeistleidernichtverfügbarbitteversuchenSieeserneut",
|
|
161
|
+
fieldHasError: true,
|
|
162
|
+
required: true,
|
|
163
|
+
options: [
|
|
164
|
+
{ id: "0", name: "zero", value: "0", label: "0" },
|
|
165
|
+
{ id: "1", name: "long", value: "long", label: "Dunkelblaugrüngrauschimmerndemitternachtsfarbemitmetallischemglanz" },
|
|
166
|
+
{ id: "2", name: "html", value: "html", label: "<i>italic?</i>" },
|
|
167
|
+
{ id: "3", name: "rtl", value: "rtl", label: "أزرق" },
|
|
168
|
+
...Array.from({ length: 20 }, (_, index) => ({
|
|
169
|
+
id: `n${index}`,
|
|
170
|
+
name: `n${index}`,
|
|
171
|
+
value: `n${index}`,
|
|
172
|
+
label: `Colour ${index + 1}`,
|
|
173
|
+
})),
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
render,
|
|
177
|
+
parameters: {
|
|
178
|
+
initialCanvas: "mobileCanvas",
|
|
179
|
+
docs: {
|
|
180
|
+
story: { inline: false, height: "520px" },
|
|
181
|
+
description: {
|
|
182
|
+
story:
|
|
183
|
+
"Long unbroken German, URL, emoji, HTML-like and right-to-left copy in the label, description, placeholder, error and options, with a long unbroken label selected. Check at every canvas width: the label, description and error wrap inside the dashed outline, the select never pushes past it, and markup renders as text.",
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
};
|
package/app/components/05.forms/input-select/tests/{InputSelectCore.spec.ts → InputSelect.spec.ts}
RENAMED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// https://nuxt.com/docs/getting-started/testing#unit-testing
|
|
2
|
+
import { nextTick } from "vue";
|
|
2
3
|
import type { VueWrapper } from "@vue/test-utils";
|
|
3
4
|
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
4
|
-
import ComponentUnderTest from "../
|
|
5
|
+
import ComponentUnderTest from "../InputSelect.vue";
|
|
5
6
|
|
|
6
7
|
const initialFieldData = {
|
|
7
8
|
data: [
|
|
@@ -30,35 +31,63 @@ const wrapperFactory = (propsData = {}) => {
|
|
|
30
31
|
});
|
|
31
32
|
};
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
wrapper?.unmount();
|
|
36
|
+
vi.restoreAllMocks();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("InputSelect", () => {
|
|
34
40
|
it("mounts without error", async () => {
|
|
35
41
|
wrapper = await wrapperFactory();
|
|
36
42
|
expect(wrapper.vm).toBeTruthy();
|
|
37
43
|
});
|
|
38
44
|
|
|
45
|
+
it("renders the native select with the .input-select class", async () => {
|
|
46
|
+
wrapper = await wrapperFactory();
|
|
47
|
+
expect(wrapper.find("select.input-select").exists()).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
39
50
|
it("renders an option per fieldData item", async () => {
|
|
40
51
|
wrapper = await wrapperFactory();
|
|
41
|
-
const options = wrapper.findAll("option");
|
|
52
|
+
const options = wrapper.findAll("option.input-select-option");
|
|
42
53
|
expect(options.map((o) => o.text())).toEqual(["Red", "Blue"]);
|
|
43
54
|
});
|
|
44
55
|
|
|
45
56
|
it("binds v-model to the select value", async () => {
|
|
46
57
|
wrapper = await wrapperFactory({ modelValue: "blue" });
|
|
47
|
-
expect(wrapper.find("select").element.value).toBe("blue");
|
|
58
|
+
expect((wrapper.find("select").element as HTMLSelectElement).value).toBe("blue");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("selects an option whose value is 0", async () => {
|
|
62
|
+
wrapper = await wrapperFactory({
|
|
63
|
+
placeholder: "Pick a number",
|
|
64
|
+
modelValue: 0,
|
|
65
|
+
fieldData: { ...initialFieldData, data: [{ id: "0", name: "zero", value: 0, label: "Zero" }] },
|
|
66
|
+
});
|
|
67
|
+
expect((wrapper.find("select").element as HTMLSelectElement).selectedOptions[0]?.textContent?.trim()).toBe("Zero");
|
|
48
68
|
});
|
|
49
69
|
|
|
50
70
|
it("renders a disabled placeholder option when placeholder is set", async () => {
|
|
51
71
|
wrapper = await wrapperFactory({ placeholder: "Choose a colour", modelValue: "" });
|
|
52
|
-
const placeholderOption = wrapper.find("option.placeholder");
|
|
72
|
+
const placeholderOption = wrapper.find("option.input-select-option-placeholder");
|
|
53
73
|
|
|
54
74
|
expect(placeholderOption.exists()).toBe(true);
|
|
55
75
|
expect(placeholderOption.text()).toBe("Choose a colour");
|
|
56
76
|
expect(placeholderOption.attributes("disabled")).toBeDefined();
|
|
77
|
+
expect((placeholderOption.element as HTMLOptionElement).selected).toBe(true);
|
|
57
78
|
});
|
|
58
79
|
|
|
59
80
|
it("does not render a placeholder option when placeholder is unset", async () => {
|
|
60
81
|
wrapper = await wrapperFactory();
|
|
61
|
-
expect(wrapper.find("option.placeholder").exists()).toBe(false);
|
|
82
|
+
expect(wrapper.find("option.input-select-option-placeholder").exists()).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("renders option labels as text, not markup", async () => {
|
|
86
|
+
wrapper = await wrapperFactory({
|
|
87
|
+
fieldData: { ...initialFieldData, data: [{ id: "1", name: "x", value: "x", label: "<b>bold</b>" }] },
|
|
88
|
+
});
|
|
89
|
+
expect(wrapper.find("option b").exists()).toBe(false);
|
|
90
|
+
expect(wrapper.find("option").text()).toBe("<b>bold</b>");
|
|
62
91
|
});
|
|
63
92
|
|
|
64
93
|
it("sets required attribute when required is true", async () => {
|
|
@@ -71,6 +100,22 @@ describe("InputSelectCore", () => {
|
|
|
71
100
|
expect(wrapper.find("select").attributes("required")).toBeUndefined();
|
|
72
101
|
});
|
|
73
102
|
|
|
103
|
+
it("forwards title, defaulting to the English copy", async () => {
|
|
104
|
+
wrapper = await wrapperFactory();
|
|
105
|
+
expect(wrapper.find("select").attributes("title")).toBe("Please select an option");
|
|
106
|
+
|
|
107
|
+
await wrapper.setProps({ title: "Veuillez choisir" });
|
|
108
|
+
expect(wrapper.find("select").attributes("title")).toBe("Veuillez choisir");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("omits aria-describedby when empty and forwards it when set", async () => {
|
|
112
|
+
wrapper = await wrapperFactory();
|
|
113
|
+
expect(wrapper.find("select").attributes("aria-describedby")).toBeUndefined();
|
|
114
|
+
|
|
115
|
+
await wrapper.setProps({ ariaDescribedby: "hint-id" });
|
|
116
|
+
expect(wrapper.find("select").attributes("aria-describedby")).toBe("hint-id");
|
|
117
|
+
});
|
|
118
|
+
|
|
74
119
|
it("sets data-theme from the theme prop", async () => {
|
|
75
120
|
wrapper = await wrapperFactory({ theme: "error" });
|
|
76
121
|
expect(wrapper.find(".input-select-wrapper").attributes("data-theme")).toBe("error");
|
|
@@ -82,19 +127,30 @@ describe("InputSelectCore", () => {
|
|
|
82
127
|
expect(wrapper.find("select").attributes("aria-invalid")).toBe("true");
|
|
83
128
|
});
|
|
84
129
|
|
|
85
|
-
it("
|
|
130
|
+
it("omits data-invalid and aria-invalid by default", async () => {
|
|
86
131
|
wrapper = await wrapperFactory();
|
|
87
|
-
expect(wrapper.find(".input-select-wrapper").
|
|
132
|
+
expect(wrapper.find(".input-select-wrapper").attributes("data-invalid")).toBeUndefined();
|
|
133
|
+
expect(wrapper.find("select").attributes("aria-invalid")).toBeUndefined();
|
|
88
134
|
});
|
|
89
135
|
|
|
90
|
-
it("
|
|
136
|
+
it("defaults data-input-variant to normal", async () => {
|
|
137
|
+
wrapper = await wrapperFactory();
|
|
138
|
+
expect(wrapper.find(".input-select-wrapper").attributes("data-input-variant")).toBe("normal");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("sets data-input-variant from inputVariant", async () => {
|
|
91
142
|
wrapper = await wrapperFactory({ inputVariant: "underlined" });
|
|
92
|
-
expect(wrapper.find(".input-select-wrapper").
|
|
143
|
+
expect(wrapper.find(".input-select-wrapper").attributes("data-input-variant")).toBe("underlined");
|
|
93
144
|
});
|
|
94
145
|
|
|
95
|
-
it("applies styleClassPassthrough to the select", async () => {
|
|
146
|
+
it("applies styleClassPassthrough to the select and reacts to changes", async () => {
|
|
96
147
|
wrapper = await wrapperFactory({ styleClassPassthrough: ["custom-select-class"] });
|
|
97
148
|
expect(wrapper.find("select").classes()).toContain("custom-select-class");
|
|
149
|
+
|
|
150
|
+
await wrapper.setProps({ styleClassPassthrough: ["other-class"] });
|
|
151
|
+
await nextTick();
|
|
152
|
+
expect(wrapper.find("select").classes()).toContain("other-class");
|
|
153
|
+
expect(wrapper.find("select").classes()).not.toContain("custom-select-class");
|
|
98
154
|
});
|
|
99
155
|
|
|
100
156
|
it("sets isActive true on focus and false on blur", async () => {
|
|
@@ -114,6 +170,13 @@ describe("InputSelectCore", () => {
|
|
|
114
170
|
expect(wrapper.emitted("update:isDirty")?.at(-1)).toEqual([true]);
|
|
115
171
|
});
|
|
116
172
|
|
|
173
|
+
it("reflects isDirty and isActive as data attributes", async () => {
|
|
174
|
+
wrapper = await wrapperFactory({ isDirty: true, isActive: true });
|
|
175
|
+
const root = wrapper.find(".input-select-wrapper");
|
|
176
|
+
expect(root.attributes("data-dirty")).toBe("");
|
|
177
|
+
expect(root.attributes("data-active")).toBe("");
|
|
178
|
+
});
|
|
179
|
+
|
|
117
180
|
it("renders an icon for an option that has one, hidden from assistive tech", async () => {
|
|
118
181
|
wrapper = await wrapperFactory({
|
|
119
182
|
fieldData: {
|
|
@@ -122,7 +185,7 @@ describe("InputSelectCore", () => {
|
|
|
122
185
|
},
|
|
123
186
|
});
|
|
124
187
|
|
|
125
|
-
const icon = wrapper.find(".input-select-
|
|
188
|
+
const icon = wrapper.find(".input-select-option-icon");
|
|
126
189
|
expect(icon.exists()).toBe(true);
|
|
127
190
|
expect(icon.attributes("aria-hidden")).toBe("true");
|
|
128
191
|
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// https://nuxt.com/docs/getting-started/testing#unit-testing
|
|
2
|
+
import { nextTick } from "vue";
|
|
3
|
+
import type { VueWrapper } from "@vue/test-utils";
|
|
4
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
5
|
+
import ComponentUnderTest from "../InputSelectField.vue";
|
|
6
|
+
|
|
7
|
+
const initialFieldData = {
|
|
8
|
+
data: [
|
|
9
|
+
{ id: "1", name: "red", value: "red", label: "Red" },
|
|
10
|
+
{ id: "2", name: "blue", value: "blue", label: "Blue" },
|
|
11
|
+
],
|
|
12
|
+
total: 2,
|
|
13
|
+
skip: 0,
|
|
14
|
+
limit: 10,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const initialPropsData = {
|
|
18
|
+
name: "colour",
|
|
19
|
+
label: "Colour",
|
|
20
|
+
errorMessage: "",
|
|
21
|
+
modelValue: "red",
|
|
22
|
+
fieldData: initialFieldData,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
let wrapper: VueWrapper<InstanceType<typeof ComponentUnderTest>>;
|
|
26
|
+
const wrapperFactory = (propsData = {}, slots = {}) => {
|
|
27
|
+
const mockPropsData = { ...initialPropsData, ...propsData };
|
|
28
|
+
|
|
29
|
+
return mountSuspended(ComponentUnderTest, {
|
|
30
|
+
attachTo: document.body,
|
|
31
|
+
props: mockPropsData,
|
|
32
|
+
slots,
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
wrapper?.unmount();
|
|
38
|
+
vi.restoreAllMocks();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("InputSelectField", () => {
|
|
42
|
+
it("mounts without error", async () => {
|
|
43
|
+
wrapper = await wrapperFactory();
|
|
44
|
+
expect(wrapper.vm).toBeTruthy();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("renders .input-select-field as the root", async () => {
|
|
48
|
+
wrapper = await wrapperFactory();
|
|
49
|
+
expect(wrapper.classes()).toContain("input-select-field");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("renders the label text, linked to the select", async () => {
|
|
53
|
+
wrapper = await wrapperFactory({ label: "Favourite colour" });
|
|
54
|
+
const label = wrapper.find("label");
|
|
55
|
+
expect(label.text()).toContain("Favourite colour");
|
|
56
|
+
expect(label.attributes("for")).toBe(wrapper.find("select").attributes("id"));
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("renders a native select with an option per fieldData item", async () => {
|
|
60
|
+
wrapper = await wrapperFactory();
|
|
61
|
+
expect(wrapper.find("select").exists()).toBe(true);
|
|
62
|
+
expect(wrapper.findAll("option").map((o) => o.text())).toEqual(["Red", "Blue"]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("sets data-invalid on the root when fieldHasError is true", async () => {
|
|
66
|
+
wrapper = await wrapperFactory({ fieldHasError: true });
|
|
67
|
+
expect(wrapper.attributes("data-invalid")).toBe("");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("renders the error message and links it via aria-describedby", async () => {
|
|
71
|
+
wrapper = await wrapperFactory({ fieldHasError: true, errorMessage: "Please choose a colour" });
|
|
72
|
+
expect(wrapper.text()).toContain("Please choose a colour");
|
|
73
|
+
const describedBy = wrapper.find("select").attributes("aria-describedby") ?? "";
|
|
74
|
+
expect(describedBy.length).toBeGreaterThan(0);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("renders the descriptionText slot before the select by default", async () => {
|
|
78
|
+
wrapper = await wrapperFactory({}, { descriptionText: () => "Pick your favourite" });
|
|
79
|
+
const html = wrapper.html();
|
|
80
|
+
expect(wrapper.text()).toContain("Pick your favourite");
|
|
81
|
+
expect(html.indexOf("Pick your favourite")).toBeLessThan(html.indexOf("<select"));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("renders the description after the select for the outlined variant", async () => {
|
|
85
|
+
wrapper = await wrapperFactory({ inputVariant: "outlined" }, { descriptionText: () => "Pick your favourite" });
|
|
86
|
+
const html = wrapper.html();
|
|
87
|
+
expect(html.indexOf("Pick your favourite")).toBeGreaterThan(html.indexOf("<select"));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("sets data-input-variant on the root", async () => {
|
|
91
|
+
wrapper = await wrapperFactory({ inputVariant: "underlined" });
|
|
92
|
+
expect(wrapper.attributes("data-input-variant")).toBe("underlined");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("applies styleClassPassthrough to its root only and reacts to changes", async () => {
|
|
96
|
+
wrapper = await wrapperFactory({ styleClassPassthrough: "custom-select" });
|
|
97
|
+
expect(wrapper.classes()).toContain("custom-select");
|
|
98
|
+
expect(wrapper.find("select").classes()).not.toContain("custom-select");
|
|
99
|
+
|
|
100
|
+
await wrapper.setProps({ styleClassPassthrough: "other-select" });
|
|
101
|
+
await nextTick();
|
|
102
|
+
expect(wrapper.classes()).toContain("other-select");
|
|
103
|
+
expect(wrapper.classes()).not.toContain("custom-select");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("forwards required and title to the native select", async () => {
|
|
107
|
+
wrapper = await wrapperFactory({ required: true, title: "Choisissez" });
|
|
108
|
+
expect(wrapper.find("select").attributes("required")).toBeDefined();
|
|
109
|
+
expect(wrapper.find("select").attributes("title")).toBe("Choisissez");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("applies the error theme to InputSelect when fieldHasError is true", async () => {
|
|
113
|
+
wrapper = await wrapperFactory({ fieldHasError: true, theme: "default" });
|
|
114
|
+
expect(wrapper.find(".input-select-wrapper").attributes("data-theme")).toBe("error");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("defaults data-testid to input-select-field", async () => {
|
|
118
|
+
wrapper = await wrapperFactory();
|
|
119
|
+
expect(wrapper.attributes("data-testid")).toBe("input-select-field");
|
|
120
|
+
});
|
|
121
|
+
});
|