@mozaic-ds/vue 2.17.0 → 2.18.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 (33) hide show
  1. package/dist/mozaic-vue.css +1 -1
  2. package/dist/mozaic-vue.d.ts +133 -55
  3. package/dist/mozaic-vue.js +1533 -4663
  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 +14 -8
  8. package/src/components/Migration.mdx +651 -0
  9. package/src/components/accordionlistitem/MAccordionListItem.spec.ts +22 -3
  10. package/src/components/accordionlistitem/MAccordionListItem.vue +38 -28
  11. package/src/components/builtinmenu/MBuiltInMenu.spec.ts +30 -1
  12. package/src/components/builtinmenu/MBuiltInMenu.vue +26 -17
  13. package/src/components/builtinmenu/README.md +2 -0
  14. package/src/components/callout/MCallout.spec.ts +35 -0
  15. package/src/components/callout/MCallout.vue +22 -4
  16. package/src/components/callout/README.md +2 -0
  17. package/src/components/checklistmenu/MCheckListMenu.spec.ts +12 -1
  18. package/src/components/checklistmenu/MCheckListMenu.vue +6 -0
  19. package/src/components/checklistmenu/README.md +2 -0
  20. package/src/components/datatable/datatable.mdx +3 -2
  21. package/src/components/navigationindicator/MNavigationIndicator.spec.ts +75 -18
  22. package/src/components/navigationindicator/MNavigationIndicator.vue +10 -12
  23. package/src/components/optionListbox/MOptionListbox.vue +16 -1
  24. package/src/components/popover/MPopover.spec.ts +126 -0
  25. package/src/components/popover/MPopover.vue +36 -1
  26. package/src/components/segmentedcontrol/MSegmentedControl.spec.ts +92 -0
  27. package/src/components/segmentedcontrol/MSegmentedControl.vue +61 -2
  28. package/src/components/starrating/MStarRating.spec.ts +19 -22
  29. package/src/components/starrating/MStarRating.vue +3 -2
  30. package/src/components/tabs/MTabs.vue +90 -4
  31. package/src/components/tabs/Mtabs.spec.ts +162 -0
  32. package/src/main.ts +1 -0
  33. package/src/components/ComponentsMapping.mdx +0 -98
@@ -82,15 +82,34 @@ describe('MAccordionListItem', () => {
82
82
  expect(button.attributes('aria-expanded')).toBe('true');
83
83
  });
84
84
 
