@mozaic-ds/vue 2.18.0 → 2.19.1

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 (132) hide show
  1. package/dist/mozaic-vue.css +1 -1
  2. package/dist/mozaic-vue.d.ts +961 -2085
  3. package/dist/mozaic-vue.js +1253 -1143
  4. package/dist/mozaic-vue.js.map +1 -1
  5. package/dist/mozaic-vue.umd.cjs +6 -6
  6. package/dist/mozaic-vue.umd.cjs.map +1 -1
  7. package/package.json +8 -6
  8. package/src/components/BrandPresets.mdx +20 -2
  9. package/src/components/accordionlist/MAccordionList.figma.ts +16 -16
  10. package/src/components/accordionlist/MAccordionList.stories.ts +1 -1
  11. package/src/components/accordionlistitem/MAccordionListItem.figma.ts +9 -5
  12. package/src/components/accordionlistitem/MAccordionListItem.vue +4 -1
  13. package/src/components/accordionlistitem/README.md +2 -0
  14. package/src/components/actionbottombar/MActionBottomBar.figma.ts +7 -7
  15. package/src/components/actionlistbox/MActionListbox.figma.ts +11 -11
  16. package/src/components/actionlistbox/MActionListbox.spec.ts +113 -0
  17. package/src/components/actionlistbox/MActionListbox.vue +63 -5
  18. package/src/components/avatar/MAvatar.figma.ts +5 -5
  19. package/src/components/breadcrumb/MBreadcrumb.figma.ts +7 -7
  20. package/src/components/breadcrumb/MBreadcrumb.vue +1 -1
  21. package/src/components/builtinmenu/MBuiltInMenu.figma.ts +8 -5
  22. package/src/components/builtinmenu/MBuiltInMenu.spec.ts +3 -1
  23. package/src/components/button/MButton.figma.ts +21 -6
  24. package/src/components/button/MButton.spec.ts +26 -0
  25. package/src/components/button/MButton.vue +2 -0
  26. package/src/components/callout/MCallout.figma.ts +7 -7
  27. package/src/components/callout/MCallout.stories.ts +0 -3
  28. package/src/components/callout/MCallout.vue +4 -3
  29. package/src/components/callout/README.md +2 -2
  30. package/src/components/carousel/MCarousel.figma.ts +10 -10
  31. package/src/components/carousel/MCarousel.spec.ts +26 -2
  32. package/src/components/carousel/MCarousel.vue +10 -4
  33. package/src/components/checkbox/MCheckbox.figma.ts +10 -10
  34. package/src/components/checkboxgroup/MCheckboxGroup.figma.ts +7 -7
  35. package/src/components/checklistmenu/MCheckListMenu.figma.ts +8 -8
  36. package/src/components/circularprogressbar/MCircularProgressbar.figma.ts +7 -3
  37. package/src/components/combobox/MCombobox.figma.ts +10 -10
  38. package/src/components/combobox/MCombobox.vue +7 -0
  39. package/src/components/container/MContainer.figma.ts +5 -5
  40. package/src/components/datatable/DataTable.stories.ts +33 -7
  41. package/src/components/datatable/DataTableCells.stories.ts +2 -2
  42. package/src/components/datatable/DataTableEmpty.stories.ts +2 -2
  43. package/src/components/datatable/DataTableExpandable.stories.ts +2 -2
  44. package/src/components/datatable/DataTableNested.stories.ts +1 -1
  45. package/src/components/datatable/DataTableSelectable.stories.ts +2 -3
  46. package/src/components/datatable/DataTableSortable.stories.ts +1 -1
  47. package/src/components/datepicker/MDatepicker.figma.ts +3 -3
  48. package/src/components/divider/MDivider.figma.ts +3 -3
  49. package/src/components/drawer/MDrawer.figma.ts +13 -13
  50. package/src/components/drawer/MDrawer.spec.ts +102 -3
  51. package/src/components/drawer/MDrawer.vue +73 -14
  52. package/src/components/field/MField.figma.ts +9 -5
  53. package/src/components/field/MField.vue +1 -0
  54. package/src/components/fileuploader/MFileUploader.figma.ts +3 -3
  55. package/src/components/fileuploader/MFileUploader.vue +2 -2
  56. package/src/components/fileuploaderitem/MFileUploaderItem.figma.ts +7 -3
  57. package/src/components/fileuploaderitem/MFileUploaderItem.vue +2 -7
  58. package/src/components/flag/MFlag.figma.ts +3 -3
  59. package/src/components/iconbutton/MIconButton.figma.ts +16 -16
  60. package/src/components/iconbutton/MIconButton.spec.ts +15 -0
  61. package/src/components/iconbutton/MIconButton.vue +1 -0
  62. package/src/components/kpiitem/MKpiItem.figma.ts +8 -3
  63. package/src/components/kpiitem/MKpiItem.spec.ts +12 -0
  64. package/src/components/kpiitem/MKpiItem.vue +7 -1
  65. package/src/components/linearprogressbarbuffer/MLinearProgressbarBuffer.figma.ts +6 -3
  66. package/src/components/linearprogressbarpercentage/MLinearProgressbarPercentage.figma.ts +5 -3
  67. package/src/components/link/MLink.figma.ts +5 -5
  68. package/src/components/loader/MLoader.figma.ts +3 -3
  69. package/src/components/loadingoverlay/MLoadingOverlay.figma.ts +3 -3
  70. package/src/components/modal/MModal.figma.ts +12 -12
  71. package/src/components/modal/MModal.spec.ts +115 -3
  72. package/src/components/modal/MModal.vue +91 -11
  73. package/src/components/modal/README.md +1 -1
  74. package/src/components/navigationindicator/MNavigationIndicator.figma.ts +7 -3
  75. package/src/components/numberbadge/MNumberBadge.figma.ts +7 -3
  76. package/src/components/optionListbox/MOptionListbox.figma.ts +10 -10
  77. package/src/components/overlay/MOverlay.figma.ts +5 -5
  78. package/src/components/overlay/MOverlay.spec.ts +1 -1
  79. package/src/components/overlay/MOverlay.vue +1 -1
  80. package/src/components/pageheader/MPageHeader.figma.ts +3 -3
  81. package/src/components/pagination/MPagination.figma.ts +10 -10
  82. package/src/components/passwordinput/MPasswordInput.figma.ts +9 -9
  83. package/src/components/phonenumber/MPhoneNumber.figma.ts +9 -9
  84. package/src/components/phonenumber/MPhoneNumber.spec.ts +6 -2
  85. package/src/components/phonenumber/MPhoneNumber.vue +21 -15
  86. package/src/components/pincode/MPincode.figma.ts +9 -9
  87. package/src/components/popover/MPopover.figma.ts +15 -15
  88. package/src/components/quantityselector/MQuantitySelector.figma.ts +12 -12
  89. package/src/components/radio/MRadio.figma.ts +9 -9
  90. package/src/components/radiogroup/MRadioGroup.figma.ts +7 -7
  91. package/src/components/segmentedcontrol/MSegmentedControl.figma.ts +8 -8
  92. package/src/components/select/MSelect.figma.ts +11 -11
  93. package/src/components/sidebar/MSidebar.figma.ts +8 -8
  94. package/src/components/sidebar/MSidebar.vue +1 -0
  95. package/src/components/sidebarexpandableitem/MSidebarExpandableItem.figma.ts +8 -5
  96. package/src/components/sidebarexpandableitem/MSidebarExpandableItem.spec.ts +12 -0
  97. package/src/components/sidebarexpandableitem/MSidebarExpandableItem.vue +1 -0
  98. package/src/components/sidebarfooter/MSidebarFooter.figma.ts +7 -3
  99. package/src/components/sidebarheader/MSidebarHeader.figma.ts +3 -3
  100. package/src/components/sidebarnavitem/MSidebarNavItem.figma.ts +3 -3
  101. package/src/components/sidebarshortcutitem/MSidebarShortcutItem.figma.ts +3 -3
  102. package/src/components/starrating/MStarRating.figma.ts +7 -7
  103. package/src/components/statusbadge/MStatusBadge.figma.ts +3 -3
  104. package/src/components/statusdot/MStatusDot.figma.ts +3 -3
  105. package/src/components/statusmessage/MStatusMessage.figma.ts +3 -3
  106. package/src/components/statusnotification/MStatusNotification.figma.ts +7 -7
  107. package/src/components/stepperbottombar/MStepperBottomBar.figma.ts +3 -3
  108. package/src/components/steppercompact/MStepperCompact.figma.ts +8 -3
  109. package/src/components/steppercompact/MStepperCompact.spec.ts +9 -0
  110. package/src/components/steppercompact/MStepperCompact.vue +1 -1
  111. package/src/components/stepperinline/MStepperInline.figma.ts +6 -3
  112. package/src/components/stepperinline/MStepperInline.spec.ts +11 -0
  113. package/src/components/stepperinline/MStepperInline.stories.ts +5 -1
  114. package/src/components/stepperinline/MStepperInline.vue +1 -1
  115. package/src/components/stepperstacked/MStepperStacked.figma.ts +6 -3
  116. package/src/components/stepperstacked/MStepperStacked.spec.ts +13 -0
  117. package/src/components/stepperstacked/MStepperStacked.vue +1 -0
  118. package/src/components/tabs/MTabs.figma.ts +8 -8
  119. package/src/components/tag/MTag.figma.ts +3 -3
  120. package/src/components/textarea/MTextArea.figma.ts +8 -8
  121. package/src/components/textinput/MTextInput.figma.ts +12 -12
  122. package/src/components/textinput/MTextInput.vue +2 -2
  123. package/src/components/textinput/README.md +1 -1
  124. package/src/components/tile/MTile.figma.ts +5 -5
  125. package/src/components/tileclickable/MTileClickable.figma.ts +6 -6
  126. package/src/components/tileexpandable/MTileExpandable.figma.ts +6 -6
  127. package/src/components/tileselectable/MTileSelectable.figma.ts +9 -5
  128. package/src/components/toaster/MToaster.figma.ts +3 -3
  129. package/src/components/toggle/MToggle.figma.ts +9 -9
  130. package/src/components/toggle/MToggle.vue +1 -1
  131. package/src/components/togglegroup/MToggleGroup.figma.ts +7 -7
  132. package/src/components/tooltip/MTooltip.figma.ts +10 -5
