@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.
Files changed (90) hide show
  1. package/dist/mozaic-vue.css +1 -1
  2. package/dist/mozaic-vue.d.ts +1088 -378
  3. package/dist/mozaic-vue.js +2662 -1854
  4. package/dist/mozaic-vue.js.map +1 -1
  5. package/dist/mozaic-vue.umd.cjs +5 -5
  6. package/dist/mozaic-vue.umd.cjs.map +1 -1
  7. package/package.json +4 -4
  8. package/src/components/actionlistbox/MActionListbox.spec.ts +53 -59
  9. package/src/components/actionlistbox/MActionListbox.stories.ts +22 -1
  10. package/src/components/actionlistbox/MActionListbox.vue +91 -28
  11. package/src/components/actionlistbox/README.md +15 -0
  12. package/src/components/breadcrumb/MBreadcrumb.vue +5 -0
  13. package/src/components/button/README.md +4 -0
  14. package/src/components/checkbox/README.md +2 -0
  15. package/src/components/divider/README.md +4 -0
  16. package/src/components/iconbutton/MIconButton.stories.ts +12 -0
  17. package/src/components/iconbutton/MIconButton.vue +13 -1
  18. package/src/components/iconbutton/README.md +27 -0
  19. package/src/components/loader/README.md +2 -0
  20. package/src/components/navigationindicator/MNavigationIndicator.spec.ts +152 -0
  21. package/src/components/navigationindicator/MNavigationIndicator.stories.ts +41 -0
  22. package/src/components/navigationindicator/MNavigationIndicator.vue +132 -0
  23. package/src/components/navigationindicator/README.md +37 -0
  24. package/src/components/pageheader/MPageHeader.spec.ts +142 -0
  25. package/src/components/pageheader/MPageHeader.stories.ts +125 -0
  26. package/src/components/pageheader/MPageHeader.vue +133 -0
  27. package/src/components/pageheader/README.md +46 -0
  28. package/src/components/popover/MPopover.spec.ts +106 -0
  29. package/src/components/popover/MPopover.stories.ts +126 -0
  30. package/src/components/popover/MPopover.vue +131 -0
  31. package/src/components/popover/README.md +42 -0
  32. package/src/components/radio/README.md +2 -0
  33. package/src/components/select/MSelect.spec.ts +2 -1
  34. package/src/components/select/MSelect.vue +30 -25
  35. package/src/components/sidebar/MSidebar.const.ts +6 -0
  36. package/src/components/sidebar/MSidebar.spec.ts +110 -0
  37. package/src/components/sidebar/MSidebar.stories.ts +108 -0
  38. package/src/components/sidebar/MSidebar.vue +124 -0
  39. package/src/components/sidebar/README.md +59 -0
  40. package/src/components/sidebar/stories/DefaultCase.stories.vue +120 -0
  41. package/src/components/sidebar/stories/README.md +27 -0
  42. package/src/components/sidebar/stories/WithExpandOnly.stories.vue +112 -0
  43. package/src/components/sidebar/stories/WithProfileInfoOnly.stories.vue +119 -0
  44. package/src/components/sidebar/stories/WithSingleLevel.stories.vue +98 -0
  45. package/src/components/sidebar/use-floating-item.composable.ts +135 -0
  46. package/src/components/sidebar/use-floating-item.spec.ts +251 -0
  47. package/src/components/sidebarexpandableitem/MSidebarExpandableItem.spec.ts +151 -0
  48. package/src/components/sidebarexpandableitem/MSidebarExpandableItem.vue +113 -0
  49. package/src/components/sidebarexpandableitem/README.md +36 -0
  50. package/src/components/sidebarfooter/MSidebarFooter.spec.ts +276 -0
  51. package/src/components/sidebarfooter/MSidebarFooter.vue +201 -0
  52. package/src/components/sidebarfooter/README.md +52 -0
  53. package/src/components/sidebarfooter/_MSidebarFooterMenu.vue +64 -0
  54. package/src/components/sidebarheader/MSidebarHeader.vue +36 -0
  55. package/src/components/sidebarheader/README.md +31 -0
  56. package/src/components/sidebarnavitem/MSidebarNavItem.spec.ts +127 -0
  57. package/src/components/sidebarnavitem/MSidebarNavItem.vue +113 -0
  58. package/src/components/sidebarnavitem/README.md +56 -0
  59. package/src/components/sidebarshortcutitem/MSidebarShortcutItem.spec.ts +59 -0
  60. package/src/components/sidebarshortcutitem/MSidebarShortcutItem.vue +52 -0
  61. package/src/components/sidebarshortcutitem/README.md +32 -0
  62. package/src/components/sidebarshortcuts/MSidebarShortcuts.spec.ts +87 -0
  63. package/src/components/sidebarshortcuts/MSidebarShortcuts.vue +101 -0
  64. package/src/components/sidebarshortcuts/README.md +36 -0
  65. package/src/components/statusbadge/README.md +12 -0
  66. package/src/components/textinput/MTextInput.stories.ts +13 -1
  67. package/src/components/textinput/MTextInput.vue +12 -0
  68. package/src/components/textinput/README.md +3 -1
  69. package/src/components/tile/MTile.spec.ts +61 -0
  70. package/src/components/tile/MTile.stories.ts +102 -0
  71. package/src/components/tile/MTile.vue +68 -0
  72. package/src/components/tile/README.md +19 -0
  73. package/src/components/tileclickable/MTileClickable.spec.ts +130 -0
  74. package/src/components/tileclickable/MTileClickable.stories.ts +60 -0
  75. package/src/components/tileclickable/MTileClickable.vue +106 -0
  76. package/src/components/tileclickable/README.md +30 -0
  77. package/src/components/tileexpandable/MTileExpandable.spec.ts +121 -0
  78. package/src/components/tileexpandable/MTileExpandable.stories.ts +50 -0
  79. package/src/components/tileexpandable/MTileExpandable.vue +131 -0
  80. package/src/components/tileexpandable/README.md +36 -0
  81. package/src/components/tileselectable/MTileSelectable.spec.ts +177 -0
  82. package/src/components/tileselectable/MTileSelectable.stories.ts +55 -0
  83. package/src/components/tileselectable/MTileSelectable.vue +142 -0
  84. package/src/components/tileselectable/README.md +44 -0
  85. package/src/components/toaster/README.md +1 -1
  86. package/src/components/tooltip/MTooltip.vue +5 -0
  87. package/src/components/tooltip/README.md +16 -1
  88. package/src/main.ts +12 -2
  89. package/src/utils/use-is-mobile.composable.ts +20 -0
  90. 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
+ });