@mozaic-ds/vue 1.0.0-beta.5 → 1.0.0-beta.8
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/README.md +19 -160
- package/dist/mozaic-vue.css +1 -1
- package/dist/mozaic-vue.d.ts +324 -141
- package/dist/mozaic-vue.js +870 -542
- package/dist/mozaic-vue.js.map +1 -1
- package/dist/mozaic-vue.umd.cjs +1 -1
- package/dist/mozaic-vue.umd.cjs.map +1 -1
- package/package.json +2 -2
- package/src/components/GettingStarted.mdx +1 -1
- package/src/components/Introduction.mdx +35 -9
- package/src/components/Support.mdx +1 -1
- package/src/components/breadcrumb/MBreadcrumb.stories.ts +27 -0
- package/src/components/divider/MDivider.spec.ts +57 -0
- package/src/components/divider/MDivider.stories.ts +64 -0
- package/src/components/divider/MDivider.vue +56 -0
- package/src/components/link/MLink.vue +1 -1
- package/src/components/numberbadge/MNumberBadge.spec.ts +56 -0
- package/src/components/{badge/MBadge.stories.ts → numberbadge/MNumberBadge.stories.ts} +8 -8
- package/src/components/{badge/MBadge.vue → numberbadge/MNumberBadge.vue} +4 -4
- package/src/components/pagination/MPagination.spec.ts +123 -0
- package/src/components/pagination/MPagination.stories.ts +83 -0
- package/src/components/pagination/MPagination.vue +140 -0
- package/src/components/tabs/MTabs.stories.ts +104 -0
- package/src/components/tabs/MTabs.vue +113 -0
- package/src/components/tabs/Mtabs.spec.ts +154 -0
- package/src/components/tag/MTag.spec.ts +107 -0
- package/src/components/tag/MTag.stories.ts +75 -0
- package/src/components/tag/MTag.vue +154 -0
- package/src/main.ts +26 -47
- package/src/components/badge/MBadge.spec.ts +0 -16
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<nav class="mc-pagination" role="navigation" aria-label="pagination">
|
|
3
|
+
<MButton
|
|
4
|
+
v-if="!compact"
|
|
5
|
+
icon-position="only"
|
|
6
|
+
aria-label="Previous page"
|
|
7
|
+
:disabled="isFirstPage"
|
|
8
|
+
@click="previous"
|
|
9
|
+
>
|
|
10
|
+
<template #icon><ChevronLeft24 /></template>
|
|
11
|
+
</MButton>
|
|
12
|
+
<MIconButton
|
|
13
|
+
v-else
|
|
14
|
+
outlined
|
|
15
|
+
aria-label="Previous page"
|
|
16
|
+
:disabled="isFirstPage"
|
|
17
|
+
@click="previous"
|
|
18
|
+
>
|
|
19
|
+
<template #icon><ChevronLeft24 /></template>
|
|
20
|
+
</MIconButton>
|
|
21
|
+
|
|
22
|
+
<div v-if="!compact" class="mc-pagination__field">
|
|
23
|
+
<MSelect
|
|
24
|
+
class="mc-pagination__select"
|
|
25
|
+
:id="id"
|
|
26
|
+
v-model="currentValue"
|
|
27
|
+
:options="options"
|
|
28
|
+
@update:model-value="emit('update:modelValue', Number($event))"
|
|
29
|
+
:aria-label="selectLabel"
|
|
30
|
+
></MSelect>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<span v-if="compact" class="mc-pagination__label" aria-current="page">
|
|
34
|
+
{{ options.find(option => option.value === currentValue)?.text }}
|
|
35
|
+
</span>
|
|
36
|
+
|
|
37
|
+
<MButton
|
|
38
|
+
v-if="!compact"
|
|
39
|
+
icon-position="only"
|
|
40
|
+
aria-label="Next page"
|
|
41
|
+
:disabled="isLastPage"
|
|
42
|
+
@click="next"
|
|
43
|
+
>
|
|
44
|
+
<template #icon><ChevronRight24 /></template>
|
|
45
|
+
</MButton>
|
|
46
|
+
<MIconButton
|
|
47
|
+
v-else
|
|
48
|
+
outlined
|
|
49
|
+
aria-label="Next page"
|
|
50
|
+
:disabled="isLastPage"
|
|
51
|
+
@click="next"
|
|
52
|
+
>
|
|
53
|
+
<template #icon><ChevronRight24 /></template>
|
|
54
|
+
</MIconButton>
|
|
55
|
+
</nav>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<script setup lang="ts">
|
|
59
|
+
import { computed, ref, watch } from 'vue';
|
|
60
|
+
import MButton from '../button/MButton.vue';
|
|
61
|
+
import MSelect from '../select/MSelect.vue';
|
|
62
|
+
import ChevronLeft24 from '@mozaic-ds/icons-vue/src/components/ChevronLeft24/ChevronLeft24.vue';
|
|
63
|
+
import ChevronRight24 from '@mozaic-ds/icons-vue/src/components/ChevronRight24/ChevronRight24.vue';
|
|
64
|
+
import MIconButton from '../iconbutton/MIconButton.vue';
|
|
65
|
+
/**
|
|
66
|
+
* Pagination is a navigation component that allows users to browse through large sets of content by dividing it into discrete pages. It typically includes previous and next buttons, numeric page selectors, or dropdowns to jump between pages efficiently. Pagination improves usability and performance in content-heavy applications such as tables, search results, and articles by preventing long scrolls and reducing page load times.
|
|
67
|
+
*/
|
|
68
|
+
const props = defineProps<{
|
|
69
|
+
/**
|
|
70
|
+
* A unique identifier for the pagination.
|
|
71
|
+
*/
|
|
72
|
+
id: string;
|
|
73
|
+
/**
|
|
74
|
+
* The current value of the selected page.
|
|
75
|
+
*/
|
|
76
|
+
modelValue: number;
|
|
77
|
+
/**
|
|
78
|
+
* If `true`, display a compact version without the select.
|
|
79
|
+
*/
|
|
80
|
+
compact?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Define the available choices for the pagination select element.
|
|
83
|
+
*/
|
|
84
|
+
options: Array<{
|
|
85
|
+
id?: string;
|
|
86
|
+
text: string;
|
|
87
|
+
value: number;
|
|
88
|
+
}>;
|
|
89
|
+
/**
|
|
90
|
+
* Accessible label for the select of the pagination.
|
|
91
|
+
*/
|
|
92
|
+
selectLabel?: string;
|
|
93
|
+
}>();
|
|
94
|
+
|
|
95
|
+
const emit = defineEmits<{
|
|
96
|
+
/**
|
|
97
|
+
* Emits when the pagination value changes, updating the modelValue prop.
|
|
98
|
+
*/
|
|
99
|
+
(on: 'update:modelValue', value: number): void;
|
|
100
|
+
}>();
|
|
101
|
+
|
|
102
|
+
const currentValue = ref(props.modelValue);
|
|
103
|
+
|
|
104
|
+
watch(currentValue, (newVal) => {
|
|
105
|
+
if (newVal !== props.modelValue) {
|
|
106
|
+
emit('update:modelValue', newVal);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const currentIndex = computed(() =>
|
|
111
|
+
props.options.findIndex(opt => opt.value === currentValue.value)
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const isFirstPage = computed(() => currentIndex.value === 0);
|
|
115
|
+
const isLastPage = computed(() => currentIndex.value === props.options.length - 1);
|
|
116
|
+
|
|
117
|
+
const previous = () => {
|
|
118
|
+
const currentIndex = props.options.findIndex(
|
|
119
|
+
(opt) => opt.value === currentValue.value,
|
|
120
|
+
);
|
|
121
|
+
if (currentIndex > 0) {
|
|
122
|
+
currentValue.value = props.options[currentIndex - 1].value;
|
|
123
|
+
emit('update:modelValue', props.options[currentIndex - 1].value);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const next = () => {
|
|
128
|
+
const currentIndex = props.options.findIndex(
|
|
129
|
+
(opt) => opt.value === currentValue.value,
|
|
130
|
+
);
|
|
131
|
+
if (currentIndex < props.options.length - 1) {
|
|
132
|
+
currentValue.value = props.options[currentIndex + 1].value;
|
|
133
|
+
emit('update:modelValue', props.options[currentIndex + 1].value);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
</script>
|
|
137
|
+
|
|
138
|
+
<style lang="scss" scoped>
|
|
139
|
+
@use '@mozaic-ds/styles/components/pagination';
|
|
140
|
+
</style>
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3';
|
|
2
|
+
import { action } from '@storybook/addon-actions';
|
|
3
|
+
|
|
4
|
+
import Mtabs from './MTabs.vue';
|
|
5
|
+
import ChevronRight24 from '@mozaic-ds/icons-vue/src/components/ChevronRight24/ChevronRight24.vue';
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof Mtabs> = {
|
|
8
|
+
title: 'Navigation/Tabs',
|
|
9
|
+
component: Mtabs,
|
|
10
|
+
parameters: {
|
|
11
|
+
docs: {
|
|
12
|
+
description: {
|
|
13
|
+
component:
|
|
14
|
+
'Tabs are a navigation component that allows users to switch between different sections within the same context. They help organize content efficiently by displaying only one section at a time, reducing clutter and improving accessibility. Tabs can include icons, labels, and notification badges to provide additional context. They are commonly used in dashboards, product management, and settings interfaces.',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
args: {
|
|
19
|
+
tabs: [
|
|
20
|
+
{
|
|
21
|
+
label: 'Label',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
label: 'Label',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
label: 'Label',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
label: 'Label',
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
render: (args) => ({
|
|
35
|
+
components: { Mtabs, ChevronRight24 },
|
|
36
|
+
setup() {
|
|
37
|
+
const handleUpdate = action('update:modelValue');
|
|
38
|
+
|
|
39
|
+
return { args, handleUpdate };
|
|
40
|
+
},
|
|
41
|
+
template: `
|
|
42
|
+
<Mtabs
|
|
43
|
+
v-bind="args"
|
|
44
|
+
@update:modelValue="handleUpdate"
|
|
45
|
+
></Mtabs>
|
|
46
|
+
`,
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
49
|
+
export default meta;
|
|
50
|
+
type Story = StoryObj<typeof Mtabs>;
|
|
51
|
+
|
|
52
|
+
export const Default: Story = {};
|
|
53
|
+
|
|
54
|
+
export const Icons: Story = {
|
|
55
|
+
args: {
|
|
56
|
+
tabs: [
|
|
57
|
+
{
|
|
58
|
+
label: 'Label',
|
|
59
|
+
icon: ChevronRight24,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
label: 'Label',
|
|
63
|
+
icon: ChevronRight24,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
label: 'Label',
|
|
67
|
+
icon: ChevronRight24,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
label: 'Label',
|
|
71
|
+
icon: ChevronRight24,
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const Centered: Story = {
|
|
78
|
+
args: { centered: true },
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const NoDivider: Story = {
|
|
82
|
+
args: { divider: false },
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const Disabled: Story = {
|
|
86
|
+
args: {
|
|
87
|
+
tabs: [
|
|
88
|
+
{
|
|
89
|
+
label: 'Label',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
label: 'Label',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
label: 'Label',
|
|
96
|
+
disabled: true,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
label: 'Label',
|
|
100
|
+
disabled: true,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<nav class="mc-tabs" :class="classObject">
|
|
3
|
+
<ul role="tablist" class="mc-tabs__list" :aria-label="description">
|
|
4
|
+
<li
|
|
5
|
+
v-for="(tab, index) in tabs"
|
|
6
|
+
:key="`tab-${index}`"
|
|
7
|
+
role="presentation"
|
|
8
|
+
class="mc-tabs__item"
|
|
9
|
+
>
|
|
10
|
+
<button
|
|
11
|
+
ref="tab"
|
|
12
|
+
role="tab"
|
|
13
|
+
class="mc-tabs__tab"
|
|
14
|
+
:class="{
|
|
15
|
+
'mc-tabs__tab--selected': isTabSelected(index),
|
|
16
|
+
'mc-tabs__tab--disabled': tab.disabled,
|
|
17
|
+
}"
|
|
18
|
+
:aria-selected="isTabSelected(index)"
|
|
19
|
+
type="button"
|
|
20
|
+
@click="onClickTab(index)"
|
|
21
|
+
>
|
|
22
|
+
<span v-if="tab.icon" class="mc-tabs__icon">
|
|
23
|
+
<component :is="tab.icon" />
|
|
24
|
+
</span>
|
|
25
|
+
<div class="mc-tabs__label">
|
|
26
|
+
<span>{{ tab.label }}</span>
|
|
27
|
+
</div>
|
|
28
|
+
</button>
|
|
29
|
+
</li>
|
|
30
|
+
</ul>
|
|
31
|
+
<MDivider v-if="divider"></MDivider>
|
|
32
|
+
</nav>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<script setup lang="ts">
|
|
36
|
+
import { computed, ref, type Component } from 'vue';
|
|
37
|
+
import MDivider from '../divider/MDivider.vue';
|
|
38
|
+
/**
|
|
39
|
+
* Tabs are a navigation component that allows users to switch between different sections within the same context. They help organize content efficiently by displaying only one section at a time, reducing clutter and improving accessibility. Tabs can include icons, labels, and notification badges to provide additional context. They are commonly used in dashboards, product management, and settings interfaces.
|
|
40
|
+
*/
|
|
41
|
+
const props = withDefaults(
|
|
42
|
+
defineProps<{
|
|
43
|
+
/**
|
|
44
|
+
* A description indicating the purpose of the set of tabs. Useful for improving the accessibility of the component.
|
|
45
|
+
*/
|
|
46
|
+
description?: string;
|
|
47
|
+
/**
|
|
48
|
+
* If `true`, the divider will appear.
|
|
49
|
+
*/
|
|
50
|
+
divider?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* If `true`, the tabs of the component will be centered.
|
|
53
|
+
*/
|
|
54
|
+
centered?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* The selected tab index, bound via v-model.
|
|
57
|
+
*/
|
|
58
|
+
modelValue?: number;
|
|
59
|
+
/**
|
|
60
|
+
* An array of objects that allows you to provide all the data needed to generate the content for each tab.
|
|
61
|
+
*/
|
|
62
|
+
tabs: Array<{
|
|
63
|
+
/**
|
|
64
|
+
* The icon displayed for the tab from Mozaic-icon-vue.
|
|
65
|
+
*/
|
|
66
|
+
icon?: Component;
|
|
67
|
+
/**
|
|
68
|
+
* The label displayed for the tab.
|
|
69
|
+
*/
|
|
70
|
+
label: string;
|
|
71
|
+
/**
|
|
72
|
+
* If `true`, the tab will be disabled.
|
|
73
|
+
*/
|
|
74
|
+
disabled?: boolean;
|
|
75
|
+
}>;
|
|
76
|
+
}>(),
|
|
77
|
+
{
|
|
78
|
+
modelValue: 0,
|
|
79
|
+
divider: true,
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const classObject = computed(() => {
|
|
84
|
+
return {
|
|
85
|
+
'mc-tabs--centered': props.centered,
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const modelValue = ref(props.modelValue);
|
|
90
|
+
|
|
91
|
+
const onClickTab = (index: number) => {
|
|
92
|
+
if (props.tabs[index].disabled) return;
|
|
93
|
+
if (index !== modelValue.value) {
|
|
94
|
+
modelValue.value = index;
|
|
95
|
+
emit('update:modelValue', index);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const isTabSelected = (index: number) => {
|
|
100
|
+
return modelValue.value === index;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const emit = defineEmits<{
|
|
104
|
+
/**
|
|
105
|
+
* Emits when the selected tab changes, updating the modelValue prop.
|
|
106
|
+
*/
|
|
107
|
+
(on: 'update:modelValue', value: number): void;
|
|
108
|
+
}>();
|
|
109
|
+
</script>
|
|
110
|
+
|
|
111
|
+
<style lang="scss" scoped>
|
|
112
|
+
@use '@mozaic-ds/styles/components/tabs';
|
|
113
|
+
</style>
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils';
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import MTabs from './MTabs.vue';
|
|
4
|
+
import { defineComponent, h } from 'vue';
|
|
5
|
+
|
|
6
|
+
describe('MTabs.vue', () => {
|
|
7
|
+
const tabs = [
|
|
8
|
+
{ label: 'Tab 1' },
|
|
9
|
+
{ label: 'Tab 2' },
|
|
10
|
+
{ label: 'Tab 3' },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
it('renders tabs with correct labels', () => {
|
|
14
|
+
const wrapper = mount(MTabs, {
|
|
15
|
+
props: {
|
|
16
|
+
tabs,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const tabElements = wrapper.findAll('li.mc-tabs__item');
|
|
21
|
+
expect(tabElements.length).toBe(tabs.length);
|
|
22
|
+
|
|
23
|
+
tabs.forEach((tab, i) => {
|
|
24
|
+
expect(tabElements[i].text()).toContain(tab.label);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('applies selected class and aria-selected attribute based on modelValue and updates on tab click', async () => {
|
|
29
|
+
const wrapper = mount(MTabs, {
|
|
30
|
+
props: {
|
|
31
|
+
tabs,
|
|
32
|
+
modelValue: 0,
|
|
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([1]);
|
|
52
|
+
|
|
53
|
+
await wrapper.setProps({ modelValue: 1 });
|
|
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
|
+
|
|
68
|
+
it('adds divider and centered classes based on props', async () => {
|
|
69
|
+
const wrapper = mount(MTabs, {
|
|
70
|
+
props: {
|
|
71
|
+
tabs,
|
|
72
|
+
divider: true,
|
|
73
|
+
centered: true,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(wrapper.classes()).toContain('mc-tabs--centered');
|
|
78
|
+
expect(wrapper.findComponent({ name: 'MDivider' }).exists()).toBe(true);
|
|
79
|
+
|
|
80
|
+
await wrapper.setProps({ divider: false, centered: false });
|
|
81
|
+
|
|
82
|
+
expect(wrapper.classes()).not.toContain('mc-tabs--centered');
|
|
83
|
+
expect(wrapper.findComponent({ name: 'MDivider' }).exists()).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('sets aria-label on tablist based on description prop', () => {
|
|
87
|
+
const description = 'Main tabs navigation';
|
|
88
|
+
const wrapper = mount(MTabs, {
|
|
89
|
+
props: {
|
|
90
|
+
tabs,
|
|
91
|
+
description,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const ul = wrapper.find('ul[role="tablist"]');
|
|
96
|
+
expect(ul.attributes('aria-label')).toBe(description);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('renders no tabs if tabs prop is empty', () => {
|
|
100
|
+
const wrapper = mount(MTabs, { props: { tabs: [] } });
|
|
101
|
+
expect(wrapper.findAll('li.mc-tabs__item').length).toBe(0);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('emits update:modelValue on tab click if tab is not disabled', async () => {
|
|
105
|
+
const wrapper = mount(MTabs, {
|
|
106
|
+
props: {
|
|
107
|
+
tabs: [
|
|
108
|
+
{ label: 'Tab 1' },
|
|
109
|
+
{ label: 'Tab 2', disabled: true },
|
|
110
|
+
{ label: 'Tab 3' },
|
|
111
|
+
],
|
|
112
|
+
modelValue: 0,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const buttons = wrapper.findAll('button.mc-tabs__tab');
|
|
117
|
+
|
|
118
|
+
await buttons[2].trigger('click');
|
|
119
|
+
expect(wrapper.emitted('update:modelValue')).toBeTruthy();
|
|
120
|
+
expect(wrapper.emitted('update:modelValue')![0]).toEqual([2]);
|
|
121
|
+
|
|
122
|
+
await buttons[1].trigger('click');
|
|
123
|
+
expect(wrapper.emitted('update:modelValue')!.length).toBe(1);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('renders icon component when icon prop is provided', () => {
|
|
127
|
+
const DummyIcon = defineComponent({
|
|
128
|
+
name: 'DummyIcon',
|
|
129
|
+
render() {
|
|
130
|
+
return h('svg', { class: 'dummy-icon' }, [
|
|
131
|
+
h('circle', { cx: 10, cy: 10, r: 10 }),
|
|
132
|
+
]);
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const tabsWithIcon = [
|
|
137
|
+
{ label: 'Tab 1', icon: DummyIcon },
|
|
138
|
+
{ label: 'Tab 2' },
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
const wrapper = mount(MTabs, {
|
|
142
|
+
props: {
|
|
143
|
+
tabs: tabsWithIcon,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const firstTabButton = wrapper.findAll('button.mc-tabs__tab')[0];
|
|
148
|
+
expect(firstTabButton.findComponent(DummyIcon).exists()).toBe(true);
|
|
149
|
+
expect(firstTabButton.find('svg.dummy-icon').exists()).toBe(true);
|
|
150
|
+
|
|
151
|
+
const secondTabButton = wrapper.findAll('button.mc-tabs__tab')[1];
|
|
152
|
+
expect(secondTabButton.findComponent(DummyIcon).exists()).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils';
|
|
2
|
+
import MTag from './MTag.vue';
|
|
3
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
describe('MTag.vue', () => {
|
|
6
|
+
it('renders a selectable tag with a checkbox and label', async () => {
|
|
7
|
+
const wrapper = mount(MTag, {
|
|
8
|
+
props: {
|
|
9
|
+
type: 'selectable',
|
|
10
|
+
label: 'Test Tag',
|
|
11
|
+
modelValue: false,
|
|
12
|
+
id: 'test-tag-id',
|
|
13
|
+
name: 'test-tag',
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const checkbox = wrapper.find('input');
|
|
18
|
+
const label = wrapper.find('.mc-tag__label');
|
|
19
|
+
|
|
20
|
+
expect(checkbox.exists()).toBe(true);
|
|
21
|
+
expect(label.text()).toBe('Test Tag');
|
|
22
|
+
expect(checkbox.element.checked).toBe(false);
|
|
23
|
+
|
|
24
|
+
await checkbox.setChecked();
|
|
25
|
+
expect(wrapper.emitted()['update:modelValue'][0]).toEqual([true]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('renders an interactive tag as a button with label', () => {
|
|
29
|
+
const wrapper = mount(MTag, {
|
|
30
|
+
props: {
|
|
31
|
+
type: 'interactive',
|
|
32
|
+
label: 'Interactive Tag',
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const button = wrapper.find('button');
|
|
37
|
+
const label = wrapper.find('.mc-tag__label');
|
|
38
|
+
|
|
39
|
+
expect(button.exists()).toBe(true);
|
|
40
|
+
expect(label.text()).toBe('Interactive Tag');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('renders a contextualised tag with badge number', () => {
|
|
44
|
+
const wrapper = mount(MTag, {
|
|
45
|
+
props: {
|
|
46
|
+
type: 'contextualised',
|
|
47
|
+
label: 'Contextualised Tag',
|
|
48
|
+
contextualisedNumber: 42,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const badge = wrapper.findComponent({ name: 'MNumberBadge' });
|
|
53
|
+
const label = wrapper.find('.mc-tag__label');
|
|
54
|
+
|
|
55
|
+
expect(badge.exists()).toBe(true);
|
|
56
|
+
expect(badge.props('label')).toBe(42);
|
|
57
|
+
expect(label.text()).toBe('Contextualised Tag');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('renders a removable tag with a delete button and emits remove event', async () => {
|
|
61
|
+
const removeTag = vi.fn();
|
|
62
|
+
const wrapper = mount(MTag, {
|
|
63
|
+
props: {
|
|
64
|
+
type: 'removable',
|
|
65
|
+
label: 'Removable Tag',
|
|
66
|
+
id: 'removable-tag-id',
|
|
67
|
+
},
|
|
68
|
+
global: {
|
|
69
|
+
mocks: {
|
|
70
|
+
emit: removeTag,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const removeButton = wrapper.find('button.mc-tag-removable__remove');
|
|
76
|
+
expect(removeButton.exists()).toBe(true);
|
|
77
|
+
|
|
78
|
+
await removeButton.trigger('click');
|
|
79
|
+
expect(removeTag).toHaveBeenCalledWith('remove-tag', 'removable-tag-id');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('renders with the correct size classes based on the size prop', () => {
|
|
83
|
+
const wrapper = mount(MTag, {
|
|
84
|
+
props: {
|
|
85
|
+
type: 'informative',
|
|
86
|
+
label: 'Informative Tag',
|
|
87
|
+
size: 'l',
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const element = wrapper.find('span.mc-tag');
|
|
92
|
+
expect(element.classes()).toContain('mc-tag--l');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should disable the tag when the disabled prop is true', () => {
|
|
96
|
+
const wrapper = mount(MTag, {
|
|
97
|
+
props: {
|
|
98
|
+
type: 'selectable',
|
|
99
|
+
label: 'Disabled Tag',
|
|
100
|
+
disabled: true,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const checkbox = wrapper.find('input');
|
|
105
|
+
expect(checkbox.element.disabled).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3';
|
|
2
|
+
import { action } from '@storybook/addon-actions';
|
|
3
|
+
import MTag from './MTag.vue';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof MTag> = {
|
|
6
|
+
title: 'Indicators/Tag',
|
|
7
|
+
component: MTag,
|
|
8
|
+
parameters: {
|
|
9
|
+
docs: {
|
|
10
|
+
description: {
|
|
11
|
+
component:
|
|
12
|
+
'A Status dot is a small visual indicator used to represent the state or condition of an element. It is often color-coded to convey different statuses at a glance, such as availability, activity, or urgency. Status Dots are commonly found in user presence indicators, system statuses, or process tracking to provide quick, unobtrusive feedback.',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
args: {
|
|
17
|
+
label: 'Tag label',
|
|
18
|
+
},
|
|
19
|
+
render: (args) => ({
|
|
20
|
+
components: { MTag },
|
|
21
|
+
setup() {
|
|
22
|
+
const handleUpdate = action('update:modelValue');
|
|
23
|
+
const handleRemoveTag = action('remove-tag');
|
|
24
|
+
|
|
25
|
+
return { args, handleUpdate, handleRemoveTag };
|
|
26
|
+
},
|
|
27
|
+
template: `
|
|
28
|
+
<MTag
|
|
29
|
+
v-bind="args"
|
|
30
|
+
@update:modelValue="handleUpdate"
|
|
31
|
+
@remove-tag="handleRemoveTag"
|
|
32
|
+
></MTag>
|
|
33
|
+
`,
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
export default meta;
|
|
37
|
+
type Story = StoryObj<typeof MTag>;
|
|
38
|
+
|
|
39
|
+
export const Default: Story = {};
|
|
40
|
+
|
|
41
|
+
export const Size: Story = {
|
|
42
|
+
args: { size: 's' },
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const Interactive: Story = {
|
|
46
|
+
args: { type: 'interactive' },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const Disabled: Story = {
|
|
50
|
+
args: {
|
|
51
|
+
type: 'interactive',
|
|
52
|
+
disabled: true,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const Contextualised: Story = {
|
|
57
|
+
args: {
|
|
58
|
+
type: 'contextualised',
|
|
59
|
+
contextualisedNumber: 99,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const Removable: Story = {
|
|
64
|
+
args: {
|
|
65
|
+
type: 'removable',
|
|
66
|
+
id: 'tagId',
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const Selectable: Story = {
|
|
71
|
+
args: {
|
|
72
|
+
type: 'selectable',
|
|
73
|
+
modelValue: true,
|
|
74
|
+
},
|
|
75
|
+
};
|