@mozaic-ds/vue 2.9.0 → 2.11.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/README.md +4 -6
- package/dist/mozaic-vue.css +1 -1
- package/dist/mozaic-vue.d.ts +424 -249
- package/dist/mozaic-vue.js +1545 -1063
- package/dist/mozaic-vue.js.map +1 -1
- package/dist/mozaic-vue.umd.cjs +5 -5
- package/dist/mozaic-vue.umd.cjs.map +1 -1
- package/package.json +14 -13
- package/src/components/carousel/MCarousel.spec.ts +138 -0
- package/src/components/carousel/MCarousel.stories.ts +94 -0
- package/src/components/carousel/MCarousel.vue +154 -0
- package/src/components/carousel/README.md +18 -0
- package/src/components/flag/MFlag.stories.ts +1 -1
- package/src/components/kpiitem/MKpiItem.spec.ts +71 -0
- package/src/components/kpiitem/MKpiItem.stories.ts +69 -0
- package/src/components/kpiitem/MKpiItem.vue +89 -0
- package/src/components/kpiitem/README.md +15 -0
- package/src/components/loader/MLoader.spec.ts +2 -2
- package/src/components/loader/MLoader.vue +2 -2
- package/src/components/phonenumber/MPhoneNumber.spec.ts +110 -1
- package/src/components/phonenumber/MPhoneNumber.stories.ts +14 -0
- package/src/components/phonenumber/MPhoneNumber.vue +16 -6
- package/src/components/phonenumber/README.md +2 -0
- package/src/components/starrating/MStarRating.spec.ts +203 -0
- package/src/components/starrating/MStarRating.stories.ts +82 -0
- package/src/components/starrating/MStarRating.vue +187 -0
- package/src/components/starrating/README.md +31 -0
- package/src/components/statusbadge/README.md +1 -1
- package/src/components/statusdot/README.md +1 -1
- package/src/components/statusmessage/MStatusMessage.spec.ts +76 -0
- package/src/components/statusmessage/MStatusMessage.stories.ts +52 -0
- package/src/components/statusmessage/MStatusMessage.vue +70 -0
- package/src/components/statusmessage/README.md +11 -0
- package/src/components/statusnotification/README.md +1 -1
- package/src/components/steppercompact/MStepperCompact.spec.ts +98 -0
- package/src/components/steppercompact/MStepperCompact.stories.ts +43 -0
- package/src/components/steppercompact/MStepperCompact.vue +105 -0
- package/src/components/steppercompact/README.md +13 -0
- package/src/components/tag/MTag.vue +2 -1
- package/src/components/tag/README.md +1 -1
- package/src/components/textinput/MTextInput.stories.ts +1 -1
- package/src/components/toaster/README.md +1 -1
- package/src/components/usingPresets.mdx +1 -1
- package/src/main.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mozaic-ds/vue",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Mozaic-Vue is the Vue.js implementation of ADEO Design system",
|
|
6
6
|
"author": "ADEO - ADEO Design system",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"*.d.ts"
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@mozaic-ds/styles": "^2.0
|
|
45
|
-
"@mozaic-ds/web-fonts": "1.65.0",
|
|
44
|
+
"@mozaic-ds/styles": "^2.4.0",
|
|
45
|
+
"@mozaic-ds/web-fonts": "^1.65.0",
|
|
46
46
|
"postcss-scss": "^4.0.9",
|
|
47
47
|
"vue": "^3.5.13"
|
|
48
48
|
},
|
|
@@ -52,22 +52,24 @@
|
|
|
52
52
|
"@mozaic-ds/css-dev-tools": "1.75.0",
|
|
53
53
|
"@mozaic-ds/icons-vue": "^1.0.0",
|
|
54
54
|
"@release-it/conventional-changelog": "^10.0.1",
|
|
55
|
-
"@storybook/addon-a11y": "^
|
|
56
|
-
"@storybook/addon-docs": "^
|
|
57
|
-
"@storybook/addon-themes": "^
|
|
58
|
-
"@storybook/vue3-vite": "^
|
|
55
|
+
"@storybook/addon-a11y": "^10.0.4",
|
|
56
|
+
"@storybook/addon-docs": "^10.0.4",
|
|
57
|
+
"@storybook/addon-themes": "^10.0.4",
|
|
58
|
+
"@storybook/vue3-vite": "^10.0.4",
|
|
59
59
|
"@types/jsdom": "^27.0.0",
|
|
60
60
|
"@vitejs/plugin-vue": "^6.0.1",
|
|
61
|
-
"@vitest/coverage-v8": "^
|
|
61
|
+
"@vitest/coverage-v8": "^4.0.7",
|
|
62
62
|
"@vitest/eslint-plugin": "^1.1.38",
|
|
63
63
|
"@vue/eslint-config-prettier": "^10.2.0",
|
|
64
64
|
"@vue/eslint-config-typescript": "^14.5.0",
|
|
65
65
|
"@vue/test-utils": "^2.4.6",
|
|
66
66
|
"eslint": "^9.22.0",
|
|
67
|
+
"eslint-plugin-storybook": "^10.0.5",
|
|
67
68
|
"eslint-plugin-vue": "^10.0.0",
|
|
68
69
|
"eslint-plugin-vuejs-accessibility": "^2.4.1",
|
|
69
70
|
"husky": "^9.1.7",
|
|
70
71
|
"jsdom": "^27.0.0",
|
|
72
|
+
"libphonenumber-js": "^1.12.23",
|
|
71
73
|
"lint-staged": "^16.1.5",
|
|
72
74
|
"mdx-mermaid": "^2.0.3",
|
|
73
75
|
"mermaid": "^11.5.0",
|
|
@@ -75,15 +77,14 @@
|
|
|
75
77
|
"prettier": "^3.5.3",
|
|
76
78
|
"release-it": "^19.0.4",
|
|
77
79
|
"sass": "^1.86.0",
|
|
78
|
-
"storybook": "^
|
|
79
|
-
"storybook-addon-tag-badges": "^
|
|
80
|
+
"storybook": "^10.0.4",
|
|
81
|
+
"storybook-addon-tag-badges": "^3.0.2",
|
|
80
82
|
"typescript": "^5.7.2",
|
|
81
83
|
"vite": "^7.1.1",
|
|
82
84
|
"vite-plugin-dts": "^4.5.3",
|
|
83
|
-
"vitest": "^
|
|
85
|
+
"vitest": "^4.0.7",
|
|
84
86
|
"vue-component-meta": "^3.0.8",
|
|
85
|
-
"vue-eslint-parser": "^10.1.1"
|
|
86
|
-
"libphonenumber-js": "^1.12.23"
|
|
87
|
+
"vue-eslint-parser": "^10.1.1"
|
|
87
88
|
},
|
|
88
89
|
"bugs": {
|
|
89
90
|
"url": "https://github.com/adeo/mozaic-vue/issues"
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils';
|
|
2
|
+
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
|
|
3
|
+
import MCarousel from './MCarousel.vue';
|
|
4
|
+
import MIconButton from '../iconbutton/MIconButton.vue';
|
|
5
|
+
import ChevronLeft20 from '@mozaic-ds/icons-vue/src/components/ChevronLeft20/ChevronLeft20.vue';
|
|
6
|
+
import ChevronRight20 from '@mozaic-ds/icons-vue/src/components/ChevronRight20/ChevronRight20.vue';
|
|
7
|
+
|
|
8
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
9
|
+
|
|
10
|
+
class MockIntersectionObserver {
|
|
11
|
+
callback: any;
|
|
12
|
+
options: any;
|
|
13
|
+
constructor(callback: any, options?: any) {
|
|
14
|
+
this.callback = callback;
|
|
15
|
+
this.options = options;
|
|
16
|
+
}
|
|
17
|
+
observe = vi.fn();
|
|
18
|
+
unobserve = vi.fn();
|
|
19
|
+
disconnect = vi.fn();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('MCarousel component', () => {
|
|
23
|
+
let originalObserver: any;
|
|
24
|
+
|
|
25
|
+
beforeAll(() => {
|
|
26
|
+
originalObserver = global.IntersectionObserver;
|
|
27
|
+
global.IntersectionObserver = MockIntersectionObserver as any;
|
|
28
|
+
|
|
29
|
+
Object.defineProperty(window.HTMLElement.prototype, 'scrollIntoView', {
|
|
30
|
+
value: vi.fn(),
|
|
31
|
+
writable: true,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterAll(() => {
|
|
36
|
+
global.IntersectionObserver = originalObserver;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const mockChildren = [
|
|
40
|
+
'<div class="slide">Slide 1</div>',
|
|
41
|
+
'<div class="slide">Slide 2</div>',
|
|
42
|
+
'<div class="slide">Slide 3</div>',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const mountCarousel = (options = {}) =>
|
|
46
|
+
mount(MCarousel, {
|
|
47
|
+
attachTo: document.body,
|
|
48
|
+
slots: {
|
|
49
|
+
default: mockChildren.join(''),
|
|
50
|
+
header: '<h2 id="mc-carousel__title">Carousel Header</h2>',
|
|
51
|
+
},
|
|
52
|
+
...options,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('renders correctly with header and default slot', () => {
|
|
56
|
+
const wrapper = mountCarousel();
|
|
57
|
+
expect(wrapper.find('.mc-carousel__headings').text()).toContain(
|
|
58
|
+
'Carousel Header',
|
|
59
|
+
);
|
|
60
|
+
expect(wrapper.findAll('.slide')).toHaveLength(3);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('renders navigation buttons with correct aria labels', () => {
|
|
64
|
+
const wrapper = mountCarousel({
|
|
65
|
+
props: {
|
|
66
|
+
previousButtonAriaLabel: 'Go back',
|
|
67
|
+
nextButtonAriaLabel: 'Go forward',
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const buttons = wrapper.findAllComponents(MIconButton);
|
|
72
|
+
expect(buttons).toHaveLength(2);
|
|
73
|
+
expect(buttons[0].attributes('aria-label')).toBe('Go back');
|
|
74
|
+
expect(buttons[1].attributes('aria-label')).toBe('Go forward');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('renders default aria labels when not provided', () => {
|
|
78
|
+
const wrapper = mountCarousel();
|
|
79
|
+
const buttons = wrapper.findAllComponents(MIconButton);
|
|
80
|
+
expect(buttons[0].attributes('aria-label')).toBe('previous');
|
|
81
|
+
expect(buttons[1].attributes('aria-label')).toBe('next');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('renders icon components inside navigation buttons', () => {
|
|
85
|
+
const wrapper = mountCarousel();
|
|
86
|
+
expect(wrapper.findComponent(ChevronLeft20).exists()).toBe(true);
|
|
87
|
+
expect(wrapper.findComponent(ChevronRight20).exists()).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('disables the previous button when on the first slide', () => {
|
|
91
|
+
const wrapper = mountCarousel();
|
|
92
|
+
const [prevButton] = wrapper.findAllComponents(MIconButton);
|
|
93
|
+
expect(prevButton.props('disabled')).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('scrolls to next slide when goNext is called', async () => {
|
|
97
|
+
const scrollIntoViewMock = vi.fn();
|
|
98
|
+
(window.HTMLElement.prototype.scrollIntoView as any) = scrollIntoViewMock;
|
|
99
|
+
|
|
100
|
+
const wrapper = mountCarousel();
|
|
101
|
+
const vm = wrapper.vm as any;
|
|
102
|
+
vi.spyOn(vm, 'getCarouselChildren').mockReturnValue([
|
|
103
|
+
{ scrollIntoView: scrollIntoViewMock },
|
|
104
|
+
{ scrollIntoView: scrollIntoViewMock },
|
|
105
|
+
{ scrollIntoView: scrollIntoViewMock },
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
vm.goNext();
|
|
109
|
+
expect(scrollIntoViewMock).toHaveBeenCalled();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('scrolls to previous slide when goPrevious is called', async () => {
|
|
113
|
+
const scrollIntoViewMock = vi.fn();
|
|
114
|
+
(window.HTMLElement.prototype.scrollIntoView as any) = scrollIntoViewMock;
|
|
115
|
+
|
|
116
|
+
const wrapper = mountCarousel();
|
|
117
|
+
const vm = wrapper.vm as any;
|
|
118
|
+
vi.spyOn(vm, 'getCarouselChildren').mockReturnValue([
|
|
119
|
+
{ scrollIntoView: scrollIntoViewMock },
|
|
120
|
+
{ scrollIntoView: scrollIntoViewMock },
|
|
121
|
+
{ scrollIntoView: scrollIntoViewMock },
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
vm.goNext();
|
|
125
|
+
vm.goNext();
|
|
126
|
+
vm.goPrevious();
|
|
127
|
+
|
|
128
|
+
expect(scrollIntoViewMock).toHaveBeenCalled();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('sets correct ARIA attributes on main container', () => {
|
|
132
|
+
const wrapper = mountCarousel();
|
|
133
|
+
const container = wrapper.find('.mc-carousel');
|
|
134
|
+
expect(container.attributes('role')).toBe('group');
|
|
135
|
+
expect(container.attributes('aria-roledescription')).toBe('carousel');
|
|
136
|
+
expect(container.attributes('aria-labelledby')).toBe('mc-carousel__title');
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite';
|
|
2
|
+
import MCarousel from './MCarousel.vue';
|
|
3
|
+
import MLink from '../link/MLink.vue';
|
|
4
|
+
import ArrowNext24 from '@mozaic-ds/icons-vue/src/components/ArrowNext24/ArrowNext24.vue';
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof MCarousel> = {
|
|
7
|
+
title: 'Content/Carousel',
|
|
8
|
+
component: MCarousel,
|
|
9
|
+
parameters: {
|
|
10
|
+
docs: {
|
|
11
|
+
description: {
|
|
12
|
+
component:
|
|
13
|
+
'A Carousel allows users to browse through multiple items within a horizontal container, using swipe gestures on mobile or navigation controls on desktop. It is primarily used to showcase products, promotions, or visual content, offering an engaging way to explore information in a condensed and interactive format. Carousels help optimize space while keeping content visually appealing and easily accessible.',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
argTypes: {
|
|
18
|
+
'aria-labelledby': {
|
|
19
|
+
table: {
|
|
20
|
+
disable: true,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
args: {
|
|
25
|
+
'aria-labelledby': 'defaultCarousel',
|
|
26
|
+
header:
|
|
27
|
+
'<h2 class="mc-carousel__title mt-title--m" id="defaultCarousel">Title of the carousel</h2>',
|
|
28
|
+
default: `
|
|
29
|
+
<div class="free-content" style="padding: 16px;" aria-labelledby="free-content__1">
|
|
30
|
+
<img class="free-content__image"
|
|
31
|
+
src="https://picsum.photos/id/1/600/300" alt="card 1">
|
|
32
|
+
<div id="free-content__1" class="free-content__title">my card1</div>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="free-content" style="padding: 16px;" aria-labelledby="free-content__2">
|
|
35
|
+
<img class="free-content__image"
|
|
36
|
+
src="https://picsum.photos/id/12/600/300" alt="card 2">
|
|
37
|
+
<div id="free-content__2" class="free-content__title">my card2</div>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="free-content" style="padding: 16px;" aria-labelledby="free-content__3">
|
|
40
|
+
<img class="free-content__image"
|
|
41
|
+
src="https://picsum.photos/id/23/600/300" alt="card 3">
|
|
42
|
+
<div id="free-content__3" class="free-content__title">my card3</div>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="free-content" style="padding: 16px;" aria-labelledby="free-content__4">
|
|
45
|
+
<img class="free-content__image"
|
|
46
|
+
src="https://picsum.photos/id/34/600/300" alt="card 4">
|
|
47
|
+
<div id="free-content__4" class="free-content__title">my card4</div>
|
|
48
|
+
</div>
|
|
49
|
+
`,
|
|
50
|
+
},
|
|
51
|
+
render: (args) => ({
|
|
52
|
+
components: {
|
|
53
|
+
MCarousel,
|
|
54
|
+
MLink,
|
|
55
|
+
ArrowNext24,
|
|
56
|
+
},
|
|
57
|
+
setup() {
|
|
58
|
+
return { args };
|
|
59
|
+
},
|
|
60
|
+
template: `
|
|
61
|
+
<MCarousel v-bind="args">
|
|
62
|
+
<template v-if="${'header' in args}" v-slot:header>${args.header}</template>
|
|
63
|
+
<template v-if="${'default' in args}" v-slot>${args.default}</template>
|
|
64
|
+
</MCarousel>
|
|
65
|
+
`,
|
|
66
|
+
}),
|
|
67
|
+
};
|
|
68
|
+
export default meta;
|
|
69
|
+
type Story = StoryObj<typeof MCarousel>;
|
|
70
|
+
|
|
71
|
+
export const Default: Story = {};
|
|
72
|
+
|
|
73
|
+
export const Subtitle: Story = {
|
|
74
|
+
args: {
|
|
75
|
+
'aria-labelledby': 'subtitleCarousel',
|
|
76
|
+
header: `
|
|
77
|
+
<h2 class="mc-carousel__title mt-title--m" id="subtitleCarousel">Title of the carousel</h2>
|
|
78
|
+
<p class="mc-carousel__sub-title mt-body-m">Longer description of the carousel</p>
|
|
79
|
+
`,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const Link: Story = {
|
|
84
|
+
'aria-labelledby': 'linkCarousel',
|
|
85
|
+
args: {
|
|
86
|
+
header: `
|
|
87
|
+
<h2 class="mc-carousel__title mt-title--m" id="linkCarousel">Title of the carousel</h2>
|
|
88
|
+
<MLink href="#" iconPosition="right">
|
|
89
|
+
Stand-alone link
|
|
90
|
+
<template #icon><ArrowNext24/></template>
|
|
91
|
+
</MLink>
|
|
92
|
+
`,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="mc-carousel"
|
|
4
|
+
role="group"
|
|
5
|
+
aria-roledescription="carousel"
|
|
6
|
+
aria-labelledby="mc-carousel__title"
|
|
7
|
+
>
|
|
8
|
+
<div class="mc-carousel__header">
|
|
9
|
+
<div class="mc-carousel__headings">
|
|
10
|
+
<slot name="header" />
|
|
11
|
+
</div>
|
|
12
|
+
<div class="mc-carousel__controls">
|
|
13
|
+
<MIconButton
|
|
14
|
+
size="s"
|
|
15
|
+
outlined
|
|
16
|
+
@click="goPrevious"
|
|
17
|
+
:disabled="isFirstChildActive"
|
|
18
|
+
:aria-label="previousButtonAriaLabel"
|
|
19
|
+
>
|
|
20
|
+
<template #icon>
|
|
21
|
+
<ChevronLeft20 />
|
|
22
|
+
</template>
|
|
23
|
+
</MIconButton>
|
|
24
|
+
<MIconButton
|
|
25
|
+
size="s"
|
|
26
|
+
outlined
|
|
27
|
+
@click="goNext"
|
|
28
|
+
:disabled="isLastChildActive"
|
|
29
|
+
:aria-label="nextButtonAriaLabel"
|
|
30
|
+
>
|
|
31
|
+
<template #icon>
|
|
32
|
+
<ChevronRight20 />
|
|
33
|
+
</template>
|
|
34
|
+
</MIconButton>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="mc-carousel__content" ref="contentContainer">
|
|
38
|
+
<template
|
|
39
|
+
v-for="(child, index) in $slots.default?.()"
|
|
40
|
+
:key="`carousel-slide-${index}`"
|
|
41
|
+
>
|
|
42
|
+
<component :is="child" />
|
|
43
|
+
</template>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</template>
|
|
47
|
+
|
|
48
|
+
<script setup lang="ts">
|
|
49
|
+
import { computed, onMounted, ref, type VNode } from 'vue';
|
|
50
|
+
import MIconButton from '../iconbutton/MIconButton.vue';
|
|
51
|
+
import ChevronLeft20 from '@mozaic-ds/icons-vue/src/components/ChevronLeft20/ChevronLeft20.vue';
|
|
52
|
+
import ChevronRight20 from '@mozaic-ds/icons-vue/src/components/ChevronRight20/ChevronRight20.vue';
|
|
53
|
+
/**
|
|
54
|
+
* A Carousel allows users to browse through multiple items within a horizontal container, using swipe gestures on mobile or navigation controls on desktop. It is primarily used to showcase products, promotions, or visual content, offering an engaging way to explore information in a condensed and interactive format. Carousels help optimize space while keeping content visually appealing and easily accessible.
|
|
55
|
+
*/
|
|
56
|
+
withDefaults(
|
|
57
|
+
defineProps<{
|
|
58
|
+
/**
|
|
59
|
+
* Aria label for the previous button.
|
|
60
|
+
*/
|
|
61
|
+
previousButtonAriaLabel?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Aria label for the next button.
|
|
64
|
+
*/
|
|
65
|
+
nextButtonAriaLabel?: string;
|
|
66
|
+
}>(),
|
|
67
|
+
{
|
|
68
|
+
previousButtonAriaLabel: 'previous',
|
|
69
|
+
nextButtonAriaLabel: 'next',
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
defineSlots<{
|
|
74
|
+
/**
|
|
75
|
+
* Use this slot to insert a list of components to be displayed in the carousel.
|
|
76
|
+
*/
|
|
77
|
+
default: () => VNode[];
|
|
78
|
+
/**
|
|
79
|
+
* Use this slot to insert the title, subtitle or link of the carousel.
|
|
80
|
+
*/
|
|
81
|
+
header: VNode;
|
|
82
|
+
}>();
|
|
83
|
+
|
|
84
|
+
const activeIndex = ref<number>(0);
|
|
85
|
+
const contentContainer = ref<HTMLElement | null>(null);
|
|
86
|
+
|
|
87
|
+
let observer: IntersectionObserver;
|
|
88
|
+
|
|
89
|
+
const scrollOptions: ScrollIntoViewOptions = {
|
|
90
|
+
behavior: 'smooth',
|
|
91
|
+
block: 'nearest',
|
|
92
|
+
inline: 'center',
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
function getCarouselChildren() {
|
|
96
|
+
return contentContainer.value ? [...contentContainer.value.children] : [];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
onMounted(() => {
|
|
100
|
+
observer = new IntersectionObserver(
|
|
101
|
+
(entries: IntersectionObserverEntry[]) => {
|
|
102
|
+
const entry = entries.find(
|
|
103
|
+
(e: IntersectionObserverEntry) => e.isIntersecting,
|
|
104
|
+
);
|
|
105
|
+
if (entry) {
|
|
106
|
+
activeIndex.value = getCarouselChildren().findIndex(
|
|
107
|
+
(e) => e === entry.target,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
root: contentContainer.value,
|
|
113
|
+
threshold: 0.9,
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
getCarouselChildren().forEach((el) => {
|
|
118
|
+
observer.observe(el);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
function scrollToChild(index: number) {
|
|
123
|
+
getCarouselChildren()[index].scrollIntoView(scrollOptions);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function goPrevious() {
|
|
127
|
+
if (activeIndex.value > 0) {
|
|
128
|
+
scrollToChild(activeIndex.value - 1);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function goNext() {
|
|
133
|
+
if (activeIndex.value < getCarouselChildren().length - 1) {
|
|
134
|
+
scrollToChild(activeIndex.value + 1);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const isFirstChildActive = computed(() => activeIndex.value === 0);
|
|
139
|
+
const isLastChildActive = computed(
|
|
140
|
+
() => activeIndex.value === getCarouselChildren().length - 1,
|
|
141
|
+
);
|
|
142
|
+
</script>
|
|
143
|
+
|
|
144
|
+
<style scoped lang="scss">
|
|
145
|
+
@use '@mozaic-ds/styles/components/carousel';
|
|
146
|
+
|
|
147
|
+
::v-deep(.mc-carousel__title) {
|
|
148
|
+
margin: 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
::v-deep(.mc-carousel__sub-title) {
|
|
152
|
+
margin: 0;
|
|
153
|
+
}
|
|
154
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# MCarousel
|
|
2
|
+
|
|
3
|
+
A Carousel allows users to browse through multiple items within a horizontal container, using swipe gestures on mobile or navigation controls on desktop. It is primarily used to showcase products, promotions, or visual content, offering an engaging way to explore information in a condensed and interactive format. Carousels help optimize space while keeping content visually appealing and easily accessible.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## Props
|
|
7
|
+
|
|
8
|
+
| Name | Description | Type | Default |
|
|
9
|
+
| --- | --- | --- | --- |
|
|
10
|
+
| `previousButtonAriaLabel` | Aria label for the previous button. | `string` | `"previous"` |
|
|
11
|
+
| `nextButtonAriaLabel` | Aria label for the next button. | `string` | `"next"` |
|
|
12
|
+
|
|
13
|
+
## Slots
|
|
14
|
+
|
|
15
|
+
| Name | Description |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| `default` | Use this slot to insert a list of components to be displayed in the carousel. |
|
|
18
|
+
| `header` | Use this slot to insert the title, subtitle or link of the carousel. |
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils';
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import KpiItem from './MKpiItem.vue';
|
|
4
|
+
|
|
5
|
+
describe('MKpiItem component', () => {
|
|
6
|
+
it('renders the large size correctly', () => {
|
|
7
|
+
const wrapper = mount(KpiItem, {
|
|
8
|
+
props: {
|
|
9
|
+
value: '85%',
|
|
10
|
+
label: 'Completion Rate',
|
|
11
|
+
trend: 'increasing',
|
|
12
|
+
information: 'Above target',
|
|
13
|
+
size: 'l',
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
expect(wrapper.text()).toContain('85%');
|
|
17
|
+
expect(wrapper.text()).toContain('Completion Rate');
|
|
18
|
+
expect(wrapper.text()).toContain('Above target');
|
|
19
|
+
expect(wrapper.find('.mc-kpi__icon').exists()).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('renders the medium size component correctly', () => {
|
|
23
|
+
const wrapper = mount(KpiItem, {
|
|
24
|
+
props: {
|
|
25
|
+
value: '85%',
|
|
26
|
+
label: 'Completion Rate',
|
|
27
|
+
information: 'Above target',
|
|
28
|
+
trend: 'increasing',
|
|
29
|
+
size: 'm',
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
expect(wrapper.text()).toContain('85%');
|
|
33
|
+
expect(wrapper.text()).toContain('Completion Rate');
|
|
34
|
+
expect(wrapper.text()).not.toContain('Above target');
|
|
35
|
+
expect(wrapper.find('.mc-kpi__icon').exists()).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('renders the small size component correctly', () => {
|
|
39
|
+
const wrapper = mount(KpiItem, {
|
|
40
|
+
props: {
|
|
41
|
+
value: '85%',
|
|
42
|
+
label: 'Completion Rate',
|
|
43
|
+
information: 'Above target',
|
|
44
|
+
trend: 'increasing',
|
|
45
|
+
size: 's',
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
expect(wrapper.text()).toContain('85%');
|
|
49
|
+
expect(wrapper.text()).not.toContain('Completion Rate');
|
|
50
|
+
expect(wrapper.text()).not.toContain('Above target');
|
|
51
|
+
expect(wrapper.find('.mc-kpi__icon').exists()).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('trend icon', () => {
|
|
55
|
+
it('does not render the icon when trend prop is not provided', () => {
|
|
56
|
+
const wrapper = mount(KpiItem, {
|
|
57
|
+
props: { value: '123' },
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(wrapper.find('.mc-kpi__icon').exists()).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('renders the icon when trend prop is provided', () => {
|
|
64
|
+
const wrapper = mount(KpiItem, {
|
|
65
|
+
props: { value: '123', trend: 'increasing' },
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(wrapper.find('.mc-kpi__icon').exists()).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite';
|
|
2
|
+
import MKpiItem from './MKpiItem.vue';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof MKpiItem> = {
|
|
5
|
+
title: 'Status/Kpi Item',
|
|
6
|
+
component: MKpiItem,
|
|
7
|
+
parameters: {
|
|
8
|
+
docs: {
|
|
9
|
+
description: {
|
|
10
|
+
component:
|
|
11
|
+
'A KPI Item is used to display Key Performance Indicators (KPIs) within an interface, providing a quick and clear visualization of essential data. It often includes contextual elements such as labels, trends, or status indicators to help users interpret the information at a glance. KPI Items are commonly used in dashboards, reports, and analytics tools to highlight critical metrics and facilitate data-driven decision-making.',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
args: {
|
|
16
|
+
value: '99.99%',
|
|
17
|
+
label: 'Label',
|
|
18
|
+
size: 's',
|
|
19
|
+
},
|
|
20
|
+
argTypes: {
|
|
21
|
+
size: {
|
|
22
|
+
control: { type: 'inline-radio' },
|
|
23
|
+
options: ['s', 'm', 'l'],
|
|
24
|
+
},
|
|
25
|
+
trend: {
|
|
26
|
+
control: { type: 'radio' },
|
|
27
|
+
options: ['increasing', 'decreasing', 'stable', undefined],
|
|
28
|
+
},
|
|
29
|
+
status: {
|
|
30
|
+
control: { type: 'radio' },
|
|
31
|
+
options: ['info', 'warning', 'error', 'success', 'neutral'],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
render: (args) => ({
|
|
35
|
+
components: { MKpiItem },
|
|
36
|
+
setup() {
|
|
37
|
+
return { args };
|
|
38
|
+
},
|
|
39
|
+
template: `
|
|
40
|
+
<MKpiItem v-bind="args"></MKpiItem>
|
|
41
|
+
`,
|
|
42
|
+
}),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export default meta;
|
|
46
|
+
|
|
47
|
+
type Story = StoryObj<typeof MKpiItem>;
|
|
48
|
+
|
|
49
|
+
export const LargeWithIconAndInformation: Story = {
|
|
50
|
+
args: {
|
|
51
|
+
trend: 'increasing',
|
|
52
|
+
information: '> 10% expected',
|
|
53
|
+
size: 'l',
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const MediumWithIconAndLabel: Story = {
|
|
58
|
+
args: {
|
|
59
|
+
trend: 'increasing',
|
|
60
|
+
size: 'm',
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const SmallWithIcon: Story = {
|
|
65
|
+
args: {
|
|
66
|
+
trend: 'increasing',
|
|
67
|
+
size: 's',
|
|
68
|
+
},
|
|
69
|
+
};
|