srcdev-nuxt-components 9.6.8 → 9.6.10

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.
Files changed (109) hide show
  1. package/.claude/skills/component-compliance-checklist.md +28 -1
  2. package/.claude/skills/component-inline-action-button.md +1 -1
  3. package/.claude/skills/component-local-style-override.md +1 -1
  4. package/.claude/skills/component-naming.md +17 -12
  5. package/.claude/skills/components/container-glow.md +2 -2
  6. package/.claude/skills/components/form-field.md +2 -2
  7. package/.claude/skills/components/form-wrapper.md +1 -1
  8. package/.claude/skills/components/glowing-border.md +1 -1
  9. package/.claude/skills/components/input-description.md +7 -7
  10. package/.claude/skills/components/input-error.md +3 -3
  11. package/.claude/skills/components/input-label.md +7 -8
  12. package/.claude/skills/components/input-number.md +17 -17
  13. package/.claude/skills/components/input-select.md +153 -0
  14. package/.claude/skills/components/input-text.md +220 -0
  15. package/.claude/skills/components/input-textarea.md +147 -0
  16. package/.claude/skills/components/select-menu.md +4 -4
  17. package/.claude/skills/components/toggle-switch.md +143 -0
  18. package/.claude/skills/composable-zod-validation.md +1 -1
  19. package/.claude/skills/css-nesting-conventions.md +22 -1
  20. package/.claude/skills/icon-sets.md +1 -1
  21. package/.claude/skills/icon-sizing.md +1 -1
  22. package/.claude/skills/index.md +5 -5
  23. package/.claude/skills/storybook-add-story.md +1 -1
  24. package/.claude/skills/theming-component-token-pattern.md +8 -8
  25. package/.claude/skills/theming-form-geometry-tokens.md +1 -1
  26. package/.vscode/srcdev-component-input-select.code-snippets +20 -14
  27. package/.vscode/srcdev-component-input-text.code-snippets +46 -40
  28. package/.vscode/srcdev-component-input-textarea.code-snippets +19 -14
  29. package/.vscode/srcdev-component-toggle-switch.code-snippets +19 -29
  30. package/app/assets/styles/setup/04.elements/forms/00.element-defaults.css +2 -2
  31. package/app/assets/styles/setup/04.elements/forms/06.button-geometry.css +1 -1
  32. package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +2 -2
  33. package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +4 -4
  34. package/app/components/02.molecules/progress-tracker/stories/ProgressTracker.stories.ts +4 -4
  35. package/app/components/05.forms/form-errors/CONSUMER-STYLING.md +1 -1
  36. package/app/components/05.forms/form-field/CONSUMER-STYLING.md +2 -2
  37. package/app/components/05.forms/input-button/stories/InputButton.stories.ts +3 -3
  38. package/app/components/05.forms/input-description/CONSUMER-STYLING.md +6 -6
  39. package/app/components/05.forms/input-label/CONSUMER-STYLING.md +7 -7
  40. package/app/components/05.forms/input-number/CONSUMER-STYLING.md +9 -5
  41. package/app/components/05.forms/input-number/InputNumber.vue +2 -2
  42. package/app/components/05.forms/input-select/CONSUMER-STYLING.md +80 -10
  43. package/app/components/05.forms/input-select/InputSelect.vue +198 -0
  44. package/app/components/05.forms/input-select/InputSelectField.vue +130 -0
  45. package/app/components/05.forms/input-select/stories/InputSelect.stories.ts +184 -0
  46. package/app/components/05.forms/input-select/stories/InputSelectField.stories.ts +187 -0
  47. package/app/components/05.forms/input-select/tests/{InputSelectCore.spec.ts → InputSelect.spec.ts} +75 -12
  48. package/app/components/05.forms/input-select/tests/InputSelectField.spec.ts +121 -0
  49. package/app/components/05.forms/input-text/CONSUMER-STYLING.md +85 -15
  50. package/app/components/05.forms/input-text/{InputPasswordWithLabel.vue → InputPasswordField.vue} +16 -19
  51. package/app/components/05.forms/input-text/{InputTextCore.vue → InputText.vue} +42 -78
  52. package/app/components/05.forms/input-text/{InputTextAsNumberWithLabel.vue → InputTextAsNumberField.vue} +66 -62
  53. package/app/components/05.forms/input-text/InputTextField.vue +168 -0
  54. package/app/components/05.forms/input-text/stories/InputPasswordField.stories.ts +163 -0
  55. package/app/components/05.forms/input-text/stories/InputText.stories.ts +194 -0
  56. package/app/components/05.forms/input-text/stories/InputTextAsNumberField.stories.ts +182 -0
  57. package/app/components/05.forms/input-text/stories/InputTextField.stories.ts +206 -0
  58. package/app/components/05.forms/input-text/tests/{InputPasswordWithLabel.spec.ts → InputPasswordField.spec.ts} +42 -3
  59. package/app/components/05.forms/input-text/tests/InputText.spec.ts +166 -0
  60. package/app/components/05.forms/input-text/tests/InputTextAsNumberField.spec.ts +135 -0
  61. package/app/components/05.forms/input-text/tests/InputTextField.spec.ts +130 -0
  62. package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +94 -12
  63. package/app/components/05.forms/input-textarea/InputTextarea.vue +165 -0
  64. package/app/components/05.forms/input-textarea/InputTextareaField.vue +159 -0
  65. package/app/components/05.forms/input-textarea/stories/InputTextarea.stories.ts +198 -0
  66. package/app/components/05.forms/input-textarea/stories/InputTextareaField.stories.ts +161 -0
  67. package/app/components/05.forms/input-textarea/tests/InputTextarea.spec.ts +149 -0
  68. package/app/components/05.forms/input-textarea/tests/InputTextareaField.spec.ts +122 -0
  69. package/app/components/05.forms/patterns/stories/ContactForm.stories.ts +13 -13
  70. package/app/components/05.forms/patterns/stories/LoginForm.stories.ts +6 -6
  71. package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +30 -32
  72. package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +71 -50
  73. package/app/components/05.forms/toggle-switch/{ToggleSwitchCore.vue → ToggleSwitch.vue} +58 -73
  74. package/app/components/05.forms/toggle-switch/{ToggleSwitchWithLabel.vue → ToggleSwitchField.vue} +65 -19
  75. package/app/components/05.forms/toggle-switch/stories/ToggleSwitch.stories.ts +148 -0
  76. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchField.stories.ts +176 -0
  77. package/app/components/05.forms/toggle-switch/tests/ToggleSwitch.spec.ts +145 -0
  78. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchField.spec.ts +155 -0
  79. package/package.json +1 -1
  80. package/.claude/skills/components/input-select-core.md +0 -151
  81. package/.claude/skills/components/input-text-core.md +0 -204
  82. package/.claude/skills/components/input-textarea-core.md +0 -154
  83. package/.claude/skills/components/toggle-switch-core.md +0 -169
  84. package/app/components/05.forms/input-select/InputSelectCore.vue +0 -188
  85. package/app/components/05.forms/input-select/InputSelectWithLabel.vue +0 -118
  86. package/app/components/05.forms/input-select/stories/InputSelectCore.stories.ts +0 -69
  87. package/app/components/05.forms/input-select/stories/InputSelectWithLabel.stories.ts +0 -115
  88. package/app/components/05.forms/input-select/tests/InputSelectWithLabel.spec.ts +0 -87
  89. package/app/components/05.forms/input-text/InputTextWithLabel.vue +0 -154
  90. package/app/components/05.forms/input-text/stories/InputPasswordWithLabel.stories.ts +0 -368
  91. package/app/components/05.forms/input-text/stories/InputTextAsNumberWithLabel.stories.ts +0 -480
  92. package/app/components/05.forms/input-text/stories/InputTextCore.stories.ts +0 -267
  93. package/app/components/05.forms/input-text/stories/InputTextWithLabel.stories.ts +0 -319
  94. package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +0 -75
  95. package/app/components/05.forms/input-text/tests/InputTextCore.spec.ts +0 -60
  96. package/app/components/05.forms/input-text/tests/InputTextWithLabel.spec.ts +0 -71
  97. package/app/components/05.forms/input-textarea/InputTextareaCore.vue +0 -159
  98. package/app/components/05.forms/input-textarea/InputTextareaWithLabel.vue +0 -136
  99. package/app/components/05.forms/input-textarea/stories/InputTextareaCore.stories.ts +0 -247
  100. package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +0 -458
  101. package/app/components/05.forms/input-textarea/tests/InputTextareaCore.spec.ts +0 -119
  102. package/app/components/05.forms/input-textarea/tests/InputTextareaWithLabel.spec.ts +0 -93
  103. package/app/components/05.forms/toggle-switch/ToggleSwitchWithLabelInline.vue +0 -81
  104. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchCore.stories.ts +0 -251
  105. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabel.stories.ts +0 -251
  106. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabelInline.stories.ts +0 -283
  107. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchCore.spec.ts +0 -101
  108. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchWithLabel.spec.ts +0 -133
  109. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchWithLabelInline.spec.ts +0 -66
