@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
@@ -17,6 +17,18 @@ SidebarShortcuts is a component that displays a list of sidebar shortcut items.
17
17
 
18
18
  ## Dependencies
19
19
 
20
+ ### Depends on
21
+
22
+ - [MDivider](../divider)
23
+
24
+ ### Graph
25
+
26
+ ```mermaid
27
+ graph TD;
28
+ MSidebarShortcuts --> MDivider
29
+ style MSidebarShortcuts fill:#008240,stroke:#333,stroke-width:4px
30
+ ```
31
+
20
32
  ### Used By
21
33
 
22
34
  - [DefaultCase.stories](../sidebar/stories)
@@ -12,7 +12,7 @@ const meta: Meta<typeof MStatusBadge> = {
12
12
  },
13
13
  },
14
14
  },
15
- args: { label: 'Badge label' },
15
+ args: { label: 'Badge label', size: 'm' },
16
16
  render: (args) => ({
17
17
  components: { MStatusBadge },
18
18
  setup() {
@@ -28,6 +28,10 @@ type Story = StoryObj<typeof MStatusBadge>;
28
28
 
29
29
  export const Info: Story = {};
30
30
 
31
+ export const Small: Story = {
32
+ args: { size: 's' },
33
+ };
34
+
31
35
  export const Success: Story = {
32
36
  args: { status: 'success' },
33
37
  };
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div class="mc-status-badge" :class="classObject">
3
- <MStatusDot :status="status" />
3
+ <MStatusDot :status="status" :size="size" />
4
4
  <span class="mc-status-badge__label">{{ label }}</span>
5
5
  </div>
6
6
  </template>
@@ -17,12 +17,17 @@ const props = withDefaults(
17
17
  * Content of the status badge
18
18
  */
19
19
  label: string;
20
+ /**
21
+ * Allows to define the status badge size
22
+ */
23
+ size?: 's' | 'm';
20
24
  /**
21
25
  * Allows to define the status badge type
22
26
  */
23
27
  status?: 'info' | 'success' | 'warning' | 'error' | 'neutral';
24
28
  }>(),
25
29
  {
30
+ size: 'm',
26
31
  status: 'info',
27
32
  },
28
33
  );
@@ -31,6 +36,7 @@ const classObject = computed(() => {
31
36
  return {
32
37
  [`mc-status-badge--${props.status}`]:
33
38
  props.status && props.status != 'info',
39
+ [`mc-status-badge--${props.size}`]: props.size && props.size != 'm',
34
40
  };
35
41
  });
36
42
  </script>
@@ -13,4 +13,35 @@ describe('MStatusBadge component', () => {
13
13
 
14
14
  expect(wrapper.text()).toContain('Badge Label');
15
15
  });
16
+
17
+ describe('size', () => {
18
+ it('does not add a size modifier class when size is "m" (default)', () => {
19
+ const wrapper = mount(MStatusBadge, { props: { label: 'Badge' } });
20
+ expect(wrapper.classes()).not.toContain('mc-status-badge--m');
21
+ });
22
+
23
+ it('adds the "mc-status-badge--s" class when size is "s"', () => {
24
+ const wrapper = mount(MStatusBadge, {
25
+ props: { label: 'Badge', size: 's' },
26
+ });
27
+ expect(wrapper.classes()).toContain('mc-status-badge--s');
28
+ });
29
+ });
30
+
31
+ describe('status', () => {
32
+ it('does not add a status modifier class when status is "info" (default)', () => {
33
+ const wrapper = mount(MStatusBadge, { props: { label: 'Badge' } });
34
+ expect(wrapper.classes()).not.toContain('mc-status-badge--info');
35
+ });
36
+
37
+ it.each(['success', 'warning', 'error', 'neutral'] as const)(
38
+ 'adds the "mc-status-badge--%s" class when status is "%s"',
39
+ (status) => {
40
+ const wrapper = mount(MStatusBadge, {
41
+ props: { label: 'Badge', status },
42
+ });
43
+ expect(wrapper.classes()).toContain(`mc-status-badge--${status}`);
44
+ },
45
+ );
46
+ });
16
47
  });
