@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mozaic-ds/vue",
3
- "version": "2.18.0",
3
+ "version": "2.19.1",
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",
@@ -43,7 +43,7 @@
43
43
  "*.d.ts"
44
44
  ],
45
45
  "dependencies": {
46
- "@mozaic-ds/styles": "^2.18.0",
46
+ "@mozaic-ds/styles": "^2.23.0",
47
47
  "@mozaic-ds/web-fonts": "^1.65.0",
48
48
  "postcss-scss": "^4.0.9"
49
49
  },
@@ -51,9 +51,10 @@
51
51
  "vue": "^3.5.13"
52
52
  },
53
53
  "devDependencies": {
54
- "@commitlint/cli": "^20.1.0",
55
- "@commitlint/config-conventional": "^20.0.0",
54
+ "@commitlint/cli": "^21.0.1",
55
+ "@commitlint/config-conventional": "^21.0.1",
56
56
  "@figma/code-connect": "^1.4.1",
57
+ "@microsoft/api-extractor": "^7.58.7",
57
58
  "@mozaic-ds/css-dev-tools": "1.75.0",
58
59
  "@mozaic-ds/datatable-vue": "^1.0.0",
59
60
  "@mozaic-ds/icons-vue": "^2.5.0",
@@ -68,6 +69,7 @@
68
69
  "@vitest/eslint-plugin": "^1.1.38",
69
70
  "@vue/eslint-config-prettier": "^10.2.0",
70
71
  "@vue/eslint-config-typescript": "^14.5.0",
72
+ "@vue/language-core": "^3.3.2",
71
73
  "@vue/test-utils": "^2.4.6",
72
74
  "dotenv-cli": "^11.0.0",
73
75
  "eslint": "^10.0.2",
@@ -78,7 +80,7 @@
78
80
  "husky": "^9.1.7",
79
81
  "jsdom": "^29.0.0",
80
82
  "libphonenumber-js": "^1.12.23",
81
- "lint-staged": "^16.1.5",
83
+ "lint-staged": "^17.0.5",
82
84
  "mdx-mermaid": "^2.0.3",
83
85
  "mermaid": "^11.5.0",
84
86
  "npm-run-all": "^4.1.5",
@@ -88,8 +90,8 @@
88
90
  "storybook": "^10.0.4",
89
91
  "storybook-addon-tag-badges": "^3.0.2",
90
92
  "typescript": "^6.0.3",
93
+ "unplugin-dts": "^1.0.1",
91
94
  "vite": "^8.0.0",
92
- "vite-plugin-dts": "^4.5.3",
93
95
  "vitest": "^4.0.7",
94
96
  "vue": "^3.5.13",
95
97
  "vue-component-meta": "^3.0.8",
@@ -80,16 +80,34 @@ The table below summarises which font to use depending on the brand.
80
80
  </tr>
81
81
  </table>
82
82
 
83
+ For example, here is how to include the Roboto font in your HTML for the Adeo brand:
84
+
85
+ <Source
86
+ language="html"
87
+ dark
88
+ code={`
89
+ <head>
90
+ <meta charset="UTF-8" />
91
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
92
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
93
+ <link
94
+ href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100..900;1,100..900&display=swap"
95
+ rel="stylesheet"
96
+ />
97
+ </head>
98
+ `}
99
+ />
100
+
83
101
  From there, we can update the main style sheet of your project, in order to import the right font.
84
102
 
85
103
  <Source
86
104
  language='css'
87
105
  dark
88
106
  code={`
89
- @use "@mozaic-ds/styles/tools/t.font" as *;
107
+ @use '@mozaic-ds/tokens/adeo/theme' as *;
90
108
 
91
109
  body {
92
- @include set-font-family();
110
+ font-family: var(--font-family, 'Roboto', sans-serif);
93
111
  }
94
112
  `} />
95
113
 
@@ -22,22 +22,22 @@ figma.connect(
22
22
  },
23
23
  example: ({ appearance }) =>
24
24
  html`<script setup>
25
- import { MAccordionList, MAccordionListItem } from '@mozaic-ds/vue';
26
- </script>
25
+ import { MAccordionList, MAccordionListItem } from '@mozaic-ds/vue';
26
+ </script>
27
27
 
28
- <MAccordionList appearance=${appearance}>
29
- <MAccordionListItem id="1" title="Content title">
30
- <p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</p>
31
- </MAccordionListItem>
32
- <MAccordionListItem id="2" title="Content title">
33
- <p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</p>
34
- </MAccordionListItem>
35
- <MAccordionListItem id="3" title="Content title">
36
- <p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</p>
37
- </MAccordionListItem>
38
- <MAccordionListItem id="4" title="Content title">
39
- <p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</p>
40
- </MAccordionListItem>
41
- </MAccordionList>`,
28
+ <MAccordionList appearance=${appearance}>
29
+ <MAccordionListItem id="1" title="Content title">
30
+ <p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</p>
31
+ </MAccordionListItem>
32
+ <MAccordionListItem id="2" title="Content title">
33
+ <p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</p>
34
+ </MAccordionListItem>
35
+ <MAccordionListItem id="3" title="Content title">
36
+ <p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</p>
37
+ </MAccordionListItem>
38
+ <MAccordionListItem id="4" title="Content title">
39
+ <p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</p>
40
+ </MAccordionListItem>
41
+ </MAccordionList>`,
42
42
  },
43
43
  );
@@ -51,7 +51,7 @@ const meta: Meta<typeof MAccordionList> = {
51
51
  return { args, Wrench32, Project32, Sharpening32, handleUpdate };
52
52
  },
53
53
  template: `
54
- <MAccordionList v-bind="args" @update:modelValue="handleUpdate">
54
+ <MAccordionList v-bind="args" v-model="args.modelValue" @update:modelValue="handleUpdate">
55
55
  ${args.default}
56
56
  </MAccordionList>
57
57
  `,
@@ -16,12 +16,16 @@ figma.connect(
16
16
  },
17
17
  example: ({ title, subtitle }) =>
18
18
  html`<script setup>
19
- import { MAccordionListItem } from '@mozaic-ds/vue';
20
- </script>
19
+ import { MAccordionListItem } from '@mozaic-ds/vue';
20
+ </script>
21
21
 
22
- <MAccordionListItem id="accordion-1" title=${title} subtitle=${subtitle}>
23
- <p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</p>
24
- </MAccordionListItem>`,
22
+ <MAccordionListItem
23
+ id="accordion-1"
24
+ title=${title}
25
+ subtitle=${subtitle}
26
+ >
27
+ <p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</p>
28
+ </MAccordionListItem>`,
25
29
  imports: ["import { MAccordionListItem } from '@mozaic-ds/vue'"],
26
30
  },
