@mozaic-ds/vue 2.13.0 → 2.14.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 +1088 -378
- package/dist/mozaic-vue.js +2662 -1854
- 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 +4 -4
- package/src/components/actionlistbox/MActionListbox.spec.ts +53 -59
- package/src/components/actionlistbox/MActionListbox.stories.ts +22 -1
- package/src/components/actionlistbox/MActionListbox.vue +91 -28
- package/src/components/actionlistbox/README.md +15 -0
- package/src/components/breadcrumb/MBreadcrumb.vue +5 -0
- package/src/components/button/README.md +4 -0
- package/src/components/checkbox/README.md +2 -0
- package/src/components/divider/README.md +4 -0
- package/src/components/iconbutton/MIconButton.stories.ts +12 -0
- package/src/components/iconbutton/MIconButton.vue +13 -1
- package/src/components/iconbutton/README.md +27 -0
- package/src/components/loader/README.md +2 -0
- package/src/components/navigationindicator/MNavigationIndicator.spec.ts +152 -0
- package/src/components/navigationindicator/MNavigationIndicator.stories.ts +41 -0
- package/src/components/navigationindicator/MNavigationIndicator.vue +132 -0
- package/src/components/navigationindicator/README.md +37 -0
- package/src/components/pageheader/MPageHeader.spec.ts +142 -0
- package/src/components/pageheader/MPageHeader.stories.ts +125 -0
- package/src/components/pageheader/MPageHeader.vue +133 -0
- package/src/components/pageheader/README.md +46 -0
- package/src/components/popover/MPopover.spec.ts +106 -0
- package/src/components/popover/MPopover.stories.ts +126 -0
- package/src/components/popover/MPopover.vue +131 -0
- package/src/components/popover/README.md +42 -0
- package/src/components/radio/README.md +2 -0
- package/src/components/select/MSelect.spec.ts +2 -1
- package/src/components/select/MSelect.vue +30 -25
- package/src/components/sidebar/MSidebar.const.ts +6 -0
- package/src/components/sidebar/MSidebar.spec.ts +110 -0
- package/src/components/sidebar/MSidebar.stories.ts +108 -0
- package/src/components/sidebar/MSidebar.vue +124 -0
- package/src/components/sidebar/README.md +59 -0
- package/src/components/sidebar/stories/DefaultCase.stories.vue +120 -0
- package/src/components/sidebar/stories/README.md +27 -0
- package/src/components/sidebar/stories/WithExpandOnly.stories.vue +112 -0
- package/src/components/sidebar/stories/WithProfileInfoOnly.stories.vue +119 -0
- package/src/components/sidebar/stories/WithSingleLevel.stories.vue +98 -0
- package/src/components/sidebar/use-floating-item.composable.ts +135 -0
- package/src/components/sidebar/use-floating-item.spec.ts +251 -0
- package/src/components/sidebarexpandableitem/MSidebarExpandableItem.spec.ts +151 -0
- package/src/components/sidebarexpandableitem/MSidebarExpandableItem.vue +113 -0
- package/src/components/sidebarexpandableitem/README.md +36 -0
- package/src/components/sidebarfooter/MSidebarFooter.spec.ts +276 -0
- package/src/components/sidebarfooter/MSidebarFooter.vue +201 -0
- package/src/components/sidebarfooter/README.md +52 -0
- package/src/components/sidebarfooter/_MSidebarFooterMenu.vue +64 -0
- package/src/components/sidebarheader/MSidebarHeader.vue +36 -0
- package/src/components/sidebarheader/README.md +31 -0
- package/src/components/sidebarnavitem/MSidebarNavItem.spec.ts +127 -0
- package/src/components/sidebarnavitem/MSidebarNavItem.vue +113 -0
- package/src/components/sidebarnavitem/README.md +56 -0
- package/src/components/sidebarshortcutitem/MSidebarShortcutItem.spec.ts +59 -0
- package/src/components/sidebarshortcutitem/MSidebarShortcutItem.vue +52 -0
- package/src/components/sidebarshortcutitem/README.md +32 -0
- package/src/components/sidebarshortcuts/MSidebarShortcuts.spec.ts +87 -0
- package/src/components/sidebarshortcuts/MSidebarShortcuts.vue +101 -0
- package/src/components/sidebarshortcuts/README.md +36 -0
- package/src/components/statusbadge/README.md +12 -0
- package/src/components/textinput/MTextInput.stories.ts +13 -1
- package/src/components/textinput/MTextInput.vue +12 -0
- package/src/components/textinput/README.md +3 -1
- package/src/components/tile/MTile.spec.ts +61 -0
- package/src/components/tile/MTile.stories.ts +102 -0
- package/src/components/tile/MTile.vue +68 -0
- package/src/components/tile/README.md +19 -0
- package/src/components/tileclickable/MTileClickable.spec.ts +130 -0
- package/src/components/tileclickable/MTileClickable.stories.ts +60 -0
- package/src/components/tileclickable/MTileClickable.vue +106 -0
- package/src/components/tileclickable/README.md +30 -0
- package/src/components/tileexpandable/MTileExpandable.spec.ts +121 -0
- package/src/components/tileexpandable/MTileExpandable.stories.ts +50 -0
- package/src/components/tileexpandable/MTileExpandable.vue +131 -0
- package/src/components/tileexpandable/README.md +36 -0
- package/src/components/tileselectable/MTileSelectable.spec.ts +177 -0
- package/src/components/tileselectable/MTileSelectable.stories.ts +55 -0
- package/src/components/tileselectable/MTileSelectable.vue +142 -0
- package/src/components/tileselectable/README.md +44 -0
- package/src/components/toaster/README.md +1 -1
- package/src/components/tooltip/MTooltip.vue +5 -0
- package/src/components/tooltip/README.md +16 -1
- package/src/main.ts +12 -2
- package/src/utils/use-is-mobile.composable.ts +20 -0
- package/src/utils/use-is-mobile.spec.ts +70 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<MSidebar
|
|
3
|
+
v-model="expanded"
|
|
4
|
+
@update:model-value="emit('update:modelValue')"
|
|
5
|
+
@close="emit('close')"
|
|
6
|
+
>
|
|
7
|
+
<template #header>
|
|
8
|
+
<MSidebarHeader title="Adeo Design System" logo="/logo.svg" />
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<template #shortcuts>
|
|
12
|
+
<MSidebarShortcuts>
|
|
13
|
+
<MSidebarShortcutItem
|
|
14
|
+
v-for="(item, index) in shortcutItems"
|
|
15
|
+
:key="index"
|
|
16
|
+
v-bind="item"
|
|
17
|
+
/>
|
|
18
|
+
</MSidebarShortcuts>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<template #nav>
|
|
22
|
+
<template v-for="(item, index) in navigationItems" :key="index">
|
|
23
|
+
<MSidebarExpandableItem
|
|
24
|
+
v-if="item.items"
|
|
25
|
+
menu-label="Sublist menu label"
|
|
26
|
+
:label="item.label"
|
|
27
|
+
:icon="item.icon"
|
|
28
|
+
>
|
|
29
|
+
<MSidebarNavItem
|
|
30
|
+
v-for="(subItem, subIndex) in item.items"
|
|
31
|
+
:key="subIndex"
|
|
32
|
+
v-bind="subItem"
|
|
33
|
+
:active="active === `subNav-${subIndex}`"
|
|
34
|
+
@click="active = `subNav-${subIndex}`"
|
|
35
|
+
/>
|
|
36
|
+
</MSidebarExpandableItem>
|
|
37
|
+
|
|
38
|
+
<MSidebarNavItem
|
|
39
|
+
v-else
|
|
40
|
+
v-bind="item"
|
|
41
|
+
:active="active === `nav-${index}`"
|
|
42
|
+
@click="active = `nav-${index}`"
|
|
43
|
+
/>
|
|
44
|
+
</template>
|
|
45
|
+
</template>
|
|
46
|
+
|
|
47
|
+
<template #footer>
|
|
48
|
+
<MSidebarFooter
|
|
49
|
+
hide-button
|
|
50
|
+
title="Dieter Rams"
|
|
51
|
+
subtitle="Industrial designer"
|
|
52
|
+
href="#"
|
|
53
|
+
avatar="/images/Avatar.png"
|
|
54
|
+
@log-out="emit('log-out')"
|
|
55
|
+
>
|
|
56
|
+
<MSidebarNavItem
|
|
57
|
+
v-for="(item, index) in footerMenuItems"
|
|
58
|
+
:key="index"
|
|
59
|
+
v-bind="item"
|
|
60
|
+
:active="active === `footer-${index}`"
|
|
61
|
+
@click="active = `footer-${index}`"
|
|
62
|
+
/>
|
|
63
|
+
</MSidebarFooter>
|
|
64
|
+
</template>
|
|
65
|
+
</MSidebar>
|
|
66
|
+
</template>
|
|
67
|
+
|
|
68
|
+
<script setup lang="ts">
|
|
69
|
+
import { ref } from 'vue';
|
|
70
|
+
import {
|
|
71
|
+
Coffee24,
|
|
72
|
+
Course24,
|
|
73
|
+
Sample24,
|
|
74
|
+
Release24,
|
|
75
|
+
Palette24,
|
|
76
|
+
Pantone24,
|
|
77
|
+
Admin24,
|
|
78
|
+
} from '@mozaic-ds/icons-vue';
|
|
79
|
+
|
|
80
|
+
import MSidebar from '../MSidebar.vue';
|
|
81
|
+
import MSidebarExpandableItem from '../../sidebarexpandableitem/MSidebarExpandableItem.vue';
|
|
82
|
+
import MSidebarFooter from '../../sidebarfooter/MSidebarFooter.vue';
|
|
83
|
+
import MSidebarHeader from '../../sidebarheader/MSidebarHeader.vue';
|
|
84
|
+
import MSidebarNavItem from '../../sidebarnavitem/MSidebarNavItem.vue';
|
|
85
|
+
import MSidebarShortcutItem from '../../sidebarshortcutitem/MSidebarShortcutItem.vue';
|
|
86
|
+
import MSidebarShortcuts from '../../sidebarshortcuts/MSidebarShortcuts.vue';
|
|
87
|
+
|
|
88
|
+
const emit = defineEmits(['close', 'update:modelValue', 'log-out']);
|
|
89
|
+
|
|
90
|
+
const expanded = ref(true);
|
|
91
|
+
|
|
92
|
+
const active = ref('');
|
|
93
|
+
|
|
94
|
+
const shortcutItems = [
|
|
95
|
+
{ label: 'Shortcut 01', icon: Coffee24, href: '#' },
|
|
96
|
+
{ label: 'Shortcut 02', icon: Course24, href: '#' },
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
const navigationItems = [
|
|
100
|
+
{ label: 'Get Started', href: '#', icon: Release24 },
|
|
101
|
+
{
|
|
102
|
+
label: 'Design Tokens',
|
|
103
|
+
icon: Sample24,
|
|
104
|
+
items: [
|
|
105
|
+
{ label: 'Subsection menu label 1', href: '#', locked: true },
|
|
106
|
+
{ label: 'Subsection menu label 2', href: '#', external: true },
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
{ label: 'Styles', href: '#', icon: Palette24 },
|
|
110
|
+
{ label: 'Components', href: '#', icon: Pantone24 },
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
const footerMenuItems = [
|
|
114
|
+
{ label: 'Action 01', icon: Admin24, href: '#' },
|
|
115
|
+
{ label: 'Action 02', icon: Admin24, href: '#' },
|
|
116
|
+
{ label: 'Action 03', icon: Admin24, href: '#' },
|
|
117
|
+
{ label: 'Action 04', icon: Admin24, href: '#' },
|
|
118
|
+
];
|
|
119
|
+
</script>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<MSidebar
|
|
3
|
+
v-model="expanded"
|
|
4
|
+
@update:model-value="emit('update:modelValue')"
|
|
5
|
+
@close="emit('close')"
|
|
6
|
+
>
|
|
7
|
+
<template #header>
|
|
8
|
+
<MSidebarHeader title="Adeo Design System" logo="/logo.svg" />
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<template #shortcuts>
|
|
12
|
+
<MSidebarShortcuts>
|
|
13
|
+
<MSidebarShortcutItem
|
|
14
|
+
v-for="(item, index) in shortcutItems"
|
|
15
|
+
:key="index"
|
|
16
|
+
v-bind="item"
|
|
17
|
+
/>
|
|
18
|
+
</MSidebarShortcuts>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<template #nav>
|
|
22
|
+
<template v-for="(item, index) in navigationItems" :key="index">
|
|
23
|
+
<MSidebarNavItem
|
|
24
|
+
v-bind="item"
|
|
25
|
+
:active="active === `nav-${index}`"
|
|
26
|
+
@click="active = `nav-${index}`"
|
|
27
|
+
/>
|
|
28
|
+
</template>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<template #footer>
|
|
32
|
+
<MSidebarFooter
|
|
33
|
+
title="Dieter Rams"
|
|
34
|
+
subtitle="Industrial designer"
|
|
35
|
+
href="#"
|
|
36
|
+
img-src="/images/Avatar.png"
|
|
37
|
+
@log-out="emit('log-out')"
|
|
38
|
+
>
|
|
39
|
+
<MSidebarNavItem
|
|
40
|
+
v-for="(item, index) in footerMenuItems"
|
|
41
|
+
:key="index"
|
|
42
|
+
v-bind="item"
|
|
43
|
+
:active="active === `footer-${index}`"
|
|
44
|
+
@click="active = `footer-${index}`"
|
|
45
|
+
/>
|
|
46
|
+
</MSidebarFooter>
|
|
47
|
+
</template>
|
|
48
|
+
</MSidebar>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<script setup lang="ts">
|
|
52
|
+
import { ref } from 'vue';
|
|
53
|
+
import {
|
|
54
|
+
Coffee24,
|
|
55
|
+
Course24,
|
|
56
|
+
Sample24,
|
|
57
|
+
Release24,
|
|
58
|
+
Palette24,
|
|
59
|
+
Pantone24,
|
|
60
|
+
Admin24,
|
|
61
|
+
} from '@mozaic-ds/icons-vue';
|
|
62
|
+
|
|
63
|
+
import MSidebar from '../MSidebar.vue';
|
|
64
|
+
import MSidebarFooter from '../../sidebarfooter/MSidebarFooter.vue';
|
|
65
|
+
import MSidebarHeader from '../../sidebarheader/MSidebarHeader.vue';
|
|
66
|
+
import MSidebarNavItem from '../../sidebarnavitem/MSidebarNavItem.vue';
|
|
67
|
+
import MSidebarShortcutItem from '../../sidebarshortcutitem/MSidebarShortcutItem.vue';
|
|
68
|
+
import MSidebarShortcuts from '../../sidebarshortcuts/MSidebarShortcuts.vue';
|
|
69
|
+
|
|
70
|
+
const emit = defineEmits(['close', 'update:modelValue', 'log-out']);
|
|
71
|
+
|
|
72
|
+
const expanded = ref(true);
|
|
73
|
+
|
|
74
|
+
const active = ref('');
|
|
75
|
+
|
|
76
|
+
const shortcutItems = [
|
|
77
|
+
{ label: 'Shortcut 01', icon: Coffee24, href: '#' },
|
|
78
|
+
{ label: 'Shortcut 02', icon: Course24, href: '#' },
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
const navigationItems = [
|
|
82
|
+
{ label: 'Get Started', href: '#', icon: Release24 },
|
|
83
|
+
{
|
|
84
|
+
label: 'Design Tokens',
|
|
85
|
+
icon: Sample24,
|
|
86
|
+
href: '#',
|
|
87
|
+
},
|
|
88
|
+
{ label: 'Styles', href: '#', icon: Palette24 },
|
|
89
|
+
{ label: 'Components', href: '#', icon: Pantone24 },
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
const footerMenuItems = [
|
|
93
|
+
{ label: 'Action 01', icon: Admin24, href: '#' },
|
|
94
|
+
{ label: 'Action 02', icon: Admin24, href: '#' },
|
|
95
|
+
{ label: 'Action 03', icon: Admin24, href: '#' },
|
|
96
|
+
{ label: 'Action 04', icon: Admin24, href: '#' },
|
|
97
|
+
];
|
|
98
|
+
</script>
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { useIsMobile } from '@/utils/use-is-mobile.composable';
|
|
2
|
+
import { ref, type ShallowRef } from 'vue';
|
|
3
|
+
|
|
4
|
+
type Options = {
|
|
5
|
+
allowItemHover?: boolean;
|
|
6
|
+
position?: 'bottom' | 'top';
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const defaultOptions: Options = { allowItemHover: true, position: 'bottom' };
|
|
10
|
+
|
|
11
|
+
export const useFloatingItem = (
|
|
12
|
+
trigger: ShallowRef<HTMLElement | null>,
|
|
13
|
+
floatingItem: ShallowRef<HTMLElement | null>,
|
|
14
|
+
options: Options = defaultOptions,
|
|
15
|
+
) => {
|
|
16
|
+
const normalizedOptions = {
|
|
17
|
+
...defaultOptions,
|
|
18
|
+
...options,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const { isMobile } = useIsMobile();
|
|
22
|
+
const floatingItemIsDisplayed = ref(false);
|
|
23
|
+
|
|
24
|
+
function getListboxPosition() {
|
|
25
|
+
const triggerRect = trigger.value?.getBoundingClientRect();
|
|
26
|
+
const floatingItemRect = floatingItem.value?.getBoundingClientRect();
|
|
27
|
+
|
|
28
|
+
if (!triggerRect || !floatingItemRect) return {};
|
|
29
|
+
|
|
30
|
+
if (isMobile.value) {
|
|
31
|
+
return {
|
|
32
|
+
top: `-${floatingItemRect.height + 24}px`,
|
|
33
|
+
left: `-${floatingItemRect.width - triggerRect.width}px`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
top:
|
|
39
|
+
options.position === 'top'
|
|
40
|
+
? `-${floatingItemRect.height - triggerRect.height}px`
|
|
41
|
+
: floatingItem.value?.style.left || '0px',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function hideFloatingItem(event?: MouseEvent | FocusEvent) {
|
|
46
|
+
if (!floatingItem.value || !trigger.value) return;
|
|
47
|
+
|
|
48
|
+
const target = event?.relatedTarget as Node | null;
|
|
49
|
+
|
|
50
|
+
// Prevent floating item from being hidden if mouse goes from trigger to listbox, unless specified
|
|
51
|
+
if (
|
|
52
|
+
normalizedOptions.allowItemHover &&
|
|
53
|
+
target &&
|
|
54
|
+
(floatingItem.value.contains(target) || trigger.value.contains(target))
|
|
55
|
+
) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
floatingItem.value.classList.add('mc-sidebar__floating-item--hidden');
|
|
60
|
+
floatingItemIsDisplayed.value = false;
|
|
61
|
+
|
|
62
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function handleClickOutside(event: MouseEvent) {
|
|
66
|
+
const target = event.target as Node;
|
|
67
|
+
if (
|
|
68
|
+
!floatingItem.value?.contains(target) &&
|
|
69
|
+
!trigger.value?.contains(target)
|
|
70
|
+
) {
|
|
71
|
+
hideFloatingItem(event);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function showFloatingItem() {
|
|
76
|
+
floatingItem.value?.classList.toggle('mc-sidebar__floating-item--hidden');
|
|
77
|
+
floatingItemIsDisplayed.value = !floatingItem.value?.classList.contains(
|
|
78
|
+
'mc-sidebar__floating-item--hidden',
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (!floatingItemIsDisplayed.value) return;
|
|
82
|
+
|
|
83
|
+
const { left, top } = getListboxPosition();
|
|
84
|
+
|
|
85
|
+
if (!floatingItem.value) return;
|
|
86
|
+
|
|
87
|
+
if (top) {
|
|
88
|
+
floatingItem.value.style.top = top;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (left) {
|
|
92
|
+
floatingItem.value.style.left = left;
|
|
93
|
+
}
|
|
94
|
+
floatingItemIsDisplayed.value = true;
|
|
95
|
+
|
|
96
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function onTriggerKeydown(event: KeyboardEvent) {
|
|
100
|
+
if (!floatingItem.value) return;
|
|
101
|
+
|
|
102
|
+
if (['ArrowDown', 'Enter', ' '].includes(event.key)) {
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
showFloatingItem();
|
|
105
|
+
focusFirstListboxItem();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (event.key === 'Tab') {
|
|
109
|
+
hideFloatingItem();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function focusFirstListboxItem() {
|
|
114
|
+
const firstItem = floatingItem.value?.querySelector<HTMLElement>(
|
|
115
|
+
'button, [href], [tabindex]:not([tabindex="-1"])',
|
|
116
|
+
);
|
|
117
|
+
firstItem?.focus();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function onListboxKeydown(event: KeyboardEvent) {
|
|
121
|
+
if (event.key === 'Escape') {
|
|
122
|
+
event.preventDefault();
|
|
123
|
+
hideFloatingItem();
|
|
124
|
+
trigger.value?.focus();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
floatingItemIsDisplayed,
|
|
130
|
+
hideFloatingItem,
|
|
131
|
+
showFloatingItem,
|
|
132
|
+
onTriggerKeydown,
|
|
133
|
+
onListboxKeydown,
|
|
134
|
+
};
|
|
135
|
+
};
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { ref, type ShallowRef } from 'vue';
|
|
3
|
+
import { useFloatingItem } from './use-floating-item.composable';
|
|
4
|
+
|
|
5
|
+
describe('useFloatingItem', () => {
|
|
6
|
+
let trigger: ShallowRef<HTMLElement | null>;
|
|
7
|
+
let floating: ShallowRef<HTMLElement | null>;
|
|
8
|
+
let triggerEl: HTMLElement;
|
|
9
|
+
let floatingEl: HTMLElement;
|
|
10
|
+
let childButton: HTMLButtonElement;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
triggerEl = document.createElement('button');
|
|
14
|
+
floatingEl = document.createElement('div');
|
|
15
|
+
childButton = document.createElement('button');
|
|
16
|
+
childButton.textContent = 'item';
|
|
17
|
+
floatingEl.appendChild(childButton);
|
|
18
|
+
|
|
19
|
+
// start hidden
|
|
20
|
+
floatingEl.classList.add('mc-sidebar__floating-item--hidden');
|
|
21
|
+
|
|
22
|
+
// attach to DOM so focus and events behave normally
|
|
23
|
+
document.body.appendChild(triggerEl);
|
|
24
|
+
document.body.appendChild(floatingEl);
|
|
25
|
+
|
|
26
|
+
trigger = ref(triggerEl);
|
|
27
|
+
floating = ref(floatingEl);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
vi.restoreAllMocks();
|
|
32
|
+
document.body.innerHTML = '';
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('showFloatingItem sets position, removes hidden class, sets flag and adds mousedown listener', () => {
|
|
36
|
+
const { showFloatingItem, floatingItemIsDisplayed } = useFloatingItem(
|
|
37
|
+
trigger,
|
|
38
|
+
floating,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// ensure getBoundingClientRect returns a known top
|
|
42
|
+
vi.spyOn(triggerEl, 'getBoundingClientRect').mockReturnValue({
|
|
43
|
+
top: 42,
|
|
44
|
+
left: 0,
|
|
45
|
+
bottom: 0,
|
|
46
|
+
right: 0,
|
|
47
|
+
width: 0,
|
|
48
|
+
height: 0,
|
|
49
|
+
x: 0,
|
|
50
|
+
y: 0,
|
|
51
|
+
toJSON: () => {},
|
|
52
|
+
} as unknown as DOMRect);
|
|
53
|
+
|
|
54
|
+
const addSpy = vi.spyOn(document, 'addEventListener');
|
|
55
|
+
|
|
56
|
+
showFloatingItem();
|
|
57
|
+
expect(
|
|
58
|
+
floatingEl.classList.contains('mc-sidebar__floating-item--hidden'),
|
|
59
|
+
).toBe(false);
|
|
60
|
+
expect(floatingItemIsDisplayed.value).toBe(true);
|
|
61
|
+
expect(addSpy).toHaveBeenCalledWith('mousedown', expect.any(Function));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('hideFloatingItem adds hidden class, clears flag and removes mousedown listener', () => {
|
|
65
|
+
const { showFloatingItem, hideFloatingItem, floatingItemIsDisplayed } =
|
|
66
|
+
useFloatingItem(trigger, floating);
|
|
67
|
+
|
|
68
|
+
// make visible first
|
|
69
|
+
vi.spyOn(triggerEl, 'getBoundingClientRect').mockReturnValue({
|
|
70
|
+
top: 1,
|
|
71
|
+
left: 0,
|
|
72
|
+
bottom: 0,
|
|
73
|
+
right: 0,
|
|
74
|
+
width: 0,
|
|
75
|
+
height: 0,
|
|
76
|
+
x: 0,
|
|
77
|
+
y: 0,
|
|
78
|
+
toJSON: () => {},
|
|
79
|
+
} as unknown as DOMRect);
|
|
80
|
+
const removeSpy = vi.spyOn(document, 'removeEventListener');
|
|
81
|
+
|
|
82
|
+
showFloatingItem();
|
|
83
|
+
// sanity
|
|
84
|
+
expect(floatingItemIsDisplayed.value).toBe(true);
|
|
85
|
+
expect(
|
|
86
|
+
floatingEl.classList.contains('mc-sidebar__floating-item--hidden'),
|
|
87
|
+
).toBe(false);
|
|
88
|
+
|
|
89
|
+
hideFloatingItem();
|
|
90
|
+
|
|
91
|
+
expect(
|
|
92
|
+
floatingEl.classList.contains('mc-sidebar__floating-item--hidden'),
|
|
93
|
+
).toBe(true);
|
|
94
|
+
expect(floatingItemIsDisplayed.value).toBe(false);
|
|
95
|
+
expect(removeSpy).toHaveBeenCalledWith('mousedown', expect.any(Function));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('hideFloatingItem does not hide when relatedTarget is inside floating or trigger if allowItemHover true', () => {
|
|
99
|
+
const { showFloatingItem, hideFloatingItem, floatingItemIsDisplayed } =
|
|
100
|
+
useFloatingItem(trigger, floating, { allowItemHover: true });
|
|
101
|
+
|
|
102
|
+
vi.spyOn(triggerEl, 'getBoundingClientRect').mockReturnValue({
|
|
103
|
+
top: 1,
|
|
104
|
+
left: 0,
|
|
105
|
+
bottom: 0,
|
|
106
|
+
right: 0,
|
|
107
|
+
width: 0,
|
|
108
|
+
height: 0,
|
|
109
|
+
x: 0,
|
|
110
|
+
y: 0,
|
|
111
|
+
toJSON: () => {},
|
|
112
|
+
} as unknown as DOMRect);
|
|
113
|
+
|
|
114
|
+
showFloatingItem();
|
|
115
|
+
expect(floatingItemIsDisplayed.value).toBe(true);
|
|
116
|
+
|
|
117
|
+
// simulate relatedTarget inside floating
|
|
118
|
+
const fakeEvent = {
|
|
119
|
+
relatedTarget: childButton,
|
|
120
|
+
} as unknown as FocusEvent;
|
|
121
|
+
|
|
122
|
+
hideFloatingItem(fakeEvent);
|
|
123
|
+
expect(floatingItemIsDisplayed.value).toBe(true);
|
|
124
|
+
expect(
|
|
125
|
+
floatingEl.classList.contains('mc-sidebar__floating-item--hidden'),
|
|
126
|
+
).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('clicking outside after showFloatingItem hides the floating item', () => {
|
|
130
|
+
const result = useFloatingItem(trigger, floating);
|
|
131
|
+
const { showFloatingItem, floatingItemIsDisplayed } = result;
|
|
132
|
+
|
|
133
|
+
vi.spyOn(triggerEl, 'getBoundingClientRect').mockReturnValue({
|
|
134
|
+
top: 5,
|
|
135
|
+
left: 0,
|
|
136
|
+
bottom: 0,
|
|
137
|
+
right: 0,
|
|
138
|
+
width: 0,
|
|
139
|
+
height: 0,
|
|
140
|
+
x: 0,
|
|
141
|
+
y: 0,
|
|
142
|
+
toJSON: () => {},
|
|
143
|
+
} as unknown as DOMRect);
|
|
144
|
+
|
|
145
|
+
showFloatingItem();
|
|
146
|
+
expect(floatingItemIsDisplayed.value).toBe(true);
|
|
147
|
+
|
|
148
|
+
// Dispatch mousedown on an outside element
|
|
149
|
+
const outside = document.createElement('div');
|
|
150
|
+
document.body.appendChild(outside);
|
|
151
|
+
|
|
152
|
+
const event = new MouseEvent('mousedown', { bubbles: true });
|
|
153
|
+
// dispatch on outside so event.target is outside
|
|
154
|
+
outside.dispatchEvent(event);
|
|
155
|
+
|
|
156
|
+
// after event loop, listener should have executed synchronously
|
|
157
|
+
expect(floatingItemIsDisplayed.value).toBe(false);
|
|
158
|
+
expect(
|
|
159
|
+
floatingEl.classList.contains('mc-sidebar__floating-item--hidden'),
|
|
160
|
+
).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('onTriggerKeydown opens on ArrowDown/Enter/Space and focuses first listbox item', () => {
|
|
164
|
+
const { onTriggerKeydown, floatingItemIsDisplayed } = useFloatingItem(
|
|
165
|
+
trigger,
|
|
166
|
+
floating,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// ensure getBoundingClientRect returns value so showFloatingItem proceeds
|
|
170
|
+
vi.spyOn(triggerEl, 'getBoundingClientRect').mockReturnValue({
|
|
171
|
+
top: 2,
|
|
172
|
+
left: 0,
|
|
173
|
+
bottom: 0,
|
|
174
|
+
right: 0,
|
|
175
|
+
width: 0,
|
|
176
|
+
height: 0,
|
|
177
|
+
x: 0,
|
|
178
|
+
y: 0,
|
|
179
|
+
toJSON: () => {},
|
|
180
|
+
} as unknown as DOMRect);
|
|
181
|
+
|
|
182
|
+
const keyEvent = new KeyboardEvent('keydown', { key: 'ArrowDown' });
|
|
183
|
+
const preventSpy = vi.spyOn(keyEvent, 'preventDefault');
|
|
184
|
+
|
|
185
|
+
const focusSpy = vi.spyOn(childButton, 'focus');
|
|
186
|
+
|
|
187
|
+
onTriggerKeydown(keyEvent);
|
|
188
|
+
expect(preventSpy).toHaveBeenCalled();
|
|
189
|
+
expect(floatingItemIsDisplayed.value).toBe(true);
|
|
190
|
+
expect(focusSpy).toHaveBeenCalled();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('onTriggerKeydown with Tab hides the floating item', () => {
|
|
194
|
+
const { onTriggerKeydown, showFloatingItem, floatingItemIsDisplayed } =
|
|
195
|
+
useFloatingItem(trigger, floating);
|
|
196
|
+
|
|
197
|
+
vi.spyOn(triggerEl, 'getBoundingClientRect').mockReturnValue({
|
|
198
|
+
top: 3,
|
|
199
|
+
left: 0,
|
|
200
|
+
bottom: 0,
|
|
201
|
+
right: 0,
|
|
202
|
+
width: 0,
|
|
203
|
+
height: 0,
|
|
204
|
+
x: 0,
|
|
205
|
+
y: 0,
|
|
206
|
+
toJSON: () => {},
|
|
207
|
+
} as unknown as DOMRect);
|
|
208
|
+
|
|
209
|
+
showFloatingItem();
|
|
210
|
+
expect(floatingItemIsDisplayed.value).toBe(true);
|
|
211
|
+
|
|
212
|
+
const tabEvent = new KeyboardEvent('keydown', { key: 'Tab' });
|
|
213
|
+
onTriggerKeydown(tabEvent);
|
|
214
|
+
|
|
215
|
+
expect(floatingItemIsDisplayed.value).toBe(false);
|
|
216
|
+
expect(
|
|
217
|
+
floatingEl.classList.contains('mc-sidebar__floating-item--hidden'),
|
|
218
|
+
).toBe(true);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('onListboxKeydown Escape hides and focuses trigger', () => {
|
|
222
|
+
const { onListboxKeydown, showFloatingItem, floatingItemIsDisplayed } =
|
|
223
|
+
useFloatingItem(trigger, floating);
|
|
224
|
+
|
|
225
|
+
vi.spyOn(triggerEl, 'getBoundingClientRect').mockReturnValue({
|
|
226
|
+
top: 6,
|
|
227
|
+
left: 0,
|
|
228
|
+
bottom: 0,
|
|
229
|
+
right: 0,
|
|
230
|
+
width: 0,
|
|
231
|
+
height: 0,
|
|
232
|
+
x: 0,
|
|
233
|
+
y: 0,
|
|
234
|
+
toJSON: () => {},
|
|
235
|
+
} as unknown as DOMRect);
|
|
236
|
+
|
|
237
|
+
const triggerFocusSpy = vi.spyOn(triggerEl, 'focus');
|
|
238
|
+
|
|
239
|
+
showFloatingItem();
|
|
240
|
+
expect(floatingItemIsDisplayed.value).toBe(true);
|
|
241
|
+
|
|
242
|
+
const escEvent = new KeyboardEvent('keydown', { key: 'Escape' });
|
|
243
|
+
const preventSpy = vi.spyOn(escEvent, 'preventDefault');
|
|
244
|
+
|
|
245
|
+
onListboxKeydown(escEvent);
|
|
246
|
+
|
|
247
|
+
expect(preventSpy).toHaveBeenCalled();
|
|
248
|
+
expect(floatingItemIsDisplayed.value).toBe(false);
|
|
249
|
+
expect(triggerFocusSpy).toHaveBeenCalled();
|
|
250
|
+
});
|
|
251
|
+
});
|