@mozaic-ds/vue 2.8.0 → 2.10.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 (32) hide show
  1. package/README.md +4 -6
  2. package/dist/mozaic-vue.css +1 -1
  3. package/dist/mozaic-vue.d.ts +334 -252
  4. package/dist/mozaic-vue.js +1367 -1182
  5. package/dist/mozaic-vue.js.map +1 -1
  6. package/dist/mozaic-vue.umd.cjs +5 -5
  7. package/dist/mozaic-vue.umd.cjs.map +1 -1
  8. package/package.json +14 -13
  9. package/src/components/carousel/MCarousel.spec.ts +138 -0
  10. package/src/components/carousel/MCarousel.stories.ts +94 -0
  11. package/src/components/carousel/MCarousel.vue +154 -0
  12. package/src/components/carousel/README.md +18 -0
  13. package/src/components/drawer/MDrawer.spec.ts +81 -9
  14. package/src/components/drawer/MDrawer.vue +76 -46
  15. package/src/components/drawer/README.md +1 -0
  16. package/src/components/field/MField.spec.ts +94 -85
  17. package/src/components/field/MField.stories.ts +16 -0
  18. package/src/components/field/MField.vue +8 -1
  19. package/src/components/field/README.md +1 -0
  20. package/src/components/flag/MFlag.stories.ts +1 -1
  21. package/src/components/loader/MLoader.spec.ts +41 -0
  22. package/src/components/loader/MLoader.vue +7 -1
  23. package/src/components/loader/README.md +1 -1
  24. package/src/components/modal/MModal.spec.ts +34 -9
  25. package/src/components/modal/MModal.vue +39 -7
  26. package/src/components/modal/README.md +1 -0
  27. package/src/components/phonenumber/MPhoneNumber.spec.ts +110 -1
  28. package/src/components/phonenumber/MPhoneNumber.stories.ts +14 -0
  29. package/src/components/phonenumber/MPhoneNumber.vue +16 -6
  30. package/src/components/textinput/MTextInput.stories.ts +1 -1
  31. package/src/components/usingPresets.mdx +1 -1
  32. package/src/main.ts +1 -0
@@ -10,6 +10,7 @@
10
10
  :aria-hidden="!open"
11
11
  v-bind="$attrs"
12
12
  @keydown.esc="onClose"
13
+ @click.stop
13
14
  >
14
15
  <div class="mc-modal__dialog" role="document">
15
16
  <header class="mc-modal__header">
@@ -47,7 +48,7 @@
47
48
  </template>
48
49
 
49
50
  <script setup lang="ts">
50
- import { computed, watch, type VNode } from 'vue';
51
+ import { computed, onMounted, onUnmounted, watch, type VNode } from 'vue';
51
52
  import Cross24 from '@mozaic-ds/icons-vue/src/components/Cross24/Cross24.vue';
52
53
  import MIconButton from '../iconbutton/MIconButton.vue';
53
54
  import MOverlay from '../overlay/MOverlay.vue';
@@ -72,6 +73,10 @@ const props = withDefaults(
72
73
  * if `true`, display the close button.
73
74
  */
74
75
  closable?: boolean;
76
+ /**
77
+ * if `false`, lock the scroll when open.
78
+ */
79
+ scroll?: boolean;
75
80
  /**
76
81
  * if `true`, close the modal when clicking the overlay.
77
82
  */
@@ -79,6 +84,7 @@ const props = withDefaults(
79
84
  }>(),
80
85
  {
81
86
  closable: true,
87
+ scroll: true,
82
88
  },
83
89
  );
84
90
 
@@ -107,12 +113,38 @@ const classObject = computed(() => {
107
113
  };
108
114
  });
109
115
 
110
- watch(
111
- () => props.open,
112
- (newValue) => {
113
- emit('update:open', newValue);
114
- },
115
- );
116
+ const isClient =
117
+ typeof window !== 'undefined' && typeof document !== 'undefined';
118
+
119
+ const lockScroll = () => {
120
+ if (!isClient) return;
121
+ document.body.style.overflow = 'hidden';
122
+ document.documentElement.style.overflow = 'hidden';
123
+ };
124
+
125
+ const unlockScroll = () => {
126
+ if (!isClient) return;
127
+ document.body.style.overflow = '';
128
+ document.documentElement.style.overflow = '';
129
+ };
130
+
131
+ onMounted(() => {
132
+ watch(
133
+ () => props.open,
134
+ (isOpen) => {
135
+ emit('update:open', isOpen);
136
+ if (props.scroll === false) {
137
+ if (isOpen) lockScroll();
138
+ else unlockScroll();
139
+ }
140
+ },
141
+ { immediate: true },
142
+ );
143
+ });
144
+
145
+ onUnmounted(() => {
146
+ unlockScroll();
147
+ });
116
148
 
