@mozaic-ds/vue 1.0.0-rc.2 → 2.1.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 (222) hide show
  1. package/LICENSE +51 -0
  2. package/README.md +76 -77
  3. package/dist/mozaic-vue.css +1 -1
  4. package/dist/mozaic-vue.d.ts +1664 -0
  5. package/dist/mozaic-vue.js +1943 -0
  6. package/dist/mozaic-vue.js.map +1 -0
  7. package/dist/mozaic-vue.umd.cjs +2 -0
  8. package/dist/mozaic-vue.umd.cjs.map +1 -0
  9. package/env.d.ts +1 -0
  10. package/package.json +78 -51
  11. package/src/components/Contributing.mdx +118 -0
  12. package/src/components/GettingStarted.mdx +45 -0
  13. package/src/components/Introduction.mdx +100 -0
  14. package/src/components/Support.mdx +18 -0
  15. package/src/components/breadcrumb/MBreadcrumb.spec.ts +105 -0
  16. package/src/components/breadcrumb/MBreadcrumb.stories.ts +82 -0
  17. package/src/components/breadcrumb/MBreadcrumb.vue +52 -55
  18. package/src/components/button/MButton.spec.ts +191 -0
  19. package/src/components/button/MButton.stories.ts +59 -0
  20. package/src/components/button/MButton.vue +98 -154
  21. package/src/components/checkbox/MCheckbox.spec.ts +104 -0
  22. package/src/components/checkbox/MCheckbox.stories.ts +83 -0
  23. package/src/components/checkbox/MCheckbox.vue +60 -101
  24. package/src/components/checkboxgroup/MCheckboxGroup.spec.ts +78 -0
  25. package/src/components/checkboxgroup/MCheckboxGroup.stories.ts +61 -0
  26. package/src/components/checkboxgroup/MCheckboxGroup.vue +97 -0
  27. package/src/components/datepicker/MDatepicker.spec.ts +95 -0
  28. package/src/components/datepicker/MDatepicker.stories.ts +75 -0
  29. package/src/components/datepicker/MDatepicker.vue +114 -0
  30. package/src/components/divider/MDivider.spec.ts +57 -0
  31. package/src/components/divider/MDivider.stories.ts +64 -0
  32. package/src/components/divider/MDivider.vue +56 -0
  33. package/src/components/drawer/MDrawer.spec.ts +100 -0
  34. package/src/components/drawer/MDrawer.stories.ts +128 -0
  35. package/src/components/drawer/MDrawer.vue +140 -0
  36. package/src/components/field/MField.spec.ts +166 -0
  37. package/src/components/field/MField.stories.ts +369 -0
  38. package/src/components/field/MField.vue +78 -61
  39. package/src/components/fieldgroup/MFieldGroup.spec.ts +165 -0
  40. package/src/components/fieldgroup/MFieldGroup.stories.ts +416 -0
  41. package/src/components/fieldgroup/MFieldGroup.vue +79 -0
  42. package/src/components/flag/MFlag.spec.ts +46 -0
  43. package/src/components/flag/MFlag.stories.ts +46 -0
  44. package/src/components/flag/MFlag.vue +28 -39
  45. package/src/components/iconbutton/MIconButton.spec.ts +108 -0
  46. package/src/components/iconbutton/MIconButton.stories.ts +74 -0
  47. package/src/components/iconbutton/MIconButton.vue +73 -0
  48. package/src/components/link/MLink.spec.ts +154 -0
  49. package/src/components/link/MLink.stories.ts +89 -0
  50. package/src/components/link/MLink.vue +86 -120
  51. package/src/components/loader/MLoader.spec.ts +104 -0
  52. package/src/components/loader/MLoader.stories.ts +43 -0
  53. package/src/components/loader/MLoader.vue +66 -55
  54. package/src/components/loadingoverlay/MLoadingOverlay.spec.ts +37 -0
  55. package/src/components/loadingoverlay/MLoadingOverlay.stories.ts +40 -0
  56. package/src/components/loadingoverlay/MLoadingOverlay.vue +28 -0
  57. package/src/components/modal/MModal.spec.ts +103 -0
  58. package/src/components/modal/MModal.stories.ts +127 -0
  59. package/src/components/modal/MModal.vue +106 -154
  60. package/src/components/numberbadge/MNumberBadge.spec.ts +56 -0
  61. package/src/components/numberbadge/MNumberBadge.stories.ts +48 -0
  62. package/src/components/numberbadge/MNumberBadge.vue +45 -0
  63. package/src/components/overlay/MOverlay.spec.ts +51 -0
  64. package/src/components/overlay/MOverlay.stories.ts +35 -0
  65. package/src/components/overlay/MOverlay.vue +27 -19
  66. package/src/components/pagination/MPagination.spec.ts +123 -0
  67. package/src/components/pagination/MPagination.stories.ts +83 -0
  68. package/src/components/pagination/MPagination.vue +120 -140
  69. package/src/components/passwordinput/MPasswordInput.spec.ts +104 -0
  70. package/src/components/passwordinput/MPasswordInput.stories.ts +75 -0
  71. package/src/components/passwordinput/MPasswordInput.vue +126 -77
  72. package/src/components/pincode/MPincode.spec.ts +126 -0
  73. package/src/components/pincode/MPincode.stories.ts +68 -0
  74. package/src/components/pincode/MPincode.vue +148 -0
  75. package/src/components/quantityselector/MQuantitySelector.spec.ts +262 -0
  76. package/src/components/quantityselector/MQuantitySelector.stories.ts +89 -0
  77. package/src/components/quantityselector/MQuantitySelector.vue +159 -148
  78. package/src/components/radio/MRadio.spec.ts +104 -0
  79. package/src/components/radio/MRadio.stories.ts +68 -0
  80. package/src/components/radio/MRadio.vue +56 -39
  81. package/src/components/radiogroup/MRadioGroup.spec.ts +54 -0
  82. package/src/components/radiogroup/MRadioGroup.stories.ts +61 -0
  83. package/src/components/radiogroup/MRadioGroup.vue +79 -0
  84. package/src/components/select/MSelect.spec.ts +114 -0
  85. package/src/components/select/MSelect.stories.ts +101 -0
  86. package/src/components/select/MSelect.vue +77 -119
  87. package/src/components/statusbadge/MStatusBadge.stories.ts +45 -0
  88. package/src/components/statusbadge/MStatusBadge.vue +40 -0
  89. package/src/components/statusbadge/MstatusBadge.spec.ts +16 -0
  90. package/src/components/statusdot/MStatusDot.spec.ts +51 -0
  91. package/src/components/statusdot/MStatusDot.stories.ts +48 -0
  92. package/src/components/statusdot/MStatusDot.vue +36 -0
  93. package/src/components/statusnotification/MStatusNotification.spec.ts +103 -0
  94. package/src/components/statusnotification/MStatusNotification.stories.ts +89 -0
  95. package/src/components/statusnotification/MStatusNotification.vue +106 -0
  96. package/src/components/tabs/MTabs.stories.ts +104 -0
  97. package/src/components/tabs/MTabs.vue +113 -0
  98. package/src/components/tabs/Mtabs.spec.ts +149 -0
  99. package/src/components/tag/MTag.spec.ts +107 -0
  100. package/src/components/tag/MTag.stories.ts +75 -0
  101. package/src/components/tag/MTag.vue +151 -0
  102. package/src/components/textarea/MTextArea.spec.ts +112 -0
  103. package/src/components/textarea/MTextArea.stories.ts +67 -0
  104. package/src/components/textarea/MTextArea.vue +81 -43
  105. package/src/components/textinput/MTextInput.spec.ts +121 -0
  106. package/src/components/textinput/MTextInput.stories.ts +107 -0
  107. package/src/components/textinput/MTextInput.vue +127 -47
  108. package/src/components/toggle/MToggle.spec.ts +99 -0
  109. package/src/components/toggle/MToggle.stories.ts +68 -0
  110. package/src/components/toggle/MToggle.vue +63 -103
  111. package/src/components/togglegroup/MToggleGroup.spec.ts +78 -0
  112. package/src/components/togglegroup/MToggleGroup.stories.ts +61 -0
  113. package/src/components/togglegroup/MToggleGroup.vue +97 -0
  114. package/src/components/usingIcons.mdx +35 -0
  115. package/src/components/usingPresets.mdx +128 -0
  116. package/src/main.ts +32 -0
  117. package/dist/demo.html +0 -1
  118. package/dist/mozaic-vue.adeo.css +0 -47
  119. package/dist/mozaic-vue.adeo.umd.js +0 -31109
  120. package/dist/mozaic-vue.common.js +0 -31099
  121. package/dist/mozaic-vue.common.js.map +0 -1
  122. package/dist/mozaic-vue.umd.js +0 -31110
  123. package/dist/mozaic-vue.umd.js.map +0 -1
  124. package/dist/mozaic-vue.umd.min.js +0 -4
  125. package/dist/mozaic-vue.umd.min.js.map +0 -1
  126. package/postinstall.js +0 -3
  127. package/src/components/accordion/MAccordion.vue +0 -128
  128. package/src/components/accordion/index.js +0 -7
  129. package/src/components/autocomplete/MAutocomplete.vue +0 -381
  130. package/src/components/autocomplete/index.js +0 -7
  131. package/src/components/badge/MBadge.vue +0 -43
  132. package/src/components/badge/index.js +0 -7
  133. package/src/components/breadcrumb/index.js +0 -7
  134. package/src/components/button/index.js +0 -7
  135. package/src/components/card/MCard.vue +0 -78
  136. package/src/components/card/index.js +0 -7
  137. package/src/components/checkbox/MCheckboxGroup.vue +0 -163
  138. package/src/components/checkbox/index.js +0 -12
  139. package/src/components/container/MContainer.vue +0 -33
  140. package/src/components/container/index.js +0 -7
  141. package/src/components/datatable/MDataTable.vue +0 -651
  142. package/src/components/datatable/MDataTableHeader.vue +0 -55
  143. package/src/components/datatable/MDataTableTop.vue +0 -35
  144. package/src/components/datatable/helpers.js +0 -132
  145. package/src/components/datatable/index.js +0 -12
  146. package/src/components/dropdown/MDropdown.vue +0 -317
  147. package/src/components/dropdown/index.js +0 -7
  148. package/src/components/field/index.js +0 -7
  149. package/src/components/fileuploader/MFileResult.vue +0 -149
  150. package/src/components/fileuploader/MFileUploader.vue +0 -142
  151. package/src/components/fileuploader/index.js +0 -7
  152. package/src/components/flag/index.js +0 -7
  153. package/src/components/heading/MHeading.vue +0 -75
  154. package/src/components/heading/index.js +0 -7
  155. package/src/components/hero/MHero.vue +0 -93
  156. package/src/components/hero/index.js +0 -7
  157. package/src/components/icon/MIcon.vue +0 -136
  158. package/src/components/icon/index.js +0 -7
  159. package/src/components/index.js +0 -44
  160. package/src/components/layer/MLayer.vue +0 -208
  161. package/src/components/layer/index.js +0 -7
  162. package/src/components/link/index.js +0 -7
  163. package/src/components/listbox/MListBox.vue +0 -146
  164. package/src/components/listbox/MListBoxActions.vue +0 -251
  165. package/src/components/listbox/index.js +0 -12
  166. package/src/components/loader/index.js +0 -7
  167. package/src/components/modal/index.js +0 -7
  168. package/src/components/notification/MNotification.vue +0 -110
  169. package/src/components/notification/index.js +0 -7
  170. package/src/components/optionbutton/MOptionButton.vue +0 -67
  171. package/src/components/optionbutton/index.js +0 -7
  172. package/src/components/optioncard/MOptionCard.vue +0 -132
  173. package/src/components/optioncard/index.js +0 -7
  174. package/src/components/optiongroup/MOptionGroup.vue +0 -18
  175. package/src/components/optiongroup/index.js +0 -7
  176. package/src/components/overlay/MOverlayLoader.vue +0 -43
  177. package/src/components/overlay/index.js +0 -12
  178. package/src/components/pagination/index.js +0 -7
  179. package/src/components/passwordinput/index.js +0 -7
  180. package/src/components/phonenumber/MPhoneNumber.vue +0 -398
  181. package/src/components/phonenumber/index.js +0 -7
  182. package/src/components/progressbar/MProgress.vue +0 -102
  183. package/src/components/progressbar/index.js +0 -7
  184. package/src/components/quantityselector/index.js +0 -7
  185. package/src/components/radio/MRadioGroup.vue +0 -111
  186. package/src/components/radio/index.js +0 -12
  187. package/src/components/ratingstars/MStarsInput.vue +0 -119
  188. package/src/components/ratingstars/MStarsResult.vue +0 -89
  189. package/src/components/ratingstars/index.js +0 -12
  190. package/src/components/select/index.js +0 -7
  191. package/src/components/stepper/MStepper.vue +0 -111
  192. package/src/components/stepper/index.js +0 -7
  193. package/src/components/tabs/MTab.vue +0 -198
  194. package/src/components/tabs/index.js +0 -7
  195. package/src/components/tags/MTag.vue +0 -173
  196. package/src/components/tags/index.js +0 -7
  197. package/src/components/textarea/index.js +0 -7
  198. package/src/components/textinput/MTextInputField.vue +0 -105
  199. package/src/components/textinput/MTextInputIcon.vue +0 -42
  200. package/src/components/textinput/index.js +0 -7
  201. package/src/components/toggle/index.js +0 -7
  202. package/src/components/tooltip/MTooltip.vue +0 -42
  203. package/src/components/tooltip/index.js +0 -7
  204. package/src/index.js +0 -63
  205. package/src/shims-tsx.d.ts +0 -13
  206. package/src/shims.vue.d.ts +0 -4
  207. package/src/tokens/adeo/android/colors.xml +0 -448
  208. package/src/tokens/adeo/android/font_dimens.xml +0 -18
  209. package/src/tokens/adeo/css/_variables.scss +0 -442
  210. package/src/tokens/adeo/css/root.scss +0 -444
  211. package/src/tokens/adeo/ios/StyleDictionaryColor.h +0 -456
  212. package/src/tokens/adeo/ios/StyleDictionaryColor.m +0 -468
  213. package/src/tokens/adeo/ios/StyleDictionaryColor.swift +0 -451
  214. package/src/tokens/adeo/ios/StyleDictionarySize.h +0 -69
  215. package/src/tokens/adeo/ios/StyleDictionarySize.m +0 -70
  216. package/src/tokens/adeo/ios/StyleDictionarySize.swift +0 -71
  217. package/src/tokens/adeo/js/tokens.js +0 -540
  218. package/src/tokens/adeo/js/tokensObject.js +0 -11641
  219. package/src/tokens/adeo/scss/_tokens.scss +0 -1514
  220. package/src/utils/mozaicClasses.js +0 -16
  221. package/src/utils/theme.validator.js +0 -19
  222. package/types/index.d.ts +0 -104