85
- it('sets aria-hidden correctly for content', async () => {
85
+ it('sets inert correctly for content', async () => {
86
86
  const { wrapper, state } = mountItem({ id: 'item-1', title: 'Title' }, []);
87
87
 
88
88
  const content = wrapper.find('.mc-accordion__content');
89
- expect(content.attributes('aria-hidden')).toBe('true');
89
+ expect(content.attributes('inert')).not.toBeUndefined();
90
90
 
91
91
  state.value.push('item-1');
92
92
  await wrapper.vm.$nextTick();
93
- expect(content.attributes('aria-hidden')).toBe('false');
93
+ expect(content.attributes('inert')).toBeUndefined();
94
+ });
95
+
96
+ it('renders heading with default level h2', () => {
97
+ const { wrapper } = mountItem({ id: '1', title: 'Title' });
98
+ expect(wrapper.find('h2.mc-accordion__title').exists()).toBe(true);
99
+ });
100
+
101
+ it('renders heading with custom headingLevel prop', () => {
102
+ const wrapper = mount(MAccordionListItem, {
103
+ props: { id: '1', title: 'Title', tag: 'h3' },
104
+ global: {
105
+ provide: {
106
+ [AccordionStateKey as symbol]: ref([]),
107
+ [AccordionToggleFnKey as symbol]: vi.fn(),
108
+ },
109
+ },
110
+ });
111
+ expect(wrapper.find('h3.mc-accordion__title').exists()).toBe(true);
112
+ expect(wrapper.find('h2').exists()).toBe(false);
94
113
  });
95
114
 
96
115
  it('calls toggleItem when button is clicked', async () => {
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div class="mc-accordion__item">
3
3
  <div class="mc-accordion__header">
4
- <h2 class="mc-accordion__title">
4
+ <component :is="tag" class="mc-accordion__title">
5
5
  <button
6
6
  :id="`accordion-${id}`"
7
7
  class="mc-accordion__trigger"
@@ -19,12 +19,12 @@
19
19
  </span>
20
20
  </div>
21
21
  </button>
22
- </h2>
22
+ </component>
23
23
  </div>
24
24
  <div
25
25
  :id="`content-${id}`"
26
26
  class="mc-accordion__content"
27
- :aria-hidden="!open"
27
+ :inert="!open || undefined"
28
28
  :aria-labelledby="`accordion-${id}`"
29
29
  role="region"
30
30
  >
@@ -49,31 +49,41 @@ import {
49
49
  AccordionToggleFnKey,
50
50
  } from '../accordionlist/m-accordion-list.const';
51
51
 
52
- const props = defineProps<{
53
- /**
54
- * A unique identifier for the accordion item.
55
- * It links the trigger button (`aria-controls`) to its associated content (`aria-labelledby`),
56
- * ensuring accessibility and tracking the open/closed state.
57
- * If no ID is provided, a unique one is generated automatically.
58
- */
59
- id: string;
60
- /**
61
- * The main heading of the accordion item. This is the primary text visible to users in the collapsed state and acts as the trigger for expanding or collapsing the content.
62
- */
63
- title: string;
64
- /**
65
- * An optional secondary heading displayed below the title. It provides additional context or detail about the content of the accordion item.
66
- */
67
- subtitle?: string;
68
- /**
69
- * The main content of the accordion item. This is the information revealed when the accordion is expanded, typically containing text, HTML, or other elements.
70
- */
71
- content?: string;
72
- /**
73
- * Icon component to display before the item title.
74
- */
75
- icon?: Component;
76
- }>();
52
+ const props = withDefaults(
53
+ defineProps<{
54
+ /**
55
+ * A unique identifier for the accordion item.
56
+ * It links the trigger button (`aria-controls`) to its associated content (`aria-labelledby`),
57
+ * ensuring accessibility and tracking the open/closed state.
58
+ * If no ID is provided, a unique one is generated automatically.
59
+ */
60
+ id: string;
61
+ /**
62
+ * The main heading of the accordion item. This is the primary text visible to users in the collapsed state and acts as the trigger for expanding or collapsing the content.
63
+ */
64
+ title: string;
65
+ /**
66
+ * An optional secondary heading displayed below the title. It provides additional context or detail about the content of the accordion item.
67
+ */
68
+ subtitle?: string;
69
+ /**
70
+ * The main content of the accordion item. This is the information revealed when the accordion is expanded, typically containing text, HTML, or other elements.
71
+ */
72
+ content?: string;
73
+ /**
74
+ * Icon component to display before the item title.
75
+ */
76
+ icon?: Component;
77
+ /**
78
+ * Heading level for the accordion trigger (h2–h6). Adjust to match the
79
+ * heading hierarchy of the page where the accordion is used.
80
+ */
81
+ tag?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
82
+ }>(),
83
+ {
84
+ tag: 'h2',
85
+ },
86
+ );
77
87
 
78
88
  defineSlots<{
79
89
  /**
@@ -86,7 +86,9 @@ describe('MBuiltInMenu', () => {
86
86
  });
87
87
  const lis = wrapper.findAll('li');
88
88
  const selectedItem = lis[2];
89
- expect(selectedItem.attributes('aria-current')).toBe('true');
89
+ // aria-current is on the interactive element, not the <li> container
90
+ const interactiveEl = selectedItem.find('button, a');
91
+ expect(interactiveEl.attributes('aria-current')).toBe('true');
90
92
  expect(selectedItem.classes()).toContain(
91
93
  'mc-built-in-menu__item--selected',
92
94
  );
@@ -108,4 +110,31 @@ describe('MBuiltInMenu', () => {
108
110
  'mc-built-in-menu--outlined',
109
111
  );
110
112
  });
113
+
114
+ it('uses default aria-label "Menu" on nav when label prop is not provided', () => {
115
+ const wrapper = mount(MBuiltInMenu, { props: { items } });
116
+ expect(wrapper.find('nav').attributes('aria-label')).toBe('Menu');
117
+ });
118
+
119
+ it('uses custom label prop as aria-label on nav', () => {
120
+ const wrapper = mount(MBuiltInMenu, {
121
+ props: { items, label: 'Settings navigation' },
122
+ });
123
+ expect(wrapper.find('nav').attributes('aria-label')).toBe('Settings navigation');
124
+ });
125
+
126
+ it('sets aria-hidden on ChevronRight and item icons', () => {
127
+ const wrapper = mount(MBuiltInMenu, { props: { items } });
128
+ // All SVG icons inside items should be aria-hidden
129
+ const ariaHiddenIcons = wrapper.findAll('[aria-hidden="true"]');
130
+ expect(ariaHiddenIcons.length).toBeGreaterThan(0);
131
+ });
132
+
133
+ it('does not put aria-current on the <li>, only on the interactive element', () => {
134
+ const wrapper = mount(MBuiltInMenu, { props: { items, modelValue: 0 } });
135
+ const li = wrapper.findAll('li')[0];
136
+ expect(li.attributes('aria-current')).toBeUndefined();
137
+ const interactive = li.find('button, a');
138
+ expect(interactive.attributes('aria-current')).toBe('true');
139
+ });
111
140
  });
@@ -4,7 +4,7 @@
4
4
  'mc-built-in-menu': true,
5
5
  'mc-built-in-menu--outlined': props.outlined,
6
6
  }"
7
- aria-label="menu"
7
+ :aria-label="props.label"
8
8
  >
9
9
  <ul class="mc-built-in-menu__list">
10
10
  <li
@@ -14,7 +14,6 @@
14
14
  'mc-built-in-menu__item': true,
15
15
  'mc-built-in-menu__item--selected': currentMenuItem === index,
16
16
  }"
17
- v-bind="currentMenuItem === index ? { 'aria-current': true } : {}"
18
17
  >
19
18
  <component
20
19
  :is="getItemTag(item)"
@@ -23,17 +22,19 @@
23
22
  'mc-built-in-menu__link': isLink(item),
24
23
  }"
25
24
  v-bind="isLink(item) ? getLinkAttrs(item) : {}"
25
+ :aria-current="currentMenuItem === index ? true : undefined"
26
26
  @click="currentMenuItem = index"
27
27
  >
28
28
  <component
29
29
  v-if="item.icon"
30
30
  :is="item.icon"
31
31
  class="mc-built-in-menu__icon"
32
+ aria-hidden="true"
32
33
  />
33
34
 
34
35
  <span class="mc-built-in-menu__label">{{ item.label }}</span>
35
36
 
36
- <ChevronRight20 class="mc-built-in-menu__icon" />
37
+ <ChevronRight20 class="mc-built-in-menu__icon" aria-hidden="true" />
37
38
  </component>
38
39
  </li>
39
40
  </ul>
@@ -56,20 +57,28 @@ export type MenuItem = {
56
57
  target?: '_self' | '_blank' | '_parent' | '_top';
57
58
  };
58
59
 
59
- const props = defineProps<{
60
- /**
61
- * 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.
62
- */
63
- modelValue?: number;
64
- /**
65
- * Defines the menu items, each of which can include an icon and act as a button, link, or router-link.
66
- */
67
- items: MenuItem[];
68
- /**
69
- * When enabled, adds a visible border around the wrapper to highlight or separate its content.
70
- */
71
- outlined?: boolean;
72
- }>();
60
+ const props = withDefaults(
61
+ defineProps<{
62
+ /**
63
+ * 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.
64
+ */
65
+ modelValue?: number;
66
+ /**
67
+ * Defines the menu items, each of which can include an icon and act as a button, link, or router-link.
68
+ */
69
+ items: MenuItem[];
70
+ /**
71
+ * When enabled, adds a visible border around the wrapper to highlight or separate its content.
72
+ */
73
+ outlined?: boolean;
74
+ /**
75
+ * Accessible label for the navigation landmark. Should describe the purpose
76
+ * of this menu to distinguish it from other navigations on the page.
77
+ */
78
+ label?: string;
79
+ }>(),
80
+ { label: 'Menu' },
81
+ );
73
82
 
74
83
  const emit = defineEmits<{
75
84
  /**
@@ -10,6 +10,8 @@ A built-in menu is a structured list of navigational or interactive options, typ
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 can include an icon and act as a button, link, or router-link. | `MenuItem[]` | - |
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` | `"Menu"` |
13
15
 
14
16
  ## Events
15
17
 
@@ -97,4 +97,39 @@ describe('MCallout.vue', () => {
97
97
 
98
98
  expect(wrapper.find('.mc-callout__footer').exists()).toBe(false);
99
99
  });
100
+
101
+ it('renders title with default tag prop (h2)', () => {
102
+ const wrapper = mount(MCallout, {
103
+ props: { title: 'Title', description: 'Desc' },
104
+ });
105
+ expect(wrapper.find('h2.mc-callout__title').exists()).toBe(true);
106
+ });
107
+
108
+ it('renders title with custom tag prop', () => {
109
+ const wrapper = mount(MCallout, {
110
+ props: { title: 'Title', description: 'Desc', tag: 'h3' },
111
+ });
112
+ expect(wrapper.find('h3.mc-callout__title').exists()).toBe(true);
113
+ expect(wrapper.find('h2').exists()).toBe(false);
114
+ });
115
+
116
+ it('has aria-labelledby pointing to the title', () => {
117
+ const wrapper = mount(MCallout, {
118
+ props: { title: 'Title', description: 'Desc' },
119
+ });
120
+ const section = wrapper.find('section');
121
+ const titleId = wrapper.find('.mc-callout__title').attributes('id');
122
+ expect(titleId).toBeDefined();
123
+ expect(section.attributes('aria-labelledby')).toBe(titleId);
124
+ });
125
+
126
+ it('sets aria-hidden on the icon wrapper', () => {
127
+ const wrapper = mount(MCallout, {
128
+ props: { title: 'Title', description: 'Desc' },
129
+ slots: { icon: '<svg class="icon" />' },
130
+ });
131
+ expect(wrapper.find('.mc-callout__icon').attributes('aria-hidden')).toBe(
132
+ 'true',
133
+ );
134
+ });
100
135
  });
@@ -1,10 +1,20 @@
1
1
  <template>
2
- <section class="mc-callout" role="status" :class="classObject">
3
- <div class="mc-callout__icon">
2
+ <section
3
+ class="mc-callout"
4
+ role="status"
5
+ :class="classObject"
6
+ :aria-labelledby="`callout-title-${id}`"
7
+ >
8
+ <div class="mc-callout__icon" aria-hidden="true">
4
9
  <slot name="icon" />
5
10
  </div>
6
11
  <div class="mc-callout__content">
7
- <h2 class="mc-callout__title">{{ title }}</h2>
12
+ <component
13
+ :is="props.tag"
14
+ :id="`callout-title-${id}`"
15
+ class="mc-callout__title"
16
+ >{{ title }}</component
17
+ >
8
18
 
9
19
  <p class="mc-callout__message">
10
20
  {{ description }}
@@ -18,7 +28,7 @@
18
28
  </template>
19
29
 
20
30
  <script setup lang="ts">
21
- import { computed, type VNode } from 'vue';
31
+ import { computed, useId, type VNode } from 'vue';
22
32
  /**
23
33
  * A callout is used to highlight additional information that can assist users with tips, extra details, or helpful guidance, without signaling a critical status or alert. Unlike notifications, callouts are not triggered by user actions and do not correspond to specific system states. They are designed to enhance the user experience by providing contextually relevant information that supports comprehension and usability.
24
34
  */
@@ -36,12 +46,20 @@ const props = withDefaults(
36
46
  * Allows to define the callout appearance.
37
47
  */
38
48
  appearance?: 'standard' | 'accent' | 'tips' | 'inverse';
49
+ /**
50
+ * Heading level for the callout title (h2–h6). Adjust to match the
51
+ * heading hierarchy of the page where the callout is used.
52
+ */
53
+ tag?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
39
54
  }>(),
40
55
  {
41
56
  appearance: 'standard',
57
+ tag: 'h2',
42
58
  },
43
59
  );
44
60
 
61
+ const id = useId();
62
+
45
63
  defineSlots<{
46
64
  /**
47
65
  * Use this slot to insert an icon.
@@ -10,6 +10,8 @@ A callout is used to highlight additional information that can assist users with
10
10
  | `title*` | Title of the callout. | `string` | - |
11
11
  | `description*` | Description of the callout. | `string` | - |
12
12
  | `appearance` | Allows to define the callout appearance. | `"standard"` `"inverse"` `"accent"` `"tips"` | `"standard"` |
13
+ | `tag` | Heading level for the callout title (h2–h6). Adjust to match the
14
+ heading hierarchy of the page where the callout is used. | `"h2"` `"h1"` `"h3"` `"h4"` `"h5"` `"h6"` | `"h2"` |
13
15
 
14
16
  ## Slots
15
17
 
@@ -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
 
@@ -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.
@@ -77,24 +77,6 @@ describe('MNavigationIndicator.vue', () => {
77
77
  expect(emitted![0][0]).toBe(1);
78
78
  });
79
79
 
80
- it('emits update:modelValue when Enter is pressed on a step', async () => {
81
- const wrapper = mount(MNavigationIndicator, {
82
- props: { steps: 3, modelValue: 0 },
83
- global: {
84
- stubs: { MButton, MIconButton, PauseCircle24, PlayCircle24 },
85
- },
86
- });
87
-
88
- const stepButtons = wrapper.findAll(
89
- '.mc-navigation-indicator__list .mc-navigation-indicator__button',
90
- );
91
- await stepButtons[2].trigger('keydown', { key: 'Enter' });
92
-
93
- const emitted = wrapper.emitted('update:modelValue');
94
- expect(emitted).toBeTruthy();
95
- expect(emitted![0][0]).toBe(2);
96
- });
97
-
98
80
  it('does not emit update:modelValue when a wrong key is pressed', async () => {
99
81
  const wrapper = mount(MNavigationIndicator, {
100
82
  props: { steps: 3, modelValue: 0 },
@@ -149,4 +131,79 @@ describe('MNavigationIndicator.vue', () => {
149
131
  expect(button.text()).toContain('Pause');
150
132
  expect(wrapper.find('[data-testid="pause-icon"]').exists()).toBe(true);
151
133
  });
134
+
135
+ it('renders a navigation landmark with an aria-label', () => {
136
+ const wrapper = mount(MNavigationIndicator, {
137
+ props: { steps: 3, modelValue: 0 },
138
+ global: {
139
+ stubs: { MButton, MIconButton, PauseCircle24, PlayCircle24 },
140
+ },
141
+ });
142
+
143
+ const navigation = wrapper.find('nav[aria-label="Navigation steps"]');
144
+ expect(navigation.exists()).toBe(true);
145
+ expect(navigation.attributes('aria-label')).toBe('Navigation steps');
146
+ });
147
+
148
+ it('player icon button has aria-label reflecting action state', () => {
149
+ const wrapperPause = mount(MNavigationIndicator, {
150
+ props: { steps: 2, modelValue: 0, action: 'pause' },
151
+ global: { stubs: { MButton, MIconButton, PauseCircle24, PlayCircle24 } },
152
+ });
153
+ expect(
154
+ wrapperPause
155
+ .find('[data-testid="m-icon-button"]')
156
+ .attributes('aria-label'),
157
+ ).toBe('Pause');
158
+
159
+ const wrapperResume = mount(MNavigationIndicator, {
160
+ props: { steps: 2, modelValue: 0, action: 'resume' },
161
+ global: { stubs: { MButton, MIconButton, PauseCircle24, PlayCircle24 } },
162
+ });
163
+ expect(
164
+ wrapperResume
165
+ .find('[data-testid="m-icon-button"]')
166
+ .attributes('aria-label'),
167
+ ).toBe('Resume');
168
+ });
169
+
170
+ it('gives each step button an action-oriented aria-label', () => {
171
+ const wrapper = mount(MNavigationIndicator, {
172
+ props: { steps: 3, modelValue: 1 },
173
+ global: {
174
+ stubs: { MButton, MIconButton, PauseCircle24, PlayCircle24 },
175
+ },
176
+ });
177
+
178
+ const buttons = wrapper.findAll('.mc-navigation-indicator__button');
179
+ expect(buttons[0].attributes('aria-label')).toBe('Go to step 1 of 3');
180
+ expect(buttons[1].attributes('aria-label')).toBe('Current step, 2 of 3');
181
+ expect(buttons[2].attributes('aria-label')).toBe('Go to step 3 of 3');
182
+ });
183
+
184
+ it('sets aria-current="step" only on the active step button', () => {
185
+ const wrapper = mount(MNavigationIndicator, {
186
+ props: { steps: 3, modelValue: 1 },
187
+ global: {
188
+ stubs: { MButton, MIconButton, PauseCircle24, PlayCircle24 },
189
+ },
190
+ });
191
+
192
+ const buttons = wrapper.findAll('.mc-navigation-indicator__button');
193
+ expect(buttons[1].attributes('aria-current')).toBe('step');
194
+ expect(buttons[0].attributes('aria-current')).not.toBe('step');
195
+ expect(buttons[2].attributes('aria-current')).not.toBe('step');
196
+ });
197
+
198
+ it('emits "action" when the player button is clicked', async () => {
199
+ const wrapper = mount(MNavigationIndicator, {
200
+ props: { steps: 2, modelValue: 0, action: 'resume' },
201
+ global: {
202
+ stubs: { MButton, MIconButton, PauseCircle24, PlayCircle24 },
203
+ },
204
+ });
205
+
206
+ await wrapper.find('[data-testid="m-icon-button"]').trigger('click');
207
+ expect(wrapper.emitted('action')).toBeTruthy();
208
+ });
152
209
  });
@@ -1,9 +1,5 @@
1
1
  <template>
2
- <div
3
- class="mc-navigation-indicator"
4
- role="navigation"
5
- aria-label="Navigations steps"
6
- >
2
+ <nav class="mc-navigation-indicator" aria-label="Navigation steps">
7
3
  <ul class="mc-navigation-indicator__list">
8
4
  <li
9
5
  v-for="(_step, index) in steps"
@@ -11,14 +7,14 @@
11
7
  class="mc-navigation-indicator__item"
12
8
  >
13
9
  <button
10
+ type="button"
14
11
  :class="{
15
12
  'mc-navigation-indicator__button': true,
16
13
  'mc-navigation-indicator__button--active': active === index,
17
14
  }"
18
- aria-label="Navigation step button"
15
+ :aria-label="getStepAriaLabel(index)"
19
16
  :aria-current="active === index && 'step'"
20
17
  @click="setActiveStep(index)"
21
- @keydown="onKeydown($event, index)"
22
18
  ></button>
23
19
  </li>
24
20
  </ul>
@@ -43,14 +39,14 @@
43
39
  size="s"
44
40
  ghost
45
41
  @click="emit('action')"
46
- aria-label="Control button"
42
+ :aria-label="props.action === 'pause' ? 'Pause' : 'Resume'"
47
43
  >
48
44
  <template #icon>
49
45
  <component :is="actionIcon" />
50
46
  </template>
51
47
  </MIconButton>
52
48
  </template>
53
- </div>
49
+ </nav>
54
50
  </template>
55
51
 
56
52
  <script setup lang="ts">
@@ -120,10 +116,12 @@ function setActiveStep(step: number) {
120
116
  active.value = step;
121
117
  }
122
118
 
123
- function onKeydown(event: KeyboardEvent, step: number) {
124
- if (event.key === 'Enter') {
125
- setActiveStep(step);
119
+ function getStepAriaLabel(step: number) {
120
+ if (active.value === step) {
121
+ return `Current step, ${step + 1} of ${props.steps}`;
126
122
  }
123
+
124
+ return `Go to step ${step + 1} of ${props.steps}`;
127
125
  }
128
126
  </script>
129
127
 
@@ -137,7 +137,22 @@ import {
137
137
  Less20,
138
138
  Check20,
139
139
  } from '@mozaic-ds/icons-vue';
140
- import { debounce } from 'lodash';
140
+
141
+ function debounce<T extends (...args: unknown[]) => unknown>(
142
+ fn: T,
143
+ wait: number,
144
+ ): (...args: Parameters<T>) => void {
145
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
146
+
147
+ return function (this: unknown, ...args: Parameters<T>) {
148
+ if (timeoutId) {
149
+ clearTimeout(timeoutId);
150
+ }
151
+ timeoutId = setTimeout(() => {
152
+ fn.apply(this, args);
153
+ }, wait);
154
+ };
155
+ }
141
156
 
142
157
  /**
143
158
  * An Option Listbox is a customizable, accessible listbox component designed to power dropdowns and comboboxes with advanced selection capabilities. It supports single or multiple selection, optional search, grouped options with section headers, and full keyboard navigation.