@mozaic-ds/vue 2.14.0 → 2.16.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 (52) hide show
  1. package/dist/mozaic-vue.css +1 -1
  2. package/dist/mozaic-vue.d.ts +1582 -500
  3. package/dist/mozaic-vue.js +8020 -3218
  4. package/dist/mozaic-vue.js.map +1 -1
  5. package/dist/mozaic-vue.umd.cjs +24 -5
  6. package/dist/mozaic-vue.umd.cjs.map +1 -1
  7. package/package.json +6 -4
  8. package/src/components/DarkMode.mdx +115 -0
  9. package/src/components/actionlistbox/MActionListbox.spec.ts +20 -10
  10. package/src/components/actionlistbox/MActionListbox.stories.ts +15 -8
  11. package/src/components/actionlistbox/MActionListbox.vue +15 -12
  12. package/src/components/actionlistbox/README.md +2 -1
  13. package/src/components/avatar/MAvatar.stories.ts +1 -1
  14. package/src/components/breadcrumb/MBreadcrumb.vue +2 -2
  15. package/src/components/button/README.md +2 -0
  16. package/src/components/combobox/MCombobox.spec.ts +246 -0
  17. package/src/components/combobox/MCombobox.stories.ts +190 -0
  18. package/src/components/combobox/MCombobox.vue +277 -0
  19. package/src/components/combobox/README.md +52 -0
  20. package/src/components/field/MField.stories.ts +105 -0
  21. package/src/components/optionListbox/MOptionListbox.spec.ts +527 -0
  22. package/src/components/optionListbox/MOptionListbox.vue +470 -0
  23. package/src/components/optionListbox/README.md +63 -0
  24. package/src/components/pageheader/MPageHeader.spec.ts +12 -12
  25. package/src/components/pageheader/MPageHeader.stories.ts +9 -1
  26. package/src/components/pageheader/MPageHeader.vue +3 -6
  27. package/src/components/segmentedcontrol/MSegmentedControl.spec.ts +57 -25
  28. package/src/components/segmentedcontrol/MSegmentedControl.stories.ts +6 -19
  29. package/src/components/segmentedcontrol/MSegmentedControl.vue +27 -13
  30. package/src/components/segmentedcontrol/README.md +6 -3
  31. package/src/components/select/MSelect.vue +4 -3
  32. package/src/components/sidebar/stories/DefaultCase.stories.vue +2 -2
  33. package/src/components/sidebar/stories/README.md +8 -0
  34. package/src/components/sidebar/stories/WithExpandOnly.stories.vue +1 -1
  35. package/src/components/sidebar/stories/WithProfileInfoOnly.stories.vue +2 -2
  36. package/src/components/sidebar/stories/WithSingleLevel.stories.vue +2 -2
  37. package/src/components/stepperinline/MStepperInline.spec.ts +63 -28
  38. package/src/components/stepperinline/MStepperInline.stories.ts +18 -10
  39. package/src/components/stepperinline/MStepperInline.vue +24 -10
  40. package/src/components/stepperinline/README.md +6 -2
  41. package/src/components/stepperstacked/MStepperStacked.spec.ts +162 -0
  42. package/src/components/stepperstacked/MStepperStacked.stories.ts +57 -0
  43. package/src/components/stepperstacked/MStepperStacked.vue +106 -0
  44. package/src/components/stepperstacked/README.md +15 -0
  45. package/src/components/tabs/MTabs.stories.ts +18 -0
  46. package/src/components/tabs/MTabs.vue +30 -14
  47. package/src/components/tabs/Mtabs.spec.ts +56 -10
  48. package/src/components/tabs/README.md +6 -3
  49. package/src/components/textinput/MTextInput.vue +13 -1
  50. package/src/components/textinput/README.md +15 -1
  51. package/src/components/tileclickable/README.md +1 -1
  52. package/src/main.ts +10 -2