@@ -1,4 +1,4 @@
1
- import { describe, it, expect } from 'vitest';
1
+ import { describe, it, expect, vi } from 'vitest';
2
2
  import { mount } from '@vue/test-utils';
3
3
  import MDrawer from '@/components/drawer/MDrawer.vue';
4
4
 
@@ -11,7 +11,7 @@ const stubs = {
11
11
  },
12
12
  MOverlay: {
13
13
  name: 'MOverlay',
14
- template: `<div class="overlay" @click="$emit('click')"><slot/></div>`,
14
+ template: `<div class="overlay" @click="$emit('click', $event)"><slot/></div>`,
15
15
  },
16
16
  };
17
17
 
@@ -211,11 +211,39 @@ describe('MDrawer component', () => {
211
211
  global: { stubs },
212
212
  });
213
213
 
214
- await wrapper.find('section.mc-drawer').trigger('keydown.esc');
214
+ await wrapper
215
+ .find('section.mc-drawer')
216
+ .trigger('keydown', { key: 'Escape' });
215
217
  expect(wrapper.emitted('update:open')).toBeTruthy();
216
218
  expect(wrapper.emitted('update:open')!.at(-1)).toEqual([false]);
217
219
  });
218
220
 
221
+ it('stops Escape propagation after closing', async () => {
222
+ const onDocumentKeydown = vi.fn();
223
+ document.addEventListener('keydown', onDocumentKeydown);
224
+
225
+ const wrapper = mount(MDrawer, {
226
+ props: {
227
+ open: true,
228
+ title: 'Test Title',
229
+ },
230
+ attachTo: document.body,
231
+ global: { stubs },
232
+ });
233
+
234
+ wrapper
235
+ .find('section.mc-drawer')
236
+ .element.dispatchEvent(
237
+ new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }),
238
+ );
239
+
240
+ expect(wrapper.emitted('update:open')!.at(-1)).toEqual([false]);
241
+ expect(onDocumentKeydown).not.toHaveBeenCalled();
242
+
243
+ document.removeEventListener('keydown', onDocumentKeydown);
244
+ wrapper.unmount();
245
+ });
246
+
219
247
  it('locks and unlocks scroll when scroll=false and open changes', async () => {
220
248
  const wrapper = mount(MDrawer, {
221
249
  props: {
@@ -252,6 +280,77 @@ describe('MDrawer component', () => {
252
280
  expect(document.body.style.overflow).toBe('');
253
281
  });
254
282
 
283
+ it('sets inert on section when closed', async () => {
284
+ const wrapper = mount(MDrawer, {
285
+ props: { open: false, title: 'Test' },
286
+ global: { stubs },
287
+ });
288
+ // JSDOM renders :inert="true" as "true" — not.toBeUndefined() is the correct check
289
+ expect(
290
+ wrapper.find('section.mc-drawer').attributes('inert'),
291
+ ).not.toBeUndefined();
292
+
293
+ await wrapper.setProps({ open: true });
294
+ expect(
295
+ wrapper.find('section.mc-drawer').attributes('inert'),
296
+ ).toBeUndefined();
297
+ });
298
+
299
+ it('does not render aria-modal when closed', async () => {
300
+ const wrapper = mount(MDrawer, {
301
+ props: { open: false, title: 'Test' },
302
+ global: { stubs },
303
+ });
304
+ expect(
305
+ wrapper.find('section.mc-drawer').attributes('aria-modal'),
306
+ ).toBeUndefined();
307
+
308
+ await wrapper.setProps({ open: true });
309
+ expect(wrapper.find('section.mc-drawer').attributes('aria-modal')).toBe(
310
+ 'true',
311
+ );
312
+ });
313
+
314
+ it('traps focus forward: Tab from last focusable wraps to first', async () => {
315
+ const wrapper = mount(MDrawer, {
316
+ props: { open: true, title: 'Test', back: true },
317
+ attachTo: document.body,
318
+ global: { stubs },
319
+ });
320
+
321
+ const buttons = wrapper.findAll('button');
322
+ const lastEl = buttons[buttons.length - 1].element as HTMLElement;
323
+ lastEl.focus();
324
+
325
+ await wrapper
326
+ .find('section.mc-drawer')
327
+ .trigger('keydown', { key: 'Tab', shiftKey: false });
328
+
329
+ // focus should wrap to the first focusable button (back button)
330
+ expect(document.activeElement).toBe(buttons[0].element);
331
+ wrapper.unmount();
332
+ });
333
+
334
+ it('traps focus backward: Shift+Tab from first focusable wraps to last', async () => {
335
+ const wrapper = mount(MDrawer, {
336
+ props: { open: true, title: 'Test', back: true },
337
+ attachTo: document.body,
338
+ global: { stubs },
339
+ });
340
+
341
+ const buttons = wrapper.findAll('button');
342
+ const firstEl = buttons[0].element as HTMLElement;
343
+ firstEl.focus();
344
+
345
+ await wrapper
346
+ .find('section.mc-drawer')
347
+ .trigger('keydown', { key: 'Tab', shiftKey: true });
348
+
349
+ // focus should wrap to the last focusable button (close button)
350
+ expect(document.activeElement).toBe(buttons[buttons.length - 1].element);
351
+ wrapper.unmount();
352
+ });
353
+
255
354
  it('emits update:open on mount reflecting initial state', () => {
256
355
  const wrapper = mount(MDrawer, {
257
356
  props: {
@@ -1,20 +1,27 @@
1
1
  <template>
2
2
  <MOverlay
3
3
  :is-visible="open"
4
- dialogLabel="drawerTitle"
4
+ :dialogLabel="`drawerTitle-${id}`"
5
5
  @click="onClickOverlay"
6
6
  >
7
7
  <section
8
+ ref="sectionRef"
8
9
  class="mc-drawer"
9
10
  :class="classObject"
10
11
  role="dialog"
11
- aria-labelledby="drawerTitle"
12
- :aria-modal="open ? 'true' : 'false'"
12
+ :aria-labelledby="`drawerTitle-${id}`"
13
13
  tabindex="-1"
14
- :aria-hidden="!open"
15
- v-bind="$attrs"
16
- @keydown.esc="onClose"
17
- @click.stop
14
+ v-bind="{
15
+ ...$attrs,
16
+ ...(open
17
+ ? {
18
+ onKeydown: handleKeydown,
19
+ 'aria-modal': 'true',
20
+ }
21
+ : {
22
+ inert: 'true',
23
+ }),
24
+ }"
18
25
  >
19
26
  <div class="mc-drawer__dialog" role="document">
20
27
  <div class="mc-drawer__header">
@@ -26,13 +33,13 @@
26
33
  @click="emit('back')"
27
34
  >
28
35
  <template #icon>
29
- <ArrowBack24 aria-hidden="true" />
36
+ <ArrowBack24 />
30
37
  </template>
31
38
  </MIconButton>
32
39
  <h2
33
40
  class="mc-drawer__title"
34
41
  tabindex="-1"
35
- id="drawerTitle"
42
+ :id="`drawerTitle-${id}`"
36
43
  ref="titleRef"
37
44
  >
38
45
  {{ title }}
@@ -44,12 +51,12 @@
44
51
  @click="onClose"
45
52
  >
46
53
  <template #icon>
47
- <Cross24 aria-hidden="true" />
54
+ <Cross24 />
48
55
  </template>
49
56
  </MIconButton>
50
57
  </div>
51
58
  <div class="mc-drawer__body">
52
- <div class="mc-drawer__content" tabindex="0">
59
+ <div class="mc-drawer__content">
53
60
  <h2 v-if="contentTitle" class="mc-drawer__content__title">
54
61
  {{ contentTitle }}
55
62
  </h2>
@@ -65,7 +72,15 @@
65
72
  </template>
66
73
 
67
74
  <script setup lang="ts">
68
- import { computed, watch, type VNode, ref, onMounted, onUnmounted } from 'vue';
75
+ import {
76
+ computed,
77
+ watch,
78
+ type VNode,
79
+ ref,
80
+ onMounted,
81
+ onUnmounted,
82
+ useId,
83
+ } from 'vue';
69
84
  import { ArrowBack24, Cross24 } from '@mozaic-ds/icons-vue';
70
85
  import MIconButton from '../iconbutton/MIconButton.vue';
71
86
  import MOverlay from '../overlay/MOverlay.vue';
@@ -123,6 +138,8 @@ defineSlots<{
123
138
  footer?: VNode;
124
139
  }>();
125
140
 
141
+ const id = useId();
142
+
126
143
  const classObject = computed(() => {
127
144
  return {
128
145
  'is-open': props.open,
@@ -133,6 +150,45 @@ const classObject = computed(() => {
133
150
  });
134
151
 
135
152
  const titleRef = ref<HTMLElement | null>(null);
153
+ const sectionRef = ref<HTMLElement | null>(null);
154
+
155
+ function getFocusableElements(): HTMLElement[] {
156
+ if (!sectionRef.value) return [];
157
+ return Array.from(
158
+ sectionRef.value.querySelectorAll<HTMLElement>(
159
+ 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',
160
+ ),
161
+ );
162
+ }
163
+
164
+ function handleKeydown(e: KeyboardEvent) {
165
+ if (e.key === 'Escape') {
166
+ e.preventDefault();
167
+ e.stopPropagation();
168
+ onClose();
169
+ return;
170
+ }
171
+ if (e.key === 'Tab') {
172
+ const focusable = getFocusableElements();
173
+ if (!focusable.length) {
174
+ e.preventDefault();
175
+ return;
176
+ }
177
+ const first = focusable[0];
178
+ const last = focusable[focusable.length - 1];
179
+ if (e.shiftKey) {
180
+ if (document.activeElement === first) {
181
+ e.preventDefault();
182
+ last.focus();
183
+ }
184
+ } else {
185
+ if (document.activeElement === last) {
186
+ e.preventDefault();
187
+ first.focus();
188
+ }
189
+ }
190
+ }
191
+ }
136
192
  const isClient =
137
193
  typeof window !== 'undefined' && typeof document !== 'undefined';
138
194
 
@@ -169,8 +225,11 @@ onUnmounted(() => {
169
225
  unlockScroll();
170
226
  });
171
227
 
172
- const onClickOverlay = () => {
173
- if (props.closeOnOverlay) {
228
+ const onClickOverlay = (event: MouseEvent) => {
229
+ if (
230
+ props.closeOnOverlay &&
231
+ !sectionRef.value?.contains(event.target as Node)
232
+ ) {
174
233
  onClose();
175
234
  }
176
235
  };
@@ -20,11 +20,15 @@ figma.connect(
20
20
  },
21
21
  example: ({ label, requirementText }) =>
22
22
  html`<script setup>
23
- import { MField, MTextInput } from '@mozaic-ds/vue';
24
- </script>
23
+ import { MField, MTextInput } from '@mozaic-ds/vue';
24
+ </script>
25
25
 
26
- <MField id="field-id" label=${label} requirement-text=${requirementText}>
27
- <MTextInput id="field-id" placeholder="Placeholder"></MTextInput>
28
- </MField>`,
26
+ <MField
27
+ id="field-id"
28
+ label=${label}
29
+ requirement-text=${requirementText}
30
+ >
31
+ <MTextInput id="field-id" placeholder="Placeholder"></MTextInput>
32
+ </MField>`,
29
33
  },
30
34
  );
@@ -20,6 +20,7 @@
20
20
  class="mc-field__validation-message"
21
21
  :id="messageId"
22
22
  :class="classObjectValidation"
23
+ :role="isInvalid ? 'alert' : 'status'"
23
24
  >
24
25
  <MLoader v-if="isLoading" size="xs"></MLoader>
25
26
  {{ message }}
@@ -15,9 +15,9 @@ figma.connect(
15
15
  },
16
16
  example: ({ hasDragDrop }) =>
17
17
  html`<script setup>
18
- import { MFileUploader } from '@mozaic-ds/vue';
19
- </script>
18
+ import { MFileUploader } from '@mozaic-ds/vue';
19
+ </script>
20
20
 
21
- <MFileUploader :model-value="[]" :has-drag-drop=${hasDragDrop} />`,
21
+ <MFileUploader :model-value="[]" :has-drag-drop=${hasDragDrop} />`,
22
22
  },
23
23
  );
@@ -9,12 +9,12 @@
9
9
  <input
10
10
  ref="fileInput"
11
11
  type="file"
12
- aria-label="File input"
12
+ aria-hidden="true"
13
+ tabindex="-1"
13
14
  :accept="props.accept"
14
15
  :multiple="props.multiple"
15
16
  class="mc-file-uploader__hidden-input"
16
17
  :disabled="props.disabled"
17
- :aria-disabled="props.disabled"
18
18
  @change="onChange"
19
19
  />
20
20
 
@@ -19,9 +19,13 @@ figma.connect(
19
19
  },
20
20
  example: ({ format, valid }) =>
21
21
  html`<script setup>
22
- import { MFileUploaderItem } from '@mozaic-ds/vue';
23
- </script>
22
+ import { MFileUploaderItem } from '@mozaic-ds/vue';
23
+ </script>
24
24
 
25
- <MFileUploaderItem :file="{ name: 'document.pdf' }" format=${format} :valid=${valid} />`,
25
+ <MFileUploaderItem
26
+ :file="{ name: 'document.pdf' }"
27
+ format=${format}
28
+ :valid=${valid}
29
+ />`,
26
30
  },
27
31
  );
@@ -40,13 +40,7 @@
40
40
  <template v-if="isStacked">
41
41
  <MDivider />
42
42
  <div class="mc-file-uploader-item__actions-container">
43
- <MButton
44
- ghost
45
- size="s"
46
- icon-position="left"
47
- aria-label="Delete file"
48
- @click="emit('delete')"
49
- >
43
+ <MButton ghost size="s" icon-position="left" @click="emit('delete')">
50
44
  {{ props.deleteButtonLabel }}
51
45
 
52
46
  <template #icon>
@@ -78,6 +72,7 @@
78
72
  <span
79
73
  v-if="!valid && (props.errorMessage || slots.errorMessage)"
80
74
  class="mc-file-uploader-item__error-message"
75
+ role="alert"
81
76
  >
82
77
  <slot name="errorMessage">
83
78
  {{ props.errorMessage }}
@@ -18,9 +18,9 @@ figma.connect(
18
18
  },
19
19
  example: ({ label, appearance }) =>
20
20
  html`<script setup>
21
- import { MFlag } from '@mozaic-ds/vue';
22
- </script>
21
+ import { MFlag } from '@mozaic-ds/vue';
22
+ </script>
23
23
 
24
- <MFlag label=${label} appearance=${appearance}></MFlag>`,
24
+ <MFlag label=${label} appearance=${appearance}></MFlag>`,
25
25
  },
26
26
  );
@@ -33,22 +33,22 @@ figma.connect(
33
33
  },
34
34
  example: ({ appearance, size, disabled, isLoading, outlined, ghost }) =>
35
35
  html`<script setup>
36
- import { MIconButton } from '@mozaic-ds/vue';
37
- import { ChevronRight24 } from '@mozaic-ds/icons-vue';
38
- </script>
36
+ import { MIconButton } from '@mozaic-ds/vue';
37
+ import { ChevronRight24 } from '@mozaic-ds/icons-vue';
38
+ </script>
39
39
 
40
- <MIconButton
41
- appearance=${appearance}
42
- size=${size}
43
- disabled=${disabled}
44
- :is-loading=${isLoading}
45
- outlined=${outlined}
46
- ghost=${ghost}
47
- aria-label="Action"
48
- >
49
- <template #icon>
50
- <ChevronRight24 />
51
- </template>
52
- </MIconButton>`,
40
+ <MIconButton
41
+ appearance=${appearance}
42
+ size=${size}
43
+ disabled=${disabled}
44
+ :is-loading=${isLoading}
45
+ outlined=${outlined}
46
+ ghost=${ghost}
47
+ aria-label="Action"
48
+ >
49
+ <template #icon>
50
+ <ChevronRight24 />
51
+ </template>
52
+ </MIconButton>`,
53
53
  },
54
54
  );
@@ -92,6 +92,21 @@ describe('MButton component', () => {
92
92
  expect(button.attributes('type')).toBe('button');
93
93
  });
94
94
 
95
+ it('sets aria-busy when isLoading is true', () => {
96
+ const wrapper = mount(MIconButton, {
97
+ props: { isLoading: true },
98
+ slots: { icon: [ChevronRight24] },
99
+ });
100
+ expect(wrapper.find('button').attributes('aria-busy')).toBe('true');
101
+ });
102
+
103
+ it('does not set aria-busy when not loading', () => {
104
+ const wrapper = mount(MIconButton, {
105
+ slots: { icon: [ChevronRight24] },
106
+ });
107
+ expect(wrapper.find('button').attributes('aria-busy')).toBeUndefined();
108
+ });
109
+
95
110
  it('can have type="submit" when the type prop is "submit"', () => {
96
111
  const wrapper = mount(MIconButton, {
97
112
  props: {
@@ -4,6 +4,7 @@
4
4
  :class="classObject"
5
5
  :disabled="disabled"
6
6
  :type="type"
7
+ :aria-busy="isLoading || undefined"
7
8
  >
8
9
  <span
9
10
  v-if="isLoading"
@@ -25,9 +25,14 @@ figma.connect(
25
25
  },
26
26
  example: ({ value, label, size, status }) =>
27
27
  html`<script setup>
28
- import { MKpiItem } from '@mozaic-ds/vue';
29
- </script>
28
+ import { MKpiItem } from '@mozaic-ds/vue';
29
+ </script>
30
30
 
31
- <MKpiItem value=${value} label=${label} size=${size} status=${status} />`,
31
+ <MKpiItem
32
+ value=${value}
33
+ label=${label}
34
+ size=${size}
35
+ status=${status}
36
+ />`,
32
37
  },
33
38
  );
@@ -67,5 +67,17 @@ describe('MKpiItem component', () => {
67
67
 
68
68
  expect(wrapper.find('.mc-kpi__icon').exists()).toBe(true);
69
69
  });
70
+
71
+ it.each([['increasing'], ['decreasing'], ['stable']] as const)(
72
+ 'gives the trend icon an accessible label for "%s"',
73
+ (trend) => {
74
+ const wrapper = mount(KpiItem, {
75
+ props: { value: '123', trend },
76
+ });
77
+ const icon = wrapper.find('.mc-kpi__icon');
78
+ expect(icon.attributes('role')).toBe('img');
79
+ expect(icon.attributes('aria-label')).toBe(trend);
80
+ },
81
+ );
70
82
  });
71
83
  });
@@ -15,7 +15,13 @@
15
15
  {{ information }}
16
16
  </span>
17
17
 
18
- <component v-if="trend" :is="getIconComponent" class="mc-kpi__icon" />
18
+ <component
19
+ v-if="trend"
20
+ :is="getIconComponent"
21
+ class="mc-kpi__icon"
22
+ role="img"
23
+ :aria-label="trend"
24
+ />
19
25
  </div>
20
26
  </div>
21
27
  </div>
@@ -23,9 +23,12 @@ figma.connect(
23
23
  },
24
24
  example: ({ size, value }) =>
25
25
  html`<script setup>
26
- import { MLinearProgressbarBuffer } from '@mozaic-ds/vue';
27
- </script>
26
+ import { MLinearProgressbarBuffer } from '@mozaic-ds/vue';
27
+ </script>
28
28
 
29
- <MLinearProgressbarBuffer size=${size} :value=${value}></MLinearProgressbarBuffer>`,
29
+ <MLinearProgressbarBuffer
30
+ size=${size}
31
+ :value=${value}
32
+ ></MLinearProgressbarBuffer>`,
30
33
  },
31
34
  );
@@ -18,9 +18,11 @@ figma.connect(
18
18
  },
19
19
  example: ({ value }) =>
20
20
  html`<script setup>
21
- import { MLinearProgressbarPercentage } from '@mozaic-ds/vue';
22
- </script>
21
+ import { MLinearProgressbarPercentage } from '@mozaic-ds/vue';
22
+ </script>
23
23
 
24
- <MLinearProgressbarPercentage :value=${value}></MLinearProgressbarPercentage>`,
24
+ <MLinearProgressbarPercentage
25
+ :value=${value}
26
+ ></MLinearProgressbarPercentage>`,
25
27
  },
26
28
  );
@@ -22,11 +22,11 @@ figma.connect(
22
22
  },
23
23
  example: ({ label, size, appearance }) =>
24
24
  html`<script setup>
25
- import { MLink } from '@mozaic-ds/vue';
26
- </script>
25
+ import { MLink } from '@mozaic-ds/vue';
26
+ </script>
27
27
 
28
- <MLink href="#" size=${size} appearance=${appearance}>
29
- ${label}
30
- </MLink>`,
28
+ <MLink href="#" size=${size} appearance=${appearance}>
29
+ ${label}
30
+ </MLink>`,
31
31
  },
32
32
  );
@@ -22,9 +22,9 @@ figma.connect(
22
22
  },
23
23
  example: ({ appearance, size }) =>
24
24
  html`<script setup>
25
- import { MLoader } from '@mozaic-ds/vue';
26
- </script>
25
+ import { MLoader } from '@mozaic-ds/vue';
26
+ </script>
27
27
 
28
- <MLoader appearance=${appearance} size=${size}></MLoader>`,
28
+ <MLoader appearance=${appearance} size=${size}></MLoader>`,
29
29
  },
30
30
  );
