@mozaic-ds/vue 2.7.0 → 2.8.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.
@@ -0,0 +1,88 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
2
+ import { action } from 'storybook/actions';
3
+ import { ref } from 'vue';
4
+
5
+ import MPhoneNumber from './MPhoneNumber.vue';
6
+
7
+ const meta: Meta<typeof MPhoneNumber> = {
8
+ title: 'Form Elements/Phone Number',
9
+ component: MPhoneNumber,
10
+ tags: ['v2'],
11
+ parameters: {
12
+ docs: {
13
+ description: {
14
+ component:
15
+ 'A phone number input is a specialized input field designed to capture and validate phone numbers, ensuring correct formatting based on country-specific dialing codes. It often includes a country selector that automatically adjusts the international dialing code. This component improves user experience by standardizing phone number entries, reducing errors, and facilitating global compatibility. It is commonly used in registration forms, authentication flows, and contact information fields.',
16
+ },
17
+ },
18
+ },
19
+ render: (args) => ({
20
+ components: { MPhoneNumber },
21
+ setup() {
22
+ const modelValue = ref(args.modelValue);
23
+ const isValid = ref(false);
24
+
25
+ const handleUpdate = (val: string) => {
26
+ modelValue.value = val;
27
+ action('update:modelValue')(val);
28
+ };
29
+
30
+ const handleValid = (valid: boolean) => {
31
+ isValid.value = valid;
32
+ action('valid')(valid);
33
+ };
34
+
35
+ return { args, modelValue, isValid, handleUpdate, handleValid };
36
+ },
37
+ template: `
38
+ <MPhoneNumber
39
+ v-bind="{ ...args, modelValue }"
40
+ @update:modelValue="handleUpdate"
41
+ @valid="handleValid"
42
+ />
43
+ `,
44
+ }),
45
+ };
46
+ export default meta;
47
+ type Story = StoryObj<typeof MPhoneNumber>;
48
+
49
+ export const Default: Story = {};
50
+
51
+ export const Size: Story = {
52
+ args: {
53
+ size: 's',
54
+ },
55
+ };
56
+
57
+ export const IsInvalid: Story = {
58
+ args: {
59
+ isInvalid: true,
60
+ modelValue: '1912',
61
+ },
62
+ };
63
+
64
+ export const NoPrefix: Story = {
65
+ args: {
66
+ prefix: false,
67
+ },
68
+ };
69
+
70
+ export const NoFlag: Story = {
71
+ args: {
72
+ flag: false,
73
+ },
74
+ };
75
+
76
+ export const Disabled: Story = {
77
+ args: {
78
+ modelValue: '0103948374',
79
+ disabled: true,
80
+ },
81
+ };
82
+
83
+ export const ReadOnly: Story = {
84
+ args: {
85
+ modelValue: '0103948374',
86
+ readonly: true,
87
+ },
88
+ };
@@ -0,0 +1,271 @@
1
+ <template>
2
+ <div id="mc-phone-number-input" class="mc-phone-number-input">
3
+ <div
4
+ class="mc-phone-number-input__select-wrapper"
5
+ :class="selectWrapperClass"
6
+ >
7
+ <select
8
+ id="selectComponentId"
9
+ v-model="selectedCountry"
10
+ name="selectComponentName"
11
+ class="mc-select mc-phone-number-input__select"
12
+ :class="sizeSelectClass"
13
+ :disabled="isDisabled"
14
+ :readonly="isReadOnly"
15
+ >
16
+ <option value="" selected hidden></option>
17
+ <option
18
+ v-for="country in countries"
19
+ :key="country"
20
+ :value="country"
21
+ :data-flag="country.toLowerCase()"
22
+ >
23
+ {{ getCountryName(country) }} (+{{ getCountryCallingCode(country) }})
24
+ </option>
25
+ </select>
26
+
27
+ <div class="mc-phone-number-input__select-display">
28
+ <div class="mc-phone-number-input__flag">
29
+ <img
30
+ class="mc-phone-number-input__flag-image"
31
+ :src="getFlagUrl(selectedCountry)"
32
+ :alt="getCountryName(selectedCountry)"
33
+ width="20"
34
+ />
35
+ </div>
36
+ <ChevronDown20
37
+ class="mc-phone-number-input__chevron"
38
+ aria-hidden="true"
39
+ />
40
+ </div>
41
+ </div>
42
+
43
+ <div class="mc-text-input mc-phone-number-input__input" :class="inputClass">
44
+ <span v-if="prefix" class="mc-phone-number-input__country-code">
45
+ +{{ getCountryCallingCode(selectedCountry) }}
46
+ </span>
47
+ <input
48
+ v-model="phoneNumber"
49
+ type="tel"
50
+ :id="id"
51
+ class="mc-phone-number-input__control mc-text-input__control"
52
+ :placeholder="dynamicPlaceholder"
53
+ name="phone-number"
54
+ :aria-invalid="isInvalid"
55
+ :disabled="isDisabled"
56
+ :readonly="isReadOnly"
57
+ @input="handleInput"
58
+ />
59
+ </div>
60
+ </div>
61
+ </template>
62
+
63
+ <script setup lang="ts">
64
+ import { ref, computed, watch } from 'vue';
65
+ import parsePhoneNumberFromString, {
66
+ isValidPhoneNumber,
67
+ getCountries,
68
+ getCountryCallingCode,
69
+ getExampleNumber,
70
+ type CountryCode,
71
+ } from 'libphonenumber-js';
72
+ import examples from 'libphonenumber-js/mobile/examples';
73
+ import ChevronDown20 from '@mozaic-ds/icons-vue/src/components/ChevronDown20/ChevronDown20.vue';
74
+ /**
75
+ * A phone number input is a specialized input field designed to capture and validate phone numbers, ensuring correct formatting based on country-specific dialing codes. It often includes a country selector that automatically adjusts the international dialing code. This component improves user experience by standardizing phone number entries, reducing errors, and facilitating global compatibility. It is commonly used in registration forms, authentication flows, and contact information fields.
76
+ */
77
+ const props = withDefaults(
78
+ defineProps<{
79
+ /**
80
+ * A unique identifier for the phone number input element, used to associate the label with the form element.
81
+ */
82
+ id: string;
83
+ /**
84
+ * The current value of the phone number input field.
85
+ */
86
+ modelValue?: string;
87
+ /**
88
+ * Default country code for phone number formatting (e.g., 'FR', 'US', 'PT').
89
+ */
90
+ defaultCountry?: CountryCode;
91
+ /**
92
+ * A placeholder text to show in the phone number input when it is empty.
93
+ */
94
+ placeholder?: string;
95
+ /**
96
+ * Determines the size of the phone number input.
97
+ */
98
+ size?: 's' | 'm';
99
+ /**
100
+ * If `true`, applies an invalid state to the password input.
101
+ */
102
+ isInvalid?: boolean;
103
+ /**
104
+ * If `true`, disables the input, making it non-interactive.
105
+ */
106
+ disabled?: boolean;
107
+ /**
108
+ * If `true`, the input is read-only (cannot be edited).
109
+ */
110
+ readonly?: boolean;
111
+ /**
112
+ * If `true`, display the country calling code prefix (+33, +1, etc.).
113
+ */
114
+ prefix?: boolean;
115
+ /**
116
+ * If `true`, display the country flag selector
117
+ */
118
+ flag?: boolean;
119
+ }>(),
120
+ {
121
+ modelValue: '',
122
+ defaultCountry: 'FR' as CountryCode,
123
+ size: 'm',
124
+ prefix: true,
125
+ flag: true,
126
+ },
127
+ );
128
+
129
+ const phoneNumber = ref(props.modelValue);
130
+ const selectedCountry = ref<CountryCode>(props.defaultCountry);
131
+ const countries = getCountries();
132
+
133
+ const dynamicPlaceholder = computed(() => {
134
+ if (props.placeholder && props.placeholder.length > 0) {
135
+ return props.placeholder;
136
+ }
137
+ return getExampleNumber(selectedCountry.value, examples)?.formatNational();
138
+ });
139
+
140
+ const isValid = computed(() => {
141
+ if (!phoneNumber.value) return false;
142
+ try {
143
+ return isValidPhoneNumber(phoneNumber.value, selectedCountry.value);
144
+ } catch {
145
+ return false;
146
+ }
147
+ });
148
+
149
+ const parsedNumber = computed(() => {
150
+ if (!phoneNumber.value) return null;
151
+ try {
152
+ return parsePhoneNumberFromString(phoneNumber.value, selectedCountry.value);
153
+ } catch {
154
+ return null;
155
+ }
156
+ });
157
+
158
+ const getCountryName = (
159
+ countryCode: CountryCode,
160
+ locale: string = 'fr',
161
+ ): string => {
162
+ try {
163
+ const regionNames = new Intl.DisplayNames([locale], { type: 'region' });
164
+ return regionNames.of(countryCode) || countryCode;
165
+ } catch {
166
+ return countryCode;
167
+ }
168
+ };
169
+
170
+ watch(
171
+ [phoneNumber, selectedCountry],
172
+ () => {
173
+ // Auto-format input when number is valid
174
+ if (isValid.value && parsedNumber.value) {
175
+ const formattedNational = parsedNumber.value.formatNational();
176
+ if (formattedNational !== phoneNumber.value) {
177
+ phoneNumber.value = formattedNational;
178
+ }
179
+ }
180
+
181
+ // Emit international format for v-model
182
+ const fullNumber = parsedNumber.value?.number || phoneNumber.value;
183
+ emit('update:modelValue', fullNumber);
184
+ emit('valid', isValid.value);
185
+ },
186
+ { flush: 'post' },
187
+ );
188
+
189
+ watch(
190
+ () => props.modelValue,
191
+ (newValue) => {
192
+ if (newValue && newValue !== phoneNumber.value) {
193
+ if (newValue.startsWith('+')) {
194
+ try {
195
+ const parsed = parsePhoneNumberFromString(
196
+ newValue,
197
+ selectedCountry.value,
198
+ );
199
+ if (parsed) {
200
+ phoneNumber.value = parsed.formatNational();
201
+ } else {
202
+ phoneNumber.value = newValue;
203
+ }
204
+ } catch {
205
+ phoneNumber.value = newValue;
206
+ }
207
+ } else {
208
+ phoneNumber.value = newValue;
209
+ }
210
+ } else if (!newValue) {
211
+ phoneNumber.value = '';
212
+ }
213
+ },
214
+ { immediate: true },
215
+ );
216
+
217
+ const sizeSelectClass = computed(() => {
218
+ return props.size !== 'm' ? `mc-select--${props.size}` : '';
219
+ });
220
+
221
+ const sizeInputClass = computed(() => {
222
+ return props.size !== 'm' ? `mc-text-input--${props.size}` : '';
223
+ });
224
+
225
+ const isDisabled = computed(() => props.disabled);
226
+ const isReadOnly = computed(() => props.readonly);
227
+
228
+ const selectWrapperClass = computed(() => {
229
+ return { 'mc-phone-number-input__select-wrapper--hidden': !props.flag };
230
+ });
231
+
232
+ const inputClass = computed(() => {
233
+ const hasValue = phoneNumber.value && phoneNumber.value.trim().length > 0;
234
+ const isInvalidState = props.isInvalid || (hasValue && !isValid.value);
235
+ return [sizeInputClass.value, { 'is-invalid': isInvalidState }];
236
+ });
237
+
238
+ const getFlagUrl = (countryCode: CountryCode): string => {
239
+ return `https://flagcdn.com/${countryCode.toLowerCase()}.svg`;
240
+ };
241
+
242
+ const handleInput = (event: Event) => {
243
+ const input = event.target as HTMLInputElement;
244
+ const value = input.value;
245
+ const sanitized = value.replace(/[^0-9+\s()-]/g, '');
246
+
247
+ phoneNumber.value = sanitized;
248
+ if (value !== sanitized) {
249
+ input.value = sanitized;
250
+ }
251
+ };
252
+
253
+ const emit = defineEmits<{
254
+ /**
255
+ * Emits when the input value changes, updating the `modelValue` prop.
256
+ */
257
+ 'update:modelValue': [value: string];
258
+ /**
259
+ * Emits when the validation state of the phone number changes.
260
+ */
261
+ valid: [isValid: boolean];
262
+ }>();
263
+ </script>
264
+
265
+ <style lang="scss">
266
+ @use '@mozaic-ds/styles/components/text-input';
267
+ @use '@mozaic-ds/styles/components/button';
268
+ @use '@mozaic-ds/styles/components/listbox';
269
+ @use '@mozaic-ds/styles/components/select';
270
+ @use '@mozaic-ds/styles/components/phone-number-input';
271
+ </style>
@@ -0,0 +1,26 @@
1
+ # MPhoneNumber
2
+
3
+ A phone number input is a specialized input field designed to capture and validate phone numbers, ensuring correct formatting based on country-specific dialing codes. It often includes a country selector that automatically adjusts the international dialing code. This component improves user experience by standardizing phone number entries, reducing errors, and facilitating global compatibility. It is commonly used in registration forms, authentication flows, and contact information fields.
4
+
5
+
6
+ ## Props
7
+
8
+ | Name | Description | Type | Default |
9
+ | --- | --- | --- | --- |
10
+ | `id*` | A unique identifier for the phone number input element, used to associate the label with the form element. | `string` | - |
11
+ | `modelValue` | The current value of the phone number input field. | `string` | `""` |
12
+ | `defaultCountry` | Default country code for phone number formatting (e.g., 'FR', 'US', 'PT'). | `CountryCode` | `"FR" as CountryCode` |
13
+ | `placeholder` | A placeholder text to show in the phone number input when it is empty. | `string` | - |
14
+ | `size` | Determines the size of the phone number input. | `"s"` `"m"` | `"m"` |
15
+ | `isInvalid` | If `true`, applies an invalid state to the password input. | `boolean` | - |
16
+ | `disabled` | If `true`, disables the input, making it non-interactive. | `boolean` | - |
17
+ | `readonly` | If `true`, the input is read-only (cannot be edited). | `boolean` | - |
18
+ | `prefix` | If `true`, display the country calling code prefix (+33, +1, etc.). | `boolean` | `true` |
19
+ | `flag` | If `true`, display the country flag selector | `boolean` | `true` |
20
+
21
+ ## Events
22
+
23
+ | Name | Description | Type |
24
+ | --- | --- | --- |
25
+ | `update:modelValue` | - | `[value: string]` |
26
+ | `valid` | - | `[isValid: boolean]` |
@@ -19,4 +19,4 @@ A pincode input is a specialized input field used to enter short numeric codes,
19
19
 
