@mozaic-ds/vue 1.0.0-rc.3 → 2.1.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 (222) hide show
  1. package/LICENSE +51 -0
  2. package/README.md +76 -77
  3. package/dist/mozaic-vue.css +1 -1
  4. package/dist/mozaic-vue.d.ts +1664 -0
  5. package/dist/mozaic-vue.js +1943 -0
  6. package/dist/mozaic-vue.js.map +1 -0
  7. package/dist/mozaic-vue.umd.cjs +2 -0
  8. package/dist/mozaic-vue.umd.cjs.map +1 -0
  9. package/env.d.ts +1 -0
  10. package/package.json +78 -51
  11. package/src/components/Contributing.mdx +118 -0
  12. package/src/components/GettingStarted.mdx +45 -0
  13. package/src/components/Introduction.mdx +100 -0
  14. package/src/components/Support.mdx +18 -0
  15. package/src/components/breadcrumb/MBreadcrumb.spec.ts +105 -0
  16. package/src/components/breadcrumb/MBreadcrumb.stories.ts +82 -0
  17. package/src/components/breadcrumb/MBreadcrumb.vue +52 -55
  18. package/src/components/button/MButton.spec.ts +191 -0
  19. package/src/components/button/MButton.stories.ts +59 -0
  20. package/src/components/button/MButton.vue +98 -154
  21. package/src/components/checkbox/MCheckbox.spec.ts +104 -0
  22. package/src/components/checkbox/MCheckbox.stories.ts +83 -0
  23. package/src/components/checkbox/MCheckbox.vue +60 -101
  24. package/src/components/checkboxgroup/MCheckboxGroup.spec.ts +78 -0
  25. package/src/components/checkboxgroup/MCheckboxGroup.stories.ts +61 -0
  26. package/src/components/checkboxgroup/MCheckboxGroup.vue +97 -0
  27. package/src/components/datepicker/MDatepicker.spec.ts +95 -0
  28. package/src/components/datepicker/MDatepicker.stories.ts +75 -0
  29. package/src/components/datepicker/MDatepicker.vue +114 -0
  30. package/src/components/divider/MDivider.spec.ts +57 -0
  31. package/src/components/divider/MDivider.stories.ts +64 -0
  32. package/src/components/divider/MDivider.vue +56 -0
  33. package/src/components/drawer/MDrawer.spec.ts +100 -0
  34. package/src/components/drawer/MDrawer.stories.ts +128 -0
  35. package/src/components/drawer/MDrawer.vue +140 -0
  36. package/src/components/field/MField.spec.ts +166 -0
  37. package/src/components/field/MField.stories.ts +369 -0
  38. package/src/components/field/MField.vue +78 -61
  39. package/src/components/fieldgroup/MFieldGroup.spec.ts +165 -0
  40. package/src/components/fieldgroup/MFieldGroup.stories.ts +416 -0
  41. package/src/components/fieldgroup/MFieldGroup.vue +79 -0
  42. package/src/components/flag/MFlag.spec.ts +46 -0
  43. package/src/components/flag/MFlag.stories.ts +46 -0
  44. package/src/components/flag/MFlag.vue +28 -39
  45. package/src/components/iconbutton/MIconButton.spec.ts +108 -0
  46. package/src/components/iconbutton/MIconButton.stories.ts +74 -0
  47. package/src/components/iconbutton/MIconButton.vue +73 -0
  48. package/src/components/link/MLink.spec.ts +154 -0
  49. package/src/components/link/MLink.stories.ts +89 -0
  50. package/src/components/link/MLink.vue +86 -120
  51. package/src/components/loader/MLoader.spec.ts +104 -0
  52. package/src/components/loader/MLoader.stories.ts +43 -0
  53. package/src/components/loader/MLoader.vue +66 -55
  54. package/src/components/loadingoverlay/MLoadingOverlay.spec.ts +37 -0
  55. package/src/components/loadingoverlay/MLoadingOverlay.stories.ts +40 -0
  56. package/src/components/loadingoverlay/MLoadingOverlay.vue +28 -0
  57. package/src/components/modal/MModal.spec.ts +103 -0
  58. package/src/components/modal/MModal.stories.ts +127 -0
  59. package/src/components/modal/MModal.vue +111 -159
  60. package/src/components/numberbadge/MNumberBadge.spec.ts +56 -0
  61. package/src/components/numberbadge/MNumberBadge.stories.ts +48 -0
  62. package/src/components/numberbadge/MNumberBadge.vue +45 -0
  63. package/src/components/overlay/MOverlay.spec.ts +51 -0
  64. package/src/components/overlay/MOverlay.stories.ts +35 -0
  65. package/src/components/overlay/MOverlay.vue +27 -19
  66. package/src/components/pagination/MPagination.spec.ts +123 -0
  67. package/src/components/pagination/MPagination.stories.ts +83 -0
  68. package/src/components/pagination/MPagination.vue +120 -140
  69. package/src/components/passwordinput/MPasswordInput.spec.ts +104 -0
  70. package/src/components/passwordinput/MPasswordInput.stories.ts +75 -0
  71. package/src/components/passwordinput/MPasswordInput.vue +126 -77
  72. package/src/components/pincode/MPincode.spec.ts +126 -0
  73. package/src/components/pincode/MPincode.stories.ts +68 -0
  74. package/src/components/pincode/MPincode.vue +148 -0
  75. package/src/components/quantityselector/MQuantitySelector.spec.ts +262 -0
  76. package/src/components/quantityselector/MQuantitySelector.stories.ts +89 -0
  77. package/src/components/quantityselector/MQuantitySelector.vue +159 -148
  78. package/src/components/radio/MRadio.spec.ts +104 -0
  79. package/src/components/radio/MRadio.stories.ts +68 -0
  80. package/src/components/radio/MRadio.vue +56 -39
  81. package/src/components/radiogroup/MRadioGroup.spec.ts +54 -0
  82. package/src/components/radiogroup/MRadioGroup.stories.ts +61 -0
  83. package/src/components/radiogroup/MRadioGroup.vue +79 -0
  84. package/src/components/select/MSelect.spec.ts +114 -0
  85. package/src/components/select/MSelect.stories.ts +101 -0
  86. package/src/components/select/MSelect.vue +77 -119
  87. package/src/components/statusbadge/MStatusBadge.stories.ts +45 -0
  88. package/src/components/statusbadge/MStatusBadge.vue +40 -0
  89. package/src/components/statusbadge/MstatusBadge.spec.ts +16 -0
  90. package/src/components/statusdot/MStatusDot.spec.ts +51 -0
  91. package/src/components/statusdot/MStatusDot.stories.ts +48 -0
  92. package/src/components/statusdot/MStatusDot.vue +36 -0
  93. package/src/components/statusnotification/MStatusNotification.spec.ts +103 -0
  94. package/src/components/statusnotification/MStatusNotification.stories.ts +89 -0
  95. package/src/components/statusnotification/MStatusNotification.vue +106 -0
  96. package/src/components/tabs/MTabs.stories.ts +104 -0
  97. package/src/components/tabs/MTabs.vue +113 -0
  98. package/src/components/tabs/Mtabs.spec.ts +149 -0
  99. package/src/components/tag/MTag.spec.ts +107 -0
  100. package/src/components/tag/MTag.stories.ts +75 -0
  101. package/src/components/tag/MTag.vue +151 -0
  102. package/src/components/textarea/MTextArea.spec.ts +112 -0
  103. package/src/components/textarea/MTextArea.stories.ts +67 -0
  104. package/src/components/textarea/MTextArea.vue +81 -43
  105. package/src/components/textinput/MTextInput.spec.ts +121 -0
  106. package/src/components/textinput/MTextInput.stories.ts +107 -0
  107. package/src/components/textinput/MTextInput.vue +127 -47
  108. package/src/components/toggle/MToggle.spec.ts +99 -0
  109. package/src/components/toggle/MToggle.stories.ts +68 -0
  110. package/src/components/toggle/MToggle.vue +63 -103
  111. package/src/components/togglegroup/MToggleGroup.spec.ts +78 -0
  112. package/src/components/togglegroup/MToggleGroup.stories.ts +61 -0
  113. package/src/components/togglegroup/MToggleGroup.vue +97 -0
  114. package/src/components/usingIcons.mdx +35 -0
  115. package/src/components/usingPresets.mdx +128 -0
  116. package/src/main.ts +32 -0
  117. package/dist/demo.html +0 -1
  118. package/dist/mozaic-vue.adeo.css +0 -47
  119. package/dist/mozaic-vue.adeo.umd.js +0 -31341
  120. package/dist/mozaic-vue.common.js +0 -31331
  121. package/dist/mozaic-vue.common.js.map +0 -1
  122. package/dist/mozaic-vue.umd.js +0 -31342
  123. package/dist/mozaic-vue.umd.js.map +0 -1
  124. package/dist/mozaic-vue.umd.min.js +0 -4
  125. package/dist/mozaic-vue.umd.min.js.map +0 -1
  126. package/postinstall.js +0 -3
  127. package/src/components/accordion/MAccordion.vue +0 -128
  128. package/src/components/accordion/index.js +0 -7
  129. package/src/components/autocomplete/MAutocomplete.vue +0 -380
  130. package/src/components/autocomplete/index.js +0 -7
  131. package/src/components/badge/MBadge.vue +0 -43
  132. package/src/components/badge/index.js +0 -7
  133. package/src/components/breadcrumb/index.js +0 -7
  134. package/src/components/button/index.js +0 -7
  135. package/src/components/card/MCard.vue +0 -78
  136. package/src/components/card/index.js +0 -7
  137. package/src/components/checkbox/MCheckboxGroup.vue +0 -163
  138. package/src/components/checkbox/index.js +0 -12
  139. package/src/components/container/MContainer.vue +0 -33
  140. package/src/components/container/index.js +0 -7
  141. package/src/components/datatable/MDataTable.vue +0 -651
  142. package/src/components/datatable/MDataTableHeader.vue +0 -55
  143. package/src/components/datatable/MDataTableTop.vue +0 -35
  144. package/src/components/datatable/helpers.js +0 -132
  145. package/src/components/datatable/index.js +0 -12
  146. package/src/components/dropdown/MDropdown.vue +0 -317
  147. package/src/components/dropdown/index.js +0 -7
  148. package/src/components/field/index.js +0 -7
  149. package/src/components/fileuploader/MFileResult.vue +0 -149
  150. package/src/components/fileuploader/MFileUploader.vue +0 -142
  151. package/src/components/fileuploader/index.js +0 -7
  152. package/src/components/flag/index.js +0 -7
  153. package/src/components/heading/MHeading.vue +0 -75
  154. package/src/components/heading/index.js +0 -7
  155. package/src/components/hero/MHero.vue +0 -93
  156. package/src/components/hero/index.js +0 -7
  157. package/src/components/icon/MIcon.vue +0 -136
  158. package/src/components/icon/index.js +0 -7
  159. package/src/components/index.js +0 -44
  160. package/src/components/layer/MLayer.vue +0 -208
  161. package/src/components/layer/index.js +0 -7
  162. package/src/components/link/index.js +0 -7
  163. package/src/components/listbox/MListBox.vue +0 -146
  164. package/src/components/listbox/MListBoxActions.vue +0 -251
  165. package/src/components/listbox/index.js +0 -12
  166. package/src/components/loader/index.js +0 -7
  167. package/src/components/modal/index.js +0 -7
  168. package/src/components/notification/MNotification.vue +0 -110
  169. package/src/components/notification/index.js +0 -7
  170. package/src/components/optionbutton/MOptionButton.vue +0 -67
  171. package/src/components/optionbutton/index.js +0 -7
  172. package/src/components/optioncard/MOptionCard.vue +0 -132
  173. package/src/components/optioncard/index.js +0 -7
  174. package/src/components/optiongroup/MOptionGroup.vue +0 -18
  175. package/src/components/optiongroup/index.js +0 -7
  176. package/src/components/overlay/MOverlayLoader.vue +0 -43
  177. package/src/components/overlay/index.js +0 -12
  178. package/src/components/pagination/index.js +0 -7
  179. package/src/components/passwordinput/index.js +0 -7
  180. package/src/components/phonenumber/MPhoneNumber.vue +0 -398
  181. package/src/components/phonenumber/index.js +0 -7
  182. package/src/components/progressbar/MProgress.vue +0 -102
  183. package/src/components/progressbar/index.js +0 -7
  184. package/src/components/quantityselector/index.js +0 -7
  185. package/src/components/radio/MRadioGroup.vue +0 -111
  186. package/src/components/radio/index.js +0 -12
  187. package/src/components/ratingstars/MStarsInput.vue +0 -119
  188. package/src/components/ratingstars/MStarsResult.vue +0 -89
  189. package/src/components/ratingstars/index.js +0 -12
  190. package/src/components/select/index.js +0 -7
  191. package/src/components/stepper/MStepper.vue +0 -111
  192. package/src/components/stepper/index.js +0 -7
  193. package/src/components/tabs/MTab.vue +0 -204
  194. package/src/components/tabs/index.js +0 -7
  195. package/src/components/tags/MTag.vue +0 -175
  196. package/src/components/tags/index.js +0 -7
  197. package/src/components/textarea/index.js +0 -7
  198. package/src/components/textinput/MTextInputField.vue +0 -105
  199. package/src/components/textinput/MTextInputIcon.vue +0 -42
  200. package/src/components/textinput/index.js +0 -7
  201. package/src/components/toggle/index.js +0 -7
  202. package/src/components/tooltip/MTooltip.vue +0 -42
  203. package/src/components/tooltip/index.js +0 -7
  204. package/src/index.js +0 -63
  205. package/src/shims-tsx.d.ts +0 -13
  206. package/src/shims.vue.d.ts +0 -4
  207. package/src/tokens/adeo/android/colors.xml +0 -452
  208. package/src/tokens/adeo/android/font_dimens.xml +0 -18
  209. package/src/tokens/adeo/css/_variables.scss +0 -446
  210. package/src/tokens/adeo/css/root.scss +0 -448
  211. package/src/tokens/adeo/ios/StyleDictionaryColor.h +0 -460
  212. package/src/tokens/adeo/ios/StyleDictionaryColor.m +0 -472
  213. package/src/tokens/adeo/ios/StyleDictionaryColor.swift +0 -455
  214. package/src/tokens/adeo/ios/StyleDictionarySize.h +0 -69
  215. package/src/tokens/adeo/ios/StyleDictionarySize.m +0 -70
  216. package/src/tokens/adeo/ios/StyleDictionarySize.swift +0 -71
  217. package/src/tokens/adeo/js/tokens.js +0 -544
  218. package/src/tokens/adeo/js/tokensObject.js +0 -11733
  219. package/src/tokens/adeo/scss/_tokens.scss +0 -1522
  220. package/src/utils/mozaicClasses.js +0 -16
  221. package/src/utils/theme.validator.js +0 -19
  222. package/types/index.d.ts +0 -104