@@ -10,9 +10,9 @@ figma.connect(
10
10
  props: {},
11
11
  example: () =>
12
12
  html`<script setup>
13
- import { MLoadingOverlay } from '@mozaic-ds/vue';
14
- </script>
13
+ import { MLoadingOverlay } from '@mozaic-ds/vue';
14
+ </script>
15
15
 
16
- <MLoadingOverlay is-visible text="Loading..."></MLoadingOverlay>`,
16
+ <MLoadingOverlay is-visible text="Loading..."></MLoadingOverlay>`,
17
17
  },
18
18
  );
@@ -10,18 +10,18 @@ figma.connect(
10
10
  props: {},
11
11
  example: () =>
12
12
  html`<script setup>
13
- import { MModal, MButton } from '@mozaic-ds/vue';
14
- </script>
13
+ import { MModal, MButton } from '@mozaic-ds/vue';
14
+ </script>
15
15
 
16
- <MModal
17
- open
18
- title="Modal title"
19
- description="A modal is a dialog window allowing you to focus the user's attention."
20
- closable
21
- >
22
- <template #footer>
23
- <MButton>Confirm</MButton>
24
- </template>
25
- </MModal>`,
16
+ <MModal
17
+ open
18
+ title="Modal title"
19
+ description="A modal is a dialog window allowing you to focus the user's attention."
20
+ closable
21
+ >
22
+ <template #footer>
23
+ <MButton>Confirm</MButton>
24
+ </template>
25
+ </MModal>`,
26
26
  },
27
27
  );