@@ -8,6 +8,7 @@ A Status Badge is used to indicate the current status of an element, providing a
8
8
  | Name | Description | Type | Default |
9
9
  | --- | --- | --- | --- |
10
10
  | `label*` | Content of the status badge | `string` | - |
11
+ | `size` | Allows to define the status badge size | `"s"` `"m"` | `"m"` |
11
12
  | `status` | Allows to define the status badge type | `"info"` `"warning"` `"error"` `"success"` `"neutral"` | `"info"` |
12
13
 
13
14
  ## Dependencies
@@ -4,8 +4,8 @@ import MStatusMessage from './MStatusMessage.vue';
4
4
 
5
5
  import {
6
6
  InfoCircleFilled24,
7
- WarningCircleFilled24,
8
- CrossCircleFilled24,
7
+ WarningTriangleFilled24,
8
+ ErrorFilled24,
9
9
  CheckCircleFilled24,
10
10
  } from '@mozaic-ds/icons-vue';
11
11
 
@@ -56,7 +56,7 @@ describe('MStatusMessage.vue', () => {
56
56
  props: { status: 'warning', label: 'Warning' },
57
57
  });
58
58
 
59
- expect(wrapper.findComponent(WarningCircleFilled24).exists()).toBe(true);
59
+ expect(wrapper.findComponent(WarningTriangleFilled24).exists()).toBe(true);
60
60
  });
61
61
 
62
62
  it('renders the error icon when status is error', () => {
@@ -64,7 +64,7 @@ describe('MStatusMessage.vue', () => {
64
64
  props: { status: 'error', label: 'Error' },
65
65
  });
66
66
 
67
- expect(wrapper.findComponent(CrossCircleFilled24).exists()).toBe(true);
67
+ expect(wrapper.findComponent(ErrorFilled24).exists()).toBe(true);
68
68
  });
69
69
 