@@ -1,188 +0,0 @@
1
- <template>
2
- <div
3
- class="input-select-wrapper"
4
- :data-theme="theme"
5
- :data-invalid="fieldHasError ? '' : null"
6
- :class="[inputVariant, { dirty: isDirty }, { active: isActive }, { error: fieldHasError }]"
7
- >
8
- <select
9
- :id
10
- v-model="modelValue"
11
- :aria-invalid="fieldHasError ? 'true' : undefined"
12
- :class="['input-select-core', elementClasses]"
13
- :name
14
- :title
15
- :required
16
- :aria-describedby="ariaDescribedby"
17
- @focusin="isActive = true"
18
- @focusout="isActive = false"
19
- @change="isDirty = true"
20
- >
21
- <option v-if="placeholder" value="" disabled :selected="!modelValue" class="input-select-core-option placeholder">
22
- {{ placeholder }}
23
- </option>
24
- <option
25
- v-for="item in fieldData.data"
26
- :key="item.id"
27
- :value="item.value"
28
- :selected="String(item.value) === String(modelValue)"
29
- class="input-select-core-option"
30
- >
31
- <Icon
32
- v-if="item.icon"
33
- :name="item.icon"
34
- class="input-select-core-option-decorator-icon"
35
- aria-hidden="true"
36
- focusable="false"
37
- />
38
- {{ item.label }}
39
- </option>
40
- </select>
41
- </div>
42
- </template>
43
-
44
- <script setup lang="ts">
45
- import type { IFormMultipleOptions, FormUiTheme, InputUiVariant } from "~/types/forms/types.forms";
46
-
47
- interface Props {
48
- id: string;
49
- name: string;
50
- placeholder?: string;
51
- title?: string;
52
- required?: boolean;
53
- fieldHasError?: boolean;
54
- styleClassPassthrough?: string | string[];
55
- theme?: FormUiTheme;
56
- inputVariant?: InputUiVariant;
57
- ariaDescribedby?: string;
58
- }
59
-
60
- const props = withDefaults(defineProps<Props>(), {
61
- placeholder: "",
62
- title: "Please select an option",
63
- required: false,
64
- fieldHasError: false,
65
- styleClassPassthrough: () => [],
66
- theme: "default",
67
- inputVariant: "normal",
68
- ariaDescribedby: "",
69
- });
70
-
71
- const modelValue = defineModel<string | number | readonly number[]>({ required: true });
72
- const isDirty = defineModel<boolean>("isDirty");
73
- const isActive = defineModel<boolean>("isActive");
74
- const fieldData = defineModel<IFormMultipleOptions>("fieldData", { required: true });
75
-
76
- const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
77
- </script>
78
-
79
- <style lang="css">
80
- @layer components {
81
- .input-select-wrapper {
82
- /* Public --input-select-* tokens, inline-fallback to the shared --theme-* tokens (see
83
- theming-component-token-pattern.md) — overriding one here doesn't touch every other themed
84
- input/control that also reads --theme-input-surface/--theme-border. --_border is the one
85
- genuinely reused value (border, underlined border-bottom, open-picker border); the rest of
86
- this component's tokens are used at exactly one point each, so they're inlined directly
87
- rather than wrapped in a private var with no composition or state-swap behind it (see
88
- pitfall #20 in CLAUDE.md). */
89
- --_border: var(--input-select-border, var(--theme-border));
90
- /* Split so hover (mouse) and :focus-visible (keyboard/assistive) can diverge — both default to
91
- the same --theme-border-focus today, identical appearance to before this token existed, but
92
- each now has its own override point. */
93
- --_border-focus: var(--input-select-border-focus, var(--theme-border-focus));
94
-
95
- background-color: var(--input-select-surface, var(--theme-input-surface));
96
- overflow: hidden;
97
-
98
- z-index: 2;
99
- position: relative;
100
- transition: all var(--theme-form-transition-duration) ease-in-out;
101
-
102
- &.normal {
103
- border: var(--form-element-border-width) solid var(--_border);
104
- border-radius: var(--form-input-border-radius);
105
- outline: var(--form-element-outline-width) solid transparent;
106
- }
107
-
108
- &.underlined {
109
- border-bottom: var(--form-element-border-bottom-width-underlined) solid var(--_border);
110
- }
111
-
112
- &:has(:hover) {
113
- outline: var(--form-element-outline-width-focus) solid
114
- var(--input-select-border-hover, var(--theme-border-focus));
115
- outline-offset: var(--form-element-outline-offset-focus);
116
- }
117
-
118
- &:has(select:focus-visible) {
119
- outline: var(--form-element-outline-width-focus) solid var(--_border-focus);
120
- outline-offset: var(--form-element-outline-offset-focus);
121
- }
122
-
123
- .input-select-core {
124
- appearance: none;
125
- background-color: transparent;
126
- display: flex;
127
- align-items: center;
128
-
129
- /* Start modern Select CSS */
130
- &,
131
- &::picker(select) {
132
- appearance: base-select;
133
- }
134
-
135
- &::picker(select) {
136
- transition:
137
- display allow-discrete var(--theme-form-transition-duration),
138
- opacity var(--theme-form-transition-duration),
139
- overlay var(--theme-form-transition-duration) allow-discrete;
140
- }
141
-
142
- &:not(:open)::picker(select) {
143
- opacity: 0;
144
- }
145
-
146
- &:open::picker(select) {
147
- opacity: 1;
148
- border: var(--form-element-border-width) solid var(--_border);
149
- outline: var(--form-element-outline-width) solid var(--_border-focus);
150
-
151
- @starting-style {
152
- opacity: 0;
153
- }
154
- }
155
-
156
- border: none;
157
- outline: none;
158
- border-radius: 0;
159
- width: 100%;
160
-
161
- /* END modern Select CSS */
162
-
163
- font-family: var(--font-family);
164
- font-size: var(--input-font-size);
165
- /* line-height: var(--input-element-line-height); */
166
- padding-block: var(--input-padding-block);
167
- padding-inline: var(--input-padding-inline);
168
- min-height: var(--input-min-height);
169
-
170
- .input-select-core-option {
171
- min-height: var(--input-select-option-min-height);
172
- gap: 1.5rem;
173
- transition: all var(--theme-form-transition-duration) ease-in-out;
174
-
175
- &:hover {
176
- background-color: var(--input-select-surface-hover, var(--theme-input-surface-hover));
177
- }
178
-
179
- .input-select-core-option-decorator-icon {
180
- display: inline-block;
181
- width: 1.5rem;
182
- height: 1.5rem;
183
- }
184
- }
185
- }
186
- }
187
- }
188
- </style>
@@ -1,118 +0,0 @@
1
- <template>
2
- <div>
3
- <div
4
- class="input-select-with-label"
5
- :class="[inputVariant, { dirty: isDirty }, { active: isActive }, { error: fieldHasError }]"
6
- :data-testid
7
- :data-theme="theme"
8
- :data-invalid="fieldHasError ? '' : null"
9
- >
10
- <InputLabel
11
- :id
12
- :required
13
- :input-variant
14
- :field-has-error
15
- :style-class-passthrough="['input-select-label']"
16
- >
17
- <template #textLabel>{{ label }}</template>
18
- </InputLabel>
19
-
20
- <InputDescription
21
- v-if="inputVariant !== 'outlined'"
22
- :description-id
23
- :input-variant
24
- :field-has-error="fieldHasError"
25
- :style-class-passthrough="['input-text-description']"
26
- >
27
- <template v-if="slots.descriptionHtml" #descriptionHtml>
28
- <slot name="descriptionHtml"></slot>
29
- </template>
30
- <template v-if="slots.descriptionText" #descriptionText>
31
- <slot name="descriptionText"></slot>
32
- </template>
33
- </InputDescription>
34
-
35
- <InputSelectCore
36
- :id
37
- v-model="modelValue"
38
- v-model:field-data="fieldData"
39
- v-model:is-dirty="isDirty"
40
- v-model:is-active="isActive"
41
- :name
42
- :placeholder
43
- :field-has-error
44
- :required
45
- :style-class-passthrough
46
- :theme="FormUiTheme"
47
- :aria-describedby="ariaDescribedby()"
48
- :input-variant
49
- />
50
-
51
- <InputError
52
- :id="errorId"
53
- :error-message="errorMessage"
54
- :show-error="fieldHasError"
55
- :is-detached="false"
56
- :input-variant
57
- />
58
- </div>
59
- <InputDescription
60
- v-if="inputVariant === 'outlined'"
61
- :description-id
62
- :input-variant
63
- :field-has-error="fieldHasError"
64
- :style-class-passthrough="['input-text-description']"
65
- >
66
- <template v-if="slots.descriptionHtml" #descriptionHtml>
67
- <slot name="descriptionHtml"></slot>
68
- </template>
69
- <template v-if="slots.descriptionText" #descriptionText>
70
- <slot name="descriptionText"></slot>
71
- </template>
72
- </InputDescription>
73
- </div>
74
- </template>
75
-
76
- <script setup lang="ts">
77
- import type { FormUiTheme, InputUiVariant, IFormMultipleOptions } from "~/types/forms/types.forms";
78
-
79
- interface Props {
80
- dataTestid?: string;
81
- name: string;
82
- label: string;
83
- placeholder?: string;
84
- errorMessage: object | string;
85
- required?: boolean;
86
- fieldHasError?: boolean;
87
- styleClassPassthrough?: string | string[];
88
- theme?: FormUiTheme;
89
- inputVariant?: InputUiVariant;
90
- }
91
-
92
- const props = withDefaults(defineProps<Props>(), {
93
- dataTestid: "input-select-with-label",
94
- placeholder: "",
95
- required: false,
96
- fieldHasError: false,
97
- styleClassPassthrough: () => [],
98
- theme: "default",
99
- inputVariant: "normal",
100
- });
101
-
102
- const slots = useSlots();
103
-
104
- const FormUiTheme = computed(() => {
105
- return props.fieldHasError ? "error" : props.theme;
106
- });
107
-
108
- const { id, errorId, descriptionId, ariaDescribedby } = useAriaDescribedById(
109
- props.name,
110
- toRef(props, "fieldHasError"),
111
- slots
112
- );
113
-
114
- const modelValue = defineModel<string | number | readonly number[]>({ required: true });
115
- const isDirty = defineModel<boolean>("isDirty");
116
- const isActive = defineModel<boolean>("isActive");
117
- const fieldData = defineModel<IFormMultipleOptions>("fieldData", { required: true });
118
- </script>
@@ -1,69 +0,0 @@
1
- import type { Meta, StoryFn } from "@nuxtjs/storybook";
2
- import { ref } from "vue";
3
- import StorybookComponent from "../InputSelectCore.vue";
4
- import type { IFormMultipleOptions, InputUiVariant } from "~/types/forms/types.forms.d";
5
-
6
- interface InputSelectCoreStoryArgs {
7
- id: string;
8
- name: string;
9
- placeholder: string;
10
- required: boolean;
11
- fieldHasError: boolean;
12
- inputVariant: InputUiVariant;
13
- }
14
-
15
- export default {
16
- title: "Components/Forms/Input Select/InputSelectCore",
17
- component: StorybookComponent,
18
- argTypes: {
19
- id: { control: "text", table: { category: "Basic" } },
20
- name: { control: "text", table: { category: "Basic" } },
21
- placeholder: { control: "text", table: { category: "Basic" } },
22
- required: { control: "boolean", table: { category: "Basic" } },
23
- fieldHasError: { control: "boolean", table: { category: "States" } },
24
- inputVariant: {
25
- control: { type: "select" },
26
- options: ["normal", "outlined", "underlined"],
27
- table: { category: "Styling" },
28
- },
29
- },
30
- args: {
31
- id: "select-default",
32
- name: "selectDefault",
33
- placeholder: "Choose a service",
34
- required: false,
35
- fieldHasError: false,
36
- inputVariant: "normal",
37
- },
38
- } as Meta<InputSelectCoreStoryArgs>;
39
-
40
- const Template: StoryFn<InputSelectCoreStoryArgs> = (args) => ({
41
- components: { StorybookComponent },
42
- setup() {
43
- const modelValue = ref<string | number>("");
44
- const fieldData = ref<IFormMultipleOptions>({
45
- data: [
46
- { id: "1", name: "balayage", value: "balayage", label: "Balayage" },
47
- { id: "2", name: "highlights", value: "highlights", label: "Highlights" },
48
- { id: "3", name: "full-colour", value: "full-colour", label: "Full Colour" },
49
- ],
50
- total: 3,
51
- skip: 0,
52
- limit: 10,
53
- });
54
- return { args, modelValue, fieldData };
55
- },
56
- template: `
57
- <div style="margin: 36px; max-width: 320px;">
58
- <StorybookComponent v-bind="args" v-model="modelValue" v-model:field-data="fieldData" />
59
- </div>
60
- `,
61
- });
62
-
63
- export const Default = Template.bind({});
64
-
65
- export const ErrorState = Template.bind({});
66
- ErrorState.args = { fieldHasError: true };
67
-
68
- export const Underlined = Template.bind({});
69
- Underlined.args = { inputVariant: "underlined" };
@@ -1,115 +0,0 @@
1
- import type { Meta, StoryFn } from "@nuxtjs/storybook";
2
- import { ref, computed } from "vue";
3
- import StorybookComponent from "../InputSelectWithLabel.vue";
4
- import type { FormUiTheme, InputUiVariant, IFormMultipleOptions } from "~/types/forms/types.forms.d";
5
-
6
- interface InputSelectWithLabelStoryArgs {
7
- modelValue: string | number;
8
- name: string;
9
- label: string;
10
- placeholder: string;
11
- errorMessage: string;
12
- fieldHasError: boolean;
13
- required: boolean;
14
- theme: FormUiTheme;
15
- inputVariant: InputUiVariant;
16
- styleClassPassthrough: string[];
17
- useDescriptionSlot: boolean;
18
- descriptionContent: string;
19
- }
20
-
21
- export default {
22
- title: "Components/Forms/Input Select/InputSelectWithLabel",
23
- component: StorybookComponent,
24
- argTypes: {
25
- modelValue: { control: "text", table: { category: "Model" } },
26
- name: { control: "text", table: { category: "Basic" } },
27
- label: { control: "text", table: { category: "Basic" } },
28
- placeholder: { control: "text", table: { category: "Basic" } },
29
- errorMessage: { control: "text", table: { category: "Validation" } },
30
- fieldHasError: { control: "boolean", table: { category: "Validation" } },
31
- required: { control: "boolean", table: { category: "Validation" } },
32
- theme: {
33
- control: { type: "select" },
34
- options: ["default", "success", "error", "warning"],
35
- table: { category: "Styling" },
36
- },
37
- inputVariant: {
38
- control: { type: "select" },
39
- options: ["normal", "outlined", "underlined"],
40
- table: { category: "Styling" },
41
- },
42
- styleClassPassthrough: { control: "object", table: { category: "Styling" } },
43
- useDescriptionSlot: { control: "boolean", table: { category: "Slots" } },
44
- descriptionContent: { control: "text", table: { category: "Slots" } },
45
- },
46
- args: {
47
- modelValue: "",
48
- name: "colour",
49
- label: "Favourite colour",
50
- placeholder: "Choose a colour",
51
- errorMessage: "",
52
- fieldHasError: false,
53
- required: false,
54
- theme: "default",
55
- inputVariant: "normal",
56
- styleClassPassthrough: [],
57
- useDescriptionSlot: false,
58
- descriptionContent: "Pick the colour you like best",
59
- },
60
- } as Meta<InputSelectWithLabelStoryArgs>;
61
-
62
- // `args` is Storybook's own reactive object — bind to it directly (`args.x`) rather than
63
- // destructuring it into local variables/refs, which would snapshot the values once at setup()
64
- // and stop reflecting later Controls-panel changes. `componentArgs` strips the non-prop
65
- // slot-toggle args so they don't leak onto the component as unknown attributes.
66
- const Template: StoryFn<InputSelectWithLabelStoryArgs> = (args) => ({
67
- components: { StorybookComponent },
68
- setup() {
69
- const fieldData = ref<IFormMultipleOptions>({
70
- data: [
71
- { id: "1", name: "red", value: "red", label: "Red" },
72
- { id: "2", name: "blue", value: "blue", label: "Blue" },
73
- { id: "3", name: "green", value: "green", label: "Green" },
74
- ],
75
- total: 3,
76
- skip: 0,
77
- limit: 10,
78
- });
79
-
80
- const componentArgs = computed(() => {
81
- const { useDescriptionSlot, descriptionContent, ...rest } = args;
82
- return rest;
83
- });
84
-
85
- return { args, componentArgs, fieldData };
86
- },
87
- template: `
88
- <div style="margin: 36px; max-width: 320px;">
89
- <StorybookComponent v-model="args.modelValue" v-model:field-data="fieldData" v-bind="componentArgs">
90
- <template v-if="args.useDescriptionSlot" #descriptionText>{{ args.descriptionContent }}</template>
91
- </StorybookComponent>
92
- </div>
93
- `,
94
- });
95
-
96
- export const Default = Template.bind({});
97
-
98
- export const WithError = Template.bind({});
99
- WithError.args = {
100
- fieldHasError: true,
101
- errorMessage: "Please choose a colour",
102
- theme: "error",
103
- };
104
-
105
- export const Required = Template.bind({});
106
- Required.args = { required: true };
107
-
108
- export const WithDescription = Template.bind({});
109
- WithDescription.args = { useDescriptionSlot: true };
110
-
111
- export const Underlined = Template.bind({});
112
- Underlined.args = { inputVariant: "underlined" };
113
-
114
- export const Outlined = Template.bind({});
115
- Outlined.args = { inputVariant: "outlined", useDescriptionSlot: true };
@@ -1,87 +0,0 @@
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 "../InputSelectWithLabel.vue";
5
-
6
- const initialFieldData = {
7
- data: [
8
- { id: "1", name: "red", value: "red", label: "Red" },
9
- { id: "2", name: "blue", value: "blue", label: "Blue" },
10
- ],
11
- total: 2,
12
- skip: 0,
13
- limit: 10,
14
- };
15
-
16
- const initialPropsData = {
17
- name: "colour",
18
- label: "Colour",
19
- errorMessage: "",
20
- modelValue: "red",
21
- fieldData: initialFieldData,
22
- };
23
-
24
- let wrapper: VueWrapper<InstanceType<typeof ComponentUnderTest>>;
25
- const wrapperFactory = (propsData = {}, slots = {}) => {
26
- const mockPropsData = { ...initialPropsData, ...propsData };
27
-
28
- return mountSuspended(ComponentUnderTest, {
29
- attachTo: document.body,
30
- props: mockPropsData,
31
- slots,
32
- });
33
- };
34
-
35
- describe("InputSelectWithLabel", () => {
36
- it("mounts without error", async () => {
37
- wrapper = await wrapperFactory();
38
- expect(wrapper.vm).toBeTruthy();
39
- });
40
-
41
- it("renders the label text", async () => {
42
- wrapper = await wrapperFactory({ label: "Favourite colour" });
43
- expect(wrapper.text()).toContain("Favourite colour");
44
- });
45
-
46
- it("renders a native select with an option per fieldData item", async () => {
47
- wrapper = await wrapperFactory();
48
- expect(wrapper.find("select").exists()).toBe(true);
49
- expect(wrapper.findAll("option").map((o) => o.text())).toEqual(["Red", "Blue"]);
50
- });
51
-
52
- it("sets data-invalid and the error class when fieldHasError is true", async () => {
53
- wrapper = await wrapperFactory({ fieldHasError: true });
54
- expect(wrapper.find(".input-select-with-label").attributes("data-invalid")).toBe("");
55
- expect(wrapper.find(".input-select-with-label").classes()).toContain("error");
56
- });
57
-
58
- it("renders the error message when fieldHasError is true", async () => {
59
- wrapper = await wrapperFactory({ fieldHasError: true, errorMessage: "Please choose a colour" });
60
- expect(wrapper.text()).toContain("Please choose a colour");
61
- });
62
-
63
- it("renders the descriptionText slot", async () => {
64
- wrapper = await wrapperFactory({}, { descriptionText: () => "Pick your favourite" });
65
- expect(wrapper.text()).toContain("Pick your favourite");
66
- });
67
-
68
- it("applies inputVariant as a class on the wrapper", async () => {
69
- wrapper = await wrapperFactory({ inputVariant: "underlined" });
70
- expect(wrapper.find(".input-select-with-label").classes()).toContain("underlined");
71
- });
72
-
73
- it("applies styleClassPassthrough", async () => {
74
- wrapper = await wrapperFactory({ styleClassPassthrough: "custom-select" });
75
- expect(wrapper.find("select").classes()).toContain("custom-select");
76
- });
77
-
78
- it("forwards required to the native select", async () => {
79
- wrapper = await wrapperFactory({ required: true });
80
- expect(wrapper.find("select").attributes("required")).toBeDefined();
81
- });
82
-
83
- it("applies the error theme to InputSelectCore when fieldHasError is true", async () => {
84
- wrapper = await wrapperFactory({ fieldHasError: true, theme: "default" });
85
- expect(wrapper.find(".input-select-wrapper").attributes("data-theme")).toBe("error");
86
- });
87
- });