@@ -0,0 +1,148 @@
1
+ <template>
2
+ <div class="mc-pincode-input" :class="classObject" @paste="onPaste">
3
+ <input
4
+ v-for="(digit, index) in otp"
5
+ :key="index"
6
+ :id="`pincodeItem${index}`"
7
+ :ref="(el) => setInputRef(el, index)"
8
+ type="text"
9
+ inputmode="numeric"
10
+ maxlength="1"
11
+ pattern="\d*"
12
+ autocomplete="one-time-code"
13
+ :name="name || `pincode-${id}`"
14
+ class="mc-pincode-input__control"
15
+ :disabled="disabled"
16
+ :readonly="readonly"
17
+ :value="digit"
18
+ v-bind="$attrs"
19
+ @input="(e) => onInput(e, index)"
20
+ @keydown.backspace="(e) => onBackspace(e, index)"
21
+ @keydown="(e) => onKeyDown(e, index)"
22
+ />
23
+ </div>
24
+ </template>
25
+
26
+ <script setup lang="ts">
27
+ import {
28
+ ref,
29
+ computed,
30
+ watch,
31
+ nextTick,
32
+ type ComponentPublicInstance,
33
+ } from 'vue';
34
+ /**
35
+ * A pincode input is a specialized input field used to enter short numeric codes, such as verification codes, security PINs, or authentication tokens. It typically separates each digit into individual fields to improve readability and ease of entry. This component is commonly used in two-factor authentication (2FA), password recovery, and secure access flows, ensuring a structured and user-friendly experience.
36
+ */
37
+ const props = withDefaults(
38
+ defineProps<{
39
+ /**
40
+ * A unique identifier for the pincode element, used to associate the label with the form element.
41
+ */
42
+ id: string;
43
+ /**
44
+ * The number of input displayed in the pincode element.
45
+ */
46
+ length?: 4 | 5 | 6;
47
+ /**
48
+ * The name attribute for the pincode element, typically used for form submission.
49
+ */
50
+ name?: string;
51
+ /**
52
+ * The current value of the pincode field.
53
+ */
54
+ modelValue?: string | number;
55
+ /**
56
+ * If `true`, applies an invalid state to the pincode.
57
+ */
58
+ isInvalid?: boolean;
59
+ /**
60
+ * If `true`, disables the pincode, making it non-interactive.
61
+ */
62
+ disabled?: boolean;
63
+ /**
64
+ * If `true`, the pincode is read-only (cannot be edited).
65
+ */
66
+ readonly?: boolean;
67
+ }>(),
68
+ {
69
+ length: 6,
70
+ },
71
+ );
72
+
73
+ const classObject = computed(() => {
74
+ return {
75
+ 'is-invalid': props.isInvalid,
76
+ };
77
+ });
78
+
79
+ const emit = defineEmits<{
80
+ (on: 'update:modelValue', value: string): void;
81
+ }>();
82
+
83
+ const otp = ref<string[]>(Array(props.length).fill(''));
84
+ const inputRefs = ref<(HTMLInputElement | null)[]>([]);
85
+
86
+ const setInputRef = (
87
+ el: Element | ComponentPublicInstance | null,
88
+ index: number,
89
+ ) => {
90
+ inputRefs.value[index] = el as HTMLInputElement | null;
91
+ };
92
+
93
+ watch(
94
+ () => props.modelValue,
95
+ (value) => {
96
+ const str = String(value ?? '');
97
+ otp.value = Array.from({ length: props.length }, (_, i) => str[i] ?? '');
98
+ },
99
+ { immediate: true },
100
+ );
101
+
102
+ const focusInput = (index: number) => {
103
+ nextTick(() => inputRefs.value[index]?.focus());
104
+ };
105
+
106
+ const onInput = (e: Event, index: number) => {
107
+ const val = (e.target as HTMLInputElement).value.replace(/\D/g, '');
108
+ if (val) {
109
+ otp.value[index] = val[0];
110
+ emit('update:modelValue', otp.value.join(''));
111
+ if (index + 1 < props.length) focusInput(index + 1);
112
+ } else {
113
+ otp.value[index] = '';
114
+ emit('update:modelValue', otp.value.join(''));
115
+ }
116
+ };
117
+
118
+ const onKeyDown = (e: KeyboardEvent, index: number) => {
119
+ if (e.key === 'ArrowLeft' && index > 0) {
120
+ focusInput(index - 1);
121
+ } else if (e.key === 'ArrowRight' && index < props.length - 1) {
122
+ focusInput(index + 1);
123
+ } else if (e.key === 'Backspace') {
124
+ onBackspace(e, index);
125
+ }
126
+ };
127
+
128
+ const onBackspace = (e: KeyboardEvent, index: number) => {
129
+ if (otp.value[index] === '' && index > 0) {
130
+ otp.value[index - 1] = '';
131
+ emit('update:modelValue', otp.value.join(''));
132
+ focusInput(index - 1);
133
+ }
134
+ };
135
+
136
+ const onPaste = (e: ClipboardEvent) => {
137
+ e.preventDefault();
138
+ const pasted = e.clipboardData?.getData('text') ?? '';
139
+ const digits = pasted.replace(/\D/g, '').slice(0, props.length).split('');
140
+ otp.value = Array.from({ length: props.length }, (_, i) => digits[i] ?? '');
141
+ emit('update:modelValue', otp.value.join(''));
142
+ focusInput(Math.min(digits.length, props.length - 1));
143
+ };
144
+ </script>
145
+
146
+ <style lang="scss" scoped>
147
+ @use '@mozaic-ds/styles/components/pincode-input';
148
+ </style>
@@ -0,0 +1,262 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import { describe, it, expect } from 'vitest';
3
+ import MQuantitySelector from './MQuantitySelector.vue';
4
+
5
+ describe('MQuantitySelector component', () => {
6
+ it('renders the component with default values', () => {
7
+ const wrapper = mount(MQuantitySelector, {
8
+ props: {
9
+ id: 'quantity-selector-1',
10
+ },
11
+ });
12
+
13
+ const input = wrapper.find('input');
14
+ const increaseButton = wrapper.find(
15
+ '.mc-quantity-selector__button--increase',
16
+ );
17
+ const decreaseButton = wrapper.find(
18
+ '.mc-quantity-selector__button--decrease',
19
+ );
20
+
21
+ expect(input.exists()).toBe(true);
22
+ expect(input.element.value).toBe('1');
23
+ expect(input.attributes('min')).toBe('1');
24
+ expect(input.attributes('max')).toBe('100');
25
+ expect(input.attributes('step')).toBe('1');
26
+
27
+ expect(increaseButton.exists()).toBe(true);
28
+ expect(decreaseButton.exists()).toBe(true);
29
+ expect(increaseButton.attributes('disabled')).toBeUndefined();
30
+ expect(decreaseButton.attributes('disabled')).toBeDefined();
31
+ });
32
+
33
+ it('emits an update when value changes via input', async () => {
34
+ const wrapper = mount(MQuantitySelector, {
35
+ props: {
36
+ id: 'quantity-selector-1',
37
+ modelValue: 5,
38
+ },
39
+ });
40
+
41
+ const input = wrapper.find('input');
42
+ await input.setValue(10);
43
+
44
+ const emittedValue = wrapper.emitted('update:modelValue');
45
+ expect(emittedValue).toBeTruthy();
46
+ expect(emittedValue?.[0]).toEqual([10]);
47
+ });
48
+
49
+ it('increments value when increase button is clicked', async () => {
50
+ const wrapper = mount(MQuantitySelector, {
51
+ props: {
52
+ id: 'quantity-selector-1',
53
+ modelValue: 5,
54
+ max: 10,
55
+ step: 1,
56
+ },
57
+ });
58
+
59
+ const increaseButton = wrapper.find(
60
+ '.mc-quantity-selector__button--increase',
61
+ );
62
+ await increaseButton.trigger('click');
63
+
64
+ const input = wrapper.find('input');
65
+ expect(input.element.value).toBe('6');
66
+ });
67
+
68
+ it('decrements value when decrease button is clicked', async () => {
69
+ const wrapper = mount(MQuantitySelector, {
70
+ props: {
71
+ id: 'quantity-selector-1',
72
+ modelValue: 5,
73
+ min: 1,
74
+ step: 1,
75
+ },
76
+ });
77
+
78
+ const decreaseButton = wrapper.find(
79
+ '.mc-quantity-selector__button--decrease',
80
+ );
81
+ await decreaseButton.trigger('click');
82
+
83
+ const input = wrapper.find('input');
84
+ expect(input.element.value).toBe('4');
85
+ });
86
+
87
+ it('does not increment beyond max value', async () => {
88
+ const wrapper = mount(MQuantitySelector, {
89
+ props: {
90
+ id: 'quantity-selector-1',
91
+ modelValue: 10,
92
+ max: 10,
93
+ step: 1,
94
+ },
95
+ });
96
+
97
+ const increaseButton = wrapper.find(
98
+ '.mc-quantity-selector__button--increase',
99
+ );
100
+ await increaseButton.trigger('click');
101
+
102
+ const input = wrapper.find('input');
103
+ expect(input.element.value).toBe('10');
104
+ });
105
+
106
+ it('does not decrement below min value', async () => {
107
+ const wrapper = mount(MQuantitySelector, {
108
+ props: {
109
+ id: 'quantity-selector-1',
110
+ modelValue: 1,
111
+ min: 1,
112
+ step: 1,
113
+ },
114
+ });
115
+
116
+ const decreaseButton = wrapper.find(
117
+ '.mc-quantity-selector__button--decrease',
118
+ );
119
+ await decreaseButton.trigger('click');
120
+
121
+ const input = wrapper.find('input');
122
+ expect(input.element.value).toBe('1');
123
+ });
124
+
125
+ it('disables input and buttons when disabled prop is true', async () => {
126
+ const wrapper = mount(MQuantitySelector, {
127
+ props: {
128
+ id: 'quantity-selector-1',
129
+ disabled: true,
130
+ },
131
+ });
132
+
133
+ const input = wrapper.find('input');
134
+ const increaseButton = wrapper.find(
135
+ '.mc-quantity-selector__button--increase',
136
+ );
137
+ const decreaseButton = wrapper.find(
138
+ '.mc-quantity-selector__button--decrease',
139
+ );
140
+
141
+ expect(input.attributes('disabled')).toBeDefined();
142
+ expect(increaseButton.attributes('disabled')).toBeDefined();
143
+ expect(decreaseButton.attributes('disabled')).toBeDefined();
144
+ });
145
+
146
+ it('renders with custom size class', () => {
147
+ const wrapper = mount(MQuantitySelector, {
148
+ props: {
149
+ id: 'quantity-selector-1',
150
+ size: 's',
151
+ },
152
+ });
153
+
154
+ const div = wrapper.find('.mc-quantity-selector');
155
+ expect(div.classes()).toContain('mc-quantity-selector--s');
156
+ });
157
+
158
+ it('hides increase and decrease buttons when readonly is true', () => {
159
+ const wrapper = mount(MQuantitySelector, {
160
+ props: {
161
+ id: 'quantity-selector-1',
162
+ readonly: true,
163
+ },
164
+ });
165
+
166
+ const increaseButton = wrapper.find(
167
+ '.mc-quantity-selector__button--increase',
168
+ );
169
+ const decreaseButton = wrapper.find(
170
+ '.mc-quantity-selector__button--decrease',
171
+ );
172
+
173
+ expect(increaseButton.exists()).toBe(false);
174
+ expect(decreaseButton.exists()).toBe(false);
175
+ });
176
+
177
+ it('renders and tests aria-label and aria-invalid for accessibility', () => {
178
+ const wrapper = mount(MQuantitySelector, {
179
+ props: {
180
+ id: 'quantity-selector-1',
181
+ isInvalid: true,
182
+ ariaLabel: 'Select quantity',
183
+ },
184
+ });
185
+
186
+ const input = wrapper.find('input');
187
+ expect(input.attributes('aria-label')).toBe('Select quantity');
188
+ expect(input.attributes('aria-invalid')).toBe('true');
189
+ });
190
+
191
+ it('does not allow invalid input values (non-numeric)', async () => {
192
+ const wrapper = mount(MQuantitySelector, {
193
+ props: {
194
+ id: 'quantity-selector-1',
195
+ },
196
+ });
197
+
198
+ const input = wrapper.find('input');
199
+ await input.setValue('abc');
200
+ expect(input.element.value).toBe('1');
201
+ });
202
+
203
+ it('supports custom step values', async () => {
204
+ const wrapper = mount(MQuantitySelector, {
205
+ props: {
206
+ id: 'quantity-selector-1',
207
+ modelValue: 10,
208
+ step: 5,
209
+ },
210
+ });
211
+
212
+ const increaseButton = wrapper.find(
213
+ '.mc-quantity-selector__button--increase',
214
+ );
215
+ await increaseButton.trigger('click');
216
+
217
+ const input = wrapper.find('input');
218
+ expect(input.element.value).toBe('15');
219
+ });
220
+
221
+ it('renders custom increment and decrement labels', () => {
222
+ const wrapper = mount(MQuantitySelector, {
223
+ props: {
224
+ id: 'quantity-selector-1',
225
+ incrementlabel: 'Increase Quantity',
226
+ decrementLabel: 'Decrease Quantity',
227
+ },
228
+ });
229
+
230
+ const increaseButton = wrapper.find(
231
+ '.mc-quantity-selector__button--increase',
232
+ );
233
+ const decreaseButton = wrapper.find(
234
+ '.mc-quantity-selector__button--decrease',
235
+ );
236
+
237
+ expect(increaseButton.text()).toBe('Increase Quantity');
238
+ expect(decreaseButton.text()).toBe('Decrease Quantity');
239
+ });
240
+
241
+ it('correctly sets aria-controls', () => {
242
+ const wrapper = mount(MQuantitySelector, {
243
+ props: {
244
+ id: 'quantity-selector-1',
245
+ },
246
+ });
247
+
248
+ const increaseButton = wrapper.find(
249
+ '.mc-quantity-selector__button--increase',
250
+ );
251
+ const decreaseButton = wrapper.find(
252
+ '.mc-quantity-selector__button--decrease',
253
+ );
254
+
255
+ expect(increaseButton.attributes('aria-controls')).toBe(
256
+ 'quantity-selector-1',
257
+ );
258
+ expect(decreaseButton.attributes('aria-controls')).toBe(
259
+ 'quantity-selector-1',
260
+ );
261
+ });
262
+ });
@@ -0,0 +1,89 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
2
+ import { action } from 'storybook/actions';
3
+
4
+ import MQuantitySelector from './MQuantitySelector.vue';
5
+
6
+ const meta: Meta<typeof MQuantitySelector> = {
7
+ title: 'Form Elements/Quantity Selector',
8
+ component: MQuantitySelector,
9
+ parameters: {
10
+ docs: {
11
+ description: {
12
+ component:
13
+ 'A quantity selector is an input component that allows users to increment or decrement a numeric value, typically using plus (+) and minus (−) buttons. It provides a simple and efficient way to adjust quantities without manual typing, ensuring controlled input. This component is commonly used in e-commerce, inventory management, and settings where users need to specify amounts.',
14
+ },
15
+ },
16
+ },
17
+ args: {
18
+ id: 'quantitySelectorId',
19
+ modelValue: 10,
20
+ min: 5,
21
+ max: 20,
22
+ },
23
+ argTypes: {
24
+ size: {
25
+ control: 'radio',
26
+ options: ['s', 'm'],
27
+ },
28
+ },
29
+ render: (args) => ({
30
+ components: { MQuantitySelector },
31
+ setup() {
32
+ const handleUpdate = action('update:modelValue');
33
+
34
+ return { args, handleUpdate };
35
+ },
36
+ template: `
37
+ <MQuantitySelector
38
+ v-bind="args"
39
+ @update:modelValue="handleUpdate"
40
+ />
41
+ `,
42
+ }),
43
+ };
44
+ export default meta;
45
+ type Story = StoryObj<typeof MQuantitySelector>;
46
+
47
+ export const Default: Story = {};
48
+
49
+ export const Step: Story = {
50
+ args: {
51
+ id: 'stepId',
52
+ step: 5,
53
+ },
54
+ };
55
+
56
+ export const Small: Story = {
57
+ args: {
58
+ id: 'smallId',
59
+ size: 's',
60
+ },
61
+ };
62
+
63
+ export const Disabled: Story = {
64
+ args: {
65
+ id: 'disableId',
66
+ disabled: true,
67
+ },
68
+ };
69
+
70
+ export const ReadOnly: Story = {
71
+ args: {
72
+ id: 'readonlyId',
73
+ readonly: true,
74
+ },
75
+ };
76
+
77
+ export const Invalid: Story = {
78
+ args: {
79
+ id: 'invalidId',
80
+ isInvalid: true,
81
+ },
82
+ };
83
+
84
+ export const readOnly: Story = {
85
+ args: {
86
+ id: 'readonlyId',
87
+ readonly: true,
88
+ },
89
+ };