@mozaic-ds/vue 2.17.0 → 2.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mozaic-vue.css +1 -1
- package/dist/mozaic-vue.d.ts +158 -72
- package/dist/mozaic-vue.js +1728 -4748
- package/dist/mozaic-vue.js.map +1 -1
- package/dist/mozaic-vue.umd.cjs +6 -25
- package/dist/mozaic-vue.umd.cjs.map +1 -1
- package/package.json +16 -10
- package/src/components/BrandPresets.mdx +20 -2
- package/src/components/Migration.mdx +651 -0
- package/src/components/accordionlistitem/MAccordionListItem.spec.ts +22 -3
- package/src/components/accordionlistitem/MAccordionListItem.vue +38 -28
- package/src/components/actionlistbox/MActionListbox.spec.ts +99 -0
- package/src/components/actionlistbox/MActionListbox.vue +54 -7
- package/src/components/breadcrumb/MBreadcrumb.vue +1 -1
- package/src/components/builtinmenu/MBuiltInMenu.spec.ts +30 -1
- package/src/components/builtinmenu/MBuiltInMenu.vue +26 -17
- package/src/components/builtinmenu/README.md +2 -0
- package/src/components/button/MButton.spec.ts +26 -0
- package/src/components/button/MButton.vue +2 -0
- package/src/components/callout/MCallout.spec.ts +35 -0
- package/src/components/callout/MCallout.stories.ts +0 -3
- package/src/components/callout/MCallout.vue +26 -7
- package/src/components/callout/README.md +4 -2
- package/src/components/carousel/MCarousel.spec.ts +26 -2
- package/src/components/carousel/MCarousel.vue +10 -4
- package/src/components/checklistmenu/MCheckListMenu.spec.ts +12 -1
- package/src/components/checklistmenu/MCheckListMenu.vue +6 -0
- package/src/components/checklistmenu/README.md +2 -0
- package/src/components/combobox/MCombobox.vue +7 -0
- package/src/components/datatable/datatable.mdx +3 -2
- package/src/components/drawer/MDrawer.spec.ts +102 -3
- package/src/components/drawer/MDrawer.vue +73 -14
- package/src/components/field/MField.vue +1 -0
- package/src/components/fileuploader/MFileUploader.vue +2 -2
- package/src/components/fileuploaderitem/MFileUploaderItem.vue +2 -7
- package/src/components/iconbutton/MIconButton.spec.ts +15 -0
- package/src/components/iconbutton/MIconButton.vue +1 -0
- package/src/components/kpiitem/MKpiItem.spec.ts +13 -0
- package/src/components/kpiitem/MKpiItem.vue +1 -1
- package/src/components/modal/MModal.spec.ts +115 -3
- package/src/components/modal/MModal.vue +91 -11
- package/src/components/modal/README.md +1 -1
- package/src/components/navigationindicator/MNavigationIndicator.spec.ts +75 -18
- package/src/components/navigationindicator/MNavigationIndicator.vue +10 -12
- package/src/components/optionListbox/MOptionListbox.vue +16 -1
- package/src/components/overlay/MOverlay.spec.ts +1 -1
- package/src/components/overlay/MOverlay.vue +1 -1
- package/src/components/phonenumber/MPhoneNumber.spec.ts +6 -2
- package/src/components/phonenumber/MPhoneNumber.vue +20 -16
- package/src/components/popover/MPopover.spec.ts +126 -0
- package/src/components/popover/MPopover.vue +36 -1
- package/src/components/segmentedcontrol/MSegmentedControl.spec.ts +92 -0
- package/src/components/segmentedcontrol/MSegmentedControl.vue +61 -2
- package/src/components/sidebarexpandableitem/MSidebarExpandableItem.spec.ts +12 -0
- package/src/components/sidebarexpandableitem/MSidebarExpandableItem.vue +1 -0
- package/src/components/starrating/MStarRating.spec.ts +19 -22
- package/src/components/starrating/MStarRating.vue +3 -2
- package/src/components/steppercompact/MStepperCompact.spec.ts +9 -0
- package/src/components/steppercompact/MStepperCompact.vue +1 -1
- package/src/components/stepperinline/MStepperInline.spec.ts +11 -0
- package/src/components/stepperinline/MStepperInline.vue +1 -1
- package/src/components/stepperstacked/MStepperStacked.spec.ts +13 -0
- package/src/components/stepperstacked/MStepperStacked.vue +1 -0
- package/src/components/tabs/MTabs.vue +90 -4
- package/src/components/tabs/Mtabs.spec.ts +162 -0
- package/src/components/textinput/MTextInput.vue +2 -2
- package/src/components/toggle/MToggle.vue +1 -1
- package/src/main.ts +1 -0
- package/src/components/ComponentsMapping.mdx +0 -98
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
import { mount } from '@vue/test-utils';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
describe,
|
|
4
|
+
it,
|
|
5
|
+
expect,
|
|
6
|
+
vi,
|
|
7
|
+
beforeAll,
|
|
8
|
+
afterAll,
|
|
9
|
+
beforeEach,
|
|
10
|
+
} from 'vitest';
|
|
3
11
|
import MCarousel from './MCarousel.vue';
|
|
4
12
|
import MIconButton from '../iconbutton/MIconButton.vue';
|
|
5
13
|
import { ChevronLeft20, ChevronRight20 } from '@mozaic-ds/icons-vue';
|
|
6
14
|
|
|
7
15
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
8
16
|
|
|
17
|
+
const observerInstances: MockIntersectionObserver[] = [];
|
|
18
|
+
|
|
9
19
|
class MockIntersectionObserver {
|
|
10
20
|
callback: any;
|
|
11
21
|
options: any;
|
|
12
22
|
constructor(callback: any, options?: any) {
|
|
13
23
|
this.callback = callback;
|
|
14
24
|
this.options = options;
|
|
25
|
+
observerInstances.push(this);
|
|
15
26
|
}
|
|
16
27
|
observe = vi.fn();
|
|
17
28
|
unobserve = vi.fn();
|
|
@@ -21,6 +32,10 @@ class MockIntersectionObserver {
|
|
|
21
32
|
describe('MCarousel component', () => {
|
|
22
33
|
let originalObserver: any;
|
|
23
34
|
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
observerInstances.length = 0;
|
|
37
|
+
});
|
|
38
|
+
|
|
24
39
|
beforeAll(() => {
|
|
25
40
|
originalObserver = global.IntersectionObserver;
|
|
26
41
|
global.IntersectionObserver = MockIntersectionObserver as any;
|
|
@@ -132,6 +147,15 @@ describe('MCarousel component', () => {
|
|
|
132
147
|
const container = wrapper.find('.mc-carousel');
|
|
133
148
|
expect(container.attributes('role')).toBe('group');
|
|
134
149
|
expect(container.attributes('aria-roledescription')).toBe('carousel');
|
|
135
|
-
|
|
150
|
+
// aria-labelledby should point to the headings wrapper (dynamic id)
|
|
151
|
+
const labelledby = container.attributes('aria-labelledby');
|
|
152
|
+
expect(labelledby).toBeDefined();
|
|
153
|
+
expect(wrapper.find(`#${labelledby}`).exists()).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('slide container has aria-live="polite"', () => {
|
|
157
|
+
const wrapper = mountCarousel();
|
|
158
|
+
const content = wrapper.find('.mc-carousel__content');
|
|
159
|
+
expect(content.attributes('aria-live')).toBe('polite');
|
|
136
160
|
});
|
|
137
161
|
});
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
class="mc-carousel"
|
|
4
4
|
role="group"
|
|
5
5
|
aria-roledescription="carousel"
|
|
6
|
-
aria-labelledby="
|
|
6
|
+
:aria-labelledby="titleId"
|
|
7
7
|
>
|
|
8
8
|
<div class="mc-carousel__header">
|
|
9
|
-
<div class="mc-carousel__headings">
|
|
9
|
+
<div :id="titleId" class="mc-carousel__headings">
|
|
10
10
|
<slot name="header" />
|
|
11
11
|
</div>
|
|
12
12
|
<div class="mc-carousel__controls">
|
|
@@ -34,7 +34,12 @@
|
|
|
34
34
|
</MIconButton>
|
|
35
35
|
</div>
|
|
36
36
|
</div>
|
|
37
|
-
<div
|
|
37
|
+
<div
|
|
38
|
+
class="mc-carousel__content"
|
|
39
|
+
ref="contentContainer"
|
|
40
|
+
aria-live="polite"
|
|
41
|
+
aria-atomic="false"
|
|
42
|
+
>
|
|
38
43
|
<template
|
|
39
44
|
v-for="(child, index) in $slots.default?.()"
|
|
40
45
|
:key="`carousel-slide-${index}`"
|
|
@@ -46,7 +51,7 @@
|
|
|
46
51
|
</template>
|
|
47
52
|
|
|
48
53
|
<script setup lang="ts">
|
|
49
|
-
import { computed, onMounted, ref, type VNode } from 'vue';
|
|
54
|
+
import { computed, onMounted, ref, useId, type VNode } from 'vue';
|
|
50
55
|
import MIconButton from '../iconbutton/MIconButton.vue';
|
|
51
56
|
import { ChevronLeft20, ChevronRight20 } from '@mozaic-ds/icons-vue';
|
|
52
57
|
/**
|
|
@@ -80,6 +85,7 @@ defineSlots<{
|
|
|
80
85
|
header: VNode;
|
|
81
86
|
}>();
|
|
82
87
|
|
|
88
|
+
const titleId = useId();
|
|
83
89
|
const activeIndex = ref<number>(0);
|
|
84
90
|
const contentContainer = ref<HTMLElement | null>(null);
|
|
85
91
|
|
|
@@ -6,7 +6,7 @@ import type { MenuItem } from '../builtinmenu/MBuiltInMenu.vue';
|
|
|
6
6
|
|
|
7
7
|
const StubMBuiltInMenu = {
|
|
8
8
|
name: 'MBuiltInMenu',
|
|
9
|
-
props: ['modelValue', 'items', 'outlined'],
|
|
9
|
+
props: ['modelValue', 'items', 'outlined', 'label'],
|
|
10
10
|
emits: ['update:modelValue'],
|
|
11
11
|
template: '<div />',
|
|
12
12
|
};
|
|
@@ -60,6 +60,17 @@ describe('MCheckListMenu', () => {
|
|
|
60
60
|
expect(builtIn.props('outlined')).toBe(true);
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
it('forwards label prop to MBuiltInMenu', () => {
|
|
64
|
+
const items = [{ label: 'A', checked: false }];
|
|
65
|
+
const wrapper = mount(MCheckListMenu, {
|
|
66
|
+
props: { items, label: 'Checklist navigation' },
|
|
67
|
+
global: { components: { MBuiltInMenu: StubMBuiltInMenu } },
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const builtIn = wrapper.findComponent(StubMBuiltInMenu);
|
|
71
|
+
expect(builtIn.props('label')).toBe('Checklist navigation');
|
|
72
|
+
});
|
|
73
|
+
|
|
63
74
|
it('emits update:modelValue when inner menu updates modelValue', async () => {
|
|
64
75
|
const items = [{ label: 'X', checked: false }];
|
|
65
76
|
const wrapper = mount(MCheckListMenu, {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
v-model="currentMenuItem"
|
|
4
4
|
:items="menuItems"
|
|
5
5
|
:outlined="props.outlined"
|
|
6
|
+
:label="props.label"
|
|
6
7
|
/>
|
|
7
8
|
</template>
|
|
8
9
|
|
|
@@ -31,6 +32,11 @@ const props = defineProps<{
|
|
|
31
32
|
* When enabled, adds a visible border around the wrapper to highlight or separate its content.
|
|
32
33
|
*/
|
|
33
34
|
outlined?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Accessible label for the navigation landmark. Should describe the purpose
|
|
37
|
+
* of this menu to distinguish it from other navigations on the page.
|
|
38
|
+
*/
|
|
39
|
+
label?: string;
|
|
34
40
|
}>();
|
|
35
41
|
|
|
36
42
|
const emit = defineEmits<{
|
|
@@ -10,6 +10,8 @@ A check-list menu is a structured vertical list where each item represents a dis
|
|
|
10
10
|
| `modelValue` | Specifies the key of the currently selected menu item. It allows the component to highlight or style the corresponding item to indicate it is selected or currently in use. | `number` | - |
|
|
11
11
|
| `items*` | Defines the menu items, each of which sets a checked state and act as a button, link, or router-link. | `CheckListMenuItem[]` | - |
|
|
12
12
|
| `outlined` | When enabled, adds a visible border around the wrapper to highlight or separate its content. | `boolean` | - |
|
|
13
|
+
| `label` | Accessible label for the navigation landmark. Should describe the purpose
|
|
14
|
+
of this menu to distinguish it from other navigations on the page. | `string` | - |
|
|
13
15
|
|
|
14
16
|
## Events
|
|
15
17
|
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
<MOptionListbox
|
|
66
66
|
ref="listbox"
|
|
67
67
|
v-model="selection"
|
|
68
|
+
@update:model-value="onSelectionUpdate"
|
|
68
69
|
:id
|
|
69
70
|
:open="isOpen"
|
|
70
71
|
:multiple
|
|
@@ -267,6 +268,12 @@ function close() {
|
|
|
267
268
|
document.removeEventListener('click', handleClickOutside);
|
|
268
269
|
}
|
|
269
270
|
|
|
271
|
+
function onSelectionUpdate() {
|
|
272
|
+
if (!props.multiple) {
|
|
273
|
+
close();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
270
277
|
function toggleListbox() {
|
|
271
278
|
return isOpen.value ? close() : open();
|
|
272
279
|
}
|
|
@@ -36,7 +36,8 @@ Then to use the component, you can proceed as follows:
|
|
|
36
36
|
dark
|
|
37
37
|
code={`
|
|
38
38
|
<script setup>
|
|
39
|
-
import
|
|
39
|
+
import '@mozaic-ds/datatable-vue/style.css';
|
|
40
|
+
import { MDataTable, MDataTableColumn } from '@mozaic-ds/datatable-vue';
|
|
40
41
|
</script>
|
|
41
42
|
|
|
42
43
|
<template>
|
|
@@ -59,4 +60,4 @@ Then to use the component, you can proceed as follows:
|
|
|
59
60
|
</template>
|
|
60
61
|
`} />
|
|
61
62
|
|
|
62
|
-
To see more examples of usage, please refer to the
|
|
63
|
+
To see more examples of usage, please refer to the other stories of the component.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
2
|
import { mount } from '@vue/test-utils';
|
|
3
3
|
import MDrawer from '@/components/drawer/MDrawer.vue';
|
|
4
4
|
|
|
@@ -11,7 +11,7 @@ const stubs = {
|
|
|
11
11
|
},
|
|
12
12
|
MOverlay: {
|
|
13
13
|
name: 'MOverlay',
|
|
14
|
-
template: `<div class="overlay" @click="$emit('click')"><slot/></div>`,
|
|
14
|
+
template: `<div class="overlay" @click="$emit('click', $event)"><slot/></div>`,
|
|
15
15
|
},
|
|
16
16
|
};
|
|
17
17
|
|
|
@@ -211,11 +211,39 @@ describe('MDrawer component', () => {
|
|
|
211
211
|
global: { stubs },
|
|
212
212
|
});
|
|
213
213
|
|
|
214
|
-
await wrapper
|
|
214
|
+
await wrapper
|
|
215
|
+
.find('section.mc-drawer')
|
|
216
|
+
.trigger('keydown', { key: 'Escape' });
|
|
215
217
|
expect(wrapper.emitted('update:open')).toBeTruthy();
|
|
216
218
|
expect(wrapper.emitted('update:open')!.at(-1)).toEqual([false]);
|
|
217
219
|
});
|
|
218
220
|
|
|
221
|
+
it('stops Escape propagation after closing', async () => {
|
|
222
|
+
const onDocumentKeydown = vi.fn();
|
|
223
|
+
document.addEventListener('keydown', onDocumentKeydown);
|
|
224
|
+
|
|
225
|
+
const wrapper = mount(MDrawer, {
|
|
226
|
+
props: {
|
|
227
|
+
open: true,
|
|
228
|
+
title: 'Test Title',
|
|
229
|
+
},
|
|
230
|
+
attachTo: document.body,
|
|
231
|
+
global: { stubs },
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
wrapper
|
|
235
|
+
.find('section.mc-drawer')
|
|
236
|
+
.element.dispatchEvent(
|
|
237
|
+
new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }),
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
expect(wrapper.emitted('update:open')!.at(-1)).toEqual([false]);
|
|
241
|
+
expect(onDocumentKeydown).not.toHaveBeenCalled();
|
|
242
|
+
|
|
243
|
+
document.removeEventListener('keydown', onDocumentKeydown);
|
|
244
|
+
wrapper.unmount();
|
|
245
|
+
});
|
|
246
|
+
|
|
219
247
|
it('locks and unlocks scroll when scroll=false and open changes', async () => {
|
|
220
248
|
const wrapper = mount(MDrawer, {
|
|
221
249
|
props: {
|
|
@@ -252,6 +280,77 @@ describe('MDrawer component', () => {
|
|
|
252
280
|
expect(document.body.style.overflow).toBe('');
|
|
253
281
|
});
|
|
254
282
|
|
|
283
|
+
it('sets inert on section when closed', async () => {
|
|
284
|
+
const wrapper = mount(MDrawer, {
|
|
285
|
+
props: { open: false, title: 'Test' },
|
|
286
|
+
global: { stubs },
|
|
287
|
+
});
|
|
288
|
+
// JSDOM renders :inert="true" as "true" — not.toBeUndefined() is the correct check
|
|
289
|
+
expect(
|
|
290
|
+
wrapper.find('section.mc-drawer').attributes('inert'),
|
|
291
|
+
).not.toBeUndefined();
|
|
292
|
+
|
|
293
|
+
await wrapper.setProps({ open: true });
|
|
294
|
+
expect(
|
|
295
|
+
wrapper.find('section.mc-drawer').attributes('inert'),
|
|
296
|
+
).toBeUndefined();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('does not render aria-modal when closed', async () => {
|
|
300
|
+
const wrapper = mount(MDrawer, {
|
|
301
|
+
props: { open: false, title: 'Test' },
|
|
302
|
+
global: { stubs },
|
|
303
|
+
});
|
|
304
|
+
expect(
|
|
305
|
+
wrapper.find('section.mc-drawer').attributes('aria-modal'),
|
|
306
|
+
).toBeUndefined();
|
|
307
|
+
|
|
308
|
+
await wrapper.setProps({ open: true });
|
|
309
|
+
expect(wrapper.find('section.mc-drawer').attributes('aria-modal')).toBe(
|
|
310
|
+
'true',
|
|
311
|
+
);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('traps focus forward: Tab from last focusable wraps to first', async () => {
|
|
315
|
+
const wrapper = mount(MDrawer, {
|
|
316
|
+
props: { open: true, title: 'Test', back: true },
|
|
317
|
+
attachTo: document.body,
|
|
318
|
+
global: { stubs },
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const buttons = wrapper.findAll('button');
|
|
322
|
+
const lastEl = buttons[buttons.length - 1].element as HTMLElement;
|
|
323
|
+
lastEl.focus();
|
|
324
|
+
|
|
325
|
+
await wrapper
|
|
326
|
+
.find('section.mc-drawer')
|
|
327
|
+
.trigger('keydown', { key: 'Tab', shiftKey: false });
|
|
328
|
+
|
|
329
|
+
// focus should wrap to the first focusable button (back button)
|
|
330
|
+
expect(document.activeElement).toBe(buttons[0].element);
|
|
331
|
+
wrapper.unmount();
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('traps focus backward: Shift+Tab from first focusable wraps to last', async () => {
|
|
335
|
+
const wrapper = mount(MDrawer, {
|
|
336
|
+
props: { open: true, title: 'Test', back: true },
|
|
337
|
+
attachTo: document.body,
|
|
338
|
+
global: { stubs },
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const buttons = wrapper.findAll('button');
|
|
342
|
+
const firstEl = buttons[0].element as HTMLElement;
|
|
343
|
+
firstEl.focus();
|
|
344
|
+
|
|
345
|
+
await wrapper
|
|
346
|
+
.find('section.mc-drawer')
|
|
347
|
+
.trigger('keydown', { key: 'Tab', shiftKey: true });
|
|
348
|
+
|
|
349
|
+
// focus should wrap to the last focusable button (close button)
|
|
350
|
+
expect(document.activeElement).toBe(buttons[buttons.length - 1].element);
|
|
351
|
+
wrapper.unmount();
|
|
352
|
+
});
|
|
353
|
+
|
|
255
354
|
it('emits update:open on mount reflecting initial state', () => {
|
|
256
355
|
const wrapper = mount(MDrawer, {
|
|
257
356
|
props: {
|
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<MOverlay
|
|
3
3
|
:is-visible="open"
|
|
4
|
-
dialogLabel="drawerTitle"
|
|
4
|
+
:dialogLabel="`drawerTitle-${id}`"
|
|
5
5
|
@click="onClickOverlay"
|
|
6
6
|
>
|
|
7
7
|
<section
|
|
8
|
+
ref="sectionRef"
|
|
8
9
|
class="mc-drawer"
|
|
9
10
|
:class="classObject"
|
|
10
11
|
role="dialog"
|
|
11
|
-
aria-labelledby="drawerTitle"
|
|
12
|
-
:aria-modal="open ? 'true' : 'false'"
|
|
12
|
+
:aria-labelledby="`drawerTitle-${id}`"
|
|
13
13
|
tabindex="-1"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
v-bind="{
|
|
15
|
+
...$attrs,
|
|
16
|
+
...(open
|
|
17
|
+
? {
|
|
18
|
+
onKeydown: handleKeydown,
|
|
19
|
+
'aria-modal': 'true',
|
|
20
|
+
}
|
|
21
|
+
: {
|
|
22
|
+
inert: 'true',
|
|
23
|
+
}),
|
|
24
|
+
}"
|
|
18
25
|
>
|
|
19
26
|
<div class="mc-drawer__dialog" role="document">
|
|
20
27
|
<div class="mc-drawer__header">
|
|
@@ -26,13 +33,13 @@
|
|
|
26
33
|
@click="emit('back')"
|
|
27
34
|
>
|
|
28
35
|
<template #icon>
|
|
29
|
-
<ArrowBack24
|
|
36
|
+
<ArrowBack24 />
|
|
30
37
|
</template>
|
|
31
38
|
</MIconButton>
|
|
32
39
|
<h2
|
|
33
40
|
class="mc-drawer__title"
|
|
34
41
|
tabindex="-1"
|
|
35
|
-
id="drawerTitle"
|
|
42
|
+
:id="`drawerTitle-${id}`"
|
|
36
43
|
ref="titleRef"
|
|
37
44
|
>
|
|
38
45
|
{{ title }}
|
|
@@ -44,12 +51,12 @@
|
|
|
44
51
|
@click="onClose"
|
|
45
52
|
>
|
|
46
53
|
<template #icon>
|
|
47
|
-
<Cross24
|
|
54
|
+
<Cross24 />
|
|
48
55
|
</template>
|
|
49
56
|
</MIconButton>
|
|
50
57
|
</div>
|
|
51
58
|
<div class="mc-drawer__body">
|
|
52
|
-
<div class="mc-drawer__content"
|
|
59
|
+
<div class="mc-drawer__content">
|
|
53
60
|
<h2 v-if="contentTitle" class="mc-drawer__content__title">
|
|
54
61
|
{{ contentTitle }}
|
|
55
62
|
</h2>
|
|
@@ -65,7 +72,15 @@
|
|
|
65
72
|
</template>
|
|
66
73
|
|
|
67
74
|
<script setup lang="ts">
|
|
68
|
-
import {
|
|
75
|
+
import {
|
|
76
|
+
computed,
|
|
77
|
+
watch,
|
|
78
|
+
type VNode,
|
|
79
|
+
ref,
|
|
80
|
+
onMounted,
|
|
81
|
+
onUnmounted,
|
|
82
|
+
useId,
|
|
83
|
+
} from 'vue';
|
|
69
84
|
import { ArrowBack24, Cross24 } from '@mozaic-ds/icons-vue';
|
|
70
85
|
import MIconButton from '../iconbutton/MIconButton.vue';
|
|
71
86
|
import MOverlay from '../overlay/MOverlay.vue';
|
|
@@ -123,6 +138,8 @@ defineSlots<{
|
|
|
123
138
|
footer?: VNode;
|
|
124
139
|
}>();
|
|
125
140
|
|
|
141
|
+
const id = useId();
|
|
142
|
+
|
|
126
143
|
const classObject = computed(() => {
|
|
127
144
|
return {
|
|
128
145
|
'is-open': props.open,
|
|
@@ -133,6 +150,45 @@ const classObject = computed(() => {
|
|
|
133
150
|
});
|
|
134
151
|
|
|
135
152
|
const titleRef = ref<HTMLElement | null>(null);
|
|
153
|
+
const sectionRef = ref<HTMLElement | null>(null);
|
|
154
|
+
|
|
155
|
+
function getFocusableElements(): HTMLElement[] {
|
|
156
|
+
if (!sectionRef.value) return [];
|
|
157
|
+
return Array.from(
|
|
158
|
+
sectionRef.value.querySelectorAll<HTMLElement>(
|
|
159
|
+
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',
|
|
160
|
+
),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
165
|
+
if (e.key === 'Escape') {
|
|
166
|
+
e.preventDefault();
|
|
167
|
+
e.stopPropagation();
|
|
168
|
+
onClose();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (e.key === 'Tab') {
|
|
172
|
+
const focusable = getFocusableElements();
|
|
173
|
+
if (!focusable.length) {
|
|
174
|
+
e.preventDefault();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const first = focusable[0];
|
|
178
|
+
const last = focusable[focusable.length - 1];
|
|
179
|
+
if (e.shiftKey) {
|
|
180
|
+
if (document.activeElement === first) {
|
|
181
|
+
e.preventDefault();
|
|
182
|
+
last.focus();
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
if (document.activeElement === last) {
|
|
186
|
+
e.preventDefault();
|
|
187
|
+
first.focus();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
136
192
|
const isClient =
|
|
137
193
|
typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
138
194
|
|
|
@@ -169,8 +225,11 @@ onUnmounted(() => {
|
|
|
169
225
|
unlockScroll();
|
|
170
226
|
});
|
|
171
227
|
|
|
172
|
-
const onClickOverlay = () => {
|
|
173
|
-
if (
|
|
228
|
+
const onClickOverlay = (event: MouseEvent) => {
|
|
229
|
+
if (
|
|
230
|
+
props.closeOnOverlay &&
|
|
231
|
+
!sectionRef.value?.contains(event.target as Node)
|
|
232
|
+
) {
|
|
174
233
|
onClose();
|
|
175
234
|
}
|
|
176
235
|
};
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
<input
|
|
10
10
|
ref="fileInput"
|
|
11
11
|
type="file"
|
|
12
|
-
aria-
|
|
12
|
+
aria-hidden="true"
|
|
13
|
+
tabindex="-1"
|
|
13
14
|
:accept="props.accept"
|
|
14
15
|
:multiple="props.multiple"
|
|
15
16
|
class="mc-file-uploader__hidden-input"
|
|
16
17
|
:disabled="props.disabled"
|
|
17
|
-
:aria-disabled="props.disabled"
|
|
18
18
|
@change="onChange"
|
|
19
19
|
/>
|
|
20
20
|
|
|
@@ -40,13 +40,7 @@
|
|
|
40
40
|
<template v-if="isStacked">
|
|
41
41
|
<MDivider />
|
|
42
42
|
<div class="mc-file-uploader-item__actions-container">
|
|
43
|
-
<MButton
|
|
44
|
-
ghost
|
|
45
|
-
size="s"
|
|
46
|
-
icon-position="left"
|
|
47
|
-
aria-label="Delete file"
|
|
48
|
-
@click="emit('delete')"
|
|
49
|
-
>
|
|
43
|
+
<MButton ghost size="s" icon-position="left" @click="emit('delete')">
|
|
50
44
|
{{ props.deleteButtonLabel }}
|
|
51
45
|
|
|
52
46
|
<template #icon>
|
|
@@ -78,6 +72,7 @@
|
|
|
78
72
|
<span
|
|
79
73
|
v-if="!valid && (props.errorMessage || slots.errorMessage)"
|
|
80
74
|
class="mc-file-uploader-item__error-message"
|
|
75
|
+
role="alert"
|
|
81
76
|
>
|
|
82
77
|
<slot name="errorMessage">
|
|
83
78
|
{{ props.errorMessage }}
|
|
@@ -92,6 +92,21 @@ describe('MButton component', () => {
|
|
|
92
92
|
expect(button.attributes('type')).toBe('button');
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
+
it('sets aria-busy when isLoading is true', () => {
|
|
96
|
+
const wrapper = mount(MIconButton, {
|
|
97
|
+
props: { isLoading: true },
|
|
98
|
+
slots: { icon: [ChevronRight24] },
|
|
99
|
+
});
|
|
100
|
+
expect(wrapper.find('button').attributes('aria-busy')).toBe('true');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('does not set aria-busy when not loading', () => {
|
|
104
|
+
const wrapper = mount(MIconButton, {
|
|
105
|
+
slots: { icon: [ChevronRight24] },
|
|
106
|
+
});
|
|
107
|
+
expect(wrapper.find('button').attributes('aria-busy')).toBeUndefined();
|
|
108
|
+
});
|
|
109
|
+
|
|
95
110
|
it('can have type="submit" when the type prop is "submit"', () => {
|
|
96
111
|
const wrapper = mount(MIconButton, {
|
|
97
112
|
props: {
|
|
@@ -67,5 +67,18 @@ describe('MKpiItem component', () => {
|
|
|
67
67
|
|
|
68
68
|
expect(wrapper.find('.mc-kpi__icon').exists()).toBe(true);
|
|
69
69
|
});
|
|
70
|
+
|
|
71
|
+
it.each([
|
|
72
|
+
['increasing'],
|
|
73
|
+
['decreasing'],
|
|
74
|
+
['stable'],
|
|
75
|
+
] as const)('gives the trend icon an accessible label for "%s"', (trend) => {
|
|
76
|
+
const wrapper = mount(KpiItem, {
|
|
77
|
+
props: { value: '123', trend },
|
|
78
|
+
});
|
|
79
|
+
const icon = wrapper.find('.mc-kpi__icon');
|
|
80
|
+
expect(icon.attributes('role')).toBe('img');
|
|
81
|
+
expect(icon.attributes('aria-label')).toBe(trend);
|
|
82
|
+
});
|
|
70
83
|
});
|
|
71
84
|
});
|