@mozaic-ds/vue 2.8.0 → 2.10.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 +334 -252
- package/dist/mozaic-vue.js +1367 -1182
- 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/drawer/MDrawer.spec.ts +81 -9
- package/src/components/drawer/MDrawer.vue +76 -46
- package/src/components/drawer/README.md +1 -0
- package/src/components/field/MField.spec.ts +94 -85
- package/src/components/field/MField.stories.ts +16 -0
- package/src/components/field/MField.vue +8 -1
- package/src/components/field/README.md +1 -0
- package/src/components/flag/MFlag.stories.ts +1 -1
- package/src/components/loader/MLoader.spec.ts +41 -0
- package/src/components/loader/MLoader.vue +7 -1
- package/src/components/loader/README.md +1 -1
- package/src/components/modal/MModal.spec.ts +34 -9
- package/src/components/modal/MModal.vue +39 -7
- package/src/components/modal/README.md +1 -0
- 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/textinput/MTextInput.stories.ts +1 -1
- package/src/components/usingPresets.mdx +1 -1
- package/src/main.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mozaic-ds/vue",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.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.1.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. |
|
|
@@ -50,8 +50,9 @@ describe('MDrawer component', () => {
|
|
|
50
50
|
const closeButton = wrapper.find('.mc-drawer__close');
|
|
51
51
|
await closeButton.trigger('click');
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
expect(
|
|
53
|
+
const emitted = wrapper.emitted('update:open');
|
|
54
|
+
expect(emitted).toBeTruthy();
|
|
55
|
+
expect(emitted![emitted!.length - 1]).toEqual([false]);
|
|
55
56
|
});
|
|
56
57
|
|
|
57
58
|
it('emits back event when back button is clicked', async () => {
|
|
@@ -132,7 +133,7 @@ describe('MDrawer component', () => {
|
|
|
132
133
|
expect(document.activeElement).toBe(titleElement);
|
|
133
134
|
});
|
|
134
135
|
|
|
135
|
-
it('does not
|
|
136
|
+
it('does not refocus the title when the drawer closes', async () => {
|
|
136
137
|
const wrapper = mount(MDrawer, {
|
|
137
138
|
props: {
|
|
138
139
|
title: 'Test Title',
|
|
@@ -143,11 +144,15 @@ describe('MDrawer component', () => {
|
|
|
143
144
|
});
|
|
144
145
|
|
|
145
146
|
const titleElement = wrapper.find('.mc-drawer__title').element;
|
|
147
|
+
|
|
148
|
+
expect(document.activeElement).toBe(titleElement);
|
|
149
|
+
|
|
146
150
|
await wrapper.setProps({ open: false });
|
|
147
|
-
|
|
151
|
+
await wrapper.vm.$nextTick();
|
|
152
|
+
|
|
153
|
+
expect(document.activeElement).toBe(titleElement);
|
|
148
154
|
});
|
|
149
155
|
|
|
150
|
-
// ✅ New tests for closeOnOverlay behavior
|
|
151
156
|
it('emits update:open false when overlay is clicked and closeOnOverlay is true', async () => {
|
|
152
157
|
const wrapper = mount(MDrawer, {
|
|
153
158
|
props: {
|
|
@@ -160,8 +165,9 @@ describe('MDrawer component', () => {
|
|
|
160
165
|
|
|
161
166
|
await wrapper.find('.overlay').trigger('click');
|
|
162
167
|
|
|
163
|
-
|
|
164
|
-
expect(
|
|
168
|
+
const emitted = wrapper.emitted('update:open');
|
|
169
|
+
expect(emitted).toBeTruthy();
|
|
170
|
+
expect(emitted![emitted!.length - 1]).toEqual([false]);
|
|
165
171
|
});
|
|
166
172
|
|
|
167
173
|
it('does not emit update:open when overlay is clicked and closeOnOverlay is false', async () => {
|
|
@@ -176,7 +182,9 @@ describe('MDrawer component', () => {
|
|
|
176
182
|
|
|
177
183
|
await wrapper.find('.overlay').trigger('click');
|
|
178
184
|
|
|
179
|
-
|
|
185
|
+
const emitted = wrapper.emitted('update:open');
|
|
186
|
+
expect(emitted).toBeTruthy();
|
|
187
|
+
expect(emitted?.length).toBe(1);
|
|
180
188
|
});
|
|
181
189
|
|
|
182
190
|
it('does not emit update:open when overlay is clicked and closeOnOverlay is not set', async () => {
|
|
@@ -190,6 +198,70 @@ describe('MDrawer component', () => {
|
|
|
190
198
|
|
|
191
199
|
await wrapper.find('.overlay').trigger('click');
|
|
192
200
|
|
|
193
|
-
|
|
201
|
+
const emitted = wrapper.emitted('update:open');
|
|
202
|
+
expect(emitted).toBeTruthy();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('emits update:open false when pressing ESC key', async () => {
|
|
206
|
+
const wrapper = mount(MDrawer, {
|
|
207
|
+
props: {
|
|
208
|
+
open: true,
|
|
209
|
+
title: 'Test Title',
|
|
210
|
+
},
|
|
211
|
+
global: { stubs },
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
await wrapper.find('section.mc-drawer').trigger('keydown.esc');
|
|
215
|
+
expect(wrapper.emitted('update:open')).toBeTruthy();
|
|
216
|
+
expect(wrapper.emitted('update:open')!.at(-1)).toEqual([false]);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('locks and unlocks scroll when scroll=false and open changes', async () => {
|
|
220
|
+
const wrapper = mount(MDrawer, {
|
|
221
|
+
props: {
|
|
222
|
+
title: 'Scroll Test',
|
|
223
|
+
open: false,
|
|
224
|
+
scroll: false,
|
|
225
|
+
},
|
|
226
|
+
global: { stubs },
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
expect(document.body.style.overflow).toBe('');
|
|
230
|
+
|
|
231
|
+
await wrapper.setProps({ open: true });
|
|
232
|
+
expect(document.body.style.overflow).toBe('hidden');
|
|
233
|
+
|
|
234
|
+
await wrapper.setProps({ open: false });
|
|
235
|
+
expect(document.body.style.overflow).toBe('');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('restores scroll when unmounted', async () => {
|
|
239
|
+
const wrapper = mount(MDrawer, {
|
|
240
|
+
props: {
|
|
241
|
+
open: true,
|
|
242
|
+
title: 'Unmount Test',
|
|
243
|
+
scroll: false,
|
|
244
|
+
},
|
|
245
|
+
global: { stubs },
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
await wrapper.setProps({ open: true });
|
|
249
|
+
expect(document.body.style.overflow).toBe('hidden');
|
|
250
|
+
|
|
251
|
+
wrapper.unmount();
|
|
252
|
+
expect(document.body.style.overflow).toBe('');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('emits update:open on mount reflecting initial state', () => {
|
|
256
|
+
const wrapper = mount(MDrawer, {
|
|
257
|
+
props: {
|
|
258
|
+
open: true,
|
|
259
|
+
title: 'Initial Test',
|
|
260
|
+
},
|
|
261
|
+
global: { stubs },
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
expect(wrapper.emitted('update:open')).toBeTruthy();
|
|
265
|
+
expect(wrapper.emitted('update:open')![0]).toEqual([true]);
|
|
194
266
|
});
|
|
195
267
|
});
|