27
31
  );
@@ -23,7 +23,10 @@
23
23
  </div>
24
24
  <div
25
25
  :id="`content-${id}`"
26
- class="mc-accordion__content"
26
+ :class="{
27
+ 'mc-accordion__content': true,
28
+ 'mc-accordion__content--open': open,
29
+ }"
27
30
  :inert="!open || undefined"
28
31
  :aria-labelledby="`accordion-${id}`"
29
32
  role="region"
@@ -15,6 +15,8 @@ If no ID is provided, a unique one is generated automatically. | `string` | - |
15
15
  | `subtitle` | An optional secondary heading displayed below the title. It provides additional context or detail about the content of the accordion item. | `string` | - |
16
16
  | `content` | The main content of the accordion item. This is the information revealed when the accordion is expanded, typically containing text, HTML, or other elements. | `string` | - |
17
17
  | `icon` | Icon component to display before the item title. | `Component` | - |
18
+ | `tag` | Heading level for the accordion trigger (h2–h6). Adjust to match the
19
+ heading hierarchy of the page where the accordion is used. | `"h2"` `"h1"` `"h3"` `"h4"` `"h5"` `"h6"` | `"h2"` |
18
20
 
19
21
  ## Slots
20
22
 
@@ -12,13 +12,13 @@ figma.connect(
12
12
  },
13
13
  example: ({ shadow }) =>
14
14
  html`<script setup>
15
- import { MActionBottomBar, MButton } from '@mozaic-ds/vue';
16
- </script>
15
+ import { MActionBottomBar, MButton } from '@mozaic-ds/vue';
16
+ </script>
17
17
 
18
- <MActionBottomBar shadow=${shadow}>
19
- <template #right>
20
- <MButton size="s" appearance="accent">Save</MButton>
21
- </template>
22
- </MActionBottomBar>`,
18
+ <MActionBottomBar shadow=${shadow}>
19
+ <template #right>
20
+ <MButton size="s" appearance="accent">Save</MButton>
21
+ </template>
22
+ </MActionBottomBar>`,
23
23
  },
24
24
  );
@@ -10,21 +10,21 @@ figma.connect(
10
10
  props: {},
11
11
  example: () =>
12
12
  html`<script setup>
