@mozaic-ds/vue 2.8.0 → 2.9.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mozaic-ds/vue",
3
- "version": "2.8.0",
3
+ "version": "2.9.0",
4
4
  "type": "module",
5
5
  "description": "Mozaic-Vue is the Vue.js implementation of ADEO Design system",
6
6
  "author": "ADEO - ADEO Design system",
@@ -50,8 +50,9 @@ describe('MDrawer component', () => {
50
50
  const closeButton = wrapper.find('.mc-drawer__close');
51
51
  await closeButton.trigger('click');
52
52
 
53
- expect(wrapper.emitted('update:open')).toBeTruthy();
54
- expect(wrapper.emitted('update:open')![0]).toEqual([false]);
53
+ const emitted = wrapper.emitted('update:open');
54
+ expect(emitted).toBeTruthy();
55
+ expect(emitted![emitted!.length - 1]).toEqual([false]);
55
56
  });
56
57
 
57
58
  it('emits back event when back button is clicked', async () => {
@@ -132,7 +133,7 @@ describe('MDrawer component', () => {
132
133
  expect(document.activeElement).toBe(titleElement);
133
134
  });
134
135
 
135
- it('does not set the focus on the title when the drawer closes', async () => {
136
+ it('does not refocus the title when the drawer closes', async () => {
136
137
  const wrapper = mount(MDrawer, {
137
138
  props: {
138
139
  title: 'Test Title',
@@ -143,11 +144,15 @@ describe('MDrawer component', () => {
143
144
  });
144
145
 
145
146
  const titleElement = wrapper.find('.mc-drawer__title').element;
147
+
148
+ expect(document.activeElement).toBe(titleElement);
149
+
146
150
  await wrapper.setProps({ open: false });
147
- expect(document.activeElement).not.toBe(titleElement);
151
+ await wrapper.vm.$nextTick();
152
+
153
+ expect(document.activeElement).toBe(titleElement);
148
154
  });
149
155
 
150
- // ✅ New tests for closeOnOverlay behavior
151
156
  it('emits update:open false when overlay is clicked and closeOnOverlay is true', async () => {
152
157
  const wrapper = mount(MDrawer, {
153
158
  props: {
@@ -160,8 +165,9 @@ describe('MDrawer component', () => {
160
165
 
161
166
  await wrapper.find('.overlay').trigger('click');
162
167
 
163
- expect(wrapper.emitted('update:open')).toBeTruthy();
164
- expect(wrapper.emitted('update:open')![0]).toEqual([false]);
168
+ const emitted = wrapper.emitted('update:open');
169
+ expect(emitted).toBeTruthy();
170
+ expect(emitted![emitted!.length - 1]).toEqual([false]);
165
171
  });
166
172
 
167
173
  it('does not emit update:open when overlay is clicked and closeOnOverlay is false', async () => {
@@ -176,7 +182,9 @@ describe('MDrawer component', () => {
176
182
 
177
183
  await wrapper.find('.overlay').trigger('click');
178
184
 
179
- expect(wrapper.emitted('update:open')).toBeFalsy();
185
+ const emitted = wrapper.emitted('update:open');
186
+ expect(emitted).toBeTruthy();
187
+ expect(emitted?.length).toBe(1);
180
188
  });
181
189
 
182
190
  it('does not emit update:open when overlay is clicked and closeOnOverlay is not set', async () => {
@@ -190,6 +198,70 @@ describe('MDrawer component', () => {
190
198
 
191
199
  await wrapper.find('.overlay').trigger('click');
192
200
 
193
- expect(wrapper.emitted('update:open')).toBeFalsy();
201
+ const emitted = wrapper.emitted('update:open');
202
+ expect(emitted).toBeTruthy();
203
+ });
204
+
205
+ it('emits update:open false when pressing ESC key', async () => {
206
+ const wrapper = mount(MDrawer, {
207
+ props: {
208
+ open: true,
209
+ title: 'Test Title',
210
+ },
211
+ global: { stubs },
212
+ });
213
+
214
+ await wrapper.find('section.mc-drawer').trigger('keydown.esc');
215
+ expect(wrapper.emitted('update:open')).toBeTruthy();
216
+ expect(wrapper.emitted('update:open')!.at(-1)).toEqual([false]);
217
+ });
218
+
219
+ it('locks and unlocks scroll when scroll=false and open changes', async () => {
220
+ const wrapper = mount(MDrawer, {
221
+ props: {
222
+ title: 'Scroll Test',
223
+ open: false,
224
+ scroll: false,
225
+ },
226
+ global: { stubs },
227
+ });
228
+
229
+ expect(document.body.style.overflow).toBe('');
230
+
231
+ await wrapper.setProps({ open: true });
232
+ expect(document.body.style.overflow).toBe('hidden');
233
+
234
+ await wrapper.setProps({ open: false });
235
+ expect(document.body.style.overflow).toBe('');
236
+ });
237
+
238
+ it('restores scroll when unmounted', async () => {
239
+ const wrapper = mount(MDrawer, {
240
+ props: {
241
+ open: true,
242
+ title: 'Unmount Test',
243
+ scroll: false,
244
+ },
245
+ global: { stubs },
246
+ });
247
+
248
+ await wrapper.setProps({ open: true });
249
+ expect(document.body.style.overflow).toBe('hidden');
250
+
251
+ wrapper.unmount();
252
+ expect(document.body.style.overflow).toBe('');
253
+ });
254
+
255
+ it('emits update:open on mount reflecting initial state', () => {
256
+ const wrapper = mount(MDrawer, {
257
+ props: {
258
+ open: true,
259
+ title: 'Initial Test',
260
+ },
261
+ global: { stubs },
262
+ });
263
+
264
+ expect(wrapper.emitted('update:open')).toBeTruthy();
265
+ expect(wrapper.emitted('update:open')![0]).toEqual([true]);
194
266
  });
195
267
  });
@@ -14,6 +14,7 @@
14
14
  :aria-hidden="!open"
15
15
  v-bind="$attrs"
16
16
  @keydown.esc="onClose"
17
+ @click.stop
17
18
  >
18
19
  <div class="mc-drawer__dialog" role="document">
19
20
  <div class="mc-drawer__header">
@@ -64,7 +65,7 @@
64
65
  </template>
65
66
 
66
67
  <script setup lang="ts">
67
- import { computed, watch, type VNode, ref } from 'vue';
68
+ import { computed, watch, type VNode, ref, onMounted, onUnmounted } from 'vue';
68
69
  import ArrowBack24 from '@mozaic-ds/icons-vue/src/components/ArrowBack24/ArrowBack24.vue';
69
70
  import Cross24 from '@mozaic-ds/icons-vue/src/components/Cross24/Cross24.vue';
70
71
  import MIconButton from '../iconbutton/MIconButton.vue';
@@ -72,36 +73,45 @@ import MOverlay from '../overlay/MOverlay.vue';
72
73
  /**
73
74
  * A drawer is a sliding panel that appears from the side of the screen, providing additional content, settings, or actions without disrupting the main view. It is often used for filtering options, or contextual details. It enhances usability by keeping interfaces clean while offering expandable functionality.
74
75
  */
75
- const props = defineProps<{
76
- /**
77
- * If `true`, display the drawer.
78
- */
79
- open?: boolean;
80
- /**
81
- * Position of the drawer.
82
- */
83
- position?: 'left' | 'right';
84
- /**
85
- * If `true`, the drawer have a bigger width.
86
- */
87
- extended?: boolean;
88
- /**
89
- * If `true`, display the back button.
90
- */
91
- back?: boolean;
92
- /**
93
- * Title of the drawer.
94
- */
95
- title: string;
96
- /**
97
- * Title of the content of the drawer.
98
- */
99
- contentTitle?: string;
100
- /**
101
- * if `true`, close the drawer when clicking the overlay.
102
- */
103
- closeOnOverlay?: boolean;
104
- }>();
76
+ const props = withDefaults(
77
+ defineProps<{
78
+ /**
79
+ * If `true`, display the drawer.
80
+ */
81
+ open?: boolean;
82
+ /**
83
+ * Position of the drawer.
84
+ */
85
+ position?: 'left' | 'right';
86
+ /**
87
+ * If `true`, the drawer have a bigger width.
88
+ */
89
+ extended?: boolean;
90
+ /**
91
+ * If `true`, display the back button.
92
+ */
93
+ back?: boolean;
94
+ /**
95
+ * Title of the drawer.
96
+ */
97
+ title: string;
98
+ /**
99
+ * Title of the content of the drawer.
100
+ */
101
+ contentTitle?: string;
102
+ /**
103
+ * if `false`, lock the scroll when open.
104
+ */
105
+ scroll?: boolean;
106
+ /**
107
+ * if `true`, close the drawer when clicking the overlay.
108
+ */
109
+ closeOnOverlay?: boolean;
110
+ }>(),
111
+ {
112
+ scroll: true,
113
+ },
114
+ );
105
115
 
106
116
  defineSlots<{
107
117
  /**
@@ -123,22 +133,42 @@ const classObject = computed(() => {
123
133
  };
124
134
  });
125
135
 
126
- watch(
127
- () => props.open,
128
- (newValue) => {
129
- emit('update:open', newValue);
130
- },
131
- );
132
-
133
136
  const titleRef = ref<HTMLElement | null>(null);
134
- watch(
135
- () => props.open,
136
- (newValue) => {
137
- if (newValue && titleRef.value) {
138
- titleRef.value.focus();
139
- }
140
- },
141
- );
137
+ const isClient =
138
+ typeof window !== 'undefined' && typeof document !== 'undefined';
139
+
140
+ const lockScroll = () => {
141
+ if (!isClient) return;
142
+ document.body.style.overflow = 'hidden';
143
+ document.documentElement.style.overflow = 'hidden';
144
+ };
145
+
146
+ const unlockScroll = () => {
147
+ if (!isClient) return;
148
+ document.body.style.overflow = '';
149
+ document.documentElement.style.overflow = '';
150
+ };
151
+
152
+ onMounted(() => {
153
+ watch(
154
+ () => props.open,
155
+ (isOpen) => {
156
+ emit('update:open', isOpen);
157
+ if (isOpen && titleRef.value) {
158
+ titleRef.value.focus();
159
+ }
160
+ if (props.scroll === false) {
161
+ if (isOpen) lockScroll();
162
+ else unlockScroll();
163
+ }
164
+ },
165
+ { immediate: true },
166
+ );
167
+ });
168
+
169
+ onUnmounted(() => {
170
+ unlockScroll();
171
+ });
142
172
 
143
173
  const onClickOverlay = () => {
144
174
  if (props.closeOnOverlay) {
@@ -13,6 +13,7 @@ A drawer is a sliding panel that appears from the side of the screen, providing
13
13
  | `back` | If `true`, display the back button. | `boolean` | - |
14
14
  | `title*` | Title of the drawer. | `string` | - |
15
15
  | `contentTitle` | Title of the content of the drawer. | `string` | - |
16
+ | `scroll` | if `false`, lock the scroll when open. | `boolean` | `true` |
16
17
  | `closeOnOverlay` | if `true`, close the drawer when clicking the overlay. | `boolean` | - |
17
18
 
18
19
  ## Slots
@@ -1,166 +1,175 @@
1
1
  import { mount } from '@vue/test-utils';
2
2
  import { describe, it, expect } from 'vitest';
3
3
  import MField from './MField.vue';
4
+ import MLoader from '../loader/MLoader.vue';
4
5
 
5
6
  describe('MField component', () => {
6
- it('should render label correctly', () => {
7
+ it('renders the label and associates it with the correct id', () => {
7
8
  const wrapper = mount(MField, {
8
9
  props: {
9
- id: 'input-id',
10
- label: 'Field Label',
10
+ id: 'username',
11
+ label: 'Username',
11
12
  },
12
13
  });
13
14
 
14
- const label = wrapper.find('label');
15
+ const label = wrapper.find('.mc-field__label');
15
16
  expect(label.exists()).toBe(true);
16
- expect(label.text()).toBe('Field Label');
17
+ expect(label.text()).toContain('Username');
18
+ expect(label.attributes('for')).toBe('username');
17
19
  });
18
20
 
19
- it('should render requirement text if provided', () => {
21
+ it('renders the requirement text when provided', () => {
20
22
  const wrapper = mount(MField, {
21
23
  props: {
22
- id: 'input-id',
23
- label: 'Field Label',
24
+ id: 'email',
25
+ label: 'Email',
24
26
  requirementText: 'required',
25
27
  },
26
28
  });
27
29
 
28
- const requirementText = wrapper.find('.mc-field__requirement');
29
- expect(requirementText.exists()).toBe(true);
30
- expect(requirementText.text()).toBe('(required)');
30
+ const requirement = wrapper.find('.mc-field__requirement');
31
+ expect(requirement.exists()).toBe(true);
32
+ expect(requirement.text()).toContain('(required)');
31
33
  });
32
34
 
33
- it('should not render requirement text if not provided', () => {
35
+ it('does not render requirement text when not provided', () => {
34
36
  const wrapper = mount(MField, {
35
- props: {
36
- id: 'input-id',
37
- label: 'Field Label',
38
- },
37
+ props: { id: 'email', label: 'Email' },
39
38
  });
40
39
 
41
- const requirementText = wrapper.find('.mc-field__requirement');
42
- expect(requirementText.exists()).toBe(false);
40
+ const requirement = wrapper.find('.mc-field__requirement');
41
+ expect(requirement.exists()).toBe(false);
43
42
  });
44
43
 
45
- it('should render help text if provided', () => {
44
+ it('renders help text when helpId and helpText are provided', () => {
46
45
  const wrapper = mount(MField, {
47
46
  props: {
48
- id: 'input-id',
49
- label: 'Field Label',
50
- helpText: 'This is some help text.',
51
- helpId: 'help-id',
47
+ id: 'password',
48
+ label: 'Password',
49
+ helpText: 'At least 8 characters.',
50
+ helpId: 'password-help',
52
51
  },
53
52
  });
54
53
 
55
- const helpText = wrapper.find('.mc-field__help');
56
- expect(helpText.exists()).toBe(true);
57
- expect(helpText.text()).toBe('This is some help text.');
54
+ const help = wrapper.find('.mc-field__help');
55
+ expect(help.exists()).toBe(true);
56
+ expect(help.attributes('id')).toBe('password-help');
57
+ expect(help.text()).toBe('At least 8 characters.');
58
58
  });
59
59
 
60
- it('should not render help text if not provided', () => {
60
+ it('does not render help text if helpId or helpText is missing', () => {
61
61
  const wrapper = mount(MField, {
62
62
  props: {
63
- id: 'input-id',
64
- label: 'Field Label',
63
+ id: 'password',
64
+ label: 'Password',
65
+ helpText: 'Help text',
65
66
  },
66
67
  });
67
68
 
68
- const helpText = wrapper.find('.mc-field__help');
69
- expect(helpText.exists()).toBe(false);
69
+ const help = wrapper.find('.mc-field__help');
70
+ expect(help.exists()).toBe(false);
70
71
  });
71
72
 
72
- it('should apply is-valid class when isValid prop is true', () => {
73
+ it('renders slot content inside .mc-field__content', () => {
73
74
  const wrapper = mount(MField, {
74
- props: {
75
- id: 'input-id',
76
- label: 'Field Label',
77
- isValid: true,
78
- message: 'Valid input',
79
- },
75
+ props: { id: 'field1', label: 'Field Label' },
76
+ slots: { default: '<input id="field1" />' },
80
77
  });
81
78
 
82
- expect(wrapper.find('.mc-field__validation-message').classes()).toContain(
83
- 'is-valid',
84
- );
85
- expect(wrapper.find('.mc-field__validation-message').text()).toBe(
86
- 'Valid input',
87
- );
79
+ const content = wrapper.find('.mc-field__content');
80
+ expect(content.exists()).toBe(true);
81
+ expect(content.find('input').exists()).toBe(true);
88
82
  });
89
83
 
90
- it('should apply is-invalid class when isInvalid prop is true', () => {
84
+ it('renders validation message when message and valid state are provided', () => {
91
85
  const wrapper = mount(MField, {
92
86
  props: {
93
- id: 'input-id',
94
- label: 'Field Label',
95
- isInvalid: true,
96
- message: 'Invalid input',
87
+ id: 'email',
88
+ label: 'Email',
89
+ isValid: true,
90
+ messageId: 'msg1',
91
+ message: 'Looks good!',
97
92
  },
98
93
  });
99
94
 
100
- expect(wrapper.find('.mc-field__validation-message').classes()).toContain(
101
- 'is-invalid',
102
- );
103
- expect(wrapper.find('.mc-field__validation-message').text()).toBe(
104
- 'Invalid input',
105
- );
95
+ const message = wrapper.find('.mc-field__validation-message');
96
+ expect(message.exists()).toBe(true);
97
+ expect(message.attributes('id')).toBe('msg1');
98
+ expect(message.text()).toContain('Looks good!');
99
+ expect(message.classes()).toContain('is-valid');
106
100
  });
107
101
 
108
- it('should render a validation message only when isValid or isInvalid is true and message is provided', () => {
102
+ it('renders validation message when invalid', () => {
109
103
  const wrapper = mount(MField, {
110
104
  props: {
111
- id: 'input-id',
112
- label: 'Field Label',
113
- isValid: true,
114
- message: 'This is a valid input',
105
+ id: 'email',
106
+ label: 'Email',
107
+ isInvalid: true,
108
+ messageId: 'msg2',
109
+ message: 'Invalid email address.',
115
110
  },
116
111
  });
117
112
 
118
- const validationMessage = wrapper.find('.mc-field__validation-message');
119
- expect(validationMessage.exists()).toBe(true);
120
- expect(validationMessage.text()).toBe('This is a valid input');
113
+ const message = wrapper.find('.mc-field__validation-message');
114
+ expect(message.exists()).toBe(true);
115
+ expect(message.text()).toContain('Invalid email address.');
116
+ expect(message.classes()).toContain('is-invalid');
121
117
  });
122
118
 
123
- it('should render the form element passed in the slot', () => {
119
+ it('renders loader and message when loading', () => {
124
120
  const wrapper = mount(MField, {
125
121
  props: {
126
- id: 'input-id',
127
- label: 'Field Label',
128
- },
129
- slots: {
130
- default: '<input type="text" />',
122
+ id: 'loadingField',
123
+ label: 'Loading Label',
124
+ isLoading: true,
125
+ messageId: 'loading-msg',
126
+ message: 'Loading...',
131
127
  },
132
128
  });
133
129
 
134
- const input = wrapper.find('input');
135
- expect(input.exists()).toBe(true);
136
- expect(input.attributes('type')).toBe('text');
130
+ const message = wrapper.find('.mc-field__validation-message');
131
+ expect(message.exists()).toBe(true);
132
+ expect(message.classes()).toContain('is-loading');
133
+
134
+ const loader = wrapper.findComponent(MLoader);
135
+ expect(loader.exists()).toBe(true);
136
+ expect(loader.props('size')).toBe('xs');
137
137
  });
138
138
 
139
- it('should render validation message when isInvalid is true and message is provided', () => {
139
+ it('does not render validation message when no state (valid/invalid/loading)', () => {
140
140
  const wrapper = mount(MField, {
141
141
  props: {
142
- id: 'input-id',
143
- label: 'Field Label',
144
- isInvalid: true,
145
- message: 'There is an error',
142
+ id: 'email',
143
+ label: 'Email',
144
+ messageId: 'msg3',
145
+ message: 'Message without state',
146
146
  },
147
147
  });
148
148
 
149
- const validationMessage = wrapper.find('.mc-field__validation-message');
150
- expect(validationMessage.exists()).toBe(true);
151
- expect(validationMessage.text()).toBe('There is an error');
149
+ const message = wrapper.find('.mc-field__validation-message');
150
+ expect(message.exists()).toBe(false);
152
151
  });
153
152
 
154
- it('should not render validation message when message is not provided', () => {
153
+ it('renders multiple validation states correctly', async () => {
155
154
  const wrapper = mount(MField, {
156
155
  props: {
157
- id: 'input-id',
158
- label: 'Field Label',
156
+ id: 'stateField',
157
+ label: 'Field',
158
+ messageId: 'msg4',
159
+ message: 'State message',
159
160
  isValid: true,
160
161
  },
161
162
  });
162
163
 
163
- const validationMessage = wrapper.find('.mc-field__validation-message');
164
- expect(validationMessage.exists()).toBe(false);
164
+ let message = wrapper.find('.mc-field__validation-message');
165
+ expect(message.classes()).toContain('is-valid');
166
+
167
+ await wrapper.setProps({ isValid: false, isInvalid: true });
168
+ message = wrapper.find('.mc-field__validation-message');
169
+ expect(message.classes()).toContain('is-invalid');
170
+
171
+ await wrapper.setProps({ isInvalid: false, isLoading: true });
172
+ message = wrapper.find('.mc-field__validation-message');
173
+ expect(message.classes()).toContain('is-loading');
165
174
  });
166
175
  });
@@ -125,6 +125,22 @@ export const InputInvalid: Story = {
125
125
  },
126
126
  };
127
127
 
128
+ export const InputLoading: Story = {
129
+ args: {
130
+ label: 'Label',
131
+ id: 'inputLoadingId',
132
+ isLoading: true,
133
+ message: 'Loading message (Be concise and use comprehensive words).',
134
+ default: `
135
+ <MTextInput
136
+ id="inputInvalidId"
137
+ placeholder="Placeholder"
138
+ @update:modelValue="handleUpdate"
139
+ />
140
+ `,
141
+ },
142
+ };
143
+
128
144
  export const Textarea: Story = {
129
145
  args: {
130
146
  label: 'Label',
@@ -16,11 +16,12 @@
16
16
  </div>
17
17
 
18
18
  <span
19
- v-if="(isValid || isInvalid) && message"
19
+ v-if="(isValid || isInvalid || isLoading) && message"
20
20
  class="mc-field__validation-message"
21
21
  :id="messageId"
22
22
  :class="classObjectValidation"
23
23
  >
24
+ <MLoader v-if="isLoading" size="xs"></MLoader>
24
25
  {{ message }}
25
26
  </span>
26
27
  </div>
@@ -28,6 +29,7 @@
28
29
 
29
30
  <script setup lang="ts">
30
31
  import { computed, type VNode } from 'vue';
32
+ import MLoader from '../loader/MLoader.vue';
31
33
  /**
32
34
  * A field label is a text element that identifies the purpose of an input field, providing users with clear guidance on what information to enter. It is typically placed above the input field and may include indicators for required or optional fields. Field Labels improve form usability, accessibility, and data entry accuracy by ensuring users understand the expected input.
33
35
  */
@@ -60,6 +62,10 @@ const props = defineProps<{
60
62
  * If `true`, applies an invalid state to the form field.
61
63
  */
62
64
  isInvalid?: boolean;
65
+ /**
66
+ * If `true`, applies a loading state to the form field.
67
+ */
68
+ isLoading?: boolean;
63
69
  /**
64
70
  * The value of the `id` attribute set on the **validationMessage** element. _This value is mandatory when using a validationMessage in order to guarantee the accessibility of the component._
65
71
  */
@@ -81,6 +87,7 @@ const classObjectValidation = computed(() => {
81
87
  return {
82
88
  'is-valid': props.isValid,
83
89
  'is-invalid': props.isInvalid,
90
+ 'is-loading': props.isLoading,
84
91
  };
85
92
  });
86
93
  </script>
@@ -14,6 +14,7 @@ A field label is a text element that identifies the purpose of an input field, p
14
14
  | `helpId` | The value of the `id` attribute set on the **helpText** element. _This value is mandatory when using a helpText in order to guarantee the accessibility of the component._ | `string` | - |
15
15
  | `isValid` | If `true`, applies a valid state to the form field. | `boolean` | - |
16
16
  | `isInvalid` | If `true`, applies an invalid state to the form field. | `boolean` | - |
17
+ | `isLoading` | If `true`, applies a loading state to the form field. | `boolean` | - |
17
18
  | `messageId` | The value of the `id` attribute set on the **validationMessage** element. _This value is mandatory when using a validationMessage in order to guarantee the accessibility of the component._ | `string` | - |
18
19
  | `message` | message displayed when the form field has a valid or invalid state, usually indicating validation or errors. | `string` | - |
19
20