@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,201 @@
1
+ <template>
2
+ <footer
3
+ :class="{
4
+ 'mc-sidebar__footer': true,
5
+ 'mc-sidebar__footer--expanded': expanded,
6
+ 'mc-sidebar__footer--collapsed': !expanded,
7
+ }"
8
+ >
9
+ <MDivider />
10
+ <div class="mc-sidebar__footer-container">
11
+ <component
12
+ v-if="avatar || title || subtitle"
13
+ :is="isMobile ? 'div' : 'button'"
14
+ class="mc-sidebar__footer-profile mc-sidebar__footer-trigger"
15
+ v-bind="desktopTriggerAttrs"
16
+ >
17
+ <img
18
+ v-if="avatar"
19
+ class="mc-sidebar__footer-avatar"
20
+ :src="avatar"
21
+ :alt="title"
22
+ loading="lazy"
23
+ />
24
+ <div v-if="title || subtitle" class="mc-sidebar__footer-content">
25
+ <p v-if="title" class="mc-sidebar__footer-title">
26
+ {{ title }}
27
+ </p>
28
+ <p v-if="subtitle" class="mc-sidebar__footer-subtitle">
29
+ {{ subtitle }}
30
+ </p>
31
+ </div>
32
+
33
+ <MSidebarFooterMenu
34
+ v-if="!isMobile"
35
+ ref="listbox"
36
+ :expanded="!!expanded"
37
+ :logOutLabel="logOutLabel"
38
+ :onListboxKeydown="onListboxKeydown"
39
+ :hideFloatingItem="hideFloatingItem"
40
+ @log-out="emit('log-out')"
41
+ >
42
+ <slot />
43
+ </MSidebarFooterMenu>
44
+ </component>
45
+
46
+ <MIconButton
47
+ v-if="expendable"
48
+ class="mc-sidebar__footer-expand"
49
+ ghost
50
+ aria-label="Expand"
51
+ :aria-expanded="expanded"
52
+ @click="toggleSidebar"
53
+ >
54
+ <template #icon>
55
+ <SidebarExpand24 />
56
+ </template>
57
+ </MIconButton>
58
+
59
+ <button
60
+ v-if="isMobile"
61
+ ref="trigger"
62
+ class="mc-button mc-button--icon-button mc-button--ghost mc-sidebar__footer-expand--mobile"
63
+ aria-label="Profile menu"
64
+ :aria-expanded="floatingItemIsDisplayed"
65
+ @click="showFloatingItem"
66
+ >
67
+ <component
68
+ :is="floatingItemIsDisplayed ? ChevronUp24 : ChevronDown24"
69
+ />
70
+
71
+ <MSidebarFooterMenu
72
+ v-if="isMobile"
73
+ ref="listbox"
74
+ :expanded="!!expanded"
75
+ :logOutLabel="logOutLabel"
76
+ :onListboxKeydown="onListboxKeydown"
77
+ :hideFloatingItem="hideFloatingItem"
78
+ @log-out="emit('log-out')"
79
+ >
80
+ <slot />
81
+ </MSidebarFooterMenu>
82
+ </button>
83
+ </div>
84
+ </footer>
85
+ </template>
86
+
87
+ <script setup lang="ts">
88
+ import { computed, inject, provide, useTemplateRef, type VNode } from 'vue';
89
+ import MIconButton from '../iconbutton/MIconButton.vue';
90
+ import MDivider from '../divider/MDivider.vue';
91
+ import MSidebarFooterMenu from './_MSidebarFooterMenu.vue';
92
+ import {
93
+ SidebarExpand24,
94
+ ChevronDown24,
95
+ ChevronUp24,
96
+ } from '@mozaic-ds/icons-vue';
97
+ import {
98
+ EXPANDED_SIDEBAR_KEY,
99
+ SUB_ITEM_KEY,
100
+ TOGGLE_SIDEBAR_KEY,
101
+ } from '../sidebar/MSidebar.const';
102
+ import { useFloatingItem } from '../sidebar/use-floating-item.composable';
103
+ import { useIsMobile } from '@/utils/use-is-mobile.composable';
104
+ /**
105
+ * SidebarFooter is a component that renders the footer of a sidebar. It can display a user profile with an optional avatar, title, subtitle, and link, and includes a button to expand or collapse the sidebar.
106
+ */
107
+ withDefaults(
108
+ defineProps<{
109
+ /**
110
+ * The main text or name displayed in the sidebar footer profile.
111
+ */
112
+ title?: string;
113
+ /**
114
+ * Secondary text or description displayed under the title in the footer.
115
+ */
116
+ subtitle?: string;
117
+ /**
118
+ * URL of the avatar image displayed in the footer profile.
119
+ */
120
+ avatar?: string;
121
+ /**
122
+ * if `true`, hides the sidebar expand/collapse button.
123
+ */
124
+ expendable?: boolean;
125
+ /**
126
+ * Text to display for the log out button.
127
+ */
128
+ logOutLabel?: string;
129
+ }>(),
130
+ {
131
+ logOutLabel: 'Log out',
132
+ expendable: true,
133
+ },
134
+ );
135
+
136
+ defineSlots<{
137
+ /**
138
+ * Contains one or more MSidebarNavItem components used to render the profile items.
139
+ */
140
+ default: VNode[];
141
+ }>();
142
+
143
+ const emit = defineEmits<{
144
+ /**
145
+ * Emits when the user clicks the log out button in the footer menu.
146
+ */
147
+ (on: 'log-out'): void;
148
+ }>();
149
+
150
+ provide(SUB_ITEM_KEY, true);
151
+
152
+ const toggleSidebar = inject(TOGGLE_SIDEBAR_KEY);
153
+ const expanded = inject(EXPANDED_SIDEBAR_KEY);
154
+
155
+ const trigger = useTemplateRef('trigger');
156
+ const listbox = useTemplateRef('listbox');
157
+
158
+ const { isMobile } = useIsMobile();
159
+
160
+ const listboxContainer = computed(
161
+ () => (listbox.value?.container as HTMLElement | null) ?? null,
162
+ );
163
+
164
+ const {
165
+ floatingItemIsDisplayed,
166
+ hideFloatingItem,
167
+ showFloatingItem,
168
+ onTriggerKeydown,
169
+ onListboxKeydown,
170
+ } = useFloatingItem(trigger, listboxContainer, {
171
+ position: 'top',
172
+ });
173
+
174
+ const desktopTriggerAttrs = computed(() => {
175
+ if (isMobile.value) return {};
176
+
177
+ const baseAttrs = {
178
+ ref: 'trigger',
179
+ ['aria-expanded']: floatingItemIsDisplayed.value,
180
+ };
181
+
182
+ return expanded?.value
183
+ ? {
184
+ ...baseAttrs,
185
+ onClick: showFloatingItem,
186
+ }
187
+ : {
188
+ ...baseAttrs,
189
+ onMouseenter: showFloatingItem,
190
+ onFocus: showFloatingItem,
191
+ onKeydown: onTriggerKeydown,
192
+ onMouseleave: hideFloatingItem,
193
+ onBlur: hideFloatingItem,
194
+ };
195
+ });
196
+ </script>
197
+
198
+ <style lang="scss" scoped>
199
+ @use '@mozaic-ds/styles/components/button';
200
+ @use '@mozaic-ds/styles/components/sidebar';
201
+ </style>
@@ -0,0 +1,52 @@
1
+ # _MSidebarFooterMenu
2
+
3
+
4
+
5
+ ## Props
6
+
7
+ | Name | Description | Type | Default |
8
+ | --- | --- | --- | --- |
9
+ | `expanded*` | - | `boolean` | - |
10
+ | `logOutLabel*` | - | `string` | - |
11
+ | `onListboxKeydown*` | - | `(event: KeyboardEvent) => void` | - |
12
+ | `hideFloatingItem*` | - | `() => void` | - |
13
+
14
+ ## Slots
15
+
16
+ | Name | Description |
17
+ | --- | --- |
18
+ | `default` | - |
19
+
20
+ ## Events
21
+
22
+ | Name | Description | Type |
23
+ | --- | --- | --- |
24
+ | `log-out` | - | `[]` |
25
+
26
+ ## Dependencies
27
+
28
+ ### Depends on
29
+
30
+ - [MDivider](../divider)
31
+ - [MSidebarNavItem](../sidebarnavitem)
32
+
33
+ ### Graph
34
+
35
+ ```mermaid
36
+ graph TD;
37
+ _MSidebarFooterMenu --> MDivider
38
+ _MSidebarFooterMenu --> MSidebarNavItem
39
+ style _MSidebarFooterMenu fill:#008240,stroke:#333,stroke-width:4px
40
+ ```
41
+
42
+ ### Used By
43
+
44
+ - [MSidebarFooter]()
45
+
46
+ ### Graph
47
+
48
+ ```mermaid
49
+ graph TD;
50
+ MSidebarFooter --> _MSidebarFooterMenu
51
+ style _MSidebarFooterMenu fill:#008240,stroke:#333,stroke-width:4px
52
+ ```
@@ -0,0 +1,64 @@
1
+ <template>
2
+ <div
3
+ ref="container"
4
+ id="listbox-footer"
5
+ tabindex="0"
6
+ role="menu"
7
+ :class="{
8
+ 'mc-sidebar__floating-item mc-sidebar__floating-item--hidden': true,
9
+ 'mc-sidebar__floating-item--expanded': expanded,
10
+ }"
11
+ v-bind="
12
+ expanded
13
+ ? {}
14
+ : {
15
+ onKeydown: onListboxKeydown,
16
+ onMouseleave: hideFloatingItem,
17
+ onBlur: hideFloatingItem,
18
+ }
19
+ "
20
+ >
21
+ <div class="mc-sidebar__footer-menu">
22
+ <ul class="mc-sidebar__list">
23
+ <slot name="default" />
24
+ </ul>
25
+
26
+ <MDivider class="mc-sidebar__footer-menu-divider" />
27
+
28
+ <MSidebarNavItem
29
+ tag="div"
30
+ :label="logOutLabel"
31
+ :icon="LogOut24"
32
+ @click="emit('log-out')"
33
+ />
34
+ </div>
35
+ </div>
36
+ </template>
37
+
38
+ <script setup lang="ts">
39
+ import { useTemplateRef } from 'vue';
40
+ import MDivider from '../divider/MDivider.vue';
41
+ import MSidebarNavItem from '../sidebarnavitem/MSidebarNavItem.vue';
42
+ import { LogOut24 } from '@mozaic-ds/icons-vue';
43
+
44
+ defineProps<{
45
+ expanded: boolean;
46
+ logOutLabel: string;
47
+ onListboxKeydown: (event: KeyboardEvent) => void;
48
+ hideFloatingItem: () => void;
49
+ }>();
50
+
51
+ const emit = defineEmits<{
52
+ (on: 'log-out'): void;
53
+ }>();
54
+
55
+ const container = useTemplateRef('container');
56
+
57
+ defineExpose({
58
+ container,
59
+ });
60
+ </script>
61
+
62
+ <style lang="scss" scoped>
63
+ @use '@mozaic-ds/styles/components/sidebar';
64
+ </style>
@@ -0,0 +1,36 @@
1
+ <template>
2
+ <header
3
+ :class="{
4
+ 'mc-sidebar__header': true,
5
+ 'mc-sidebar__header--expanded': expanded,
6
+ 'mc-sidebar__header--collapsed': !expanded,
7
+ }"
8
+ >
9
+ <img class="mc-sidebar__header-logo" :src="props.logo" alt="logo" />
10
+ <p class="mc-sidebar__header-title">{{ props.title }}</p>
11
+ </header>
12
+ </template>
13
+
14
+ <script setup lang="ts">
15
+ import { inject } from 'vue';
16
+ import { EXPANDED_SIDEBAR_KEY } from '../sidebar/MSidebar.const';
17
+ /**
18
+ * SidebarHeader is a component that renders the header of a sidebar, displaying a logo and a main title.
19
+ */
20
+ const props = defineProps<{
21
+ /**
22
+ * The main title text displayed in the sidebar header.
23
+ */
24
+ title: string;
25
+ /**
26
+ * URL of the logo image displayed in the sidebar header.
27
+ */
28
+ logo: string;
29
+ }>();
30
+
31
+ const expanded = inject(EXPANDED_SIDEBAR_KEY);
32
+ </script>
33
+
34
+ <style lang="scss" scoped>
35
+ @use '@mozaic-ds/styles/components/sidebar';
36
+ </style>
@@ -0,0 +1,31 @@
1
+ # MSidebarHeader
2
+
3
+ SidebarHeader is a component that renders the header of a sidebar, displaying a logo and a main title.
4
+
5
+
6
+ ## Props
7
+
8
+ | Name | Description | Type | Default |
9
+ | --- | --- | --- | --- |
10
+ | `title*` | The main title text displayed in the sidebar header. | `string` | - |
11
+ | `logo*` | URL of the logo image displayed in the sidebar header. | `string` | - |
12
+
13
+ ## Dependencies
14
+
15
+ ### Used By
16
+
17
+ - [DefaultCase.stories](../sidebar/stories)
18
+ - [WithExpandOnly.stories](../sidebar/stories)
19
+ - [WithProfileInfoOnly.stories](../sidebar/stories)
20
+ - [WithSingleLevel.stories](../sidebar/stories)
21
+
22
+ ### Graph
23
+
24
+ ```mermaid
25
+ graph TD;
26
+ DefaultCase.stories --> MSidebarHeader
27
+ WithExpandOnly.stories --> MSidebarHeader
28
+ WithProfileInfoOnly.stories --> MSidebarHeader
29
+ WithSingleLevel.stories --> MSidebarHeader
30
+ style MSidebarHeader fill:#008240,stroke:#333,stroke-width:4px
31
+ ```
@@ -0,0 +1,127 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
+
4
+ const showFloatingItemMock = vi.fn();
5
+ const hideFloatingItemMock = vi.fn();
6
+
7
+ vi.mock('../sidebar/use-floating-item.composable', () => ({
8
+ useFloatingItem: () => ({
9
+ showFloatingItem: showFloatingItemMock,
10
+ hideFloatingItem: hideFloatingItemMock,
11
+ }),
12
+ }));
13
+
14
+ import MSidebarNavItem from './MSidebarNavItem.vue';
15
+ import { h } from 'vue';
16
+ import { EXPANDED_SIDEBAR_KEY, SUB_ITEM_KEY } from '../sidebar/MSidebar.const';
17
+
18
+ const DummyIcon = {
19
+ name: 'DummyIcon',
20
+ render() {
21
+ return h('svg', { class: 'dummy-icon' });
22
+ },
23
+ };
24
+
25
+ describe('MSidebarNavItem', () => {
26
+ it('renders label, href and aria-label', () => {
27
+ const wrapper = mount(MSidebarNavItem, {
28
+ props: { label: 'Home', href: '/home' },
29
+ global: {
30
+ provide: { [EXPANDED_SIDEBAR_KEY]: true, [SUB_ITEM_KEY]: false },
31
+ },
32
+ });
33
+
34
+ const a = wrapper.get('a');
35
+ expect(a.attributes('href')).toBe('/home');
36
+ expect(a.attributes('aria-label')).toBe('Home');
37
+ expect(wrapper.get('.mc-sidebar__text').text()).toBe('Home');
38
+ });
39
+
40
+ it('renders the passed icon component', () => {
41
+ const wrapper = mount(MSidebarNavItem, {
42
+ props: { label: 'Item', href: '#', icon: DummyIcon },
43
+ global: {
44
+ provide: { [EXPANDED_SIDEBAR_KEY]: true, [SUB_ITEM_KEY]: false },
45
+ },
46
+ });
47
+
48
+ expect(wrapper.find('.mc-sidebar__icon').exists()).toBe(true);
49
+ expect(wrapper.find('.dummy-icon').exists()).toBe(true);
50
+ });
51
+
52
+ it('uses _blank target and shows ExternalLink24 when external is true', () => {
53
+ const wrapper = mount(MSidebarNavItem, {
54
+ props: { label: 'Ext', href: '/ext', external: true },
55
+ global: {
56
+ provide: { [EXPANDED_SIDEBAR_KEY]: true, [SUB_ITEM_KEY]: false },
57
+ },
58
+ });
59
+
60
+ const a = wrapper.get('a');
61
+ expect(a.attributes('target')).toBe('_blank');
62
+ });
63
+
64
+ it('shows Lock24 and locked class when locked is true', () => {
65
+ const wrapper = mount(MSidebarNavItem, {
66
+ props: { label: 'Locked', href: '/locked', locked: true },
67
+ global: {
68
+ provide: { [EXPANDED_SIDEBAR_KEY]: true, [SUB_ITEM_KEY]: false },
69
+ },
70
+ });
71
+
72
+ const a = wrapper.get('a');
73
+ expect(a.classes()).toContain('mc-sidebar__link--locked');
74
+ });
75
+
76
+ it('applies selected class when active is true', () => {
77
+ const wrapper = mount(MSidebarNavItem, {
78
+ props: { label: 'Active', href: '/active', active: true },
79
+ global: {
80
+ provide: { [EXPANDED_SIDEBAR_KEY]: true, [SUB_ITEM_KEY]: false },
81
+ },
82
+ });
83
+
84
+ const a = wrapper.get('a');
85
+ expect(a.classes()).toContain('mc-sidebar__link--selected');
86
+ });
87
+
88
+ describe('Component events', () => {
89
+ let wrapper: ReturnType<typeof mount>;
90
+
91
+ beforeEach(() => {
92
+ showFloatingItemMock.mockClear();
93
+ hideFloatingItemMock.mockClear();
94
+
95
+ wrapper = mount(MSidebarNavItem, {
96
+ props: { label: 'Item label', href: '#' },
97
+ global: {
98
+ provide: {
99
+ [EXPANDED_SIDEBAR_KEY]: true,
100
+ [SUB_ITEM_KEY]: false,
101
+ },
102
+ },
103
+ });
104
+ });
105
+
106
+ it('calls showFloatingItem on mouseenter and focus', async () => {
107
+ const anchor = wrapper.find('a');
108
+ await anchor.trigger('mouseenter');
109
+ await anchor.trigger('focus');
110
+ expect(showFloatingItemMock).toHaveBeenCalledTimes(2);
111
+ });
112
+
113
+ it('calls hideFloatingItem on mouseleave and blur', async () => {
114
+ const anchor = wrapper.find('a');
115
+ await anchor.trigger('mouseleave');
116
+ await anchor.trigger('blur');
117
+ expect(hideFloatingItemMock).toHaveBeenCalledTimes(2);
118
+ });
119
+
120
+ it('emits click when anchor is clicked', async () => {
121
+ const anchor = wrapper.find('a');
122
+ await anchor.trigger('click');
123
+ expect(wrapper.emitted()).toHaveProperty('click');
124
+ expect(wrapper.emitted('click')?.[0]).toHaveLength(1);
125
+ });
126
+ });
127
+ });
@@ -0,0 +1,113 @@
1
+ <template>
2
+ <component
3
+ :is="tag || 'li'"
4
+ :class="{
5
+ 'mc-sidebar__item': true,
6
+ 'mc-sidebar__item--collapsed': !expanded && !isSubItem,
7
+ 'mc-sidebar__item--sub': isSubItem,
8
+ }"
9
+ >
10
+ <component
11
+ :is="href ? 'a' : 'button'"
12
+ ref="trigger"
13
+ :href="href"
14
+ :target="external ? '_blank' : '_self'"
15
+ :class="{
16
+ 'mc-sidebar__link': true,
17
+ 'mc-sidebar__link--selected': active,
18
+ 'mc-sidebar__link--locked': locked,
19
+ }"
20
+ :aria-label="label"
21
+ @mouseenter="showFloatingItem"
22
+ @focus="showFloatingItem"
23
+ @mouseleave="hideFloatingItem"
24
+ @blur="hideFloatingItem"
25
+ @click="emit('click', $event)"
26
+ >
27
+ <component :is="icon" class="mc-sidebar__icon" />
28
+
29
+ <span class="mc-sidebar__text">{{ label }}</span>
30
+
31
+ <component
32
+ v-if="locked || external"
33
+ :is="locked ? Lock20 : ExternalLink20"
34
+ />
35
+ </component>
36
+
37
+ <div
38
+ v-if="!expanded && !isSubItem"
39
+ ref="tooltip"
40
+ class="mc-sidebar__floating-item mc-sidebar__floating-item--tooltip mc-sidebar__floating-item--hidden"
41
+ >
42
+ <MTooltip :id="id" :text="label" standalone :pointer="false" />
43
+ </div>
44
+ </component>
45
+ </template>
46
+
47
+ <script setup lang="ts">
48
+ import { Lock20, ExternalLink20 } from '@mozaic-ds/icons-vue';
49
+ import {
50
+ inject,
51
+ useId,
52
+ useTemplateRef,
53
+ type Component,
54
+ type ShallowRef,
55
+ } from 'vue';
56
+ import MTooltip from '../tooltip/MTooltip.vue';
57
+ import { SUB_ITEM_KEY, EXPANDED_SIDEBAR_KEY } from '../sidebar/MSidebar.const';
58
+ import { useFloatingItem } from '../sidebar/use-floating-item.composable';
59
+ /**
60
+ * SidebarNavItem is a component that renders a sidebar navigation item with optional icon support. It handles active, locked, and external states, shows tooltips or floating items when the sidebar is collapsed, and supports click events.
61
+ */
62
+ defineProps<{
63
+ tag?: string;
64
+ /**
65
+ * The text label displayed for the navigation item.
66
+ */
67
+ label: string;
68
+ /**
69
+ * Optional icon component displayed alongside the label.
70
+ */
71
+ icon?: Component;
72
+ /**
73
+ * The URL the navigation item links to.
74
+ */
75
+ href?: string;
76
+ /**
77
+ * Marks the item as locked. Shows a lock icon and prevents interaction.
78
+ */
79
+ locked?: boolean;
80
+ /**
81
+ * Indicates the link is external. Opens in a new tab and shows an external link icon.
82
+ */
83
+ external?: boolean;
84
+ /**
85
+ * Marks the item as active, applying the active styling.
86
+ */
87
+ active?: boolean;
88
+ }>();
89
+
90
+ const emit = defineEmits<{ (on: 'click', value: MouseEvent): void }>();
91
+
92
+ const id = useId();
93
+
94
+ const expanded = inject(EXPANDED_SIDEBAR_KEY);
95
+ const isSubItem = inject(SUB_ITEM_KEY, false);
96
+
97
+ const trigger = useTemplateRef(
98
+ 'trigger',
99
+ ) as unknown as ShallowRef<HTMLElement | null>;
100
+ const tooltip = useTemplateRef('tooltip');
101
+
102
+ const { hideFloatingItem, showFloatingItem } = useFloatingItem(
103
+ trigger,
104
+ tooltip,
105
+ {
106
+ allowItemHover: false,
107
+ },
108
+ );
109
+ </script>
110
+
111
+ <style lang="scss" scoped>
112
+ @use '@mozaic-ds/styles/components/sidebar';
113
+ </style>
@@ -0,0 +1,56 @@
1
+ # MSidebarNavItem
2
+
3
+ SidebarNavItem is a component that renders a sidebar navigation item with optional icon support. It handles active, locked, and external states, shows tooltips or floating items when the sidebar is collapsed, and supports click events.
4
+
5
+
6
+ ## Props
7
+
8
+ | Name | Description | Type | Default |
9
+ | --- | --- | --- | --- |
10
+ | `tag` | - | `string` | - |
11
+ | `label*` | The text label displayed for the navigation item. | `string` | - |
12
+ | `icon` | Optional icon component displayed alongside the label. | `Component` | - |
13
+ | `href` | The URL the navigation item links to. | `string` | - |
14
+ | `locked` | Marks the item as locked. Shows a lock icon and prevents interaction. | `boolean` | - |
15
+ | `external` | Indicates the link is external. Opens in a new tab and shows an external link icon. | `boolean` | - |
16
+ | `active` | Marks the item as active, applying the active styling. | `boolean` | - |
17
+
18
+ ## Events
19
+
20
+ | Name | Description | Type |
21
+ | --- | --- | --- |
22
+ | `click` | - | `[value: MouseEvent]` |
23
+
24
+ ## Dependencies
25
+
26
+ ### Depends on
27
+
28
+ - [MTooltip](../tooltip)
29
+
30
+ ### Graph
31
+
32
+ ```mermaid
33
+ graph TD;
34
+ MSidebarNavItem --> MTooltip
35
+ style MSidebarNavItem fill:#008240,stroke:#333,stroke-width:4px
36
+ ```
37
+
38
+ ### Used By
39
+
40
+ - [DefaultCase.stories](../sidebar/stories)
41
+ - [WithExpandOnly.stories](../sidebar/stories)
42
+ - [WithProfileInfoOnly.stories](../sidebar/stories)
43
+ - [WithSingleLevel.stories](../sidebar/stories)
44
+ - [_MSidebarFooterMenu](../sidebarfooter)
45
+
46
+ ### Graph
47
+
48
+ ```mermaid
49
+ graph TD;
50
+ DefaultCase.stories --> MSidebarNavItem
51
+ WithExpandOnly.stories --> MSidebarNavItem
52
+ WithProfileInfoOnly.stories --> MSidebarNavItem
53
+ WithSingleLevel.stories --> MSidebarNavItem
54
+ _MSidebarFooterMenu --> MSidebarNavItem
55
+ style MSidebarNavItem fill:#008240,stroke:#333,stroke-width:4px
56
+ ```