117
149
  const onClickOverlay = () => {
118
150
  if (props.closeOnOverlay) {
@@ -11,6 +11,7 @@ A modal is a dialog window that appears on top of the main content, requiring us
11
11
  | `title*` | Title of the modal. | `string` | - |
12
12
  | `description` | Description of the modal. | `string` | - |
13
13
  | `closable` | if `true`, display the close button. | `boolean` | `true` |
14
+ | `scroll` | if `false`, lock the scroll when open. | `boolean` | `true` |
14
15
  | `closeOnOverlay` | if `true`, close the modal when clicking the overlay. | `boolean` | - |
15
16
 
16
17
  ## Slots
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
2
2
  import { mount, VueWrapper } from '@vue/test-utils';
3
3
  import { nextTick } from 'vue';
4
4
  import MPhoneNumber from './MPhoneNumber.vue';
5
- import { isValidPhoneNumber } from 'libphonenumber-js';
5
+ import { isValidPhoneNumber, type CountryCode } from 'libphonenumber-js';
6
6
 
7
7
  vi.mock('libphonenumber-js', () => ({
8
8
  default: vi.fn(),
@@ -291,4 +291,113 @@ describe('MPhoneNumber', () => {
291
291
  expect((input.element as HTMLInputElement).value).toBe('+33123456789');
292
292
  });
293
293
  });
294
+
295
+ describe('Locale', () => {
296
+ it('should use French locale by default', () => {
297
+ const options = wrapper.findAll('option');
298
+ const franceOption = options.find(
299
+ (opt) => opt.attributes('value') === 'FR',
300
+ );
301
+ expect(franceOption?.text()).toContain('France');
302
+ });
303
+
304
+ it('should display country names in specified locale', () => {
305
+ wrapper = mount(MPhoneNumber, {
306
+ props: { ...defaultProps, locale: 'en' },
307
+ });
308
+ const options = wrapper.findAll('option');
309
+ const franceOption = options.find(
310
+ (opt) => opt.attributes('value') === 'FR',
311
+ );
312
+ // Country names should be translated according to the locale
313
+ expect(franceOption?.text()).toBeTruthy();
314
+ });
315
+
316
+ it('should update country names when locale prop changes', async () => {
317
+ await wrapper.setProps({ locale: 'es' });
318
+ await nextTick();
319
+ const options = wrapper.findAll('option');
320
+ expect(options.length).toBeGreaterThan(1);
321
+ });
322
+ });
323
+
324
+ describe('Country Codes', () => {
325
+ it('should display all countries by default', () => {
326
+ const options = wrapper.findAll('option');
327
+ // Minus 1 for the empty hidden option
328
+ expect(options.length - 1).toBe(5); // Mocked to return 5 countries
329
+ });
330
+
331
+ it('should display only specified countries when countryCodes prop is provided', () => {
332
+ wrapper = mount(MPhoneNumber, {
333
+ props: {
334
+ ...defaultProps,
335
+ countryCodes: ['FR', 'US', 'GB'] as CountryCode[],
336
+ },
337
+ });
338
+ const options = wrapper.findAll('option');
339
+ // Plus 1 for the empty hidden option
340
+ expect(options.length).toBe(4);
341
+ expect(options.some((opt) => opt.attributes('value') === 'FR')).toBe(
342
+ true,
343
+ );
344
+ expect(options.some((opt) => opt.attributes('value') === 'US')).toBe(
345
+ true,
346
+ );
347
+ expect(options.some((opt) => opt.attributes('value') === 'GB')).toBe(
348
+ true,
349
+ );
350
+ });
351
+
352
+ it('should update available countries when countryCodes prop changes', async () => {
353
+ await wrapper.setProps({ countryCodes: ['FR', 'DE'] as CountryCode[] });
354
+ await nextTick();
355
+ const options = wrapper.findAll('option');
356
+ // Plus 1 for the empty hidden option
357
+ expect(options.length).toBe(3);
358
+ expect(options.some((opt) => opt.attributes('value') === 'FR')).toBe(
359
+ true,
360
+ );
361
+ expect(options.some((opt) => opt.attributes('value') === 'DE')).toBe(
362
+ true,
363
+ );
364
+ });
365
+
366
+ it('should work with single country in countryCodes', () => {
367
+ wrapper = mount(MPhoneNumber, {
368
+ props: {
369
+ ...defaultProps,
370
+ countryCodes: ['FR'] as CountryCode[],
371
+ },
372
+ });
373
+ const options = wrapper.findAll('option');
374
+ // Plus 1 for the empty hidden option
375
+ expect(options.length).toBe(2);
376
+ expect(options.some((opt) => opt.attributes('value') === 'FR')).toBe(
377
+ true,
378
+ );
379
+ });
380
+
381
+ it('should combine locale and countryCodes props', () => {
382
+ wrapper = mount(MPhoneNumber, {
383
+ props: {
384
+ ...defaultProps,
385
+ locale: 'en',
386
+ countryCodes: ['US', 'GB', 'CA'] as CountryCode[],
387
+ },
388
+ });
389
+ const options = wrapper.findAll('option');
390
+ // Plus 1 for the empty hidden option
391
+ expect(options.length).toBe(4);
392
+ expect(options.some((opt) => opt.attributes('value') === 'US')).toBe(
393
+ true,
394
+ );
395
+ expect(options.some((opt) => opt.attributes('value') === 'GB')).toBe(
396
+ true,
397
+ );
398
+ expect(options.some((opt) => opt.attributes('value') === 'CA')).toBe(
399
+ true,
400
+ );
401
+ });
402
+ });
294
403
  });
@@ -86,3 +86,17 @@ export const ReadOnly: Story = {
86
86
  readonly: true,
87
87
  },
88
88
  };