20
20
  | Name | Description | Type |
21
21
  | --- | --- | --- |
22
- | `update:modelValue` | - | `[value: string]` |
22
+ | `update:modelValue` | Emits when the pincode value changes, updating the modelValue prop. | [value: string] |
@@ -80,10 +80,3 @@ export const Invalid: Story = {
80
80
  isInvalid: true,
81
81
  },
82
82
  };
83
-
84
- export const readOnly: Story = {
85
- args: {
86
- id: 'readonlyId',
87
- readonly: true,
88
- },
89
- };
@@ -5,7 +5,6 @@
5
5
  :id="option.id"
6
6
  :key="option.id"
7
7
  :label="option.label"
8
- :is-invalid="option.isInvalid"
9
8
  :name="name"
10
9
  :class="classObjectItem"
11
10
  :model-value="modelValue ? modelValue.includes(option.value) : undefined"
@@ -40,7 +39,6 @@ const props = defineProps<{
40
39
  label: string;
41
40
  value: string;
42
41
  disabled?: boolean;
43
- isInvalid?: boolean;
44
42
  size?: 's' | 'm';
45
43
  }>;
46
44
  /**
@@ -10,7 +10,7 @@ A toggle is a switch component that allows users to enable or disable a setting,
10
10
  | `name*` | The name attribute for the toggle element, typically used for form submission. | `string` | - |
11
11
  | `modelValue` | Property used to manage the values checked by v-model
12
12
  (Do not use directly) | `string[]` | - |
13
- | `options*` | List of properties of each toggle of the toggle group. | `{ id: string; label: string; value: string; disabled?: boolean` `undefined; isInvalid?: boolean` `undefined; size?: "s"` `"m"` `undefined; }[]` | - |
13
+ | `options*` | List of properties of each toggle of the toggle group. | `{ id: string; label: string; value: string; disabled?: boolean` `undefined; size?: "s"` `"m"` `undefined; }[]` | - |
14
14
  | `inline` | If `true`, make the form element of the group inline. | `boolean` | - |
15
15
 
16
16
  ## Events
package/src/main.ts CHANGED
@@ -23,6 +23,7 @@ export { default as MNumberBadge } from './components/numberbadge/MNumberBadge.v
23
23
  export { default as MOverlay } from './components/overlay/MOverlay.vue';
24
24
  export { default as MPagination } from './components/pagination/MPagination.vue';
25
25
  export { default as MPasswordInput } from './components/passwordinput/MPasswordInput.vue';
26
+ export { default as MPhoneNumber } from './components/phonenumber/MPhoneNumber.vue';
26
27
  export { default as MPincode } from './components/pincode/MPincode.vue';
27
28
  export { default as MQuantitySelector } from './components/quantityselector/MQuantitySelector.vue';
28
29
  export { default as MRadio } from './components/radio/MRadio.vue';