@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.
- package/dist/mozaic-vue.css +1 -1
- package/dist/mozaic-vue.d.ts +1582 -500
- package/dist/mozaic-vue.js +8020 -3218
- package/dist/mozaic-vue.js.map +1 -1
- package/dist/mozaic-vue.umd.cjs +24 -5
- package/dist/mozaic-vue.umd.cjs.map +1 -1
- package/package.json +6 -4
- package/src/components/DarkMode.mdx +115 -0
- package/src/components/actionlistbox/MActionListbox.spec.ts +20 -10
- package/src/components/actionlistbox/MActionListbox.stories.ts +15 -8
- package/src/components/actionlistbox/MActionListbox.vue +15 -12
- package/src/components/actionlistbox/README.md +2 -1
- package/src/components/avatar/MAvatar.stories.ts +1 -1
- package/src/components/breadcrumb/MBreadcrumb.vue +2 -2
- package/src/components/button/README.md +2 -0
- package/src/components/combobox/MCombobox.spec.ts +246 -0
- package/src/components/combobox/MCombobox.stories.ts +190 -0
- package/src/components/combobox/MCombobox.vue +277 -0
- package/src/components/combobox/README.md +52 -0
- package/src/components/field/MField.stories.ts +105 -0
- package/src/components/optionListbox/MOptionListbox.spec.ts +527 -0
- package/src/components/optionListbox/MOptionListbox.vue +470 -0
- package/src/components/optionListbox/README.md +63 -0
- package/src/components/pageheader/MPageHeader.spec.ts +12 -12
- package/src/components/pageheader/MPageHeader.stories.ts +9 -1
- package/src/components/pageheader/MPageHeader.vue +3 -6
- package/src/components/segmentedcontrol/MSegmentedControl.spec.ts +57 -25
- package/src/components/segmentedcontrol/MSegmentedControl.stories.ts +6 -19
- package/src/components/segmentedcontrol/MSegmentedControl.vue +27 -13
- package/src/components/segmentedcontrol/README.md +6 -3
- package/src/components/select/MSelect.vue +4 -3
- package/src/components/sidebar/stories/DefaultCase.stories.vue +2 -2
- package/src/components/sidebar/stories/README.md +8 -0
- package/src/components/sidebar/stories/WithExpandOnly.stories.vue +1 -1
- package/src/components/sidebar/stories/WithProfileInfoOnly.stories.vue +2 -2
- package/src/components/sidebar/stories/WithSingleLevel.stories.vue +2 -2
- package/src/components/stepperinline/MStepperInline.spec.ts +63 -28
- package/src/components/stepperinline/MStepperInline.stories.ts +18 -10
- package/src/components/stepperinline/MStepperInline.vue +24 -10
- package/src/components/stepperinline/README.md +6 -2
- package/src/components/stepperstacked/MStepperStacked.spec.ts +162 -0
- package/src/components/stepperstacked/MStepperStacked.stories.ts +57 -0
- package/src/components/stepperstacked/MStepperStacked.vue +106 -0
- package/src/components/stepperstacked/README.md +15 -0
- package/src/components/tabs/MTabs.stories.ts +18 -0
- package/src/components/tabs/MTabs.vue +30 -14
- package/src/components/tabs/Mtabs.spec.ts +56 -10
- package/src/components/tabs/README.md +6 -3
- package/src/components/textinput/MTextInput.vue +13 -1
- package/src/components/textinput/README.md +15 -1
- package/src/components/tileclickable/README.md +1 -1
- 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
|
-
*
|
|
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
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
|
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 = [
|
|
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:
|
|
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([
|
|
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 = [
|
|
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` |
|
|
14
|
-
|
|
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
|
|
23
|
+
| `update:modelValue` | Emits when the selected tab changes, updating the modelValue prop. | [value?: string | number] |
|
|
21
24
|
|
|
22
25
|
## Dependencies
|
|
23
26
|
|