13
- import { MActionListbox, MButton } from '@mozaic-ds/vue';
14
- import { Copy20, Download20, Trash20 } from '@mozaic-ds/icons-vue';
15
- </script>
13
+ import { MActionListbox, MButton } from '@mozaic-ds/vue';
14
+ import { Copy20, Download20, Trash20 } from '@mozaic-ds/icons-vue';
15
+ </script>
16
16
 
17
- <MActionListbox
18
- title="Listbox title"
19
- :items="[
17
+ <MActionListbox
18
+ title="Listbox title"
19
+ :items="[
20
20
  { id: '1', label: 'Duplicate', icon: Copy20 },
21
21
  { id: '2', label: 'Download', icon: Download20 },
22
22
  { id: '3', label: 'Delete', icon: Trash20, appearance: 'danger', divider: true },
23
23
  ]"
24
- >
25
- <template #activator="{ id }">
26
- <MButton :popovertarget="id">Open menu</MButton>
27
- </template>
28
- </MActionListbox>`,
24
+ >
25
+ <template #activator="{ id }">
26
+ <MButton :popovertarget="id">Open menu</MButton>
27
+ </template>
28
+ </MActionListbox>`,
29
29
  },
30
30
  );
@@ -135,4 +135,117 @@ describe('MActionListbox', () => {
135
135
  await actions[1].trigger('click');
136
136
  expect(wrapper.emitted('action')?.[1][0]).toBe('move');
137
137
  });
138
+
139
+ it('has role="menu" on the list and role="menuitem" on each button', () => {
140
+ const wrapper = mountComponent();
141
+ expect(wrapper.find('ul.mc-action-list').attributes('role')).toBe('menu');
142
+ const menuItems = wrapper.findAll('button[role="menuitem"]');
143
+ expect(menuItems.length).toBe(items.length);
144
+ });
145
+
146
+ describe('keyboard navigation', () => {
147
+ function mountAttached(props = {}) {
148
+ const div = document.createElement('div');
149
+ document.body.appendChild(div);
150
+ const wrapper = mount(MActionListbox, {
151
+ props: { items, ...props },
152
+ attachTo: div,
153
+ global: { components: { MDivider, MIconButton, Cross24 } },
154
+ });
155
+ return wrapper;
156
+ }
157
+
158
+ it('ArrowDown moves focus to the next item', async () => {
159
+ const wrapper = mountAttached();
160
+ const buttons = wrapper.findAll('button[role="menuitem"]');
161
+ await buttons[0].element.focus();
162
+ await wrapper
163
+ .find('ul[role="menu"]')
164
+ .trigger('keydown', { key: 'ArrowDown' });
165
+ expect(document.activeElement).toBe(buttons[1].element);
166
+ wrapper.unmount();
167
+ });
168
+
169
+ it('ArrowDown wraps from last to first item', async () => {
170
+ const wrapper = mountAttached();
171
+ const buttons = wrapper.findAll('button[role="menuitem"]');
172
+ await buttons[buttons.length - 1].element.focus();
173
+ await wrapper
174
+ .find('ul[role="menu"]')
175
+ .trigger('keydown', { key: 'ArrowDown' });
176
+ expect(document.activeElement).toBe(buttons[0].element);
177
+ wrapper.unmount();
178
+ });
179
+
180
+ it('ArrowUp moves focus to the previous item', async () => {
181
+ const wrapper = mountAttached();
182
+ const buttons = wrapper.findAll('button[role="menuitem"]');
183
+ await buttons[1].element.focus();
184
+ await wrapper
185
+ .find('ul[role="menu"]')
186
+ .trigger('keydown', { key: 'ArrowUp' });
187
+ expect(document.activeElement).toBe(buttons[0].element);
188
+ wrapper.unmount();
189
+ });
190
+
191
+ it('ArrowUp wraps from first to last item', async () => {
192
+ const wrapper = mountAttached();
193
+ const buttons = wrapper.findAll('button[role="menuitem"]');
194
+ await buttons[0].element.focus();
195
+ await wrapper
196
+ .find('ul[role="menu"]')
197
+ .trigger('keydown', { key: 'ArrowUp' });
198
+ expect(document.activeElement).toBe(buttons[buttons.length - 1].element);
199
+ wrapper.unmount();
200
+ });
201
+
202
+ it('Home moves focus to the first item', async () => {
203
+ const wrapper = mountAttached();
204
+ const buttons = wrapper.findAll('button[role="menuitem"]');
205
+ await buttons[2].element.focus();
206
+ await wrapper.find('ul[role="menu"]').trigger('keydown', { key: 'Home' });
207
+ expect(document.activeElement).toBe(buttons[0].element);
208
+ wrapper.unmount();
209
+ });
210
+
211
+ it('End moves focus to the last item', async () => {
212
+ const wrapper = mountAttached();
213
+ const buttons = wrapper.findAll('button[role="menuitem"]');
214
+ await buttons[0].element.focus();
215
+ await wrapper.find('ul[role="menu"]').trigger('keydown', { key: 'End' });
216
+ expect(document.activeElement).toBe(buttons[buttons.length - 1].element);
217
+ wrapper.unmount();
218
+ });
219
+
220
+ it('Escape emits "close"', async () => {
221
+ const wrapper = mountAttached();
222
+ await wrapper
223
+ .find('ul[role="menu"]')
224
+ .trigger('keydown', { key: 'Escape' });
225
+ expect(wrapper.emitted('close')).toBeTruthy();
226
+ wrapper.unmount();
227
+ });
228
+
229
+ it('disabled items are skipped during ArrowDown navigation', async () => {
230
+ const itemsWithDisabled = [
231
+ { label: 'First' },
232
+ { label: 'Disabled', disabled: true },
233
+ { label: 'Third' },
234
+ ];
235
+ const wrapper = mount(MActionListbox, {
236
+ props: { items: itemsWithDisabled },
237
+ attachTo: document.body,
238
+ global: { components: { MDivider, MIconButton, Cross24 } },
239
+ });
240
+ const enabledButtons = wrapper.findAll(
241
+ 'button[role="menuitem"]:not([disabled])',
242
+ );
243
+ await enabledButtons[0].element.focus();
244
+ await wrapper
245
+ .find('ul[role="menu"]')
246
+ .trigger('keydown', { key: 'ArrowDown' });
247
+ expect(document.activeElement).toBe(enabledButtons[1].element);
248
+ wrapper.unmount();
249
+ });
250
+ });
138
251
  });