@@ -0,0 +1,162 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+ import MStepperStacked from './MStepperStacked.vue';
4
+
5
+ const defaultSteps = [
6
+ { id: '1', label: 'Step 1' },
7
+ { id: '2', label: 'Step 2', additionalInfo: 'Additional info' },
8
+ { id: '3', label: 'Step 3' },
9
+ ];
10
+
11
+ describe('MStepperStacked', () => {
12
+ describe('Basic rendering', () => {
13
+ it('renders as many li elements as there are steps', () => {
14
+ const wrapper = mount(MStepperStacked, {
15
+ props: { steps: defaultSteps },
16
+ });
17
+ const items = wrapper.findAll('.mc-stepper-stacked__item');
18
+ expect(items).toHaveLength(defaultSteps.length);
19
+ });
20
+
21
+ it('renders the label of each step', () => {
22
+ const wrapper = mount(MStepperStacked, {
23
+ props: { steps: defaultSteps },
24
+ });
25
+ defaultSteps.forEach((step) => {
26
+ expect(wrapper.text()).toContain(step.label);
27
+ });
28
+ });
29
+
30
+ it('renders step numbers (1, 2, 3...)', () => {
31
+ const wrapper = mount(MStepperStacked, {
32
+ props: { steps: defaultSteps, currentStep: '1' },
33
+ });
34
+ expect(wrapper.text()).toContain('2');
35
+ expect(wrapper.text()).toContain('3');
36
+ });
37
+
38
+ it('renders additionalInfo when provided', () => {
39
+ const wrapper = mount(MStepperStacked, {
40
+ props: { steps: defaultSteps },
41
+ });
42
+ expect(wrapper.text()).toContain('Additional info');
43
+ });
44
+
45
+ it('adds the has-additional class on items with additionalInfo', () => {
46
+ const wrapper = mount(MStepperStacked, {
47
+ props: { steps: defaultSteps },
48
+ });
49
+ const items = wrapper.findAll('.mc-stepper-stacked__item');
50
+ expect(items[1].classes()).toContain('has-additional');
51
+ expect(items[0].classes()).not.toContain('has-additional');
52
+ });
53
+ });
54
+
55
+ describe('Step states', () => {
56
+ it('marks the current step circle with the is-current class', () => {
57
+ const wrapper = mount(MStepperStacked, {
58
+ props: { steps: defaultSteps, currentStep: '2' },
59
+ });
60
+ const circles = wrapper.findAll('.mc-stepper-stacked__circle');
61
+ const currentCircle = circles.find((c) => c.classes('is-current'));
62
+ expect(currentCircle).toBeTruthy();
63
+ expect(currentCircle?.text()).toBe('2');
64
+ });
65
+
66
+ it('marks the current step label with the is-current class', () => {
67
+ const wrapper = mount(MStepperStacked, {
68
+ props: { steps: defaultSteps, currentStep: '2' },
69
+ });
70
+ const labels = wrapper.findAll('.mc-stepper-stacked__label');
71
+ expect(labels[1].classes()).toContain('is-current');
72
+ expect(labels[0].classes()).not.toContain('is-current');
73
+ expect(labels[2].classes()).not.toContain('is-current');
74
+ });
75
+
76
+ it('renders the Check icon for completed steps', () => {
77
+ const wrapper = mount(MStepperStacked, {
78
+ props: { steps: defaultSteps, currentStep: '3' },
79
+ });
80
+ const checkIcons = wrapper.findAll('.mc-stepper-stacked__icon--check');
81
+ expect(checkIcons).toHaveLength(2);
82
+ });
83
+
84
+ it('does not render the Check icon for the current step', () => {
85
+ const wrapper = mount(MStepperStacked, {
86
+ props: { steps: defaultSteps, currentStep: '1' },
87
+ });
88
+ const checkIcons = wrapper.findAll('.mc-stepper-stacked__icon--check');
89
+ expect(checkIcons).toHaveLength(0);
90
+ });
91
+
92
+ it('renders a numbered circle for non-completed steps', () => {
93
+ const wrapper = mount(MStepperStacked, {
94
+ props: { steps: defaultSteps, currentStep: '1' },
95
+ });
96
+ const circles = wrapper.findAll('.mc-stepper-stacked__circle');
97
+ expect(circles).toHaveLength(3);
98
+ });
99
+ });
100
+
101
+ describe('active step — string type', () => {
102
+ it('falls back to step 1 if currentStep is less than 1', () => {
103
+ const wrapper = mount(MStepperStacked, {
104
+ props: { steps: defaultSteps, currentStep: 'unknown id' },
105
+ });
106
+ const labels = wrapper.findAll('.mc-stepper-stacked__label');
107
+ expect(labels[0].classes()).toContain('is-current');
108
+ });
109
+
110
+ it('works with no steps (default value)', () => {
111
+ const wrapper = mount(MStepperStacked);
112
+ expect(wrapper.findAll('.mc-stepper-stacked__item')).toHaveLength(0);
113
+ });
114
+ });
115
+
116
+ describe('active step — number type', () => {
117
+ it('clamps to step 1 when currentStep is 0 or less', () => {
118
+ const wrapper = mount(MStepperStacked, {
119
+ props: { steps: defaultSteps, currentStep: 0 },
120
+ });
121
+ const labels = wrapper.findAll('.mc-stepper-stacked__label');
122
+ expect(labels[0].classes()).toContain('is-current');
123
+ });
124
+
125
+ it('clamps to the last step when currentStep exceeds steps length', () => {
126
+ const wrapper = mount(MStepperStacked, {
127
+ props: { steps: defaultSteps, currentStep: 99 },
128
+ });
129
+ const labels = wrapper.findAll('.mc-stepper-stacked__label');
130
+ expect(labels[defaultSteps.length - 1].classes()).toContain('is-current');
131
+ });
132
+ });
133
+
134
+ describe('Default currentStep', () => {
135
+ it('defaults to currentStep=1', () => {
136
+ const wrapper = mount(MStepperStacked, {
137
+ props: { steps: defaultSteps },
138
+ });
139
+ const labels = wrapper.findAll('.mc-stepper-stacked__label');
140
+ expect(labels[0].classes()).toContain('is-current');
141
+ });
142
+ });
143
+
144
+ describe('First and last step', () => {
145
+ it('has no completed steps when currentStep=1', () => {
146
+ const wrapper = mount(MStepperStacked, {
147
+ props: { steps: defaultSteps, currentStep: '1' },
148
+ });
149
+ expect(wrapper.findAll('.mc-stepper-stacked__icon--check')).toHaveLength(
150
+ 0,
151
+ );
152
+ });
153
+
154
+ it('marks all previous steps as completed when on the last step', () => {
155
+ const wrapper = mount(MStepperStacked, {
156
+ props: { steps: defaultSteps, currentStep: '3' },
157
+ });
158
+ const checkIcons = wrapper.findAll('.mc-stepper-stacked__icon--check');
159
+ expect(checkIcons).toHaveLength(defaultSteps.length - 1);
160
+ });
161
+ });
162
+ });
@@ -0,0 +1,57 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
2
+ import MStepperStacked from './MStepperStacked.vue';
3
+
4
+ const meta: Meta<typeof MStepperStacked> = {
5
+ title: 'Indicators/Stepper Stacked',
6
+ component: MStepperStacked,
7
+ tags: ['v2'],
8
+ parameters: {
9
+ docs: {
10
+ description: {
11
+ component:
12
+ 'A stepper is a navigation component that guides users through a sequence of steps in a structured process. It visually represents progress, completed steps, and upcoming steps, helping users understand their position within a workflow. Steppers are commonly used in multi-step forms, onboarding flows, checkout processes, and task completion sequences to improve clarity and reduce cognitive load.',
13
+ },
14
+ },
15
+ },
16
+ args: {
17
+ currentStep: 1,
18
+ steps: [
19
+ { id: '1', label: 'Cart' },
20
+ { id: '2', label: 'Delivery address' },
21
+ { id: '3', label: 'Payment' },
22
+ { id: '4', label: 'Order confirmation' },
23
+ ],
24
+ },
25
+ render: (args) => ({
26
+ components: { MStepperStacked },
27
+ setup() {
28
+ return { args };
29
+ },
30
+ template: `<MStepperStacked v-bind="args" />`,
31
+ }),
32
+ };
33
+
34
+ export default meta;
35
+ type Story = StoryObj<typeof MStepperStacked>;
36
+
37
+ export const Default: Story = {};
38
+
39
+ export const AditionnalInfo: Story = {
40
+ args: {
41
+ currentStep: 2,
42
+ steps: [
43
+ { id: '1', label: 'Cart', additionalInfo: 'Additional information' },
44
+ {
45
+ id: '2',
46
+ label: 'Delivery address',
47
+ additionalInfo: 'Additional information',
48
+ },
49
+ { id: '3', label: 'Payment', additionalInfo: 'Additional information' },
50
+ {
51
+ id: '4',
52
+ label: 'Order confirmation',
53
+ additionalInfo: 'Additional information',
54
+ },
55
+ ],
56
+ },
57
+ };
@@ -0,0 +1,106 @@
1
+ <template>
2
+ <nav class="mc-stepper-stacked" aria-label="Stepper">
3
+ <ol class="mc-stepper-stacked__container">
4
+ <li
5
+ v-for="(step, index) in steps"
6
+ :key="index"
7
+ :class="{
8
+ 'mc-stepper-stacked__item': true,
9
+ 'has-additional': step.additionalInfo,
10
+ }"
11
+ >
12
+ <div class="mc-stepper-stacked__indicator">
13
+ <Check24
14
+ v-if="stepStates[index].completed"
15
+ class="mc-stepper-stacked__icon mc-stepper-stacked__icon--check"
16
+ />
17
+ <span
18
+ v-else
19
+ :class="{
20
+ 'mc-stepper-stacked__circle': true,
21
+ 'is-current': stepStates[index].current,
22
+ }"
23
+ >
24
+ {{ index + 1 }}
25
+ </span>
26
+ </div>
27
+ <div class="mc-stepper-stacked__content">
28
+ <span
29
+ :class="{
30
+ 'mc-stepper-stacked__label': true,
31
+ 'is-current': stepStates[index].current,
32
+ }"
33
+ >
34
+ {{ step.label }}
35
+ </span>
36
+ <span class="mc-stepper-stacked__additional">
37
+ {{ step.additionalInfo }}
38
+ </span>
39
+ </div>
40
+ </li>
41
+ </ol>
42
+ </nav>
43
+ </template>
44
+
45
+ <script setup lang="ts">
46
+ import { computed } from 'vue';
47
+ import { Check24 } from '@mozaic-ds/icons-vue';
48
+ /**
49
+ * A stepper is a navigation component that guides users through a sequence of steps in a structured process. It visually represents progress, completed steps, and upcoming steps, helping users understand their position within a workflow. Steppers are commonly used in multi-step forms, onboarding flows, checkout processes, and task completion sequences to improve clarity and reduce cognitive load.
50
+ */
51
+ const props = withDefaults(
52
+ defineProps<{
53
+ /**
54
+ * Defines the currently active step.
55
+ *
56
+ * - If a `number` is provided, it represents the 1-based position of the step
57
+ * in the `steps` array (e.g. `1` for the first step).
58
+ * - If a `string` is provided, it must match the `id` of one of the steps.
59
+ */
60
+ currentStep?: string | number;
61
+ /**
62
+ * Steps of the stepper inline.
63
+ */
64
+ steps?: Array<{
65
+ /**
66
+ * Unique identifier for the step.
67
+ */
68
+ id?: string;
69
+ /**
70
+ * Label of the step.
71
+ */
72
+ label: string;
73
+ /**
74
+ * Optional additional information under the label.
75
+ */
76
+ additionalInfo?: string;
77
+ }>;
78
+ }>(),
79
+ {
80
+ currentStep: '1',
81
+ steps: () => [],
82
+ },
83
+ );
84
+
85
+ const activeStep = computed(() => {
86
+ if (typeof props.currentStep === 'number') {
87
+ return Math.min(Math.max(props.currentStep, 1), props.steps.length) - 1;
88
+ } else {
89
+ const activeIndex = props.steps.findIndex(
90
+ (step) => step.id === props.currentStep,
91
+ );
92
+ return activeIndex === -1 ? 0 : activeIndex;
93
+ }
94
+ });
95
+
96
+ const stepStates = computed(() =>
97
+ props.steps.map((_, i) => ({
98
+ completed: i < activeStep.value,
99
+ current: i === activeStep.value,
100
+ })),
101
+ );
102
+ </script>
103
+
104
+ <style scoped lang="scss">
105
+ @use '@mozaic-ds/styles/components/stepper-stacked';
106
+ </style>
@@ -0,0 +1,15 @@
1
+ # MStepperStacked
2
+
3
+ A stepper is a navigation component that guides users through a sequence of steps in a structured process. It visually represents progress, completed steps, and upcoming steps, helping users understand their position within a workflow. Steppers are commonly used in multi-step forms, onboarding flows, checkout processes, and task completion sequences to improve clarity and reduce cognitive load.
4
+
5
+
6
+ ## Props
7
+
8
+ | Name | Description | Type | Default |
9
+ | --- | --- | --- | --- |
10
+ | `currentStep` | Defines the currently active step.
11
+
12
+ - If a `number` is provided, it represents the 1-based position of the step
13
+ in the `steps` array (e.g. `1` for the first step).
14
+ - If a `string` is provided, it must match the `id` of one of the steps. | `string` `number` | `"1"` |
15
+ | `steps` | Steps of the stepper inline. | `{ id?: string` `undefined; label: string; additionalInfo?: string` `undefined; }[]` | `[]` |
@@ -16,17 +16,22 @@ const meta: Meta<typeof Mtabs> = {
16
16
  },