@@ -0,0 +1,103 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+ import MModal from './MModal.vue';
4
+
5
+ const stubs = {
6
+ Cross24: { template: '<svg />' },
7
+ MIconButton: {
8
+ template: `<button @click="$emit('click')"><slot name="icon"/></button>`,
9
+ },
10
+ MOverlay: {
11
+ template: `<div class="overlay"><slot/></div>`,
12
+ },
13
+ };
14
+
15
+ describe('MModal component', () => {
16
+ it('renders title and description', () => {
17
+ const wrapper = mount(MModal, {
18
+ props: {
19
+ open: true,
20
+ title: 'Test Modal',
21
+ description: 'This is a description',
22
+ },
23
+ global: { stubs },
24
+ });
25
+
26
+ expect(wrapper.text()).toContain('Test Modal');
27
+ expect(wrapper.text()).toContain('This is a description');
28
+ });
29
+
30
+ it('renders close button if closable is true', () => {
31
+ const wrapper = mount(MModal, {
32
+ props: { open: true, title: 'Title', closable: true },
33
+ global: { stubs },
34
+ });
35
+
36
+ expect(wrapper.find('button.mc-modal__close').exists()).toBe(true);
37
+ });
38
+
39
+ it('does not render close button if closable is false', () => {
40
+ const wrapper = mount(MModal, {
41
+ props: { open: true, title: 'Title', closable: false },
42
+ global: { stubs },
43
+ });
44
+
45
+ expect(wrapper.find('button.mc-modal__close').exists()).toBe(false);
46
+ });
47
+
48
+ it('emits update:open with false when close button clicked', async () => {
49
+ const wrapper = mount(MModal, {
50
+ props: { open: true, title: 'Title', closable: true },
51
+ global: { stubs },
52
+ });
53
+
54
+ await wrapper.find('button.mc-modal__close').trigger('click');
55
+
56
+ expect(wrapper.emitted('update:open')).toBeTruthy();
57
+ expect(wrapper.emitted('update:open')![0]).toEqual([false]);
58
+ });
59
+
60
+ it('renders slots content', () => {
61
+ const wrapper = mount(MModal, {
62
+ props: { open: true, title: 'Title' },
63
+ slots: {
64
+ default: '<div class="default-slot">Default Content</div>',
65
+ icon: '<span class="icon-slot">ICON</span>',
66
+ footer: '<div class="footer-slot">Footer Content</div>',
67
+ link: '<a href="#" class="link-slot">Link Content</a>',
68
+ },
69
+ global: { stubs },
70
+ });
71
+
72
+ expect(wrapper.find('.icon-slot').exists()).toBe(true);
73
+ expect(wrapper.find('.default-slot').exists()).toBe(true);
74
+ expect(wrapper.find('.footer-slot').exists()).toBe(true);
75
+ expect(wrapper.find('.link-slot').exists()).toBe(true);
76
+ });
77
+
78
+ it('has aria-hidden set correctly based on open prop', async () => {
79
+ const wrapper = mount(MModal, {
80
+ props: { open: false, title: 'Title' },
81
+ global: { stubs },
82
+ });
83
+
84
+ expect(wrapper.find('.mc-modal').attributes('aria-hidden')).toBe('true');
85
+
86
+ await wrapper.setProps({ open: true });
87
+
88
+ expect(wrapper.find('.mc-modal').attributes('aria-hidden')).toBe('false');
89
+ });
90
+
91
+ it('adds "is-open" class when open is true', async () => {
92
+ const wrapper = mount(MModal, {
93
+ props: { open: false, title: 'Title' },
94
+ global: { stubs },
95
+ });
96
+
97
+ expect(wrapper.find('.mc-modal').classes()).not.toContain('is-open');
98
+
99
+ await wrapper.setProps({ open: true });
100
+
101
+ expect(wrapper.find('.mc-modal').classes()).toContain('is-open');
102
+ });
103
+ });
@@ -0,0 +1,127 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
2
+ import { ref, watch } from 'vue';
3
+ import { action } from 'storybook/actions';
4
+ import MModal from './MModal.vue';
5
+ import MButton from '../button/MButton.vue';
6
+ import MLink from '../link/MLink.vue';
7
+ import InfoCircle32 from '@mozaic-ds/icons-vue/src/components/InfoCircle32/InfoCircle32.vue';
8
+ import ExternalLink24 from '@mozaic-ds/icons-vue/src/components/ExternalLink24/ExternalLink24.vue';
9
+
10
+ const meta: Meta<typeof MModal> = {
11
+ title: 'Overlay/Modal',
12
+ component: MModal,
13
+ parameters: {
14
+ layout: 'fullscreen',
15
+ docs: {
16
+ story: { height: '400px' },
17
+ description: {
18
+ component:
19
+ 'A modal is a dialog window that appears on top of the main content, requiring user interaction before returning to the main interface. It is used to focus attention on a specific task, provide important information, or request confirmation for an action. Modals typically include a title, description, and primary/secondary actions and should be used for single, focused tasks to avoid disrupting the user experience.',
20
+ },
21
+ },
22
+ },
23
+ args: {
24
+ open: true,
25
+ title: 'Modal title',
26
+ description: `A modal is a dialog window allowing you to focus the user's attention on a specific task, a piece of information or a mandatory action. It must be used for single action only and must have a call to action button in the bottom.`,
27
+ footer: `
28
+ <MButton ghost>Cancel</MButton>
29
+ <MButton>Primary action</MButton>
30
+ `,
31
+ },
32
+ render: (args) => ({
33
+ components: { MModal, MButton, MLink, InfoCircle32, ExternalLink24 },
34
+ setup() {
35
+ const handleUpdate = action('update:open');
36
+ const openState = ref(args.open);
37
+
38
+ watch(
39
+ () => args.open,
40
+ (val) => {
41
+ openState.value = val;
42
+ },
43
+ );
44
+
45
+ const onUpdateOpen = (val: boolean) => {
46
+ openState.value = val;
47
+ handleUpdate(val);
48
+ args.open = val;
49
+ };
50
+
51
+ return { args, openState, onUpdateOpen };
52
+ },
53
+ template: `
54
+ <MModal
55
+ v-bind="args"
56
+ @update:open="onUpdateOpen"
57
+ v-model:open="openState"
58
+ >
59
+ <template v-if="${'icon' in args}" v-slot:icon>${args.icon}</template>
60
+ <template v-if="${'default' in args}" v-slot>${args.default}</template>
61
+ <template v-if="${'link' in args}" v-slot:link>${args.link}</template>
62
+ <template v-if="${'footer' in args}" v-slot:footer>${args.footer}</template>
63
+ </MModal>
64
+ `,
65
+ }),
66
+ };
67
+ export default meta;
68
+ type Story = StoryObj<typeof MModal>;
69
+
70
+ export const Default: Story = {};
71
+
72
+ export const Icon = {
73
+ args: {
74
+ icon: '<InfoCircle32/>',
75
+ },
76
+ };
77
+
78
+ export const ValidationOnly = {
79
+ args: {
80
+ footer: `
81
+ <MButton>Primary action</MButton>
82
+ `,
83
+ },
84
+ };
85
+
86
+ export const TwoOptions = {
87
+ args: {
88
+ footer: `
89
+ <MButton outlined>Secondary action</MButton>
90
+ <MButton>Primary action</MButton>
91
+ `,
92
+ },
93
+ };
94
+
95
+ export const Cancel = {
96
+ args: {
97
+ footer: `
98
+ <MButton ghost>Cancel</MButton>
99
+ <MButton outlined>Secondary action</MButton>
100
+ <MButton>Primary action</MButton>
101
+ `,
102
+ },
103
+ };
104
+
105
+ export const Link = {
106
+ args: {
107
+ link: `
108
+ <MLink class="mc-modal__link" href="#" size="m" icon-position="right">
109
+ Learn more
110
+ <template #icon><ExternalLink24 /></template>
111
+ </MLink>
112
+ `,
113
+ footer: `
114
+ <MButton ghost>Cancel</MButton>
115
+ <MButton>Primary action</MButton>
116
+ `,
117
+ },
118
+ };
119
+
120
+ export const Danger = {
121
+ args: {
122
+ footer: `
123
+ <MButton ghost>Cancel</MButton>
124
+ <MButton appearance="danger">Primary action</MButton>
125
+ `,
126
+ },
127
+ };
@@ -1,179 +1,131 @@
1
1
  <template>
