@mozaic-ds/vue 2.19.2 → 2.20.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 (88) hide show
  1. package/dist/mozaic-vue.css +1 -1
  2. package/dist/mozaic-vue.d.ts +512 -296
  3. package/dist/mozaic-vue.js +2005 -1758
  4. package/dist/mozaic-vue.js.map +1 -1
  5. package/dist/mozaic-vue.umd.cjs +6 -6
  6. package/dist/mozaic-vue.umd.cjs.map +1 -1
  7. package/package.json +5 -2
  8. package/src/components/Catalog.scss +84 -0
  9. package/src/components/Catalog.stories.ts +146 -0
  10. package/src/components/GettingStarted.mdx +258 -2
  11. package/src/components/Migration.mdx +4135 -532
  12. package/src/components/Welcome.mdx +1 -1
  13. package/src/components/carousel/MCarousel.spec.ts +174 -55
  14. package/src/components/carousel/MCarousel.vue +106 -33
  15. package/src/components/checkboxgroup/MCheckboxGroup.vue +2 -9
  16. package/src/components/combobox/MCombobox.spec.ts +144 -0
  17. package/src/components/combobox/MCombobox.stories.ts +2 -1
  18. package/src/components/combobox/MCombobox.vue +68 -38
  19. package/src/components/combobox/README.md +6 -0
  20. package/src/components/datatable/DataTable.stories.ts +2 -2
  21. package/src/components/datatable/DataTableCells.stories.ts +1 -0
  22. package/src/components/datatable/DataTableEmpty.stories.ts +1 -0
  23. package/src/components/datatable/DataTableExpandable.stories.ts +1 -0
  24. package/src/components/datatable/DataTableNested.stories.ts +1 -0
  25. package/src/components/datatable/DataTablePageable.stories.ts +194 -0
  26. package/src/components/datatable/DataTableSelectable.stories.ts +1 -0
  27. package/src/components/datatable/DataTableSortable.stories.ts +1 -0
  28. package/src/components/datepicker/MDatepicker.stories.ts +2 -0
  29. package/src/components/datepicker/MDatepicker.vue +17 -11
  30. package/src/components/divider/README.md +2 -0
  31. package/src/components/drawer/MDrawer.vue +7 -5
  32. package/src/components/link/MLink.stories.ts +18 -0
  33. package/src/components/link/MLink.vue +20 -4
  34. package/src/components/link/README.md +2 -1
  35. package/src/components/optionListbox/MOptionListbox.spec.ts +78 -1
  36. package/src/components/optionListbox/MOptionListbox.types.ts +7 -0
  37. package/src/components/optionListbox/MOptionListbox.vue +31 -217
  38. package/src/components/optionListbox/README.md +2 -0
  39. package/src/components/optionListbox/use-listbox-navigation.composable.ts +147 -0
  40. package/src/components/optionListbox/use-listbox-selection.composable.ts +149 -0
  41. package/src/components/pageheader/MPageHeader.spec.ts +17 -3
  42. package/src/components/pageheader/MPageHeader.stories.ts +37 -41
  43. package/src/components/pageheader/MPageHeader.vue +26 -16
  44. package/src/components/pageheader/README.md +2 -1
  45. package/src/components/pageheadercontext/MPageHeaderContext.spec.ts +155 -0
  46. package/src/components/pageheadercontext/MPageHeaderContext.vue +126 -0
  47. package/src/components/pageheadercontext/README.md +33 -0
  48. package/src/components/pageheaderfeature/MPageHeaderFeature.spec.ts +39 -0
  49. package/src/components/pageheaderfeature/MPageHeaderFeature.vue +40 -0
  50. package/src/components/pageheaderfeature/README.md +17 -0
  51. package/src/components/radiogroup/MRadioGroup.stories.ts +6 -0
  52. package/src/components/radiogroup/MRadioGroup.vue +2 -9
  53. package/src/components/sidebar/MSidebar.const.ts +2 -0
  54. package/src/components/sidebar/MSidebar.stories.ts +33 -0
  55. package/src/components/sidebar/MSidebar.vue +10 -1
  56. package/src/components/sidebar/README.md +1 -0
  57. package/src/components/sidebar/stories/DefaultCase.stories.vue +12 -11
  58. package/src/components/sidebar/use-floating-item.composable.ts +1 -1
  59. package/src/components/sidebarexpandableitem/MSidebarExpandableItem.vue +13 -7
  60. package/src/components/sidebarexpandableitem/README.md +2 -1
  61. package/src/components/sidebarfooter/MSidebarFooter.vue +16 -9
  62. package/src/components/sidebarfooter/_MSidebarFooterMenu.vue +10 -2
  63. package/src/components/sidebarnavitem/MSidebarNavItem.vue +36 -5
  64. package/src/components/sidebarnavitem/README.md +1 -0
  65. package/src/components/sidebarshortcutitem/MSidebarShortcutItem.vue +24 -3
  66. package/src/components/sidebarshortcutitem/README.md +3 -1
  67. package/src/components/sidebarshortcuts/MSidebarShortcuts.vue +6 -1
  68. package/src/components/sidebarshortcuts/README.md +12 -0
  69. package/src/components/statusbadge/MStatusBadge.stories.ts +5 -1
  70. package/src/components/statusbadge/MStatusBadge.vue +7 -1
  71. package/src/components/statusbadge/MstatusBadge.spec.ts +31 -0
  72. package/src/components/statusbadge/README.md +1 -0
  73. package/src/components/statusmessage/MStatusMessage.spec.ts +4 -4
  74. package/src/components/statusmessage/MStatusMessage.vue +5 -10
  75. package/src/components/statusnotification/MStatusNotification.spec.ts +9 -9
  76. package/src/components/statusnotification/MStatusNotification.vue +9 -8
  77. package/src/components/tabs/MTabs.stories.ts +30 -1
  78. package/src/components/tabs/MTabs.vue +12 -3
  79. package/src/components/tabs/Mtabs.spec.ts +19 -0
  80. package/src/components/tabs/README.md +1 -0
  81. package/src/components/tileselectable/MTileSelectable.spec.ts +69 -0
  82. package/src/components/tileselectable/MTileSelectable.stories.ts +8 -2
  83. package/src/components/tileselectable/MTileSelectable.vue +18 -1
  84. package/src/components/tileselectable/README.md +1 -0
  85. package/src/main.ts +3 -0
  86. package/src/utils/debounce.ts +15 -0
  87. package/src/components/BrandPresets.mdx +0 -146
  88. package/src/components/DarkMode.mdx +0 -115
