@mozaic-ds/vue 2.17.0 → 2.19.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 (69) hide show
  1. package/dist/mozaic-vue.css +1 -1
  2. package/dist/mozaic-vue.d.ts +158 -72
  3. package/dist/mozaic-vue.js +1728 -4748
  4. package/dist/mozaic-vue.js.map +1 -1
  5. package/dist/mozaic-vue.umd.cjs +6 -25
  6. package/dist/mozaic-vue.umd.cjs.map +1 -1
  7. package/package.json +16 -10
  8. package/src/components/BrandPresets.mdx +20 -2
  9. package/src/components/Migration.mdx +651 -0
  10. package/src/components/accordionlistitem/MAccordionListItem.spec.ts +22 -3
  11. package/src/components/accordionlistitem/MAccordionListItem.vue +38 -28
  12. package/src/components/actionlistbox/MActionListbox.spec.ts +99 -0
  13. package/src/components/actionlistbox/MActionListbox.vue +54 -7
  14. package/src/components/breadcrumb/MBreadcrumb.vue +1 -1
  15. package/src/components/builtinmenu/MBuiltInMenu.spec.ts +30 -1
  16. package/src/components/builtinmenu/MBuiltInMenu.vue +26 -17
  17. package/src/components/builtinmenu/README.md +2 -0
  18. package/src/components/button/MButton.spec.ts +26 -0
  19. package/src/components/button/MButton.vue +2 -0
  20. package/src/components/callout/MCallout.spec.ts +35 -0
  21. package/src/components/callout/MCallout.stories.ts +0 -3
  22. package/src/components/callout/MCallout.vue +26 -7
  23. package/src/components/callout/README.md +4 -2
  24. package/src/components/carousel/MCarousel.spec.ts +26 -2
  25. package/src/components/carousel/MCarousel.vue +10 -4
  26. package/src/components/checklistmenu/MCheckListMenu.spec.ts +12 -1
  27. package/src/components/checklistmenu/MCheckListMenu.vue +6 -0
  28. package/src/components/checklistmenu/README.md +2 -0
  29. package/src/components/combobox/MCombobox.vue +7 -0
  30. package/src/components/datatable/datatable.mdx +3 -2
  31. package/src/components/drawer/MDrawer.spec.ts +102 -3
  32. package/src/components/drawer/MDrawer.vue +73 -14
  33. package/src/components/field/MField.vue +1 -0
  34. package/src/components/fileuploader/MFileUploader.vue +2 -2
  35. package/src/components/fileuploaderitem/MFileUploaderItem.vue +2 -7
  36. package/src/components/iconbutton/MIconButton.spec.ts +15 -0
  37. package/src/components/iconbutton/MIconButton.vue +1 -0
  38. package/src/components/kpiitem/MKpiItem.spec.ts +13 -0
  39. package/src/components/kpiitem/MKpiItem.vue +1 -1
  40. package/src/components/modal/MModal.spec.ts +115 -3
  41. package/src/components/modal/MModal.vue +91 -11
  42. package/src/components/modal/README.md +1 -1
  43. package/src/components/navigationindicator/MNavigationIndicator.spec.ts +75 -18
  44. package/src/components/navigationindicator/MNavigationIndicator.vue +10 -12
  45. package/src/components/optionListbox/MOptionListbox.vue +16 -1
  46. package/src/components/overlay/MOverlay.spec.ts +1 -1
  47. package/src/components/overlay/MOverlay.vue +1 -1
  48. package/src/components/phonenumber/MPhoneNumber.spec.ts +6 -2
  49. package/src/components/phonenumber/MPhoneNumber.vue +20 -16
  50. package/src/components/popover/MPopover.spec.ts +126 -0
  51. package/src/components/popover/MPopover.vue +36 -1
  52. package/src/components/segmentedcontrol/MSegmentedControl.spec.ts +92 -0
  53. package/src/components/segmentedcontrol/MSegmentedControl.vue +61 -2
  54. package/src/components/sidebarexpandableitem/MSidebarExpandableItem.spec.ts +12 -0
  55. package/src/components/sidebarexpandableitem/MSidebarExpandableItem.vue +1 -0
  56. package/src/components/starrating/MStarRating.spec.ts +19 -22
  57. package/src/components/starrating/MStarRating.vue +3 -2
  58. package/src/components/steppercompact/MStepperCompact.spec.ts +9 -0
  59. package/src/components/steppercompact/MStepperCompact.vue +1 -1
  60. package/src/components/stepperinline/MStepperInline.spec.ts +11 -0
  61. package/src/components/stepperinline/MStepperInline.vue +1 -1
  62. package/src/components/stepperstacked/MStepperStacked.spec.ts +13 -0
  63. package/src/components/stepperstacked/MStepperStacked.vue +1 -0
  64. package/src/components/tabs/MTabs.vue +90 -4
  65. package/src/components/tabs/Mtabs.spec.ts +162 -0
  66. package/src/components/textinput/MTextInput.vue +2 -2
  67. package/src/components/toggle/MToggle.vue +1 -1
  68. package/src/main.ts +1 -0
  69. package/src/components/ComponentsMapping.mdx +0 -98
