@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.
Files changed (32) hide show
  1. package/README.md +4 -6
  2. package/dist/mozaic-vue.css +1 -1
  3. package/dist/mozaic-vue.d.ts +334 -252
  4. package/dist/mozaic-vue.js +1367 -1182
  5. package/dist/mozaic-vue.js.map +1 -1
  6. package/dist/mozaic-vue.umd.cjs +5 -5
  7. package/dist/mozaic-vue.umd.cjs.map +1 -1
  8. package/package.json +14 -13
  9. package/src/components/carousel/MCarousel.spec.ts +138 -0
  10. package/src/components/carousel/MCarousel.stories.ts +94 -0
  11. package/src/components/carousel/MCarousel.vue +154 -0
  12. package/src/components/carousel/README.md +18 -0
  13. package/src/components/drawer/MDrawer.spec.ts +81 -9
  14. package/src/components/drawer/MDrawer.vue +76 -46
  15. package/src/components/drawer/README.md +1 -0
  16. package/src/components/field/MField.spec.ts +94 -85
  17. package/src/components/field/MField.stories.ts +16 -0
  18. package/src/components/field/MField.vue +8 -1
  19. package/src/components/field/README.md +1 -0
  20. package/src/components/flag/MFlag.stories.ts +1 -1
  21. package/src/components/loader/MLoader.spec.ts +41 -0
  22. package/src/components/loader/MLoader.vue +7 -1
  23. package/src/components/loader/README.md +1 -1
  24. package/src/components/modal/MModal.spec.ts +34 -9
  25. package/src/components/modal/MModal.vue +39 -7
  26. package/src/components/modal/README.md +1 -0
  27. package/src/components/phonenumber/MPhoneNumber.spec.ts +110 -1
  28. package/src/components/phonenumber/MPhoneNumber.stories.ts +14 -0
  29. package/src/components/phonenumber/MPhoneNumber.vue +16 -6
  30. package/src/components/textinput/MTextInput.stories.ts +1 -1
  31. package/src/components/usingPresets.mdx +1 -1
  32. 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.8.0",
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.1",
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": "^9.0.18",
56
- "@storybook/addon-docs": "^9.0.18",
57
- "@storybook/addon-themes": "^9.0.18",
58
- "@storybook/vue3-vite": "^9.0.18",
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": "^3.0.9",
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": "^9.0.18",
79
- "storybook-addon-tag-badges": "^2.0.2",
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": "^3.0.9",
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
- expect(wrapper.emitted('update:open')).toBeTruthy();
54
- expect(wrapper.emitted('update:open')![0]).toEqual([false]);
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 set the focus on the title when the drawer closes', async () => {
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
- expect(document.activeElement).not.toBe(titleElement);
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
- expect(wrapper.emitted('update:open')).toBeTruthy();
164
- expect(wrapper.emitted('update:open')![0]).toEqual([false]);
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
- expect(wrapper.emitted('update:open')).toBeFalsy();
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
- expect(wrapper.emitted('update:open')).toBeFalsy();
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
  });