@@ -0,0 +1,39 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import { describe, it, expect } from 'vitest';
3
+ import { defineComponent } from 'vue';
4
+ import MPageHeaderFeature from './MPageHeaderFeature.vue';
5
+
6
+ const IconStub = defineComponent({
7
+ name: 'IconStub',
8
+ template: '<svg class="icon-stub" />',
9
+ });
10
+
11
+ describe('MPageHeaderFeature', () => {
12
+ it('renders label and icon', () => {
13
+ const wrapper = mount(MPageHeaderFeature, {
14
+ props: {
15
+ label: 'Search',
16
+ icon: IconStub,
17
+ },
18
+ });
19
+
20
+ expect(wrapper.find('.mc-page-header__feature-item').exists()).toBe(true);
21
+ expect(wrapper.find('.mc-page-header__feature-item-icon').exists()).toBe(
22
+ true,
23
+ );
24
+ expect(wrapper.text()).toContain('Search');
25
+ });
26
+
27
+ it('emits action when clicked', async () => {
28
+ const wrapper = mount(MPageHeaderFeature, {
29
+ props: {
30
+ label: 'Help',
31
+ icon: IconStub,
32
+ },
33
+ });
34
+
35
+ await wrapper.find('.mc-page-header__feature-item').trigger('click');
36
+
37
+ expect(wrapper.emitted()).toHaveProperty('action');
38
+ });
39
+ });
@@ -0,0 +1,40 @@
1
+ <template>
2
+ <button
3
+ type="button"
4
+ class="mc-page-header__feature-item"
5
+ @click="emit('action')"
6
+ >
7
+ <component :is="props.icon" class="mc-page-header__feature-item-icon" />
8
+
9
+ <span>{{ props.label }}</span>
10
+ </button>
11
+ </template>
12
+
13
+ <script setup lang="ts">
14
+ import { type Component } from 'vue';
15
+
16
+ /**
17
+ * The Page Header Feature component renders a clickable feature item in the page header action area.
18
+ */
19
+ const props = defineProps<{
20
+ /**
21
+ * Text label displayed for the feature item.
22
+ */
23
+ label: string;
24
+ /**
25
+ * Icon component displayed next to the feature label.
26
+ */
27
+ icon: Component;
28
+ }>();
29
+
30
+ const emit = defineEmits<{
31
+ /**
32
+ * Emits when the feature item is clicked.
33
+ */
34
+ (on: 'action'): void;
35
+ }>();
36
+ </script>
37
+
38
+ <style lang="scss" scoped>
39
+ @use '@mozaic-ds/styles/components/page-header';
40
+ </style>
@@ -0,0 +1,17 @@
1
+ # MPageHeaderFeature
2
+
3
+ The Page Header Feature component renders a clickable feature item in the page header action area.
4
+
5
+
6
+ ## Props
7
+
8
+ | Name | Description | Type | Default |
9
+ | --- | --- | --- | --- |
10
+ | `label*` | Text label displayed for the feature item. | `string` | - |
11
+ | `icon*` | Icon component displayed next to the feature label. | `Component` | - |
12
+
13
+ ## Events
14
+
15
+ | Name | Description | Type |
16
+ | --- | --- | --- |
17
+ | `action` | Emits when the feature item is clicked. | [] |
@@ -59,3 +59,9 @@ export default meta;
59
59
  type Story = StoryObj<typeof MRadioGroup>;