@@ -9,9 +9,12 @@
9
9
  ref="popover"
10
10
  class="mc-listbox__content"
11
11
  v-bind="$slots.activator ? { id, popover: '' } : {}"
12
+ @toggle="onPopoverToggle"
12
13
  >
13
14
  <div class="mc-listbox__header">
14
- <h3 v-if="title" class="mc-listbox__title">{{ title }}</h3>
15
+ <h3 v-if="title" :id="`${id}-title`" class="mc-listbox__title">
16
+ {{ title }}
17
+ </h3>
15
18
  <MIconButton
16
19
  class="mc-listbox__close"
17
20
  ghost
@@ -24,7 +27,15 @@
24
27
  </MIconButton>
25
28
  </div>
26
29
  <div class="mc-listbox__body">
27
- <ul class="mc-action-list" role="menu">
30
+ <ul
31
+ ref="menuEl"
32
+ class="mc-action-list"
33
+ role="menu"
34
+ tabindex="-1"
35
+ :aria-label="title || undefined"
36
+ :aria-labelledby="title ? `${id}-title` : undefined"
37
+ @keydown="onMenuKeydown"
38
+ >
28
39
  <template v-for="(item, index) in items" :key="`item-${index}`">
29
40
  <MDivider
30
41
  v-if="item.divider"
@@ -40,17 +51,20 @@
40
51
  'mc-action-list__element--disabled': item.disabled,
41
52
  },
42
53
  ]"
43
- role="menuitem"
54
+ role="presentation"
44
55
  >
45
56
  <button
46
57
  type="button"
58
+ role="menuitem"
47
59
  class="mc-action-list__button"
60
+ :disabled="item.disabled || undefined"
48
61
  @click="emit('action', item?.id || index)"
49
62
  >
50
63
  <component
51
64
  v-if="item.icon"
52
65
  class="mc-action-list__icon"
53
66
  :is="item.icon"
67
+ aria-hidden="true"
54
68
  />
55
69
  <p class="mc-action-list__text">{{ item.label }}</p>
56
70
  </button>
