srcdev-nuxt-components 9.4.6 → 9.4.7
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/commands/migrate-component.md +34 -3
- package/.claude/component-ledger/audit.json +1 -1
- package/.claude/component-ledger/build.mjs +27 -0
- package/.claude/component-ledger/output.html +8 -3
- package/.claude/component-ledger/template.html +7 -2
- package/.claude/skills/components/column-flow-grid.md +93 -0
- package/.claude/skills/components/entry-animation.md +88 -0
- package/.claude/skills/components/input-copy.md +2 -0
- package/.claude/skills/components/input-text-core.md +186 -0
- package/.claude/skills/components/masonry-grid.md +153 -0
- package/.claude/skills/components/pop-over.md +100 -0
- package/.claude/skills/index.md +6 -1
- package/.vscode/srcdev-component-column-flow-grid.code-snippets +39 -0
- package/.vscode/srcdev-component-entry-animation.code-snippets +29 -0
- package/.vscode/srcdev-component-input-text.code-snippets +107 -0
- package/.vscode/srcdev-component-masonry-grid.code-snippets +51 -0
- package/.vscode/srcdev-component-pop-over.code-snippets +44 -0
- package/app/components/01.atoms/animations/entry/EntryAnimation.vue +7 -1
- package/app/components/01.atoms/animations/entry/stories/EntryAnimation.stories.ts +47 -0
- package/app/components/01.atoms/animations/entry/tests/EntryAnimation.spec.ts +57 -0
- package/app/components/01.atoms/canvas-switcher/stories/CanvasSwitcher.stories.ts +9 -10
- package/app/components/01.atoms/content-wrappers/docs-pages/stories/ContentDocs.stories.ts +12 -13
- package/app/components/01.atoms/grids/column-flow-grid/CONSUMER-STYLING.md +33 -0
- package/app/components/01.atoms/grids/column-flow-grid/ColumnFlowGrid.vue +55 -0
- package/app/components/01.atoms/grids/column-flow-grid/stories/ColumnFlowGrid.stories.ts +178 -0
- package/app/components/01.atoms/grids/column-flow-grid/tests/ColumnFlowGrid.spec.ts +75 -0
- package/app/components/01.atoms/grids/masonry-grid/CONSUMER-STYLING.md +35 -0
- package/app/components/01.atoms/grids/masonry-grid/MasonryGrid.vue +178 -0
- package/app/components/01.atoms/grids/masonry-grid/stories/MasonryGrid.stories.ts +199 -0
- package/app/components/01.atoms/grids/masonry-grid/tests/MasonryGrid.spec.ts +158 -0
- package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +54 -0
- package/app/components/01.atoms/pop-over/PopOver.vue +201 -0
- package/app/components/01.atoms/pop-over/stories/PopOver.stories.ts +141 -0
- package/app/components/01.atoms/pop-over/tests/PopOver.spec.ts +195 -0
- package/app/components/01.atoms/pop-over/tests/__snapshots__/PopOver.spec.ts.snap +11 -0
- package/app/components/02.molecules/action-menu/stories/ActionMenu.stories.ts +8 -2
- package/app/components/02.molecules/input-copy/InputCopy.vue +14 -0
- package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +15 -0
- package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +41 -0
- package/app/components/05.forms/input-select/InputSelectCore.vue +1 -1
- package/app/components/05.forms/input-text/InputTextCore.vue +6 -0
- package/app/components/05.forms/input-text/stories/InputPasswordWithLabel.stories.ts +49 -20
- package/app/components/05.forms/input-text/stories/InputTextAsNumberWithLabel.stories.ts +41 -22
- package/app/components/05.forms/input-text/stories/InputTextCore.stories.ts +48 -22
- package/app/components/05.forms/input-text/stories/InputTextWithLabel.stories.ts +43 -17
- package/app/components/05.forms/input-text/tests/InputPasswordWithLabel.spec.ts +56 -0
- package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +61 -0
- package/app/components/05.forms/input-text/tests/InputTextCore.spec.ts +60 -0
- package/app/components/05.forms/input-text/tests/InputTextWithLabel.spec.ts +46 -0
- package/app/components/05.forms/input-text/variants/InputPasswordWithLabel.vue +7 -1
- package/app/components/05.forms/input-text/variants/InputTextAsNumberWithLabel.vue +8 -2
- package/app/components/05.forms/input-text/variants/InputTextWithLabel.vue +6 -0
- package/app/components/05.forms/input-textarea/stories/InputTextareaCore.stories.ts +20 -20
- package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +32 -26
- package/app/types/forms/types.forms.d.ts +1 -1
- package/package.json +1 -1
- package/app/components/masonry-grid/MasonryGrid.vue +0 -68
- package/app/components/masonry-grid-ordered/MasonryGridOrdered.vue +0 -163
- package/app/components/masonry-grid-ordered/MasonryGridOrderedGridExperiment.vue +0 -259
- package/app/components/masonry-grid-ordered/stories/MasonryGridOrdered.stories.ts +0 -354
- package/app/components/masonry-grid-sorted/MasonryGridSorted.vue +0 -120
- package/app/components/pop-over/PopOver.vue +0 -90
- package/app/layouts/default.vue +0 -308
- package/app/layouts/site-navigation-demo.vue +0 -188
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
+
import { computed } from "vue";
|
|
2
3
|
import StorybookComponent from "../InputTextCore.vue";
|
|
3
4
|
import type { FormUiTheme, InputUiVariant } from "~/types/forms/types.forms.d";
|
|
4
5
|
|
|
5
6
|
interface InputTextCoreStoryArgs {
|
|
6
7
|
modelValue: string;
|
|
7
|
-
type: "text" | "email" | "password" | "tel" | "url" | "search";
|
|
8
|
+
type: "text" | "email" | "password" | "tel" | "url" | "search" | "date";
|
|
8
9
|
maxlength: number;
|
|
10
|
+
min: string | number;
|
|
11
|
+
max: string | number;
|
|
9
12
|
name: string;
|
|
10
13
|
placeholder: string;
|
|
11
14
|
label: string;
|
|
@@ -13,7 +16,7 @@ interface InputTextCoreStoryArgs {
|
|
|
13
16
|
fieldHasError: boolean;
|
|
14
17
|
required: boolean;
|
|
15
18
|
theme: FormUiTheme;
|
|
16
|
-
|
|
19
|
+
inputVariant: InputUiVariant;
|
|
17
20
|
styleClassPassthrough: string[];
|
|
18
21
|
leftSlotContent: string;
|
|
19
22
|
rightSlotContent: string;
|
|
@@ -37,7 +40,7 @@ export default {
|
|
|
37
40
|
// Basic Configuration
|
|
38
41
|
type: {
|
|
39
42
|
control: { type: "select" },
|
|
40
|
-
options: ["text", "email", "password", "tel", "url", "search"],
|
|
43
|
+
options: ["text", "email", "password", "tel", "url", "search", "date"],
|
|
41
44
|
description: "Input type",
|
|
42
45
|
table: {
|
|
43
46
|
category: "Basic",
|
|
@@ -50,6 +53,20 @@ export default {
|
|
|
50
53
|
category: "Basic",
|
|
51
54
|
},
|
|
52
55
|
},
|
|
56
|
+
min: {
|
|
57
|
+
control: "text",
|
|
58
|
+
description: 'Passed straight through to the native input — e.g. earliest allowed date for type="date", or lowest number for type="number".',
|
|
59
|
+
table: {
|
|
60
|
+
category: "Basic",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
max: {
|
|
64
|
+
control: "text",
|
|
65
|
+
description: 'Passed straight through to the native input — e.g. latest allowed date for type="date", or highest number for type="number".',
|
|
66
|
+
table: {
|
|
67
|
+
category: "Basic",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
53
70
|
name: {
|
|
54
71
|
control: "text",
|
|
55
72
|
description: "Input name attribute",
|
|
@@ -104,7 +121,7 @@ export default {
|
|
|
104
121
|
category: "Styling",
|
|
105
122
|
},
|
|
106
123
|
},
|
|
107
|
-
|
|
124
|
+
inputVariant: {
|
|
108
125
|
control: { type: "select" },
|
|
109
126
|
options: ["normal", "outlined", "underlined"],
|
|
110
127
|
description: "Input variant style",
|
|
@@ -170,31 +187,30 @@ export default {
|
|
|
170
187
|
},
|
|
171
188
|
} as Meta<typeof StorybookComponent>;
|
|
172
189
|
|
|
190
|
+
// `args` is Storybook's own reactive object — bind to it directly (`args.x`) rather than
|
|
191
|
+
// destructuring it into local variables/refs, which would snapshot the values once at setup()
|
|
192
|
+
// and stop reflecting later Controls-panel changes. `componentArgs` strips the non-prop
|
|
193
|
+
// slot-toggle args so they don't leak onto the component as unknown attributes.
|
|
173
194
|
const Template: StoryFn<InputTextCoreStoryArgs> = (args) => ({
|
|
174
195
|
components: { StorybookComponent },
|
|
175
196
|
setup() {
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
leftSlotContent: args.leftSlotContent,
|
|
183
|
-
rightSlotContent: args.rightSlotContent,
|
|
184
|
-
useLeftSlot: args.useLeftSlot,
|
|
185
|
-
useRightSlot: args.useRightSlot,
|
|
186
|
-
};
|
|
197
|
+
const componentArgs = computed(() => {
|
|
198
|
+
const { useLeftSlot, useRightSlot, leftSlotContent, rightSlotContent, ...rest } = args;
|
|
199
|
+
return rest;
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
return { args, componentArgs };
|
|
187
203
|
},
|
|
188
204
|
template: `
|
|
189
205
|
<StorybookComponent
|
|
190
|
-
v-model="
|
|
191
|
-
v-bind="
|
|
206
|
+
v-model="args.modelValue"
|
|
207
|
+
v-bind="componentArgs"
|
|
192
208
|
>
|
|
193
|
-
<template v-if="useLeftSlot" #left>{{ leftSlotContent }}</template>
|
|
194
|
-
<template v-if="useRightSlot" #right>{{ rightSlotContent }}</template>
|
|
209
|
+
<template v-if="args.useLeftSlot" #left>{{ args.leftSlotContent }}</template>
|
|
210
|
+
<template v-if="args.useRightSlot" #right>{{ args.rightSlotContent }}</template>
|
|
195
211
|
</StorybookComponent>
|
|
196
212
|
<div class="mt-4 text-sm text-gray-600">
|
|
197
|
-
Current value: {{
|
|
213
|
+
Current value: {{ args.modelValue }}
|
|
198
214
|
</div>
|
|
199
215
|
`,
|
|
200
216
|
});
|
|
@@ -230,12 +246,22 @@ WithSlots.args = {
|
|
|
230
246
|
|
|
231
247
|
export const Outlined = Template.bind({});
|
|
232
248
|
Outlined.args = {
|
|
233
|
-
|
|
249
|
+
inputVariant: "outlined",
|
|
234
250
|
placeholder: "Outlined input",
|
|
235
251
|
};
|
|
236
252
|
|
|
237
253
|
export const Underlined = Template.bind({});
|
|
238
254
|
Underlined.args = {
|
|
239
|
-
|
|
255
|
+
inputVariant: "underlined",
|
|
240
256
|
placeholder: "Underlined input",
|
|
241
257
|
};
|
|
258
|
+
|
|
259
|
+
export const DateWithMinMax = Template.bind({});
|
|
260
|
+
DateWithMinMax.args = {
|
|
261
|
+
type: "date",
|
|
262
|
+
label: "Appointment date",
|
|
263
|
+
name: "appointmentDate",
|
|
264
|
+
// Prevents picking a date in the past in the native browser date picker.
|
|
265
|
+
min: new Date().toISOString().split("T")[0],
|
|
266
|
+
max: "2027-12-31",
|
|
267
|
+
};
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
+
import { computed } from "vue";
|
|
2
3
|
import StorybookComponent from "../variants/InputTextWithLabel.vue";
|
|
3
4
|
import type { FormUiTheme, InputUiVariant } from "~/types/forms/types.forms.d";
|
|
4
5
|
|
|
5
6
|
interface InputTextWithLabelStoryArgs {
|
|
6
7
|
modelValue: string;
|
|
7
|
-
type: "text" | "email" | "password" | "tel" | "url" | "search";
|
|
8
|
+
type: "text" | "email" | "password" | "tel" | "url" | "search" | "date";
|
|
8
9
|
maxlength: number;
|
|
10
|
+
min: string | number;
|
|
11
|
+
max: string | number;
|
|
9
12
|
name: string;
|
|
10
13
|
placeholder: string;
|
|
11
14
|
label: string;
|
|
@@ -37,7 +40,7 @@ export default {
|
|
|
37
40
|
// Basic Configuration
|
|
38
41
|
type: {
|
|
39
42
|
control: { type: "select" },
|
|
40
|
-
options: ["text", "email", "password", "tel", "url", "search"],
|
|
43
|
+
options: ["text", "email", "password", "tel", "url", "search", "date"],
|
|
41
44
|
description: "Input type",
|
|
42
45
|
table: {
|
|
43
46
|
category: "Basic",
|
|
@@ -50,6 +53,20 @@ export default {
|
|
|
50
53
|
category: "Basic",
|
|
51
54
|
},
|
|
52
55
|
},
|
|
56
|
+
min: {
|
|
57
|
+
control: "text",
|
|
58
|
+
description: 'Passed straight through to the native input — e.g. earliest allowed date for type="date".',
|
|
59
|
+
table: {
|
|
60
|
+
category: "Basic",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
max: {
|
|
64
|
+
control: "text",
|
|
65
|
+
description: 'Passed straight through to the native input — e.g. latest allowed date for type="date".',
|
|
66
|
+
table: {
|
|
67
|
+
category: "Basic",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
53
70
|
name: {
|
|
54
71
|
control: "text",
|
|
55
72
|
description: "Input name attribute",
|
|
@@ -170,31 +187,30 @@ export default {
|
|
|
170
187
|
},
|
|
171
188
|
} as Meta<typeof StorybookComponent>;
|
|
172
189
|
|
|
190
|
+
// `args` is Storybook's own reactive object — bind to it directly (`args.x`) rather than
|
|
191
|
+
// destructuring it into local variables/refs, which would snapshot the values once at setup()
|
|
192
|
+
// and stop reflecting later Controls-panel changes. `componentArgs` strips the non-prop
|
|
193
|
+
// slot-toggle args so they don't leak onto the component as unknown attributes.
|
|
173
194
|
const Template: StoryFn<InputTextWithLabelStoryArgs> = (args) => ({
|
|
174
195
|
components: { StorybookComponent },
|
|
175
196
|
setup() {
|
|
176
|
-
const
|
|
177
|
-
|
|
197
|
+
const componentArgs = computed(() => {
|
|
198
|
+
const { useDescriptionSlot, useDescriptionHtmlSlot, descriptionContent, descriptionHtmlContent, ...rest } = args;
|
|
199
|
+
return rest;
|
|
200
|
+
});
|
|
178
201
|
|
|
179
|
-
return {
|
|
180
|
-
inputValue,
|
|
181
|
-
args: otherArgs,
|
|
182
|
-
descriptionContent: args.descriptionContent,
|
|
183
|
-
descriptionHtmlContent: args.descriptionHtmlContent,
|
|
184
|
-
useDescriptionSlot: args.useDescriptionSlot,
|
|
185
|
-
useDescriptionHtmlSlot: args.useDescriptionHtmlSlot,
|
|
186
|
-
};
|
|
202
|
+
return { args, componentArgs };
|
|
187
203
|
},
|
|
188
204
|
template: `
|
|
189
205
|
<StorybookComponent
|
|
190
|
-
v-model="
|
|
191
|
-
v-bind="
|
|
206
|
+
v-model="args.modelValue"
|
|
207
|
+
v-bind="componentArgs"
|
|
192
208
|
>
|
|
193
|
-
<template v-if="useDescriptionSlot" #description>{{ descriptionContent }}</template>
|
|
194
|
-
<template v-if="useDescriptionHtmlSlot" #descriptionHtml v-html="descriptionHtmlContent"></template>
|
|
209
|
+
<template v-if="args.useDescriptionSlot" #description>{{ args.descriptionContent }}</template>
|
|
210
|
+
<template v-if="args.useDescriptionHtmlSlot" #descriptionHtml v-html="args.descriptionHtmlContent"></template>
|
|
195
211
|
</StorybookComponent>
|
|
196
212
|
<div class="mt-4 text-sm text-gray-600">
|
|
197
|
-
Current value: {{
|
|
213
|
+
Current value: {{ args.modelValue }}
|
|
198
214
|
</div>
|
|
199
215
|
`,
|
|
200
216
|
});
|
|
@@ -256,6 +272,16 @@ Underlined.args = {
|
|
|
256
272
|
type: "url",
|
|
257
273
|
};
|
|
258
274
|
|
|
275
|
+
export const DateWithMinMax = Template.bind({});
|
|
276
|
+
DateWithMinMax.args = {
|
|
277
|
+
type: "date",
|
|
278
|
+
label: "Appointment date",
|
|
279
|
+
name: "appointmentDate",
|
|
280
|
+
// Prevents picking a date in the past in the native browser date picker.
|
|
281
|
+
min: new Date().toISOString().split("T")[0],
|
|
282
|
+
max: "2027-12-31",
|
|
283
|
+
};
|
|
284
|
+
|
|
259
285
|
export const AllThemes = Template.bind({});
|
|
260
286
|
AllThemes.storyName = "All Themes";
|
|
261
287
|
AllThemes.args = {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// https://nuxt.com/docs/getting-started/testing#unit-testing
|
|
2
|
+
import type { VueWrapper } from "@vue/test-utils";
|
|
3
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
4
|
+
import ComponentUnderTest from "../variants/InputPasswordWithLabel.vue";
|
|
5
|
+
|
|
6
|
+
const initialPropsData = {
|
|
7
|
+
name: "password",
|
|
8
|
+
label: "Password",
|
|
9
|
+
errorMessage: "",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
let wrapper: VueWrapper<InstanceType<typeof ComponentUnderTest>>;
|
|
13
|
+
const wrapperFactory = (propsData = {}) => {
|
|
14
|
+
const mockPropsData = { ...initialPropsData, ...propsData };
|
|
15
|
+
|
|
16
|
+
return mountSuspended(ComponentUnderTest, {
|
|
17
|
+
attachTo: document.body,
|
|
18
|
+
props: mockPropsData,
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
describe("InputPasswordWithLabel", () => {
|
|
23
|
+
it("mounts without error", async () => {
|
|
24
|
+
wrapper = await wrapperFactory();
|
|
25
|
+
expect(wrapper.vm).toBeTruthy();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("defaults to type=password and the default toggle button text", async () => {
|
|
29
|
+
wrapper = await wrapperFactory();
|
|
30
|
+
|
|
31
|
+
expect(wrapper.find("input").attributes("type")).toBe("password");
|
|
32
|
+
expect(wrapper.find("button").text()).toContain("Show password");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("toggles the input type and button text when the toggle button is clicked", async () => {
|
|
36
|
+
wrapper = await wrapperFactory();
|
|
37
|
+
|
|
38
|
+
await wrapper.find("button").trigger("click");
|
|
39
|
+
|
|
40
|
+
expect(wrapper.find("input").attributes("type")).toBe("text");
|
|
41
|
+
expect(wrapper.find("button").text()).toContain("Hide password");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("uses custom showPasswordText/hidePasswordText when provided", async () => {
|
|
45
|
+
wrapper = await wrapperFactory({
|
|
46
|
+
showPasswordText: "Afficher le mot de passe",
|
|
47
|
+
hidePasswordText: "Masquer le mot de passe",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(wrapper.find("button").text()).toContain("Afficher le mot de passe");
|
|
51
|
+
|
|
52
|
+
await wrapper.find("button").trigger("click");
|
|
53
|
+
|
|
54
|
+
expect(wrapper.find("button").text()).toContain("Masquer le mot de passe");
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// https://nuxt.com/docs/getting-started/testing#unit-testing
|
|
2
|
+
import type { VueWrapper } from "@vue/test-utils";
|
|
3
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
4
|
+
import ComponentUnderTest from "../variants/InputTextAsNumberWithLabel.vue";
|
|
5
|
+
|
|
6
|
+
const initialPropsData = {
|
|
7
|
+
name: "quantity",
|
|
8
|
+
label: "Quantity",
|
|
9
|
+
errorMessage: "",
|
|
10
|
+
min: 0,
|
|
11
|
+
max: 10,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
let wrapper: VueWrapper<InstanceType<typeof ComponentUnderTest>>;
|
|
15
|
+
const wrapperFactory = (propsData = {}) => {
|
|
16
|
+
const mockPropsData = { ...initialPropsData, ...propsData };
|
|
17
|
+
|
|
18
|
+
return mountSuspended(ComponentUnderTest, {
|
|
19
|
+
attachTo: document.body,
|
|
20
|
+
props: mockPropsData,
|
|
21
|
+
slots: {
|
|
22
|
+
left: () => "−",
|
|
23
|
+
right: () => "+",
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
describe("InputTextAsNumberWithLabel", () => {
|
|
29
|
+
it("mounts without error", async () => {
|
|
30
|
+
wrapper = await wrapperFactory();
|
|
31
|
+
expect(wrapper.vm).toBeTruthy();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("uses the default step button text", async () => {
|
|
35
|
+
wrapper = await wrapperFactory();
|
|
36
|
+
|
|
37
|
+
const [stepDown, stepUp] = wrapper.findAll("button");
|
|
38
|
+
expect(stepDown?.text()).toContain("Step down");
|
|
39
|
+
expect(stepUp?.text()).toContain("Step up");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("uses custom stepDownText/stepUpText when provided", async () => {
|
|
43
|
+
wrapper = await wrapperFactory({
|
|
44
|
+
stepDownText: "Diminuer",
|
|
45
|
+
stepUpText: "Augmenter",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const [stepDown, stepUp] = wrapper.findAll("button");
|
|
49
|
+
expect(stepDown?.text()).toContain("Diminuer");
|
|
50
|
+
expect(stepUp?.text()).toContain("Augmenter");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("increments the value when the step-up button is clicked", async () => {
|
|
54
|
+
wrapper = await wrapperFactory({ modelValue: 5 });
|
|
55
|
+
|
|
56
|
+
const [, stepUp] = wrapper.findAll("button");
|
|
57
|
+
await stepUp?.trigger("click");
|
|
58
|
+
|
|
59
|
+
expect(wrapper.emitted("update:modelValue")?.[0]).toEqual([6]);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// https://nuxt.com/docs/getting-started/testing#unit-testing
|
|
2
|
+
import type { VueWrapper } from "@vue/test-utils";
|
|
3
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
4
|
+
import ComponentUnderTest from "../InputTextCore.vue";
|
|
5
|
+
|
|
6
|
+
const initialPropsData = {
|
|
7
|
+
id: "test-input",
|
|
8
|
+
name: "testName",
|
|
9
|
+
modelValue: "",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
let wrapper: VueWrapper<InstanceType<typeof ComponentUnderTest>>;
|
|
13
|
+
const wrapperFactory = (propsData = {}) => {
|
|
14
|
+
const mockPropsData = { ...initialPropsData, ...propsData };
|
|
15
|
+
|
|
16
|
+
return mountSuspended(ComponentUnderTest, {
|
|
17
|
+
attachTo: document.body,
|
|
18
|
+
props: mockPropsData,
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
describe("InputTextCore", () => {
|
|
23
|
+
it("mounts without error", async () => {
|
|
24
|
+
wrapper = await wrapperFactory();
|
|
25
|
+
expect(wrapper.vm).toBeTruthy();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("does not set min/max attributes when not provided", async () => {
|
|
29
|
+
wrapper = await wrapperFactory();
|
|
30
|
+
const input = wrapper.find("input");
|
|
31
|
+
|
|
32
|
+
expect(input.attributes("min")).toBeUndefined();
|
|
33
|
+
expect(input.attributes("max")).toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("passes through a string min/max (e.g. for type=date)", async () => {
|
|
37
|
+
wrapper = await wrapperFactory({
|
|
38
|
+
type: "date",
|
|
39
|
+
min: "2026-01-01",
|
|
40
|
+
max: "2026-12-31",
|
|
41
|
+
});
|
|
42
|
+
const input = wrapper.find("input");
|
|
43
|
+
|
|
44
|
+
expect(input.attributes("type")).toBe("date");
|
|
45
|
+
expect(input.attributes("min")).toBe("2026-01-01");
|
|
46
|
+
expect(input.attributes("max")).toBe("2026-12-31");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("passes through a numeric min/max (e.g. for type=number)", async () => {
|
|
50
|
+
wrapper = await wrapperFactory({
|
|
51
|
+
type: "number",
|
|
52
|
+
min: 0,
|
|
53
|
+
max: 100,
|
|
54
|
+
});
|
|
55
|
+
const input = wrapper.find("input");
|
|
56
|
+
|
|
57
|
+
expect(input.attributes("min")).toBe("0");
|
|
58
|
+
expect(input.attributes("max")).toBe("100");
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// https://nuxt.com/docs/getting-started/testing#unit-testing
|
|
2
|
+
import type { VueWrapper } from "@vue/test-utils";
|
|
3
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
4
|
+
import ComponentUnderTest from "../variants/InputTextWithLabel.vue";
|
|
5
|
+
|
|
6
|
+
const initialPropsData = {
|
|
7
|
+
type: "text" as const,
|
|
8
|
+
name: "testName",
|
|
9
|
+
label: "Test label",
|
|
10
|
+
errorMessage: "",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
let wrapper: VueWrapper<InstanceType<typeof ComponentUnderTest>>;
|
|
14
|
+
const wrapperFactory = (propsData = {}) => {
|
|
15
|
+
const mockPropsData = { ...initialPropsData, ...propsData };
|
|
16
|
+
|
|
17
|
+
return mountSuspended(ComponentUnderTest, {
|
|
18
|
+
attachTo: document.body,
|
|
19
|
+
props: mockPropsData,
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
describe("InputTextWithLabel", () => {
|
|
24
|
+
it("mounts without error", async () => {
|
|
25
|
+
wrapper = await wrapperFactory();
|
|
26
|
+
expect(wrapper.vm).toBeTruthy();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("renders the label text", async () => {
|
|
30
|
+
wrapper = await wrapperFactory({ label: "Email address" });
|
|
31
|
+
expect(wrapper.text()).toContain("Email address");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("forwards min/max through to the underlying InputTextCore input (e.g. for type=date)", async () => {
|
|
35
|
+
wrapper = await wrapperFactory({
|
|
36
|
+
type: "date",
|
|
37
|
+
min: "2026-01-01",
|
|
38
|
+
max: "2026-12-31",
|
|
39
|
+
});
|
|
40
|
+
const input = wrapper.find("input");
|
|
41
|
+
|
|
42
|
+
expect(input.attributes("type")).toBe("date");
|
|
43
|
+
expect(input.attributes("min")).toBe("2026-01-01");
|
|
44
|
+
expect(input.attributes("max")).toBe("2026-12-31");
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -53,6 +53,10 @@ interface Props {
|
|
|
53
53
|
styleClassPassthrough?: string | string[];
|
|
54
54
|
theme?: FormUiTheme;
|
|
55
55
|
inputVariant?: InputUiVariant;
|
|
56
|
+
/** Accessible name for the toggle button while the password is hidden. */
|
|
57
|
+
showPasswordText?: string;
|
|
58
|
+
/** Accessible name for the toggle button while the password is visible. */
|
|
59
|
+
hidePasswordText?: string;
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -64,6 +68,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
64
68
|
styleClassPassthrough: () => [],
|
|
65
69
|
theme: "default",
|
|
66
70
|
inputVariant: "normal",
|
|
71
|
+
showPasswordText: "Show password",
|
|
72
|
+
hidePasswordText: "Hide password",
|
|
67
73
|
});
|
|
68
74
|
|
|
69
75
|
const modelValue = defineModel<string>();
|
|
@@ -76,7 +82,7 @@ const inputType = computed<"text" | "password">(() => {
|
|
|
76
82
|
|
|
77
83
|
const displayPassword = ref(false);
|
|
78
84
|
const buttonText = computed(() => {
|
|
79
|
-
return displayPassword.value ?
|
|
85
|
+
return displayPassword.value ? props.hidePasswordText : props.showPasswordText;
|
|
80
86
|
});
|
|
81
87
|
const toggleDisplayPassword = () => {
|
|
82
88
|
displayPassword.value = !displayPassword.value;
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
type="button"
|
|
57
57
|
:readonly="(modelValue ?? 0) <= min"
|
|
58
58
|
:is-pending="false"
|
|
59
|
-
button-text="
|
|
59
|
+
:button-text="stepDownText"
|
|
60
60
|
theme="default"
|
|
61
61
|
variant="inline"
|
|
62
62
|
@click.stop.prevent="updateValue(-step, (modelValue ?? 0) > min)"
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
type="button"
|
|
72
72
|
:readonly="(modelValue ?? 0) >= max"
|
|
73
73
|
:is-pending="false"
|
|
74
|
-
button-text="
|
|
74
|
+
:button-text="stepUpText"
|
|
75
75
|
theme="default"
|
|
76
76
|
variant="inline"
|
|
77
77
|
@click.stop.prevent="updateValue(step, (modelValue ?? 0) < max)"
|
|
@@ -103,6 +103,10 @@ interface Props {
|
|
|
103
103
|
max: number;
|
|
104
104
|
step?: number;
|
|
105
105
|
inputVariant?: InputUiVariant;
|
|
106
|
+
/** Accessible name for the decrement button. */
|
|
107
|
+
stepDownText?: string;
|
|
108
|
+
/** Accessible name for the increment button. */
|
|
109
|
+
stepUpText?: string;
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -114,6 +118,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
114
118
|
theme: "default",
|
|
115
119
|
step: 1,
|
|
116
120
|
inputVariant: "normal",
|
|
121
|
+
stepDownText: "Step down",
|
|
122
|
+
stepUpText: "Step up",
|
|
117
123
|
});
|
|
118
124
|
|
|
119
125
|
const slots = useSlots();
|
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
:type
|
|
42
42
|
:inputmode
|
|
43
43
|
:maxlength
|
|
44
|
+
:min
|
|
45
|
+
:max
|
|
44
46
|
:name
|
|
45
47
|
:placeholder
|
|
46
48
|
:label
|
|
@@ -86,6 +88,10 @@ import type { InputTypesText, FormUiTheme, InputUiVariant, InputMode } from "~/t
|
|
|
86
88
|
|
|
87
89
|
interface Props {
|
|
88
90
|
maxlength?: number;
|
|
91
|
+
/** Passed straight through to the native input — meaningful for type="date"/"number" etc. */
|
|
92
|
+
min?: string | number;
|
|
93
|
+
/** Passed straight through to the native input — meaningful for type="date"/"number" etc. */
|
|
94
|
+
max?: string | number;
|
|
89
95
|
type: InputTypesText;
|
|
90
96
|
inputmode?: InputMode;
|
|
91
97
|
name: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
+
import { computed } from "vue";
|
|
2
3
|
import StorybookComponent from "../InputTextareaCore.vue";
|
|
3
4
|
import type { FormUiTheme, InputUiVariant } from "~/types/forms/types.forms.d";
|
|
4
5
|
|
|
@@ -12,7 +13,7 @@ interface InputTextareaCoreStoryArgs {
|
|
|
12
13
|
fieldHasError: boolean;
|
|
13
14
|
required: boolean;
|
|
14
15
|
theme: FormUiTheme;
|
|
15
|
-
|
|
16
|
+
inputVariant: InputUiVariant;
|
|
16
17
|
styleClassPassthrough: string[];
|
|
17
18
|
leftSlotContent: string;
|
|
18
19
|
rightSlotContent: string;
|
|
@@ -95,7 +96,7 @@ export default {
|
|
|
95
96
|
category: "Styling",
|
|
96
97
|
},
|
|
97
98
|
},
|
|
98
|
-
|
|
99
|
+
inputVariant: {
|
|
99
100
|
control: { type: "select" },
|
|
100
101
|
options: ["normal", "outlined", "underlined"],
|
|
101
102
|
description: "Textarea variant style",
|
|
@@ -160,31 +161,30 @@ export default {
|
|
|
160
161
|
},
|
|
161
162
|
} as Meta<typeof StorybookComponent>;
|
|
162
163
|
|
|
164
|
+
// `args` is Storybook's own reactive object — bind to it directly (`args.x`) rather than
|
|
165
|
+
// destructuring it into local variables/refs, which would snapshot the values once at setup()
|
|
166
|
+
// and stop reflecting later Controls-panel changes. `componentArgs` strips the non-prop
|
|
167
|
+
// slot-toggle args so they don't leak onto the component as unknown attributes.
|
|
163
168
|
const Template: StoryFn<InputTextareaCoreStoryArgs> = (args) => ({
|
|
164
169
|
components: { StorybookComponent },
|
|
165
170
|
setup() {
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
leftSlotContent: args.leftSlotContent,
|
|
173
|
-
rightSlotContent: args.rightSlotContent,
|
|
174
|
-
useLeftSlot: args.useLeftSlot,
|
|
175
|
-
useRightSlot: args.useRightSlot,
|
|
176
|
-
};
|
|
171
|
+
const componentArgs = computed(() => {
|
|
172
|
+
const { useLeftSlot, useRightSlot, leftSlotContent, rightSlotContent, ...rest } = args;
|
|
173
|
+
return rest;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return { args, componentArgs };
|
|
177
177
|
},
|
|
178
178
|
template: `
|
|
179
179
|
<StorybookComponent
|
|
180
|
-
v-model="
|
|
181
|
-
v-bind="
|
|
180
|
+
v-model="args.modelValue"
|
|
181
|
+
v-bind="componentArgs"
|
|
182
182
|
>
|
|
183
|
-
<template v-if="useLeftSlot" #left>{{ leftSlotContent }}</template>
|
|
184
|
-
<template v-if="useRightSlot" #right>{{ rightSlotContent }}</template>
|
|
183
|
+
<template v-if="args.useLeftSlot" #left>{{ args.leftSlotContent }}</template>
|
|
184
|
+
<template v-if="args.useRightSlot" #right>{{ args.rightSlotContent }}</template>
|
|
185
185
|
</StorybookComponent>
|
|
186
186
|
<div class="mt-4 text-sm text-gray-600">
|
|
187
|
-
Character count: {{
|
|
187
|
+
Character count: {{ args.modelValue.length }}
|
|
188
188
|
</div>
|
|
189
189
|
`,
|
|
190
190
|
});
|
|
@@ -229,13 +229,13 @@ WithSlots.args = {
|
|
|
229
229
|
|
|
230
230
|
export const Outlined = Template.bind({});
|
|
231
231
|
Outlined.args = {
|
|
232
|
-
|
|
232
|
+
inputVariant: "outlined",
|
|
233
233
|
placeholder: "Outlined textarea",
|
|
234
234
|
};
|
|
235
235
|
|
|
236
236
|
export const Underlined = Template.bind({});
|
|
237
237
|
Underlined.args = {
|
|
238
|
-
|
|
238
|
+
inputVariant: "underlined",
|
|
239
239
|
placeholder: "Underlined textarea",
|
|
240
240
|
};
|
|
241
241
|
|