2
- <div>
3
- <div
4
- ref="modal"
2
+ <MOverlay :is-visible="open" dialogLabel="modalTitle">
3
+ <section
5
4
  class="mc-modal"
6
- tabindex="-1"
5
+ :class="classObject"
7
6
  role="dialog"
8
- aria-labelledby="modal_title"
7
+ aria-labelledby="modalTitle"
8
+ :aria-modal="open ? 'true' : 'false'"
9
+ tabindex="-1"
9
10
  :aria-hidden="!open"
10
- @keyup.esc="closeModal()"
11
+ v-bind="$attrs"
12
+ @keydown.esc="onClose"
11
13
  >
12
- <div
13
- class="mc-modal__dialog"
14
- :class="{ 'is-open': open }"
15
- role="document"
16
- >
17
- <div class="mc-modal__header">
18
- <component
19
- :is="modalTitleTag"
20
- id="modal_title"
21
- class="mc-modal__title"
22
- >
23
- {{ modalTitle }}
24
- </component>
25
- <button
26
- v-if="displayCloseButton"
27
- ref="close"
14
+ <div class="mc-modal__dialog" role="document">
15
+ <header class="mc-modal__header">
16
+ <span v-if="$slots.icon" class="mc-modal__icon">
17
+ <slot name="icon" />
18
+ </span>
19
+ <h2 class="mc-modal__title" id="modalTitle">
20
+ {{ title }}
21
+ </h2>
22
+ <MIconButton
23
+ v-if="closable"
28
24
  class="mc-modal__close"
