srcdev-nuxt-forms 3.0.0 → 4.0.0

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 (86) hide show
  1. package/assets/styles/ally/_utils.css +20 -0
  2. package/assets/styles/ally/_variables.css +8 -0
  3. package/assets/styles/ally/index.css +2 -0
  4. package/assets/styles/forms/index.css +2 -0
  5. package/assets/styles/forms/themes/_error.css +85 -0
  6. package/assets/styles/forms/themes/_ghost.css +85 -0
  7. package/assets/styles/forms/themes/_input-action-underlined.css +20 -0
  8. package/assets/styles/forms/themes/_input-action.css +20 -0
  9. package/assets/styles/forms/themes/_primary.css +92 -0
  10. package/assets/styles/forms/themes/_secondary.css +85 -0
  11. package/assets/styles/forms/themes/_success.css +85 -0
  12. package/assets/styles/forms/themes/_tertiary.css +85 -0
  13. package/assets/styles/forms/themes/_warning.css +85 -0
  14. package/assets/styles/forms/themes/index.css +9 -0
  15. package/assets/styles/forms/variables/_sizes.css +71 -0
  16. package/assets/styles/forms/variables/_theme.css +11 -0
  17. package/assets/styles/forms/variables/index.css +2 -0
  18. package/assets/styles/main.css +5 -0
  19. package/assets/styles/typography/index.css +2 -0
  20. package/assets/styles/typography/utils/_font-classes.css +190 -0
  21. package/assets/styles/typography/utils/_weights.css +69 -0
  22. package/assets/styles/typography/utils/index.css +2 -0
  23. package/assets/styles/typography/variables/_colors.css +14 -0
  24. package/assets/styles/typography/variables/_reponsive-font-size.css +10 -0
  25. package/assets/styles/typography/variables/index.css +2 -0
  26. package/assets/styles/utils/_margin-helpers.css +334 -0
  27. package/assets/styles/utils/_padding-helpers.css +308 -0
  28. package/assets/styles/utils/_page.css +49 -0
  29. package/assets/styles/utils/index.css +3 -0
  30. package/assets/styles/variables/colors/_blue.css +15 -0
  31. package/assets/styles/variables/colors/_gray.css +16 -0
  32. package/assets/styles/variables/colors/_green.css +15 -0
  33. package/assets/styles/variables/colors/_orange.css +15 -0
  34. package/assets/styles/variables/colors/_red.css +15 -0
  35. package/assets/styles/variables/colors/_yellow.css +15 -0
  36. package/assets/styles/variables/colors/colors.css +6 -0
  37. package/assets/styles/variables/index.css +1 -0
  38. package/components/forms/c12/prop-validators/index.ts +38 -0
  39. package/components/forms/c12/utils.ts +14 -0
  40. package/components/forms/form-errors/InputError.vue +172 -0
  41. package/components/forms/form-errors/tests/InputError.spec.ts +67 -0
  42. package/components/forms/input-button/InputButtonCore.vue +191 -0
  43. package/components/forms/input-button/variants/InputButtonConfirm.vue +66 -0
  44. package/components/forms/input-button/variants/InputButtonSubmit.vue +62 -0
  45. package/components/forms/input-checkbox/MultipleCheckboxes.vue +203 -0
  46. package/components/forms/input-checkbox/SingleCheckbox.vue +169 -0
  47. package/components/forms/input-checkbox/tests/MultipleCheckboxes.spec.ts +98 -0
  48. package/components/forms/input-checkbox/tests/data/tags.json +67 -0
  49. package/components/forms/input-checkbox-radio/InputCheckboxRadioButton.vue +214 -0
  50. package/components/forms/input-checkbox-radio/InputCheckboxRadioCore.vue +191 -0
  51. package/components/forms/input-checkbox-radio/InputCheckboxRadioWithLabel.vue +111 -0
  52. package/components/forms/input-number/InputNumberCore.vue +203 -0
  53. package/components/forms/input-number/variants/InputNumberDefault.vue +154 -0
  54. package/components/forms/input-radio/MultipleRadiobuttons.vue +201 -0
  55. package/components/forms/input-radio/tests/MultipleRadioButtons.spec.ts +89 -0
  56. package/components/forms/input-radio/tests/data/tags.json +67 -0
  57. package/components/forms/input-range/InputRangeCore.vue +274 -0
  58. package/components/forms/input-range/variants/InputRangeDefault.vue +156 -0
  59. package/components/forms/input-range-fancy/InputRangeFancyCore.vue +450 -0
  60. package/components/forms/input-range-fancy/InputRangeFancyWithLabel.vue +124 -0
  61. package/components/forms/input-text/InputTextCore.vue +331 -0
  62. package/components/forms/input-text/variants/InputPasswordWithLabel.vue +130 -0
  63. package/components/forms/input-text/variants/InputTextAsNumberWithLabel.vue +187 -0
  64. package/components/forms/input-text/variants/InputTextWithLabel.vue +298 -0
  65. package/components/forms/input-textarea/InputTextareaCore.vue +234 -0
  66. package/components/forms/input-textarea/variants/InputTextareaWithLabel.vue +267 -0
  67. package/components/forms/toggle-switch/ToggleSwitchCore.vue +198 -0
  68. package/components/forms/toggle-switch/ToggleSwitchCoreOld.vue +216 -0
  69. package/components/forms/toggle-switch/variants/ToggleSwitchWithLabel.vue +105 -0
  70. package/components/forms/toggle-switch/variants/ToggleSwitchWithLabelInline.vue +102 -0
  71. package/components/forms/ui/FormField.vue +78 -0
  72. package/components/forms/ui/FormWrapper.vue +35 -0
  73. package/components/utils/colour-scheme-select/ColourSchemeSelect.vue +270 -0
  74. package/components/utils/colour-scheme-select/ColourSchemeSelectOld.vue +225 -0
  75. package/components/utils/dark-mode-switcher/DarkModeSwitcher.vue +47 -0
  76. package/composables/useApiRequest.ts +25 -0
  77. package/composables/useColourScheme.ts +25 -0
  78. package/composables/useErrorMessages.ts +59 -0
  79. package/composables/useFormControl.ts +248 -0
  80. package/composables/useSleep.ts +5 -0
  81. package/composables/useStyleClassPassthrough.ts +30 -0
  82. package/composables/useZodValidation.ts +148 -0
  83. package/nuxt.config.ts +0 -3
  84. package/package.json +1 -1
  85. package/types/types.forms.ts +216 -0
  86. package/types/types.zodFormControl.ts +21 -0