@@ -65,7 +79,13 @@
65
79
  </template>
66
80
 
67
81
  <script setup lang="ts">
68
- import { useId, useTemplateRef, type Component, type VNode } from 'vue';
82
+ import {
83
+ nextTick,
84
+ useId,
85
+ useTemplateRef,
86
+ type Component,
87
+ type VNode,
88
+ } from 'vue';
69
89
  import MIconButton from '../iconbutton/MIconButton.vue';
70
90
  import MDivider from '../divider/MDivider.vue';
71
91
  import { Cross24 } from '@mozaic-ds/icons-vue';
@@ -137,8 +157,46 @@ const slots = defineSlots<{
137
157
  }>();
138
158
 
139
159
  const id = useId();
140
-
141
160
  const popover = useTemplateRef('popover');
161
+ const menuEl = useTemplateRef('menuEl');
162
+
163
+ function getMenuItems(): HTMLButtonElement[] {
164
+ return Array.from(
165
+ menuEl.value?.querySelectorAll<HTMLButtonElement>(
166
+ 'button[role="menuitem"]:not(:disabled)',
167
+ ) ?? [],
168
+ );
169
+ }
170
+
171
+ function onMenuKeydown(e: KeyboardEvent) {
172
+ const items = getMenuItems();
173
+ if (!items.length) return;
174
+ const current = items.findIndex((el) => el === document.activeElement);
175
+
176
+ if (e.key === 'ArrowDown') {
177
+ e.preventDefault();
178
+ items[(current + 1) % items.length].focus();
179
+ } else if (e.key === 'ArrowUp') {
180
+ e.preventDefault();
181
+ items[(current - 1 + items.length) % items.length].focus();
182
+ } else if (e.key === 'Home') {
183
+ e.preventDefault();
184
+ items[0].focus();
185
+ } else if (e.key === 'End') {
186
+ e.preventDefault();
187
+ items[items.length - 1].focus();
188
+ } else if (e.key === 'Escape') {
189
+ close();
190
+ }
191
+ }
192
+
193
+ function onPopoverToggle(e: ToggleEvent) {
194
+ if (e.newState === 'open') {
195
+ nextTick(() => getMenuItems()[0]?.focus());
196
+ } else {
197
+ document.querySelector<HTMLElement>(`[popovertarget="${id}"]`)?.focus();
198
+ }
199
+ }
142
200
 
143
201
  function close() {
144
202
  emit('close');
@@ -21,11 +21,11 @@ figma.connect(
21
21
  },
22
22
  example: ({ size }) =>
23
23
  html`<script setup>
24
- import { MAvatar } from '@mozaic-ds/vue';
25
- </script>
24
+ import { MAvatar } from '@mozaic-ds/vue';
25
+ </script>
26
26
 
27
- <MAvatar size=${size}>
28
- <img src="/placeholder.png" alt="Avatar" />
29
- </MAvatar>`,
27
+ <MAvatar size=${size}>
28
+ <img src="/placeholder.png" alt="Avatar" />
29
+ </MAvatar>`,
30
30
  },
31
31
  );
@@ -15,17 +15,17 @@ figma.connect(
15
15
  },
16
16
  example: ({ appearance }) =>
17
17
  html`<script setup>
18
- import { MBreadcrumb } from '@mozaic-ds/vue';
19
- </script>
18
+ import { MBreadcrumb } from '@mozaic-ds/vue';
19
+ </script>
20
20
 
21
- <MBreadcrumb
22
- appearance=${appearance}
23
- :links="[
21
+ <MBreadcrumb
22
+ appearance=${appearance}
23
+ :links="[
24
24
  { label: 'Home', href: '#' },
25
25
  { label: 'Level 01', href: '#' },
26
26
  { label: 'Current Page', href: '#' },
27
27
  ]"
28
- aria-label="breadcrumb"
29
- ></MBreadcrumb>`,
28
+ aria-label="breadcrumb"
29
+ ></MBreadcrumb>`,
30
30
  },
31
31
  );
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <nav class="mc-breadcrumb" :class="classObject">
2
+ <nav class="mc-breadcrumb" :class="classObject" aria-label="Breadcrumb">
3
3
  <ul class="mc-breadcrumb__container">
4
4
  <li
5
5
  class="mc-breadcrumb__item"