29
- type="button"
30
- @click="closeModal()"
25
+ aria-label="Close"
26
+ ghost
27
+ @click="onClose"
31
28
  >
32
- <span class="mc-modal__close-text">
33
- {{ closeButtonText }}
34
- </span>
35
- </button>
36
- </div>
37
- <div ref="body" class="mc-modal__body">
38
- <article ref="content" class="mc-modal__content">
39
- <component :is="titleTag" v-if="title" class="mc-modal__heading">
40
- {{ title }}
41
- </component>
42
- <div class="mt-body-m">
43
- <slot />
44
- </div>
45
- </article>
46
- </div>
47
- <div v-if="$slots.footer" class="mc-modal__footer">
29
+ <template #icon>
30
+ <Cross24 aria-hidden="true" />
31
+ </template>
32
+ </MIconButton>
33
+ </header>
34
+ <main class="mc-modal__body">
35
+ <p>{{ description }}</p>
36
+ <slot />
37
+ </main>
38
+ <footer v-if="$slots.footer" class="mc-modal__footer">
39
+ <span class="mc-modal__link">
40
+ <slot name="link" />
41
+ </span>
48
42
  <slot name="footer" />
49
- </div>
43
+ </footer>
50
44
  </div>
51
- </div>
52
- <keep-alive>
53
- <div
54
- class="mc-modal-overlay"
55
- :class="{ 'is-visible': open }"
56
- tabindex="-1"
57
- role="dialog"
58
- />
59
- </keep-alive>
60
- </div>
45
+ </section>
46
+ </MOverlay>
61
47
  </template>