@@ -1,17 +1,28 @@
1
1
  import { mount } from '@vue/test-utils';
2
- import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
2
+ import {
3
+ describe,
4
+ it,
5
+ expect,
6
+ vi,
7
+ beforeAll,
8
+ afterAll,
9
+ beforeEach,
10
+ } from 'vitest';
3
11
  import MCarousel from './MCarousel.vue';
4
12
  import MIconButton from '../iconbutton/MIconButton.vue';
5
13
  import { ChevronLeft20, ChevronRight20 } from '@mozaic-ds/icons-vue';
6
14
 
7
15
  /* eslint-disable @typescript-eslint/no-explicit-any */
8
16
 
17
+ const observerInstances: MockIntersectionObserver[] = [];
18
+
9
19
  class MockIntersectionObserver {
10
20
  callback: any;
11
21
  options: any;
12
22
  constructor(callback: any, options?: any) {
13
23
  this.callback = callback;
14
24
  this.options = options;
25
+ observerInstances.push(this);
15
26
  }
16
27
  observe = vi.fn();
17
28
  unobserve = vi.fn();
@@ -21,6 +32,10 @@ class MockIntersectionObserver {
21
32
  describe('MCarousel component', () => {
22
33
  let originalObserver: any;
23
34
 
35
+ beforeEach(() => {
36
+ observerInstances.length = 0;
37
+ });
38
+
24
39
  beforeAll(() => {
25
40
  originalObserver = global.IntersectionObserver;
26
41
  global.IntersectionObserver = MockIntersectionObserver as any;
@@ -132,6 +147,15 @@ describe('MCarousel component', () => {
132
147
  const container = wrapper.find('.mc-carousel');
133
148
  expect(container.attributes('role')).toBe('group');
134
149
  expect(container.attributes('aria-roledescription')).toBe('carousel');
135
- expect(container.attributes('aria-labelledby')).toBe('mc-carousel__title');
150
+ // aria-labelledby should point to the headings wrapper (dynamic id)
151
+ const labelledby = container.attributes('aria-labelledby');
152
+ expect(labelledby).toBeDefined();
153
+ expect(wrapper.find(`#${labelledby}`).exists()).toBe(true);
154
+ });
155
+
156
+ it('slide container has aria-live="polite"', () => {
157
+ const wrapper = mountCarousel();
158
+ const content = wrapper.find('.mc-carousel__content');
159
+ expect(content.attributes('aria-live')).toBe('polite');
136
160
  });
137
161
  });
@@ -3,10 +3,10 @@
3
3
  class="mc-carousel"
4
4
  role="group"
5
5
  aria-roledescription="carousel"
6
- aria-labelledby="mc-carousel__title"
6
+ :aria-labelledby="titleId"
7
7
  >
8
8
  <div class="mc-carousel__header">
9
- <div class="mc-carousel__headings">
9
+ <div :id="titleId" class="mc-carousel__headings">
10
10
  <slot name="header" />
11
11
  </div>
12
12
  <div class="mc-carousel__controls">
@@ -34,7 +34,12 @@
34
34
  </MIconButton>
35
35
  </div>
36
36
  </div>
37
- <div class="mc-carousel__content" ref="contentContainer">
37
+ <div
38
+ class="mc-carousel__content"
39
+ ref="contentContainer"
40
+ aria-live="polite"
41
+ aria-atomic="false"
42
+ >
38
43
  <template
39
44
  v-for="(child, index) in $slots.default?.()"
40
45
  :key="`carousel-slide-${index}`"
@@ -46,7 +51,7 @@
46
51
  </template>
47
52
 
48
53
  <script setup lang="ts">
49
- import { computed, onMounted, ref, type VNode } from 'vue';
54
+ import { computed, onMounted, ref, useId, type VNode } from 'vue';
50
55
  import MIconButton from '../iconbutton/MIconButton.vue';
51
56
  import { ChevronLeft20, ChevronRight20 } from '@mozaic-ds/icons-vue';
52
57
  /**
@@ -80,6 +85,7 @@ defineSlots<{
80
85
  header: VNode;
81
86
  }>();
82
87
 
88
+ const titleId = useId();
83
89
  const activeIndex = ref<number>(0);
84
90
  const contentContainer = ref<HTMLElement | null>(null);
85
91
 
@@ -6,7 +6,7 @@ import type { MenuItem } from '../builtinmenu/MBuiltInMenu.vue';
6
6
 
7
7
  const StubMBuiltInMenu = {
8
8
  name: 'MBuiltInMenu',
9
- props: ['modelValue', 'items', 'outlined'],
9
+ props: ['modelValue', 'items', 'outlined', 'label'],
10
10
  emits: ['update:modelValue'],
11
11
  template: '<div />',
12
12
  };
@@ -60,6 +60,17 @@ describe('MCheckListMenu', () => {
60
60
  expect(builtIn.props('outlined')).toBe(true);
61
61
  });
62
62
 
63
+ it('forwards label prop to MBuiltInMenu', () => {
64
+ const items = [{ label: 'A', checked: false }];
65
+ const wrapper = mount(MCheckListMenu, {
66
+ props: { items, label: 'Checklist navigation' },
67
+ global: { components: { MBuiltInMenu: StubMBuiltInMenu } },
68
+ });
69
+
70
+ const builtIn = wrapper.findComponent(StubMBuiltInMenu);
71
+ expect(builtIn.props('label')).toBe('Checklist navigation');
72
+ });
73
+
63
74
  it('emits update:modelValue when inner menu updates modelValue', async () => {
64
75
  const items = [{ label: 'X', checked: false }];
65
76
  const wrapper = mount(MCheckListMenu, {
@@ -3,6 +3,7 @@
3
3
  v-model="currentMenuItem"
4
4
  :items="menuItems"
5
5
  :outlined="props.outlined"
6
+ :label="props.label"
6
7
  />
7
8
  </template>
8
9
 
@@ -31,6 +32,11 @@ const props = defineProps<{
31
32
  * When enabled, adds a visible border around the wrapper to highlight or separate its content.
32
33
  */
33
34
  outlined?: boolean;
35
+ /**
36
+ * Accessible label for the navigation landmark. Should describe the purpose
37
+ * of this menu to distinguish it from other navigations on the page.
38
+ */
39
+ label?: string;
34
40
  }>();
35
41
 
36
42
  const emit = defineEmits<{
@@ -10,6 +10,8 @@ A check-list menu is a structured vertical list where each item represents a dis
10
10
  | `modelValue` | Specifies the key of the currently selected menu item. It allows the component to highlight or style the corresponding item to indicate it is selected or currently in use. | `number` | - |
11
11
  | `items*` | Defines the menu items, each of which sets a checked state and act as a button, link, or router-link. | `CheckListMenuItem[]` | - |
12
12
  | `outlined` | When enabled, adds a visible border around the wrapper to highlight or separate its content. | `boolean` | - |
13
+ | `label` | Accessible label for the navigation landmark. Should describe the purpose
14
+ of this menu to distinguish it from other navigations on the page. | `string` | - |
13
15
 
14
16
  ## Events
15
17
 
@@ -65,6 +65,7 @@
65
65
  <MOptionListbox
66
66
  ref="listbox"
67
67
  v-model="selection"
68
+ @update:model-value="onSelectionUpdate"
68
69
  :id
69
70
  :open="isOpen"
70
71
  :multiple
@@ -267,6 +268,12 @@ function close() {
267
268
  document.removeEventListener('click', handleClickOutside);
268
269
  }
269
270
 
271
+ function onSelectionUpdate() {
272
+ if (!props.multiple) {
273
+ close();
274
+ }
275
+ }
276
+
270
277
  function toggleListbox() {
271
278
  return isOpen.value ? close() : open();
272
279
  }
@@ -36,7 +36,8 @@ Then to use the component, you can proceed as follows:
36
36
  dark
37
37
  code={`
38
38
  <script setup>
39
- import { MButton } from '@mozaic-ds/vue';
39
+ import '@mozaic-ds/datatable-vue/style.css';
40
+ import { MDataTable, MDataTableColumn } from '@mozaic-ds/datatable-vue';
40
41
  </script>
41
42
 
42
43
  <template>
@@ -59,4 +60,4 @@ Then to use the component, you can proceed as follows:
59
60
  </template>
60
61
  `} />
61
62
 
62
- To see more examples of usage, please refer to the [stories](https://adeo.github.io/mozaic-vue/?path=/docs/datatable-mdatatable-default--docs) of the component.
63
+ To see more examples of usage, please refer to the other stories of the component.
@@ -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,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 }}
@@ -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
 
@@ -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 }}
@@ -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"
@@ -67,5 +67,18 @@ describe('MKpiItem component', () => {
67
67
 
68
68
  expect(wrapper.find('.mc-kpi__icon').exists()).toBe(true);
69
69
  });
70
+
71
+ it.each([
72
+ ['increasing'],
73
+ ['decreasing'],
74
+ ['stable'],
75
+ ] as const)('gives the trend icon an accessible label for "%s"', (trend) => {
76
+ const wrapper = mount(KpiItem, {
77
+ props: { value: '123', trend },
78
+ });
79
+ const icon = wrapper.find('.mc-kpi__icon');
80
+ expect(icon.attributes('role')).toBe('img');
81
+ expect(icon.attributes('aria-label')).toBe(trend);
82
+ });
70
83
  });
71
84
  });
@@ -15,7 +15,7 @@
15
15
  {{ information }}
16
16
  </span>
17
17
 
18
- <component v-if="trend" :is="getIconComponent" class="mc-kpi__icon" />
18
+ <component v-if="trend" :is="getIconComponent" class="mc-kpi__icon" role="img" :aria-label="trend" />
19
19
  </div>
20
20
  </div>
21
21
  </div>