17
17
  },
18
18
  args: {
19
+ modelValue: 'label1',
19
20
  tabs: [
20
21
  {
22
+ id: 'label1',
21
23
  label: 'Label',
22
24
  },
23
25
  {
26
+ id: 'label2',
24
27
  label: 'Label',
25
28
  },
26
29
  {
30
+ id: 'label3',
27
31
  label: 'Label',
28
32
  },
29
33
  {
34
+ id: 'label4',
30
35
  label: 'Label',
31
36
  },
32
37
  ],
@@ -41,6 +46,7 @@ const meta: Meta<typeof Mtabs> = {
41
46
  template: `
42
47
  <Mtabs
43
48
  v-bind="args"
49
+ v-model="args.modelValue"
44
50
  @update:modelValue="handleUpdate"
45
51
  ></Mtabs>
46
52
  `,
@@ -55,18 +61,22 @@ export const Icons: Story = {
55
61
  args: {
56
62
  tabs: [
57
63
  {
64
+ id: 'label1',
58
65
  label: 'Label',
59
66
  icon: ChevronRight24,
60
67
  },
61
68
  {
69
+ id: 'label2',
62
70
  label: 'Label',
63
71
  icon: ChevronRight24,
64
72
  },
65
73
  {
74
+ id: 'label3',
66
75
  label: 'Label',
67
76
  icon: ChevronRight24,
68
77
  },
69
78
  {
79
+ id: 'label4',
70
80
  label: 'Label',
71
81
  icon: ChevronRight24,
72
82
  },
@@ -86,16 +96,20 @@ export const Disabled: Story = {
86
96
  args: {
87
97
  tabs: [
88
98
  {
99
+ id: 'label1',
89
100
  label: 'Label',
90
101
  },
91
102
  {
103
+ id: 'label2',
92
104
  label: 'Label',
93
105
  },
94
106
  {
107
+ id: 'label3',
95
108
  label: 'Label',
96
109
  disabled: true,
97
110
  },
98
111
  {
112
+ id: 'label4',
99
113
  label: 'Label',
100
114
  disabled: true,
101
115
  },
@@ -107,17 +121,21 @@ export const WithBadges: Story = {
107
121
  args: {
108
122
  tabs: [
109
123
  {
124
+ id: 'label1',
110
125
  label: 'Label',
111
126
  badge: 3,
112
127
  },
113
128
  {
129
+ id: 'label2',
114
130
  label: 'Label',
115
131
  },
116
132
  {
133
+ id: 'label3',
117
134
  label: 'Label',
118
135
  badge: 99,
119
136
  },
120
137
  {
138
+ id: 'label4',
121
139
  label: 'Label',
122
140
  badge: 100,
123
141
  },
@@ -12,12 +12,12 @@
12
12
  role="tab"
13
13
  class="mc-tabs__tab"
14
14
  :class="{
15
- 'mc-tabs__tab--selected': isTabSelected(index),
15
+ 'mc-tabs__tab--selected': isTabSelected(index, tab.id),
16
16
  'mc-tabs__tab--disabled': tab.disabled,
17
17
  }"
18
- :aria-selected="isTabSelected(index)"
18
+ :aria-selected="isTabSelected(index, tab.id)"
19
19
  type="button"
20
- @click="onClickTab(index)"
20
+ @click="onClickTab(index, tab.id)"
21
21
  >
22
22
  <span v-if="tab.icon" class="mc-tabs__icon">
23
23
  <component :is="tab.icon" />
@@ -57,13 +57,20 @@ const props = withDefaults(
57
57
  */
58
58
  centered?: boolean;
59
59
  /**
60
- * The selected tab index, bound via v-model.
60
+ * Defines the currently active tab.
61
+ *
62
+ * - If a `number` is provided, it represents the index of the tab.
63
+ * - If a `string` is provided, it must match the `id` of one of the tabs.
61
64
  */
62
- modelValue?: number;
65
+ modelValue?: string | number;
63
66
  /**
64
67
  * An array of objects that allows you to provide all the data needed to generate the content for each tab.
65
68
  */
66
69
  tabs: Array<{
70
+ /**
71
+ * Unique identifier for the tab.
72
+ */
73
+ id?: string;
67
74
  /**
68
75
  * The icon displayed for the tab from Mozaic-icon-vue.
69
76
  */
@@ -94,25 +101,34 @@ const classObject = computed(() => {
94
101
  };
95
102
  });
96
103
 
97
- const modelValue = ref(props.modelValue);
104
+ const modelValue = ref<string | number | undefined>(props.modelValue);
105
+
106
+ const onClickTab = (index: number, id?: string) => {
107
+ const tab =
108
+ typeof props.modelValue === 'string'
109
+ ? props.tabs.find((tab) => tab.id === id)
110
+ : props.tabs[index];
98
111
 
99
- const onClickTab = (index: number) => {
100
- if (props.tabs[index].disabled) return;
101
- if (index !== modelValue.value) {
102
- modelValue.value = index;
103
- emit('update:modelValue', index);
112
+ const value = typeof props.modelValue === 'string' ? id : index;
113
+
114
+ if (tab?.disabled) return;
115
+ if (value !== modelValue.value) {
116
+ modelValue.value = value;
117
+ emit('update:modelValue', value);
104
118
  }
105
119
  };
106
120
 
107
- const isTabSelected = (index: number) => {
108
- return modelValue.value === index;
121
+ const isTabSelected = (index: number, id?: string) => {
122
+ const value = typeof props.modelValue === 'string' ? id : index;
123
+
124
+ return modelValue.value === value;
109
125
  };
110
126
 
111
127
  const emit = defineEmits<{
112
128
  /**
113
129
  * Emits when the selected tab changes, updating the modelValue prop.
114
130
  */
115
- (on: 'update:modelValue', value: number): void;
131
+ (on: 'update:modelValue', value?: string | number): void;
116
132
  }>();
117
133
  </script>
118
134
 
@@ -4,7 +4,11 @@ import MTabs from './MTabs.vue';
4
4
  import { defineComponent, h, markRaw } from 'vue';
5
5
 
6
6
  describe('MTabs.vue', () => {
7
- const tabs = [{ label: 'Tab 1' }, { label: 'Tab 2' }, { label: 'Tab 3' }];
7
+ const tabs = [
8
+ { id: '1', label: 'Tab 1' },
9
+ { id: '2', label: 'Tab 2' },
10
+ { id: '3', label: 'Tab 3' },
11
+ ];
8
12
 
9
13
  it('renders tabs with correct labels', () => {
10
14
  const wrapper = mount(MTabs, {
@@ -21,7 +25,46 @@ describe('MTabs.vue', () => {
21
25
  });
22
26
  });
23
27
 
24
- it('applies selected class and aria-selected attribute based on modelValue and updates on tab click', async () => {
28
+ it('applies selected class and aria-selected attribute based on modelValue and updates on tab click - string type model', async () => {
29
+ const wrapper = mount(MTabs, {
30
+ props: {
31
+ tabs,
32
+ modelValue: '1',
33
+ },
34
+ });
35
+
36
+ const buttons = wrapper.findAll('button.mc-tabs__tab');
37
+
38
+ buttons.forEach((button, i) => {
39
+ if (i === 0) {
40
+ expect(button.classes()).toContain('mc-tabs__tab--selected');
41
+ expect(button.attributes('aria-selected')).toBe('true');
42
+ } else {
43
+ expect(button.classes()).not.toContain('mc-tabs__tab--selected');
44
+ expect(button.attributes('aria-selected')).toBe('false');
45
+ }
46
+ });
47
+
48
+ await buttons[1].trigger('click');
49
+
50
+ expect(wrapper.emitted('update:modelValue')).toBeTruthy();
51
+ expect(wrapper.emitted('update:modelValue')![0]).toEqual(['2']);
52
+
53
+ await wrapper.setProps({ modelValue: '2' });
54
+
55
+ const updatedButtons = wrapper.findAll('button.mc-tabs__tab');
56
+ updatedButtons.forEach((button, i) => {
57
+ if (i === 1) {
58
+ expect(button.classes()).toContain('mc-tabs__tab--selected');
59
+ expect(button.attributes('aria-selected')).toBe('true');
60
+ } else {
61
+ expect(button.classes()).not.toContain('mc-tabs__tab--selected');
62
+ expect(button.attributes('aria-selected')).toBe('false');
63
+ }
64
+ });
65
+ });
66
+
67
+ it('applies selected class and aria-selected attribute based on modelValue and updates on tab click - number type model', async () => {
25
68
  const wrapper = mount(MTabs, {
26
69
  props: {
27
70
  tabs,
@@ -100,11 +143,11 @@ describe('MTabs.vue', () => {
100
143
  const wrapper = mount(MTabs, {
101
144
  props: {
102
145
  tabs: [
103
- { label: 'Tab 1' },
104
- { label: 'Tab 2', disabled: true },
105
- { label: 'Tab 3' },
146
+ { id: '1', label: 'Tab 1' },
147
+ { id: '2', label: 'Tab 2', disabled: true },
148
+ { id: '3', label: 'Tab 3' },
106
149
  ],
107
- modelValue: 0,
150
+ modelValue: '1',
108
151
  },
109
152
  });
110
153
 
@@ -112,7 +155,7 @@ describe('MTabs.vue', () => {
112
155
 
113
156
  await buttons[2].trigger('click');
114
157
  expect(wrapper.emitted('update:modelValue')).toBeTruthy();
115
- expect(wrapper.emitted('update:modelValue')![0]).toEqual([2]);
158
+ expect(wrapper.emitted('update:modelValue')![0]).toEqual(['3']);
116
159
 
117
160
  await buttons[1].trigger('click');
118
161
  expect(wrapper.emitted('update:modelValue')!.length).toBe(1);
@@ -131,8 +174,8 @@ describe('MTabs.vue', () => {
131
174
  );
132
175
 
133
176
  const tabsWithIcon = [
134
- { label: 'Tab 1', icon: DummyIcon },
135
- { label: 'Tab 2' },
177
+ { id: '1', label: 'Tab 1', icon: DummyIcon },
178
+ { id: '2', label: 'Tab 2' },
136
179
  ];
137
180
 
138
181
  const wrapper = mount(MTabs, {
@@ -150,7 +193,10 @@ describe('MTabs.vue', () => {
150
193
  });
151
194
 
152
195
  it('renders badge when badge prop is provided', () => {
153
- const tabsWithBadges = [{ label: 'Tab 1', badge: 5 }, { label: 'Tab 2' }];
196
+ const tabsWithBadges = [
197
+ { id: '1', label: 'Tab 1', badge: 5 },
198
+ { id: '2', label: 'Tab 2' },
199
+ ];
154
200
 
155
201
  const wrapper = mount(MTabs, {
156
202
  props: {
@@ -10,14 +10,17 @@ Tabs are a navigation component that allows users to switch between different se
10
10
  | `description` | A description indicating the purpose of the set of tabs. Useful for improving the accessibility of the component. | `string` | - |
11
11
  | `divider` | If `true`, the divider will appear. | `boolean` | `true` |
12
12
  | `centered` | If `true`, the tabs of the component will be centered. | `boolean` | - |
13
- | `modelValue` | The selected tab index, bound via v-model. | `number` | `0` |
14
- | `tabs*` | An array of objects that allows you to provide all the data needed to generate the content for each tab. | `{ icon?: Component` `undefined; badge?: number` `undefined; label: string; disabled?: boolean` `undefined; }[]` | - |
13
+ | `modelValue` | Defines the currently active tab.
14
+
15
+ - If a `number` is provided, it represents the index of the tab.
16
+ - If a `string` is provided, it must match the `id` of one of the tabs. | `string` `number` | `0` |
17
+ | `tabs*` | An array of objects that allows you to provide all the data needed to generate the content for each tab. | `{ id?: string` `undefined; icon?: Component` `undefined; badge?: number` `undefined; label: string; disabled?: boolean` `undefined; }[]` | - |
15
18
 
16
19
  ## Events
17
20
 
18
21
  | Name | Description | Type |
19
22
  | --- | --- | --- |
20
- | `update:modelValue` | Emits when the selected tab changes, updating the modelValue prop. | [value: number] |
23
+ | `update:modelValue` | Emits when the selected tab changes, updating the modelValue prop. | [value?: string | number] |
21
24
 
22
25
  ## Dependencies
23
26