62
48
 
63
- <script>
64
- export default {
65
- name: 'MModal',
66
-
67
- props: {
68
- open: {
69
- type: Boolean,
70
- default: false,
71
- },
72
- modalTitle: {
73
- type: String,
74
- required: true,
75
- },
76
- modalTitleTag: {
77
- type: String,
78
- default: 'h2',
79
- },
80
- title: {
81
- type: String,
82
- default: null,
83
- },
84
- titleTag: {
85
- type: String,
86
- default: 'h3',
87
- },
88
- closeButtonText: {
89
- type: String,
90
- default: 'Close',
91
- validator: (value) => value !== '',
92
- },
93
- displayCloseButton: {
94
- type: Boolean,
95
- default: true,
96
- },
49
+ <script setup lang="ts">
50
+ import { computed, watch, type VNode } from 'vue';
51
+ import Cross24 from '@mozaic-ds/icons-vue/src/components/Cross24/Cross24.vue';
52
+ import MIconButton from '../iconbutton/MIconButton.vue';
53
+ import MOverlay from '../overlay/MOverlay.vue';
54
+ /**
55
+ * A modal is a dialog window that appears on top of the main content, requiring user interaction before returning to the main interface. It is used to focus attention on a specific task, provide important information, or request confirmation for an action. Modals typically include a title, description, and primary/secondary actions and should be used for single, focused tasks to avoid disrupting the user experience.
56
+ */
57
+ const props = withDefaults(
58
+ defineProps<{
59
+ /**
60
+ * if `true`, display the modal.
61
+ */
62
+ open?: boolean;
63
+ /**
64
+ * Title of the modal
65
+ */
66
+ title: string;
67
+ /**
68
+ * Description of the modal
69
+ */
70
+ description?: string;
71
+ /**
72
+ * if `true`, display the close button.
73
+ */
74
+ closable?: boolean;
75
+ }>(),
76
+ {
77
+ closable: true,
97
78
  },
79
+ );
98
80
 
99
- watch: {
100
- open: {
101
- immediate: true,
102
- handler: function (isOpen) {
103
- this.$nextTick(() => {
104
- if (isOpen) {
105
- this.trapFocus();
106
- this.setFocusOnModal();
107
- this.setOverflow();
108
- this.$emit('modal-opened');
109
- } else {
110
- this.$emit('modal-closed');
111
- }
112
- });
113
- },
114
- },
115
- },
116
-
117
- methods: {
118
- trapFocus: function () {
119
- const modal = document.querySelector('.mc-modal');
120
- const focusableElements = modal.querySelectorAll(
121
- 'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
122
- );
123
-
124
- const firstFocusableElement = focusableElements[0]; // get first element to be focused inside modal
125
- const lastFocusableElement =
126
- focusableElements[focusableElements.length - 1];
127
-
128
- document.addEventListener('keydown', function (e) {
129
- const isTabPressed = e.key === 'Tab' || e.keyCode === 9;
81
+ defineSlots<{
82
+ /**
83
+ * Use this slot to insert an icon next to the title of the modal
84
+ */
85
+ icon?: VNode;
86
+ /**
87
+ * Use this slot to insert the content of the modal
88
+ */
89
+ default?: VNode;
90
+ /**
91
+ * Use this slot to insert a link in the footer
92
+ */
93
+ link?: VNode;
94
+ /**
95
+ * Use this slot to insert buttons in the footer
96
+ */
97
+ footer?: VNode;
98
+ }>();
130
99
 
131
- if (!isTabPressed) {
132
- return;
133
- }
100
+ const classObject = computed(() => {
101
+ return {
102
+ 'is-open': props.open,
103
+ };
104
+ });
134
105
 
135
- if (e.shiftKey) {
136
- // if shift key pressed for shift + tab combination
137
- if (document.activeElement === firstFocusableElement) {
138
- lastFocusableElement.focus(); // add focus for the last focusable element
139
- e.preventDefault();
140
- }
141
- } else {
142
- // if tab key is pressed
143
- if (document.activeElement === lastFocusableElement) {
144
- // if focused has reached to last focusable element then focus first focusable element after pressing tab
145
- firstFocusableElement.focus(); // add focus for the first focusable element
146
- e.preventDefault();
147
- }
148
- }
149
- });
150
- },
151
-
152
- closeModal() {
153
- this.$emit('update:open', false);
154
- },
155
-
156
- setOverflow: function () {
157
- const modal = this.$refs.modal;
158
- const bodyHeight = this.$refs.body.clientHeight;
159
- const contentHeight = this.$refs.content.clientHeight;
160
-
161
- if (contentHeight > bodyHeight) {
162
- modal.classList.add('mc-modal--overflow');
163
- }
164
- },
165
-
166
- setFocusOnModal() {
167
- const modal = this.$refs.modal;
168
-
169
- modal.focus();
170
- },
106
+ watch(
107
+ () => props.open,
108
+ (newValue) => {
109
+ emit('update:open', newValue);
171
110
  },
111
+ );
112
+
113
+ const onClose = () => {
114
+ emit('update:open', false);
172
115
  };
116
+
117
+ const emit = defineEmits<{
118
+ /**
119
+ * Emits when the checkbox value changes, updating the modelValue prop.
120
+ */
121
+ (on: 'update:open', value: boolean): void;
122
+ }>();
173
123
  </script>
174
124
 
175
- <style lang="scss">
176
- @import 'settings-tools/all-settings';
177
- @import 'typography/_t.bodys';
178
- @import 'components/_c.modal';
125
+ <style lang="scss" scoped>
126
+ @use '@mozaic-ds/styles/components/modal';
127
+
128
+ .mc-overlay {
129
+ filter: none;
130
+ }
179
131
  </style>
@@ -0,0 +1,56 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import { describe, it, expect } from 'vitest';
3
+ import MNumberBadge from './MNumberBadge.vue';
4
+
5
+ describe('MNumberBadge component', () => {
6
+ it('renders the label correctly', () => {
7
+ const wrapper = mount(MNumberBadge, {
8
+ props: { label: 42 },
9
+ });
10
+ expect(wrapper.text()).toBe('42');
11
+ });
12
+
13
+ it('applies no modifier class by default', () => {
14
+ const wrapper = mount(MNumberBadge, {
15
+ props: { label: 1 },
16
+ });
17
+ expect(wrapper.classes()).toContain('mc-number-badge');
18
+ expect(wrapper.classes()).not.toContain('mc-number-badge--standard');
19
+ expect(wrapper.classes()).not.toContain('mc-number-badge--s');
20
+ });
21
+
22
+ it('applies appearance modifier class when set to danger', () => {
23
+ const wrapper = mount(MNumberBadge, {
24
+ props: {
25
+ label: 5,
26
+ appearance: 'danger',
27
+ },
28
+ });
29
+ expect(wrapper.classes()).toContain('mc-number-badge');
30
+ expect(wrapper.classes()).toContain('mc-number-badge--danger');
31
+ });
32
+
33
+ it('applies size modifier class when size is m', () => {
34
+ const wrapper = mount(MNumberBadge, {
35
+ props: {
36
+ label: 10,
37
+ size: 'm',
38
+ },
39
+ });
40
+ expect(wrapper.classes()).toContain('mc-number-badge');
41
+ expect(wrapper.classes()).toContain('mc-number-badge--m');
42
+ });
43
+
44
+ it('applies both appearance and size modifier classes when set', () => {
45
+ const wrapper = mount(MNumberBadge, {
46
+ props: {
47
+ label: 3,
48
+ appearance: 'accent',
49
+ size: 'm',
50
+ },
51
+ });
52
+ expect(wrapper.classes()).toContain('mc-number-badge');
53
+ expect(wrapper.classes()).toContain('mc-number-badge--accent');
54
+ expect(wrapper.classes()).toContain('mc-number-badge--m');
55
+ });
56
+ });
@@ -0,0 +1,48 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
2
+ import MNumberBadge from './MNumberBadge.vue';
3
+
4
+ const meta: Meta<typeof MNumberBadge> = {
5
+ title: 'Indicators/Number Badge',
6
+ component: MNumberBadge,
7
+ parameters: {
8
+ docs: {
9
+ description: {
10
+ component:
11
+ 'A Number Badge represents a numeric count, often used to indicate notifications, updates, or items requiring attention. Its distinct appearance makes it easy to spot changes at a glance, ensuring users stay informed without breaking their workflow. Badges are commonly attached to icons, buttons, or tabs to provide contextual awareness.',
12
+ },
13
+ },
14
+ },
15
+ args: { label: 99 },
16
+ render: (args) => ({
17
+ components: { MNumberBadge },
18
+ setup() {
19
+ return { args };
20
+ },
21
+ template: `
22
+ <MNumberBadge v-bind="args"></MNumberBadge>
23
+ `,
24
+ }),
25
+ };
26
+ export default meta;
27
+ type Story = StoryObj<typeof MNumberBadge>;
28
+
29
+ export const Standard: Story = {};
30
+
31
+ export const Accent: Story = {
32
+ args: { appearance: 'accent' },
33
+ };
34
+
35
+ export const Danger: Story = {
36
+ args: { appearance: 'danger' },
37
+ };
38
+
39
+ export const Inverse: Story = {
40
+ globals: {
41
+ backgrounds: { value: 'inverse' },
42
+ },
43
+ args: { appearance: 'inverse' },
44
+ };
45
+
46
+ export const Size: Story = {
47
+ args: { size: 'm' },
48
+ };
@@ -0,0 +1,45 @@
1
+ <template>
2
+ <span class="mc-number-badge" :class="classObject">
3
+ {{ label }}
4
+ </span>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { computed } from 'vue';
9
+ /**
10
+ * A badge indicates the status of an entity and can evolve at any time.
11
+ */
12
+ const props = withDefaults(
13
+ defineProps<{
14
+ /**
15
+ * Content of the badge
16
+ */
17
+ label: number;
18
+ /**
19
+ * Allows to define the Badge style
20
+ */
21
+ appearance?: 'danger' | 'accent' | 'inverse' | 'standard';
22
+
23
+ /**
24
+ * Allows to define the Badge size
25
+ */
26
+ size?: 's' | 'm';
27
+ }>(),
28
+ {
29
+ appearance: 'standard',
30
+ size: 's',
31
+ },
32
+ );
33
+
34
+ const classObject = computed(() => {
35
+ return {
36
+ [`mc-number-badge--${props.appearance}`]:
37
+ props.appearance && props.appearance != 'standard',
38
+ [`mc-number-badge--${props.size}`]: props.size && props.size != 's',
39
+ };
40
+ });
41
+ </script>
42
+
43
+ <style lang="scss" scoped>
44
+ @use '@mozaic-ds/styles/components/number-badge';
45
+ </style>