70
70
  it('adds correct modifier class for non-info statuses', () => {
@@ -15,8 +15,8 @@
15
15
  import { computed } from 'vue';
16
16
  import {
17
17
  InfoCircleFilled24,
18
- WarningCircleFilled24,
19
- CrossCircleFilled24,
18
+ WarningTriangleFilled24,
19
+ ErrorFilled24,
20
20
  CheckCircleFilled24,
21
21
  } from '@mozaic-ds/icons-vue';
22
22
  import MLoader from '../loader/MLoader.vue';
@@ -29,12 +29,7 @@ const props = withDefaults(
29
29
  * Allows to define the status message style.
30
30
  */
31
31
  status?:
32
- | 'info'
33
- | 'success'
34
- | 'warning'
35
- | 'error'
36
- | 'neutral'
37
- | 'inprogress';
32
+ 'info' | 'success' | 'warning' | 'error' | 'neutral' | 'inprogress';
38
33
  /**
39
34
  * Allows to define the status message appearance.
40
35
  * The accent status is used to highlight a specific message.
@@ -64,9 +59,9 @@ const iconComponent = computed(() => {
64
59
  case 'success':
65
60
  return CheckCircleFilled24;
66
61
  case 'warning':
67
- return WarningCircleFilled24;
62
+ return WarningTriangleFilled24;
68
63
  case 'error':
69
- return CrossCircleFilled24;
64
+ return ErrorFilled24;
70
65
  case 'info':
71
66
  default:
72
67
  return InfoCircleFilled24;
@@ -1,11 +1,11 @@
1
1
  import { mount } from '@vue/test-utils';
2
- import { describe, it, expect } from 'vitest';
2
+ import { describe, expect, it } from 'vitest';
3
3
  import MStatusNotification from './MStatusNotification.vue';
4
4
  import {
5
- InfoCircleFilled32,
6
- WarningCircleFilled32,
7
- CrossCircleFilled32,
8
- CheckCircleFilled32,
5
+ InfoCircleFilled24,
6
+ WarningTriangleFilled24,
7
+ ErrorFilled24,
8
+ CheckCircleFilled24,
9
9
  } from '@mozaic-ds/icons-vue';
10
10
 
11
11
  describe('MStatusNotification.vue', () => {
@@ -19,7 +19,7 @@ describe('MStatusNotification.vue', () => {
19
19
 
20
20
  expect(wrapper.text()).toContain('Test Title');
21
21
  expect(wrapper.text()).toContain('Test Description');
22
- expect(wrapper.findComponent(InfoCircleFilled32).exists()).toBe(true); // Default icon is InfoCircle32
22
+ expect(wrapper.findComponent(InfoCircleFilled24).exists()).toBe(true);
23
23
  expect(wrapper.classes()).toContain('mc-status-notification');
24
24
  });
25
25
 
@@ -31,7 +31,7 @@ describe('MStatusNotification.vue', () => {
31
31
  status: 'success',
32
32
  },
33
33
  });
34
- expect(wrapperSuccess.findComponent(CheckCircleFilled32).exists()).toBe(
34
+ expect(wrapperSuccess.findComponent(CheckCircleFilled24).exists()).toBe(
35
35
  true,
36
36
  );
37
37
 
@@ -42,7 +42,7 @@ describe('MStatusNotification.vue', () => {
42
42
  status: 'warning',
43
43
  },
44
44
  });
45
- expect(wrapperWarning.findComponent(WarningCircleFilled32).exists()).toBe(
45
+ expect(wrapperWarning.findComponent(WarningTriangleFilled24).exists()).toBe(
46
46
  true,
47
47
  );
48
48
 
@@ -53,7 +53,7 @@ describe('MStatusNotification.vue', () => {
53
53
  status: 'error',
54
54
  },
55
55
  });
56
- expect(wrapperError.findComponent(CrossCircleFilled32).exists()).toBe(true);
56
+ expect(wrapperError.findComponent(ErrorFilled24).exists()).toBe(true);
57
57
  });
58
58
 
59
59
  it('should show the close button if closable prop is true', () => {
@@ -36,12 +36,13 @@
36
36
  import { computed, type VNode } from 'vue';
37
37
  import {
38
38
  Cross20,
39
- InfoCircleFilled32,
40
- WarningCircleFilled32,
41
- CrossCircleFilled32,
42
- CheckCircleFilled32,
39
+ InfoCircleFilled24,
40
+ WarningTriangleFilled24,
41
+ ErrorFilled24,
42
+ CheckCircleFilled24,
43
43
  } from '@mozaic-ds/icons-vue';
44
44
  import MIconButton from '../iconbutton/MIconButton.vue';
45
+
45
46
  /**
46
47
  * A Status Notification is used to draw the user’s attention to important information that needs to be acknowledged. It often provides feedback on a process, highlights a status update, or alerts users about an issue. Notifications are typically triggered by user actions or system events and are designed to be easily noticeable while maintaining a non-intrusive experience.
47
48
  */
@@ -86,14 +87,14 @@ const classObject = computed(() => {
86
87
  const iconComponent = computed(() => {
87
88
  switch (props.status) {
88
89
  case 'success':
89
- return CheckCircleFilled32;
90
+ return CheckCircleFilled24;
90
91
  case 'warning':
91
- return WarningCircleFilled32;
92
+ return WarningTriangleFilled24;
92
93
  case 'error':
93
- return CrossCircleFilled32;
94
+ return ErrorFilled24;
94
95
  case 'info':
95
96
  default:
96
- return InfoCircleFilled32;
97
+ return InfoCircleFilled24;
97
98
  }
98
99
  });
99
100
 
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite';
2
2
  import { action } from 'storybook/actions';
3
3
 
4
4
  import Mtabs from './MTabs.vue';
5
- import { ChevronRight24 } from '@mozaic-ds/icons-vue';
5
+ import { ChevronRight20, ChevronRight24 } from '@mozaic-ds/icons-vue';
6
6
 
7
7
  const meta: Meta<typeof Mtabs> = {
8
8
  title: 'Navigation/Tabs',
@@ -17,6 +17,7 @@ const meta: Meta<typeof Mtabs> = {
17
17
  },
18
18
  args: {
19
19
  modelValue: 'label1',
20
+ size: 'm',
20
21
  tabs: [
21
22
  {
22
23
  id: 'label1',
@@ -57,6 +58,34 @@ type Story = StoryObj<typeof Mtabs>;
57
58
 
58
59
  export const Default: Story = {};
59
60
 
61
+ export const Small: Story = {
62
+ args: {
63
+ size: 's',
64
+ tabs: [
65
+ {
66
+ id: 'label1',
67
+ label: 'Label',
68
+ icon: ChevronRight20,
69
+ },
70
+ {
71
+ id: 'label2',
72
+ label: 'Label',
73
+ icon: ChevronRight20,
74
+ },
75
+ {
76
+ id: 'label3',
77
+ label: 'Label',
78
+ icon: ChevronRight20,
79
+ },
80
+ {
81
+ id: 'label4',
82
+ label: 'Label',
83
+ icon: ChevronRight20,
84
+ },
85
+ ],
86
+ },
87
+ };
88
+
60
89
  export const Icons: Story = {
61
90
  args: {
62
91
  tabs: [
@@ -23,9 +23,12 @@
23
23
  @click="onClickTab(index, tab.id)"
24
24
  @keydown="onKeydown($event, index)"
25
25
  >
26
- <span v-if="tab.icon" class="mc-tabs__icon">
27
- <component :is="tab.icon" aria-hidden="true" />
28
- </span>
26
+ <component
27
+ v-if="tab.icon"
28
+ :is="tab.icon"
29
+ class="mc-tabs__icon"
30
+ aria-hidden="true"
31
+ />
29
32
  <div class="mc-tabs__label">
30
33
  <span>{{ tab.label }}</span>
31
34
  </div>
@@ -52,6 +55,10 @@ const props = withDefaults(
52
55
  * A description indicating the purpose of the set of tabs. Useful for improving the accessibility of the component.
53
56
  */
54
57
  description?: string;
58
+ /**
59
+ * Defines the size of the tab.
60
+ */
61
+ size?: 's' | 'm';
55
62
  /**
56
63
  * If `true`, the divider will appear.
57
64
  */
@@ -95,6 +102,7 @@ const props = withDefaults(
95
102
  }>(),
96
103
  {
97
104
  modelValue: 0,
105
+ size: 'm',
98
106
  divider: true,
99
107
  },
100
108
  );
@@ -102,6 +110,7 @@ const props = withDefaults(
102
110
  const classObject = computed(() => {
103
111
  return {
104
112
  'mc-tabs--centered': props.centered,
113
+ [`mc-tabs--${props.size}`]: props.size !== 'm',
105
114
  };
106
115
  });
107
116
 
@@ -103,6 +103,25 @@ describe('MTabs.vue', () => {
103
103
  });
104
104
  });
105
105
 
106
+ it('does not add size class when size is "m" (default)', () => {
107
+ const wrapper = mount(MTabs, { props: { tabs } });
108
+ expect(wrapper.classes()).not.toContain('mc-tabs--m');
109
+ expect(wrapper.classes()).not.toContain('mc-tabs--s');
110
+ });
111
+
112
+ it('adds mc-tabs--s class when size is "s" and removes it when changed to "m"', async () => {
113
+ const wrapper = mount(MTabs, {
114
+ props: { tabs, size: 's' },
115
+ });
116
+
117
+ expect(wrapper.classes()).toContain('mc-tabs--s');
118
+
119
+ await wrapper.setProps({ size: 'm' });
120
+
121
+ expect(wrapper.classes()).not.toContain('mc-tabs--s');
122
+ expect(wrapper.classes()).not.toContain('mc-tabs--m');
123
+ });
124
+
106
125
  it('adds divider and centered classes based on props', async () => {
107
126
  const wrapper = mount(MTabs, {
108
127
  props: {
@@ -8,6 +8,7 @@ Tabs are a navigation component that allows users to switch between different se
8
8
  | Name | Description | Type | Default |
9
9
  | --- | --- | --- | --- |
10
10
  | `description` | A description indicating the purpose of the set of tabs. Useful for improving the accessibility of the component. | `string` | - |
11
+ | `size` | Defines the size of the tab. | `"s"` `"m"` | `"m"` |
11
12
  | `divider` | If `true`, the divider will appear. | `boolean` | `true` |
12
13
  | `centered` | If `true`, the tabs of the component will be centered. | `boolean` | - |
13
14
  | `modelValue` | Defines the currently active tab.
@@ -174,4 +174,73 @@ describe('MTileSelectable', () => {
174
174
 
175
175
  expect(wrapper.find('.mc-tile__content').exists()).toBe(false);
176
176
  });
177
+
178
+ it('emits update:modelValue when card is clicked and clickable is true', async () => {
179
+ const wrapper = mount(MTileSelectable, {
180
+ props: {
181
+ modelValue: false,
182
+ trigger: 'container',
183
+ },
184
+ });
185
+
186
+ await wrapper.find('.mc-tile').trigger('click');
187
+
188
+ expect(wrapper.emitted('update:modelValue')).toBeTruthy();
189
+ expect(wrapper.emitted('update:modelValue')![0]).toEqual([true]);
190
+ });
191
+
192
+ it('does not emit update:modelValue when card is clicked but clickable is false', async () => {
193
+ const wrapper = mount(MTileSelectable, {
194
+ props: {
195
+ modelValue: false,
196
+ },
197
+ });
198
+
199
+ await wrapper.find('.mc-tile').trigger('click');
200
+
201
+ expect(wrapper.emitted('update:modelValue')).toBeFalsy();
202
+ });
203
+
204
+ it('toggles off when card is clicked and already selected with clickable and checkbox', async () => {
205
+ const wrapper = mount(MTileSelectable, {
206
+ props: {
207
+ modelValue: true,
208
+ trigger: 'container',
209
+ inputType: 'checkbox',
210
+ },
211
+ });
212
+
213
+ await wrapper.find('.mc-tile').trigger('click');
214
+
215
+ expect(wrapper.emitted('update:modelValue')![0]).toEqual([false]);
216
+ });
217
+
218
+ it('always sets true when card is clicked with clickable and radio', async () => {
219
+ const wrapper = mount(MTileSelectable, {
220
+ props: {
221
+ modelValue: false,
222
+ trigger: 'container',
223
+ inputType: 'radio',
224
+ },
225
+ });
226
+
227
+ await wrapper.find('.mc-tile').trigger('click');
228
+
229
+ expect(wrapper.emitted('update:modelValue')![0]).toEqual([true]);
230
+ });
231
+
232
+ it('does not double-toggle when the input itself is clicked with clickable enabled', async () => {
233
+ const wrapper = mount(MTileSelectable, {
234
+ props: {
235
+ modelValue: false,
236
+ trigger: 'container',
237
+ inputType: 'checkbox',
238
+ },
239
+ });
240
+
241
+ const input = wrapper.find('input[type="checkbox"]');
242
+ await input.trigger('click');
243
+ // Clicking the input itself should not invoke the card click handler
244
+ expect(wrapper.emitted('update:modelValue')).toBeFalsy();
245
+ });
177
246
  });
@@ -42,14 +42,20 @@ type Story = StoryObj<typeof MTileSelectable>;
42
42
 
43
43
  export const Default: Story = {};
44
44
 
45
- export const RadioButton: Story = {
45
+ export const Radio: Story = {
46
46
  args: {
47
47
  inputType: 'radio',
48
48
  },
49
49
  };
50
50
 
51
- export const LeftPositionInput: Story = {
51
+ export const Left: Story = {
52
52
  args: {
53
53
  inputPosition: 'left',
54
54
  },
55
55
  };
56
+
57
+ export const ClickableContainer: Story = {
58
+ args: {
59
+ trigger: 'container',
60
+ },
61
+ };
@@ -8,7 +8,11 @@
8
8
  'mc-tile--input-center': props.centered,
9
9
  'mc-tile--no-input': props.hideInput,
10
10
  }"
11
- v-bind="props.hideInput && { onClick: () => (checked = !checked) }"
11
+ v-bind="
12
+ (props.hideInput || props.trigger === 'container') && {
13
+ onClick: handleCardClick,
14
+ }
15
+ "
12
16
  >
13
17
  <div v-if="$slots.details" class="mc-tile__header">
14
18
  <div class="mc-tile__body">
@@ -22,6 +26,7 @@
22
26
  :id="`checkbox-${id}`"
23
27
  name="tile-select-group"
24
28
  aria-label="Tile checkbox"
29
+ @click.stop
25
30
  />
26
31
 
27
32
  <MRadio
@@ -30,6 +35,7 @@
30
35
  :id="`radio-${id}`"
31
36
  name="tile-select-group"
32
37
  aria-label="Tile radio button"
38
+ @click.stop
33
39
  />
34
40
  </div>
35
41
  </div>
@@ -46,6 +52,7 @@
46
52
  :id="`checkbox-${id}`"
47
53
  name="tile-select-group"
48
54
  aria-label="Tile checkbox"
55
+ @click.stop
49
56
  />
50
57
 
51
58
  <MRadio
@@ -54,6 +61,7 @@
54
61
  :id="`radio-${id}`"
55
62
  name="tile-select-group"
56
63
  aria-label="Tile radio button"
64
+ @click.stop
57
65
  />
58
66
  </div>
59
67
  </template>
@@ -99,6 +107,10 @@ const props = withDefaults(
99
107
  * If `true`, hides the input and makes the whole tile selectable.
100
108
  */
101
109
  hideInput?: boolean;
110
+ /**
111
+ * Determines how the tile expansion is triggered.
112
+ */
113
+ trigger?: 'container' | 'input';
102
114
  }>(),
103
115
  {
104
116
  inputType: 'checkbox',
@@ -135,6 +147,11 @@ const checked = computed({
135
147
  emit('update:modelValue', value);
136
148
  },
137
149
  });
150
+
151
+ function handleCardClick() {
152
+ if (props.inputType === 'radio' && checked.value) return;
153
+ checked.value = !checked.value;
154
+ }
138
155
  </script>
139
156
 
140
157
  <style lang="scss" scoped>
@@ -13,6 +13,7 @@ A tile is a container component used to group related content and actions within
13
13
  | `inputPosition` | Position of the input relative to the tile content. | `"left"` `"right"` | `"right"` |
14
14
  | `centered` | Vertical alignment of the input. | `boolean` | - |
15
15
  | `hideInput` | If `true`, hides the input and makes the whole tile selectable. | `boolean` | - |
16
+ | `trigger` | Determines how the tile expansion is triggered. | `"input"` `"container"` | - |
16
17
 
17
18
  ## Slots
18
19
 
package/src/main.ts CHANGED
@@ -32,8 +32,11 @@ export { default as MLoadingOverlay } from './components/loadingoverlay/MLoading
32
32
  export { default as MModal } from './components/modal/MModal.vue';
33
33
  export { default as MNavigationIndicator } from './components/navigationindicator/MNavigationIndicator.vue';
34
34
  export { default as MNumberBadge } from './components/numberbadge/MNumberBadge.vue';
35
+ export { default as MOptionListbox } from './components/optionListbox/MOptionListbox.vue';
35
36
  export { default as MOverlay } from './components/overlay/MOverlay.vue';
36
37
  export { default as MPageHeader } from './components/pageheader/MPageHeader.vue';
38
+ export { default as MPageHeaderContext } from './components/pageheadercontext/MPageHeaderContext.vue';
39
+ export { default as MPageHeaderFeature } from './components/pageheaderfeature/MPageHeaderFeature.vue';
37
40
  export { default as MPagination } from './components/pagination/MPagination.vue';
38
41
  export { default as MPasswordInput } from './components/passwordinput/MPasswordInput.vue';
39
42
  export { default as MPhoneNumber } from './components/phonenumber/MPhoneNumber.vue';
@@ -0,0 +1,15 @@
1
+ export function debounce<T extends (...args: unknown[]) => unknown>(
2
+ fn: T,
3
+ wait: number,
4
+ ): (...args: Parameters<T>) => void {
5
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
6
+
7
+ return function (this: unknown, ...args: Parameters<T>) {
8
+ if (timeoutId) {
9
+ clearTimeout(timeoutId);
10
+ }
11
+ timeoutId = setTimeout(() => {
12
+ fn.apply(this, args);
13
+ }, wait);
14
+ };
15
+ }