@@ -0,0 +1,203 @@
1
+ <template>
2
+ <fieldset class="multiple-checkboxes-fieldset" :class="[elementClasses, { error: fieldHasError }]" :data-testid>
3
+ <legend :class="[{ 'has-description': hasDescription }]">{{ legend }}</legend>
4
+
5
+ <div v-if="hasDescriptionSlot" :id="`${id}-description`">
6
+ <slot name="description"></slot>
7
+ </div>
8
+
9
+ <div class="multiple-checkboxes-items" :class="[optionsLayout]">
10
+ <template v-for="item in fieldData.data" :key="item.id">
11
+ <InputCheckboxRadioButton
12
+ v-if="isButton"
13
+ type="checkbox"
14
+ :id="`${name}-${item.value}`"
15
+ :name
16
+ :required
17
+ :label="item.label"
18
+ :fieldHasError
19
+ v-model="modelValue"
20
+ :true-value="item.value"
21
+ :size
22
+ :optionsLayout
23
+ :theme
24
+ :direction
25
+ :ariaDescribedby
26
+ >
27
+ <template #checkedIcon>
28
+ <slot name="checkedIcon"></slot>
29
+ </template>
30
+ <template #itemIcon>
31
+ <slot name="itemIcon">
32
+ <Icon name="material-symbols:add-2" class="icon" />
33
+ </slot>
34
+ </template>
35
+ </InputCheckboxRadioButton>
36
+ <InputCheckboxRadioWithLabel
37
+ v-else
38
+ type="checkbox"
39
+ :id="`${name}-${item.value}`"
40
+ :name
41
+ :required
42
+ :label="item.label"
43
+ :fieldHasError
44
+ v-model="modelValue"
45
+ :true-value="item.value"
46
+ :size
47
+ :optionsLayout
48
+ :theme
49
+ :ariaDescribedby
50
+ >
51
+ <template #checkedIcon>
52
+ <slot name="checkedIcon"></slot>
53
+ </template>
54
+ </InputCheckboxRadioWithLabel>
55
+ </template>
56
+ </div>
57
+ <InputError :errorMessage="errorMessage" :showError="fieldHasError" :id="errorId" :isDetached="true" />
58
+ </fieldset>
59
+ </template>
60
+
61
+ <script setup lang="ts">
62
+ import propValidators from '../c12/prop-validators';
63
+ import type { IOptionsConfig, IFormMultipleOptions } from '@/types/types.forms';
64
+
65
+ const { dataTestid, name, legend, label, required, fieldHasError, placeholder, isButton, errorMessage, size, optionsLayout, equalCols, styleClassPassthrough, theme, direction } = defineProps({
66
+ dataTestid: {
67
+ type: String,
68
+ default: 'multiple-checkboxes',
69
+ },
70
+ name: {
71
+ type: String,
72
+ required: true,
73
+ },
74
+ legend: {
75
+ type: String,
76
+ required: true,
77
+ },
78
+ label: {
79
+ type: String,
80
+ required: true,
81
+ },
82
+ placeholder: {
83
+ type: String,
84
+ default: '',
85
+ },
86
+ isButton: {
87
+ type: Boolean,
88
+ default: false,
89
+ },
90
+ errorMessage: {
91
+ type: [Object, String],
92
+ required: true,
93
+ },
94
+ required: {
95
+ type: Boolean,
96
+ default: false,
97
+ },
98
+ fieldHasError: {
99
+ type: Boolean,
100
+ default: false,
101
+ },
102
+ multipleOptions: {
103
+ type: Boolean,
104
+ default: false,
105
+ },
106
+ size: {
107
+ type: String as PropType<string>,
108
+ default: 'medium',
109
+ validator(value: string) {
110
+ return propValidators.size.includes(value);
111
+ },
112
+ },
113
+ optionsLayout: {
114
+ type: String as PropType<string>,
115
+ default: 'equal-widths',
116
+ validator(value: string) {
117
+ return propValidators.optionsLayout.includes(value);
118
+ },
119
+ },
120
+ equalCols: {
121
+ type: Boolean,
122
+ default: true,
123
+ },
124
+ styleClassPassthrough: {
125
+ type: Array as PropType<string[]>,
126
+ default: () => [],
127
+ },
128
+ theme: {
129
+ type: String as PropType<string>,
130
+ default: 'primary',
131
+ validator(value: string) {
132
+ return propValidators.theme.includes(value);
133
+ },
134
+ },
135
+ direction: {
136
+ type: String as PropType<'row' | 'row-reverse'>,
137
+ default: 'row',
138
+ validator(value: string) {
139
+ return ['row', 'row-reverse'].includes(value);
140
+ },
141
+ },
142
+ });
143
+
144
+ const slots = useSlots();
145
+ const hasDescriptionSlot = computed(() => slots.description !== undefined);
146
+ const hasDescription = computed(() => slots.description !== undefined);
147
+ const { elementClasses, updateElementClasses } = useStyleClassPassthrough(styleClassPassthrough);
148
+
149
+ const modelValue = defineModel();
150
+ const fieldData = defineModel('fieldData') as Ref<IFormMultipleOptions>;
151
+
152
+ const id = useId();
153
+ const errorId = `${name}-error-message`;
154
+ const ariaDescribedby = computed(() => {
155
+ const ariaDescribedbyId = hasDescriptionSlot.value ? `${name}-description` : undefined;
156
+ return fieldHasError ? errorId : ariaDescribedbyId;
157
+ });
158
+ </script>
159
+
160
+ <style lang="css">
161
+ .multiple-checkboxes-fieldset {
162
+ margin: 0;
163
+ padding: 0;
164
+ border: 0;
165
+
166
+ legend {
167
+ font-family: var(--font-family);
168
+ font-size: 1.6rem;
169
+ font-weight: 500;
170
+
171
+ &.has-description {
172
+ margin-bottom: 0;
173
+ }
174
+ }
175
+
176
+ .label-description {
177
+ font-family: var(--font-family);
178
+ font-size: 1.6rem;
179
+ margin-top: 1.2rem;
180
+ }
181
+ }
182
+
183
+ .multiple-checkboxes-items {
184
+ display: grid;
185
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
186
+ gap: 1.2rem;
187
+ margin-top: 1.2rem;
188
+
189
+ &.inline {
190
+ flex-direction: row;
191
+ flex-wrap: wrap;
192
+ }
193
+
194
+ &.block {
195
+ flex-direction: column;
196
+ }
197
+
198
+ &.equal-widths {
199
+ display: grid;
200
+ grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
201
+ }
202
+ }
203
+ </style>
@@ -0,0 +1,169 @@
1
+ <template>
2
+ <fieldset class="single-checkbox-fieldset" :class="[elementClasses, { error: fieldHasError }]" :data-testid>
3
+ <legend :class="[{ 'has-description': hasDescription }]">{{ legend }}</legend>
4
+
5
+ <div v-if="hasDescriptionSlot" :id="`${name}-description`">
6
+ <slot name="description"></slot>
7
+ </div>
8
+
9
+ <div class="single-checkbox-items" :class="[optionsLayout]">
10
+ <InputCheckboxRadioWithLabel type="checkbox" :name :required :label :fieldHasError v-model="modelValue" :trueValue :falseValue :size :theme :ariaDescribedby>
11
+ <template #checkedIcon>
12
+ <slot name="checkedIcon"></slot>
13
+ </template>
14
+ <template v-if="hasLabelContent" #labelContent>
15
+ <slot name="labelContent"></slot>
16
+ </template>
17
+ </InputCheckboxRadioWithLabel>
18
+ </div>
19
+ <InputError :errorMessage :showError="fieldHasError" :id="errorId" :isDetached="true" :styleClassPassthrough="inputErrorStyles" />
20
+ </fieldset>
21
+ </template>
22
+
23
+ <script setup lang="ts">
24
+ import propValidators from '../c12/prop-validators';
25
+ import type { IFormMultipleOptions } from '@/types/types.forms';
26
+
27
+ const props = defineProps({
28
+ dataTestid: {
29
+ type: String,
30
+ default: 'multiple-radio-buttons',
31
+ },
32
+ name: {
33
+ type: String,
34
+ required: true,
35
+ },
36
+ legend: {
37
+ type: String,
38
+ required: true,
39
+ },
40
+ label: {
41
+ type: String,
42
+ required: false,
43
+ default: '',
44
+ },
45
+ errorMessage: {
46
+ type: [Object, String],
47
+ required: true,
48
+ },
49
+ required: {
50
+ type: Boolean,
51
+ default: false,
52
+ },
53
+ fieldHasError: {
54
+ type: Boolean,
55
+ default: false,
56
+ },
57
+ multipleOptions: {
58
+ type: Boolean,
59
+ default: false,
60
+ },
61
+ size: {
62
+ type: String as PropType<string>,
63
+ default: 'medium',
64
+ validator(value: string) {
65
+ return propValidators.size.includes(value);
66
+ },
67
+ },
68
+ trueValue: {
69
+ type: [String, Number, Boolean],
70
+ default: true,
71
+ },
72
+ falseValue: {
73
+ type: [String, Number, Boolean],
74
+ default: false,
75
+ },
76
+ optionsLayout: {
77
+ type: String as PropType<string>,
78
+ default: 'equal-widths',
79
+ validator(value: string) {
80
+ return propValidators.optionsLayout.includes(value);
81
+ },
82
+ },
83
+ equalCols: {
84
+ type: Boolean,
85
+ default: true,
86
+ },
87
+ styleClassPassthrough: {
88
+ type: Array as PropType<string[]>,
89
+ default: () => [],
90
+ },
91
+ theme: {
92
+ type: String as PropType<string>,
93
+ default: 'primary',
94
+ validator(value: string) {
95
+ return propValidators.theme.includes(value);
96
+ },
97
+ },
98
+ });
99
+
100
+ const slots = useSlots();
101
+ const hasDescriptionSlot = computed(() => slots.description !== undefined);
102
+ const hasDescription = computed(() => slots.description !== undefined);
103
+ const hasLabelContent = computed(() => slots.labelContent !== undefined);
104
+
105
+ const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
106
+
107
+ const modelValue = defineModel();
108
+ const fieldData = defineModel('fieldData') as Ref<IFormMultipleOptions>;
109
+
110
+ const inputErrorStyles = ref<string[]>(props.styleClassPassthrough);
111
+
112
+ const errorId = `${name}-error-message`;
113
+ const ariaDescribedby = computed(() => {
114
+ const ariaDescribedbyId = hasDescriptionSlot.value ? `${name}-description` : undefined;
115
+ return props.fieldHasError ? errorId : ariaDescribedbyId;
116
+ });
117
+
118
+ watchEffect(() => {
119
+ if (!hasDescription.value && props.fieldHasError) {
120
+ inputErrorStyles.value.push('mbs-12');
121
+ } else {
122
+ inputErrorStyles.value = inputErrorStyles.value.filter((style) => style !== 'mbs-12');
123
+ }
124
+ });
125
+ </script>
126
+
127
+ <style lang="css">
128
+ .single-checkbox-fieldset {
129
+ margin: 0;
130
+ padding: 0;
131
+ border: 0;
132
+
133
+ legend {
134
+ font-family: var(--font-family);
135
+ font-size: 1.6rem;
136
+ font-weight: 500;
137
+
138
+ &.has-description {
139
+ margin-bottom: 0;
140
+ }
141
+ }
142
+
143
+ .label-description {
144
+ font-family: var(--font-family);
145
+ font-size: 1.6rem;
146
+ margin-top: 1.2rem;
147
+ }
148
+ }
149
+
150
+ .single-checkbox-items {
151
+ display: flex;
152
+ gap: 1.2rem;
153
+ margin-top: 1.2rem;
154
+
155
+ &.inline {
156
+ flex-direction: row;
157
+ flex-wrap: wrap;
158
+ }
159
+
160
+ &.block {
161
+ flex-direction: column;
162
+ }
163
+
164
+ &.equal-widths {
165
+ display: grid;
166
+ grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
167
+ }
168
+ }
169
+ </style>
@@ -0,0 +1,98 @@
1
+ // https://nuxt.com/docs/getting-started/testing#unit-testing
2
+ import { describe, it, expect } from 'vitest';
3
+ import { VueWrapper } from '@vue/test-utils';
4
+ import { mountSuspended } from '@nuxt/test-utils/runtime';
5
+ import ComponentUnderTest from '../MultipleCheckboxes.vue';
6
+ import tagsData from './data/tags.json';
7
+
8
+ let initialPropsData = {
9
+ dataTestid: 'multiple-checkboxes',
10
+ id: 'tags',
11
+ name: 'tags',
12
+ legend: 'Choose tags (as checkboxes)',
13
+ required: true,
14
+ label: 'Check between 3 and 8 tags',
15
+ placeholder: 'eg. Type something here',
16
+ isButton: true,
17
+ errorMessage: 'Please select between 3 and 8 tags',
18
+ fieldHasError: false,
19
+ fieldData: tagsData,
20
+ size: 'small',
21
+ optionsLayout: 'inline',
22
+ styleClassPassthrough: ['testClass'],
23
+ theme: 'primary',
24
+ };
25
+
26
+ const initialSlots = {
27
+ checkedIcon: () => ``,
28
+ itemIcon: () => `<Icon name="material-symbols:add-2" class="icon" />`,
29
+ };
30
+
31
+ let wrapper: VueWrapper<InstanceType<typeof ComponentUnderTest>>;
32
+ const wrapperFactory = (propsData = {}, slotsData = {}) => {
33
+ const mockPropsData = { ...initialPropsData, ...propsData };
34
+ const mockSlotsData = { ...initialSlots, ...slotsData };
35
+
36
+ return mountSuspended(ComponentUnderTest, {
37
+ attachTo: document.body,
38
+ props: mockPropsData,
39
+ slots: mockSlotsData,
40
+ });
41
+ };
42
+
43
+ describe('MultipleCheckboxes Component', () => {
44
+ it('Mounts', async () => {
45
+ wrapper = await wrapperFactory();
46
+ expect(wrapper).toBeTruthy();
47
+ });
48
+
49
+ it('renders properly', async () => {
50
+ wrapper = await wrapperFactory();
51
+ const dataTestIdElem = wrapper.attributes('data-testid');
52
+ expect(dataTestIdElem).toBe(initialPropsData.dataTestid);
53
+ expect(wrapper.find('[data-testid]').classes()).toContain('testClass');
54
+ });
55
+
56
+ it('updates checkbox modelValue when items clicked', async () => {
57
+ const modelValue = ref<string[]>([]);
58
+ const propsData = {
59
+ modelValue,
60
+ };
61
+ wrapper = await wrapperFactory(propsData);
62
+ const checkboxElements = wrapper.findAll('input[type="checkbox"]');
63
+
64
+ /*
65
+ * Test the first checkbox checked
66
+ **/
67
+ const firstCheckbox = checkboxElements[0];
68
+ expect(firstCheckbox.attributes('aria-checked')).toBe('false');
69
+ const firstCheckboxTrueValue = firstCheckbox.attributes('true-value');
70
+
71
+ await firstCheckbox.trigger('click');
72
+
73
+ expect(wrapper.emitted()).toHaveProperty('update:modelValue');
74
+ const firstEmittedEvents = wrapper.emitted('update:modelValue');
75
+ if (firstEmittedEvents && firstEmittedEvents[0]) {
76
+ expect(firstEmittedEvents[0]).includes(firstCheckboxTrueValue);
77
+ }
78
+
79
+ expect(firstCheckbox.attributes('aria-checked')).toBe('true');
80
+
81
+ /*
82
+ * Test the second checkbox chekced
83
+ **/
84
+ const secondCheckbox = checkboxElements[1];
85
+ expect(secondCheckbox.attributes('aria-checked')).toBe('false');
86
+ const secondCheckboxTrueValue = secondCheckbox.attributes('true-value');
87
+
88
+ await secondCheckbox.trigger('click');
89
+
90
+ expect(wrapper.emitted()).toHaveProperty('update:modelValue');
91
+ const secondEmittedEvents = wrapper.emitted('update:modelValue');
92
+ if (secondEmittedEvents && secondEmittedEvents[0]) {
93
+ expect(secondEmittedEvents[1]).includes(secondCheckboxTrueValue);
94
+ }
95
+
96
+ expect(secondCheckbox.attributes('aria-checked')).toBe('true');
97
+ });
98
+ });
@@ -0,0 +1,67 @@
1
+ {
2
+ "data": [
3
+ {
4
+ "id": "pizza",
5
+ "name": "tags",
6
+ "value": "pizza",
7
+ "label": "Pizza"
8
+ },
9
+ {
10
+ "id": "italian",
11
+ "name": "tags",
12
+ "value": "italian",
13
+ "label": "Italian"
14
+ },
15
+ {
16
+ "id": "vegetarian",
17
+ "name": "tags",
18
+ "value": "vegetarian",
19
+ "label": "Vegetarian"
20
+ },
21
+ {
22
+ "id": "stir-fry",
23
+ "name": "tags",
24
+ "value": "stir-fry",
25
+ "label": "Stir-fry"
26
+ },
27
+ {
28
+ "id": "asian",
29
+ "name": "tags",
30
+ "value": "asian",
31
+ "label": "Asian"
32
+ },
33
+ {
34
+ "id": "cookies",
35
+ "name": "tags",
36
+ "value": "cookies",
37
+ "label": "Cookies"
38
+ },
39
+ {
40
+ "id": "dessert",
41
+ "name": "tags",
42
+ "value": "dessert",
43
+ "label": "Dessert"
44
+ },
45
+ {
46
+ "id": "baking",
47
+ "name": "tags",
48
+ "value": "baking",
49
+ "label": "Baking"
50
+ },
51
+ {
52
+ "id": "pasta",
53
+ "name": "tags",
54
+ "value": "pasta",
55
+ "label": "Pasta"
56
+ },
57
+ {
58
+ "id": "chicken",
59
+ "name": "tags",
60
+ "value": "chicken",
61
+ "label": "Chicken"
62
+ }
63
+ ],
64
+ "total": 5,
65
+ "skip": 0,
66
+ "limit": 10
67
+ }