89
+
90
+ export const LimitedCountries: Story = {
91
+ args: {
92
+ countryCodes: ['FR', 'US', 'GB', 'DE', 'ES', 'IT'],
93
+ },
94
+ parameters: {
95
+ docs: {
96
+ description: {
97
+ story:
98
+ 'Limit the country selector to only specific countries instead of showing all available countries.',
99
+ },
100
+ },
101
+ },
102
+ };
@@ -116,6 +116,14 @@ const props = withDefaults(
116
116
  * If `true`, display the country flag selector
117
117
  */
118
118
  flag?: boolean;
119
+ /**
120
+ * Locale for displaying country names (e.g., 'fr', 'en', 'es', 'pt').
121
+ */
122
+ locale?: string;
123
+ /**
124
+ * List of country codes to display in the selector. If not provided, all countries will be shown.
125
+ */
126
+ countryCodes?: CountryCode[];
119
127
  }>(),
120
128
  {
121
129
  modelValue: '',
@@ -123,12 +131,15 @@ const props = withDefaults(
123
131
  size: 'm',
124
132
  prefix: true,
125
133
  flag: true,
134
+ locale: 'en',
126
135
  },
127
136
  );
128
137
 
129
138
  const phoneNumber = ref(props.modelValue);
130
139
  const selectedCountry = ref<CountryCode>(props.defaultCountry);
131
- const countries = getCountries();
140
+ const countries = computed(() => {
141
+ return props.countryCodes || getCountries();
142
+ });
132
143
 
133
144
  const dynamicPlaceholder = computed(() => {
134
145
  if (props.placeholder && props.placeholder.length > 0) {
@@ -155,12 +166,11 @@ const parsedNumber = computed(() => {
155
166
  }
156
167
  });
157
168
 
158
- const getCountryName = (
159
- countryCode: CountryCode,
160
- locale: string = 'fr',
161
- ): string => {
169
+ const getCountryName = (countryCode: CountryCode): string => {
162
170
  try {
163
- const regionNames = new Intl.DisplayNames([locale], { type: 'region' });
171
+ const regionNames = new Intl.DisplayNames([props.locale], {
172
+ type: 'region',
173
+ });
164
174
  return regionNames.of(countryCode) || countryCode;
165
175
  } catch {
166
176
  return countryCode;
@@ -82,7 +82,7 @@ export const WithIcon: Story = {
82
82
  }),
83
83
  };
84
84
 
85
- export const minValue: Story = {
85
+ export const MinValue: Story = {
86
86
  args: {
87
87
  id: 'minValueId',
88
88
  inputType: 'number',
@@ -39,7 +39,7 @@ All it has to do is insert the following code into its main Sass file (entrypoin
39
39
  />
40
40
 
41
41
  > [!NOTE]
42
- > The `<presetName>` string should be replaced by the name of the preset you want, one of the following values: `adeo | enki | mbrand`.
42
+ > The `<presetName>` string should be replaced by the name of the preset you want, one of the following values: `adeo | mbrand`.
43
43
  > As the `leroymerlin` preset is the default preset, you don't need to use this syntax to use it.
44
44
 
45
45
  For example, for ADEO
package/src/main.ts CHANGED
@@ -2,6 +2,7 @@ export { default as MAvatar } from './components/avatar/MAvatar.vue';
2
2
  export { default as MBreadcrumb } from './components/breadcrumb/MBreadcrumb.vue';
3
3
  export { default as MButton } from './components/button/MButton.vue';
4
4
  export { default as MCallout } from './components/callout/MCallout.vue';
5
+ export { default as MCarousel } from './components/carousel/MCarousel.vue';
5
6
  export { default as MCheckbox } from './components/checkbox/MCheckbox.vue';
6
7
  export { default as MCheckboxGroup } from './components/checkboxgroup/MCheckboxGroup.vue';
7
8
  export { default as MCircularProgressbar } from './components/circularprogressbar/MCircularProgressbar.vue';