@@ -9,15 +9,18 @@ figma.connect(
9
9
  {
10
10
  props: {
11
11
  outlined: figma.enum('is outlined', {
12
- 'true': true,
13
- 'false': false,
12
+ true: true,
13
+ false: false,
14
14
  }),
15
15
  },
16
16
  example: ({ outlined }) =>
17
17
  html`<script setup>
18
- import { MBuiltInMenu } from '@mozaic-ds/vue';
19
- </script>
18
+ import { MBuiltInMenu } from '@mozaic-ds/vue';
19
+ </script>
20
20
 
21
- <MBuiltInMenu :items="[{ label: 'Label' }, { label: 'Label' }]" outlined=${outlined} />`,
21
+ <MBuiltInMenu
22
+ :items="[{ label: 'Label' }, { label: 'Label' }]"
23
+ outlined=${outlined}
24
+ />`,
22
25
  },
23
26
  );
@@ -120,7 +120,9 @@ describe('MBuiltInMenu', () => {
120
120
  const wrapper = mount(MBuiltInMenu, {
121
121
  props: { items, label: 'Settings navigation' },
122
122
  });
123
- expect(wrapper.find('nav').attributes('aria-label')).toBe('Settings navigation');
123
+ expect(wrapper.find('nav').attributes('aria-label')).toBe(
124
+ 'Settings navigation',
125
+ );
124
126
  });
125
127
 
126
128
  it('sets aria-hidden on ChevronRight and item icons', () => {
@@ -29,13 +29,28 @@ figma.connect(
29
29
  outlined: figma.enum('Type', { Outlined: true }),
30
30
  ghost: figma.enum('Type', { Ghost: true }),
31
31
  },
32
- example: ({ label, appearance, size, disabled, isLoading, outlined, ghost }) =>
32
+ example: ({
33
+ label,
34
+ appearance,
35
+ size,
36
+ disabled,
37
+ isLoading,
38
+ outlined,
39
+ ghost,
40
+ }) =>
33
41
  html`<script setup>
34
- import { MButton } from '@mozaic-ds/vue';
35
- </script>
42
+ import { MButton } from '@mozaic-ds/vue';
43
+ </script>
36
44
 
37
- <MButton appearance=${appearance} size=${size} disabled=${disabled} :is-loading=${isLoading} outlined=${outlined} ghost=${ghost}>
38
- ${label}
39
- </MButton>`,
45
+ <MButton
46
+ appearance=${appearance}
47
+ size=${size}
48
+ disabled=${disabled}
49
+ :is-loading=${isLoading}
50
+ outlined=${outlined}
51
+ ghost=${ghost}
52
+ >
53
+ ${label}
54
+ </MButton>`,
40
55
  },
41
56
  );
@@ -188,4 +188,30 @@ describe('MButton component', () => {
188
188
  expect(label.exists()).toBe(true);
189
189
  expect(label.text()).toBe('Normal Button');
190
190
  });
191
+
192
+ it('sets aria-busy="true" when isLoading is true', () => {
193
+ const wrapper = mount(MButton, {
194
+ props: { isLoading: true },
195
+ slots: { default: 'Loading' },
196
+ });
197
+ expect(wrapper.find('button').attributes('aria-busy')).toBe('true');
198
+ });
199
+
200
+ it('does not set aria-busy when isLoading is false', () => {
201
+ const wrapper = mount(MButton, {
202
+ props: { isLoading: false },
203
+ slots: { default: 'Normal' },
204
+ });
205
+ expect(wrapper.find('button').attributes('aria-busy')).toBeUndefined();
206
+ });
207
+
208
+ it('sets aria-hidden on the loader wrapper', () => {
209
+ const wrapper = mount(MButton, {
210
+ props: { isLoading: true },
211
+ slots: { default: 'Loading' },
212
+ });
213
+ const loaderWrapper = wrapper.find('.mc-button__icon[aria-hidden]');
214
+ expect(loaderWrapper.exists()).toBe(true);
215
+ expect(loaderWrapper.attributes('aria-hidden')).toBe('true');
216
+ });
191
217
  });
@@ -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="$slots.icon && iconPosition == 'left' && !isLoading"
@@ -15,6 +16,7 @@
15
16
  v-if="isLoading"
16
17
  class="mc-button__icon"
17
18
  :style="{ position: 'absolute' }"
19
+ aria-hidden="true"
18
20
  >
19
21
  <MLoader :style="{ color: 'currentColor' }" size="s" />
20
22
  </span>