60
60
 
61
61
  export const Default: Story = {};
62
+
63
+ export const Inline: Story = {
64
+ args: {
65
+ inline: true,
66
+ },
67
+ };
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="mc-field__container" :class="classObjectContainer">
2
+ <div class="mc-field__content" :class="classObjectContainer">
3
3
  <MRadio
4
4
  v-for="option in options"
5
5
  :id="option.id"
@@ -8,7 +8,6 @@
8
8
  :is-invalid="isInvalid"
9
9
  :name="name"
10
10
  class="mc-field__item"
11
- :class="classObjectItem"
12
11
  :model-value="modelValue === option.value"
13
12
  :disabled="option.disabled"
14
13
  @update:model-value="
@@ -56,13 +55,7 @@ const props = defineProps<{
56
55
 
57
56
  const classObjectContainer = computed(() => {
58
57
  return {
59
- 'mc-field__container--inline': props.inline,
60
- };
61
- });
62
-
63
- const classObjectItem = computed(() => {
64
- return {
65
- 'mc-field__container--inline__item': props.inline,
58
+ 'mc-field__content--inline': props.inline,
66
59
  };
67
60
  });
68
61
 
@@ -4,3 +4,5 @@ export const EXPANDED_SIDEBAR_KEY = Symbol() as InjectionKey<Ref<boolean>>;
4
4
  export const TOGGLE_SIDEBAR_KEY = Symbol() as InjectionKey<() => void>;
5
5
  export const SUB_ITEM_KEY = Symbol() as InjectionKey<boolean>;
6
6
  export const STACKED_SHORTCUTS_KEY = Symbol() as InjectionKey<boolean>;
7
+ export const FOOTER_ITEM_KEY = Symbol() as InjectionKey<boolean>;
8
+ export const HIDE_MENU_KEY = Symbol() as InjectionKey<() => void>;
@@ -70,6 +70,17 @@ export const Default: Story = {};
70
70
 
71
71
  export const ExpandOnly: Story = {
72
72
  render: () => ({
73
+ setup() {
74
+ const handleClose = action('close');
75
+ const handleUpdateModelValue = action('update:modelValue');
76
+ const handleLogOut = action('log-out');
77
+
78
+ return {
79
+ handleClose,
80
+ handleUpdateModelValue,
81
+ handleLogOut,
82
+ };
83
+ },
73
84
  components: { WithExpandOnlyStory },
74
85
  template: `
75
86
  <WithExpandOnlyStory
@@ -83,6 +94,17 @@ export const ExpandOnly: Story = {
83
94
 
84
95
  export const ProfileInfoOnly: Story = {
85
96
  render: () => ({
97
+ setup() {
98
+ const handleClose = action('close');
99
+ const handleUpdateModelValue = action('update:modelValue');
100
+ const handleLogOut = action('log-out');
101
+
102
+ return {
103
+ handleClose,
104
+ handleUpdateModelValue,
105
+ handleLogOut,
106
+ };
107
+ },
86
108
  components: { WithProfileInfoOnlyStory },
87
109
  template: `
88
110
  <WithProfileInfoOnlyStory
@@ -96,6 +118,17 @@ export const ProfileInfoOnly: Story = {
96
118
 
97
119
  export const SingleLevel: Story = {
98
120
  render: () => ({
121
+ setup() {
122
+ const handleClose = action('close');
123
+ const handleUpdateModelValue = action('update:modelValue');
124
+ const handleLogOut = action('log-out');
125
+
126
+ return {
127
+ handleClose,
128
+ handleUpdateModelValue,
129
+ handleLogOut,
130
+ };
131
+ },
99
132
  components: { WithSingleLevelStory },
100
133
  template: `
101
134
  <WithSingleLevelStory
@@ -5,7 +5,12 @@
5
5
  `mc-sidebar--${expanded ? 'expanded' : 'collapsed'}`,
6
6
  ]"
7
7
  >
8
- <div class="mc-sidebar__wrapper">
8
+ <div
9
+ :class="{
10
+ 'mc-sidebar__wrapper': true,
11
+ 'mc-sidebar__wrapper--divider': props.divider,
12
+ }"
13
+ >
9
14
  <slot name="header" />
10
15
 
11
16
  <slot name="shortcuts" />
@@ -55,6 +60,10 @@ const props = defineProps<{
55
60
  * the state of the expanded sidebar, bound via v-model.
56
61
  */
57
62
  modelValue: boolean;
63
+ /**
64
+ * Whether to display a right border on the sidebar.
65
+ */
66
+ divider?: boolean;
58
67
  }>();
59
68
 
60
69
  const emit = defineEmits<{
@@ -8,6 +8,7 @@ A sidebar is a vertical navigation component that provides quick access to key s
8
8
  | Name | Description | Type | Default |
9
9
  | --- | --- | --- | --- |
10
10
  | `modelValue*` | the state of the expanded sidebar, bound via v-model. | `boolean` | - |
11
+ | `divider` | Whether to display a right border on the sidebar. | `boolean` | - |
11
12
 
12
13
  ## Slots
13
14
 
@@ -1,6 +1,7 @@
1
1
  <template>
2
2
  <MSidebar
3
3
  v-model="expanded"
4
+ divider
4
5
  @update:model-value="emit('update:modelValue')"
5
6
  @close="emit('close')"
6
7
  >
@@ -96,25 +97,25 @@ const shortcutItems = [
96
97
  ];
97
98
 
98
99
  const navigationItems = [
99
- { label: 'Get Started', href: '#', icon: Release24 },
100
+ { label: 'Get Started', to: 'get-started', icon: Release24 },
100
101
  {
101
102
  label: 'Design Tokens',
102
103
  icon: Sample24,
103
104
  items: [
104
- { label: 'Subsection menu label 1', href: '#', locked: true },
105
- { label: 'Subsection menu label 2', href: '#', external: true },
106
- { label: 'Subsection menu label 2', href: '#' },
107
- { label: 'Subsection menu label 2', href: '#' },
105
+ { label: 'Subsection menu label 1', to: 'subsection-1', locked: true },
106
+ { label: 'Subsection menu label 2', to: 'subsection-2', external: true },
107
+ { label: 'Subsection menu label 2', to: 'subsection-3' },
108
+ { label: 'Subsection menu label 2', to: 'subsection-4' },
108
109
  ],
109
110
  },
110
- { label: 'Styles', href: '#', icon: Palette24 },
111
- { label: 'Components', href: '#', icon: Pantone24 },
111
+ { label: 'Styles', to: 'styles', icon: Palette24 },
112
+ { label: 'Components', to: 'components', icon: Pantone24 },
112
113
  ];
113
114
 
114
115
  const footerMenuItems = [
115
- { label: 'Action 01', icon: Admin24, href: '#' },
116
- { label: 'Action 02', icon: Admin24, href: '#' },
117
- { label: 'Action 03', icon: Admin24, href: '#' },
118
- { label: 'Action 04', icon: Admin24, href: '#' },
116
+ { label: 'Action 01', icon: Admin24, to: 'action-01' },
117
+ { label: 'Action 02', icon: Admin24, to: 'action-02' },
118
+ { label: 'Action 03', icon: Admin24, to: 'action-03' },
119
+ { label: 'Action 04', icon: Admin24, to: 'action-04' },
119
120
  ];
120
121
  </script>
@@ -73,7 +73,7 @@ export const useFloatingItem = (
73
73
  }
74
74
 
75
75
  function showFloatingItem() {
76
- floatingItem.value?.classList.toggle('mc-sidebar__floating-item--hidden');
76
+ floatingItem.value?.classList.remove('mc-sidebar__floating-item--hidden');
77
77
  floatingItemIsDisplayed.value = !floatingItem.value?.classList.contains(
78
78
  'mc-sidebar__floating-item--hidden',
79
79
  );
@@ -11,12 +11,12 @@
11
11
  <ChevronDown20 />
12
12
  </summary>
13
13
  <ul class="mc-sidebar__sublist">
14
- <slot name="default" />
14
+ <slot name="default" v-bind="{ close: hideFloatingItem }" />
15
15
  </ul>
16
16
  </details>
17
17
  </li>
18
18
 
19
- <div v-else class="mc-sidebar__item">
19
+ <div v-else class="mc-sidebar__item mc-sidebar__item--collapsed">
20
20
  <button
21
21
  ref="trigger"
22
22
  class="mc-sidebar__trigger mc-sidebar__link"
@@ -47,7 +47,7 @@
47
47
  <div class="mc-sidebar__listbox">
48
48
  <h3 class="mc-sidebar__listbox-title">{{ props.menuLabel }}</h3>
49
49
  <ul class="mc-sidebar__list">
50
- <slot name="default" />
50
+ <slot name="default" v-bind="{ close: hideFloatingItem }" />
51
51
  </ul>
52
52
  </div>
53
53
  </div>
@@ -65,7 +65,11 @@ import {
65
65
  } from 'vue';
66
66
  import { ChevronDown20 } from '@mozaic-ds/icons-vue';
67
67
  import { useFloatingItem } from '../sidebar/use-floating-item.composable';
68
- import { SUB_ITEM_KEY, EXPANDED_SIDEBAR_KEY } from '../sidebar/MSidebar.const';
68
+ import {
69
+ SUB_ITEM_KEY,
70
+ EXPANDED_SIDEBAR_KEY,
71
+ HIDE_MENU_KEY,
72
+ } from '../sidebar/MSidebar.const';
69
73
  /**
70
74
  * SidebarExpandableItem is a component that renders a sidebar item which can either expand to show a nested submenu or display a floating menu when the sidebar is collapsed. It supports optional icons, keyboard navigation, and accessibility attributes, and allows for custom submenu items via slots.
71
75
  */
@@ -87,12 +91,11 @@ const props = defineProps<{
87
91
  defineSlots<{
88
92
  /**
89
93
  * Contains one or more MSidebarNavItem components used to render the submenu items.
94
+ * The slot scope provides a `close` function that can be called to hide the floating menu.
90
95
  */
91
- default: VNode[];
96
+ default(props: { close: () => void }): VNode[];
92
97
  }>();
93
98
 
94
- const expanded = inject(EXPANDED_SIDEBAR_KEY);
95
-
96
99
  const id = useId();
97
100
 
98
101
  const trigger = useTemplateRef('trigger');
@@ -106,6 +109,9 @@ const {
106
109
  onListboxKeydown,
107
110
  } = useFloatingItem(trigger, listbox);
108
111
 
112
+ const expanded = inject(EXPANDED_SIDEBAR_KEY);
113
+
114
+ provide(HIDE_MENU_KEY, hideFloatingItem);
109
115
  provide(SUB_ITEM_KEY, true);
110
116
  </script>
111
117
 
@@ -15,7 +15,8 @@ SidebarExpandableItem is a component that renders a sidebar item which can eithe
15
15
 
16
16
  | Name | Description |
17
17
  | --- | --- |
18
- | `default` | Contains one or more MSidebarNavItem components used to render the submenu items. |
18
+ | `default` | Contains one or more MSidebarNavItem components used to render the submenu items.
19
+ The slot scope provides a `close` function that can be called to hide the floating menu. |
19
20
 
20
21
  ## Dependencies
21
22
 
@@ -39,7 +39,7 @@
39
39
  :hideFloatingItem="hideFloatingItem"
40
40
  @log-out="emit('log-out')"
41
41
  >
42
- <slot />
42
+ <slot v-bind="{ close: hideFloatingItem }" />
43
43
  </MSidebarFooterMenu>
44
44
  </component>
45
45
 
@@ -77,7 +77,7 @@
77
77
  :hideFloatingItem="hideFloatingItem"
78
78
  @log-out="emit('log-out')"
79
79
  >
80
- <slot />
80
+ <slot v-bind="{ close: hideFloatingItem }" />
81
81
  </MSidebarFooterMenu>
82
82
  </button>
83
83
  </div>
@@ -96,6 +96,8 @@ import {
96
96
  } from '@mozaic-ds/icons-vue';
97
97
  import {
98
98
  EXPANDED_SIDEBAR_KEY,
99
+ FOOTER_ITEM_KEY,
100
+ HIDE_MENU_KEY,
99
101
  SUB_ITEM_KEY,
100
102
  TOGGLE_SIDEBAR_KEY,
101
103
  } from '../sidebar/MSidebar.const';
@@ -136,8 +138,9 @@ withDefaults(
136
138
  defineSlots<{
137
139
  /**
138
140
  * Contains one or more MSidebarNavItem components used to render the profile items.
141
+ * The slot scope provides a `close` function that can be called to hide the floating menu.
139
142
  */
140
- default: VNode[];
143
+ default(props: { close: () => void }): VNode[];
141
144
  }>();
142
145
 
143
146
  const emit = defineEmits<{
@@ -147,11 +150,6 @@ const emit = defineEmits<{
147
150
  (on: 'log-out'): void;
148
151
  }>();
149
152
 
150
- provide(SUB_ITEM_KEY, true);
151
-
152
- const toggleSidebar = inject(TOGGLE_SIDEBAR_KEY);
153
- const expanded = inject(EXPANDED_SIDEBAR_KEY);
154
-
155
153
  const trigger = useTemplateRef('trigger');
156
154
  const listbox = useTemplateRef('listbox');
157
155
 
@@ -171,6 +169,13 @@ const {
171
169
  position: 'top',
172
170
  });
173
171
 
172
+ provide(SUB_ITEM_KEY, true);
173
+ provide(FOOTER_ITEM_KEY, true);
174
+ provide(HIDE_MENU_KEY, hideFloatingItem);
175
+
176
+ const toggleSidebar = inject(TOGGLE_SIDEBAR_KEY);
177
+ const expanded = inject(EXPANDED_SIDEBAR_KEY);
178
+
174
179
  const desktopTriggerAttrs = computed(() => {
175
180
  if (isMobile.value) return {};
176
181
 
@@ -182,7 +187,9 @@ const desktopTriggerAttrs = computed(() => {
182
187
  return expanded?.value
183
188
  ? {
184
189
  ...baseAttrs,
185
- onClick: showFloatingItem,
190
+ onClick: floatingItemIsDisplayed.value
191
+ ? hideFloatingItem
192
+ : showFloatingItem,
186
193
  }
187
194
  : {
188
195
  ...baseAttrs,
@@ -8,6 +8,7 @@
8
8
  'mc-sidebar__floating-item mc-sidebar__floating-item--hidden': true,
9
9
  'mc-sidebar__floating-item--expanded': expanded,
10
10
  }"
11
+ @click.stop
11
12
  v-bind="
12
13
  expanded
13
14
  ? {}
@@ -23,7 +24,7 @@
23
24
  <slot name="default" />
24
25
  </ul>
25
26
 
26
- <MDivider class="mc-sidebar__footer-menu-divider" />
27
+ <MDivider v-if="hasNavItems" class="mc-sidebar__footer-menu-divider" />
27
28
 
28
29
  <MSidebarNavItem
29
30
  tag="div"
@@ -36,7 +37,7 @@
36
37
  </template>
37
38
 
38
39
  <script setup lang="ts">
39
- import { useTemplateRef } from 'vue';
40
+ import { computed, useSlots, useTemplateRef, type VNode } from 'vue';
40
41
  import MDivider from '../divider/MDivider.vue';
41
42
  import MSidebarNavItem from '../sidebarnavitem/MSidebarNavItem.vue';
42
43
  import { LogOut24 } from '@mozaic-ds/icons-vue';
@@ -54,6 +55,13 @@ const emit = defineEmits<{
54
55
 
55
56
  const container = useTemplateRef('container');
56
57
 
58
+ const slots = useSlots();
59
+
60
+ const hasNavItems = computed(() => {
61
+ const content = slots.default?.() ?? [];
62
+ return content.some((vnode: VNode) => vnode.children?.length);
63
+ });
64
+
57
65
  defineExpose({
58
66
  container,
59
67
  });
@@ -8,13 +8,14 @@
8
8
  }"
9
9
  >
10
10
  <component
11
- :is="href ? 'a' : 'button'"
11
+ :is="itemTag"
12
12
  ref="trigger"
13
13
  :href="href"
14
+ :to="to"
14
15
  :target="external ? '_blank' : '_self'"
15
16
  :class="{
16
17
  'mc-sidebar__link': true,
17
- 'mc-sidebar__link--selected': active,
18
+ 'mc-sidebar__link--selected': active && !isFooterItem,
18
19
  'mc-sidebar__link--locked': locked,
19
20
  }"
20
21
  :aria-label="label"
@@ -22,7 +23,8 @@
22
23
  @focus="showFloatingItem"
23
24
  @mouseleave="hideFloatingItem"
24
25
  @blur="hideFloatingItem"
25
- @click="emit('click', $event)"
26
+ @click="handleClick"
27
+ v-bind="itemTag === 'button' ? { type: 'button' } : {}"
26
28
  >
27
29
  <component :is="icon" class="mc-sidebar__icon" />
28
30
 
@@ -47,6 +49,7 @@
47
49
  <script setup lang="ts">
48
50
  import { Lock20, ExternalLink20 } from '@mozaic-ds/icons-vue';
49
51
  import {
52
+ computed,
50
53
  inject,
51
54
  useId,
52
55
  useTemplateRef,
@@ -54,12 +57,17 @@ import {
54
57
  type ShallowRef,
55
58
  } from 'vue';
56
59
  import MTooltip from '../tooltip/MTooltip.vue';
57
- import { SUB_ITEM_KEY, EXPANDED_SIDEBAR_KEY } from '../sidebar/MSidebar.const';
60
+ import {
61
+ SUB_ITEM_KEY,
62
+ EXPANDED_SIDEBAR_KEY,
63
+ FOOTER_ITEM_KEY,
64
+ HIDE_MENU_KEY,
65
+ } from '../sidebar/MSidebar.const';
58
66
  import { useFloatingItem } from '../sidebar/use-floating-item.composable';
59
67
  /**
60
68
  * 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
69
  */
62
- defineProps<{
70
+ const props = defineProps<{
63
71
  tag?: string;
64
72
  /**
65
73
  * The text label displayed for the navigation item.
@@ -73,6 +81,10 @@ defineProps<{
73
81
  * The URL the navigation item links to.
74
82
  */
75
83
  href?: string;
84
+ /**
85
+ * If defined, the item will act as a router-link to the specified route.
86
+ */
87
+ to?: string;
76
88
  /**
77
89
  * Marks the item as locked. Shows a lock icon and prevents interaction.
78
90
  */
@@ -93,6 +105,9 @@ const id = useId();
93
105
 
94
106
  const expanded = inject(EXPANDED_SIDEBAR_KEY);
95
107
  const isSubItem = inject(SUB_ITEM_KEY, false);
108
+ const isFooterItem = inject(FOOTER_ITEM_KEY, false);
109
+
110
+ const hideFooterMenu = inject(HIDE_MENU_KEY);
96
111
 
97
112
  const trigger = useTemplateRef(
98
113
  'trigger',
@@ -106,6 +121,22 @@ const { hideFloatingItem, showFloatingItem } = useFloatingItem(
106
121
  allowItemHover: false,
107
122
  },
108
123
  );
124
+
125
+ const itemTag = computed(() => {
126
+ if (props.href) return 'a';
127
+ if (props.to) return 'router-link';
128
+ return 'button';
129
+ });
130
+
131
+ function handleClick(event: MouseEvent) {
132
+ if (props.locked) {
133
+ event.preventDefault();
134
+ event.stopPropagation();
135
+ return;
136
+ }
137
+ emit('click', event);
138
+ hideFooterMenu?.();
139
+ }
109
140
  </script>
110
141
 
111
142
  <style lang="scss" scoped>
@@ -11,6 +11,7 @@ SidebarNavItem is a component that renders a sidebar navigation item with option
11
11
  | `label*` | The text label displayed for the navigation item. | `string` | - |
12
12
  | `icon` | Optional icon component displayed alongside the label. | `Component` | - |
13
13
  | `href` | The URL the navigation item links to. | `string` | - |
14
+ | `to` | If defined, the item will act as a router-link to the specified route. | `string` | - |
14
15
  | `locked` | Marks the item as locked. Shows a lock icon and prevents interaction. | `boolean` | - |
15
16
  | `external` | Indicates the link is external. Opens in a new tab and shows an external link icon. | `boolean` | - |
16
17
  | `active` | Marks the item as active, applying the active styling. | `boolean` | - |
@@ -8,14 +8,27 @@
8
8
  ]"
9
9
  role="menuitem"
10
10
  >
11
- <a :href="props.href" class="mc-sidebar__shortcut-link">
11
+ <component
12
+ :is="props.to ? 'router-link' : 'a'"
13
+ :target="props.external ? '_blank' : '_self'"
14
+ class="mc-sidebar__shortcut-link"
15
+ v-bind="
16
+ props.to
17
+ ? {
18
+ to: props.to,
19
+ }
20
+ : {
21
+ href: props.href,
22
+ }
23
+ "
24
+ >
12
25
  <component
13
26
  v-if="props.icon"
14
27
  :is="props.icon"
15
28
  class="mc-sidebar__shortcut-icon"
16
29
  />
17
30
  <span class="mc-sidebar__shortcut-label">{{ props.label }}</span>
18
- </a>
31
+ </component>
19
32
  </li>
20
33
  </template>
21
34
 
@@ -36,7 +49,15 @@ const props = defineProps<{
36
49
  /**
37
50
  * The URL the shortcut item links to.
38
51
  */
39
- href: string;
52
+ href?: string;
53
+ /**
54
+ * If defined, the item will act as a router-link to the specified route.
55
+ */
56
+ to?: string;
57
+ /**
58
+ * Indicates the link is external and opens in a new tab.
59
+ */
60
+ external?: boolean;
40
61
  /**
41
62
  * Optional icon component displayed alongside the label.
42
63
  */
@@ -8,7 +8,9 @@ SidebarShortcutItem is a component that renders a sidebar shortcut link with an
8
8
  | Name | Description | Type | Default |
9
9
  | --- | --- | --- | --- |
10
10
  | `label*` | The text label displayed for the shortcut item. | `string` | - |
11
- | `href*` | The URL the shortcut item links to. | `string` | - |
11
+ | `href` | The URL the shortcut item links to. | `string` | - |
12
+ | `to` | If defined, the item will act as a router-link to the specified route. | `string` | - |
13
+ | `external` | Indicates the link is external and opens in a new tab. | `boolean` | - |
12
14
  | `icon` | Optional icon component displayed alongside the label. | `Component` | - |
13
15
 
14
16
  ## Dependencies
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <section v-if="expanded">
2
+ <section v-if="expanded" class="mc-sidebar__shortcuts">
3
3
  <nav role="navigation" aria-label="shortcut items">
4
4
  <ul
5
5
  :class="{
@@ -12,6 +12,8 @@
12
12
  <slot name="default" />
13
13
  </ul>
14
14
  </nav>
15
+
16
+ <MDivider />
15
17
  </section>
16
18
 
17
19
  <div v-else class="mc-sidebar__shortcut-wrapper">
@@ -32,6 +34,8 @@
32
34
  <ViewGridX424 />
33
35
  </button>
34
36
 
37
+ <MDivider />
38
+
35
39
  <div
36
40
  ref="listbox"
37
41
  :id="`listbox-${id}`"
@@ -54,6 +58,7 @@
54
58
  </template>
55
59
 
56
60
  <script setup lang="ts">
61
+ import MDivider from '../divider/MDivider.vue';
57
62
  import { ViewGridX424 } from '@mozaic-ds/icons-vue';
58
63
  import { inject, provide, useId, useTemplateRef, type VNode } from 'vue';
59
64
  import { useFloatingItem } from '../sidebar/use-floating-item.composable';