@mozaic-ds/vue 2.16.0 → 2.18.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 (132) hide show
  1. package/dist/mozaic-vue.css +2 -1
  2. package/dist/mozaic-vue.d.ts +258 -137
  3. package/dist/mozaic-vue.js +14054 -10878
  4. package/dist/mozaic-vue.js.map +1 -1
  5. package/dist/mozaic-vue.umd.cjs +7 -25
  6. package/dist/mozaic-vue.umd.cjs.map +1 -1
  7. package/package.json +22 -11
  8. package/src/components/BrandPresets.mdx +2 -2
  9. package/src/components/Migration.mdx +651 -0
  10. package/src/components/accordionlist/MAccordionList.figma.ts +43 -0
  11. package/src/components/accordionlistitem/MAccordionListItem.figma.ts +27 -0
  12. package/src/components/accordionlistitem/MAccordionListItem.spec.ts +22 -3
  13. package/src/components/accordionlistitem/MAccordionListItem.vue +38 -28
  14. package/src/components/actionbottombar/MActionBottomBar.figma.ts +24 -0
  15. package/src/components/actionlistbox/MActionListbox.figma.ts +30 -0
  16. package/src/components/avatar/MAvatar.figma.ts +31 -0
  17. package/src/components/breadcrumb/MBreadcrumb.figma.ts +31 -0
  18. package/src/components/builtinmenu/MBuiltInMenu.figma.ts +23 -0
  19. package/src/components/builtinmenu/MBuiltInMenu.spec.ts +30 -1
  20. package/src/components/builtinmenu/MBuiltInMenu.vue +26 -17
  21. package/src/components/builtinmenu/README.md +2 -0
  22. package/src/components/button/MButton.figma.ts +41 -0
  23. package/src/components/callout/MCallout.figma.ts +29 -0
  24. package/src/components/callout/MCallout.spec.ts +35 -0
  25. package/src/components/callout/MCallout.vue +22 -4
  26. package/src/components/callout/README.md +2 -0
  27. package/src/components/carousel/MCarousel.figma.ts +32 -0
  28. package/src/components/checkbox/MCheckbox.figma.ts +45 -0
  29. package/src/components/checkboxgroup/MCheckboxGroup.figma.ts +30 -0
  30. package/src/components/checklistmenu/MCheckListMenu.figma.ts +29 -0
  31. package/src/components/checklistmenu/MCheckListMenu.spec.ts +12 -1
  32. package/src/components/checklistmenu/MCheckListMenu.vue +6 -0
  33. package/src/components/checklistmenu/README.md +2 -0
  34. package/src/components/circularprogressbar/MCircularProgressbar.figma.ts +31 -0
  35. package/src/components/combobox/MCombobox.figma.ts +48 -0
  36. package/src/components/combobox/MCombobox.spec.ts +1 -1
  37. package/src/components/combobox/MCombobox.vue +18 -9
  38. package/src/components/combobox/README.md +2 -2
  39. package/src/components/container/MContainer.figma.ts +30 -0
  40. package/src/components/datatable/DataTable.stories.ts +277 -0
  41. package/src/components/datatable/DataTableCells.stories.ts +251 -0
  42. package/src/components/datatable/DataTableEmpty.stories.ts +102 -0
  43. package/src/components/datatable/DataTableExpandable.stories.ts +95 -0
  44. package/src/components/datatable/DataTableNested.stories.ts +96 -0
  45. package/src/components/datatable/DataTableSelectable.stories.ts +124 -0
  46. package/src/components/datatable/DataTableSortable.stories.ts +164 -0
  47. package/src/components/datatable/MDataTable.types.ts +54 -0
  48. package/src/components/datatable/assets/styles.scss +10 -0
  49. package/src/components/datatable/datatable.mdx +63 -0
  50. package/src/components/datatable/tools/data.js +8 -0
  51. package/src/components/datatable/tools/data.json +2018 -0
  52. package/src/components/datatable/utils.js +19 -0
  53. package/src/components/datepicker/MDatepicker.figma.ts +20 -0
  54. package/src/components/divider/MDivider.figma.ts +30 -0
  55. package/src/components/drawer/MDrawer.figma.ts +37 -0
  56. package/src/components/drawer/README.md +1 -1
  57. package/src/components/field/MField.figma.ts +30 -0
  58. package/src/components/fileuploader/MFileUploader.figma.ts +23 -0
  59. package/src/components/fileuploaderitem/MFileUploaderItem.figma.ts +27 -0
  60. package/src/components/flag/MFlag.figma.ts +26 -0
  61. package/src/components/iconbutton/MIconButton.figma.ts +54 -0
  62. package/src/components/kpiitem/MKpiItem.figma.ts +33 -0
  63. package/src/components/linearprogressbarbuffer/MLinearProgressbarBuffer.figma.ts +31 -0
  64. package/src/components/linearprogressbarpercentage/MLinearProgressbarPercentage.figma.ts +26 -0
  65. package/src/components/link/MLink.figma.ts +32 -0
  66. package/src/components/loader/MLoader.figma.ts +30 -0
  67. package/src/components/loadingoverlay/MLoadingOverlay.figma.ts +18 -0
  68. package/src/components/modal/MModal.figma.ts +27 -0
  69. package/src/components/navigationindicator/MNavigationIndicator.figma.ts +24 -0
  70. package/src/components/navigationindicator/MNavigationIndicator.spec.ts +75 -18
  71. package/src/components/navigationindicator/MNavigationIndicator.vue +10 -12
  72. package/src/components/numberbadge/MNumberBadge.figma.ts +31 -0
  73. package/src/components/optionListbox/MOptionListbox.figma.ts +36 -0
  74. package/src/components/optionListbox/MOptionListbox.vue +34 -19
  75. package/src/components/optionListbox/README.md +1 -1
  76. package/src/components/overlay/MOverlay.figma.ts +20 -0
  77. package/src/components/pageheader/MPageHeader.figma.ts +21 -0
  78. package/src/components/pagination/MPagination.figma.ts +34 -0
  79. package/src/components/passwordinput/MPasswordInput.figma.ts +30 -0
  80. package/src/components/phonenumber/MPhoneNumber.figma.ts +47 -0
  81. package/src/components/pincode/MPincode.figma.ts +41 -0
  82. package/src/components/pincode/MPincode.spec.ts +1 -4
  83. package/src/components/pincode/MPincode.vue +11 -15
  84. package/src/components/popover/MPopover.figma.ts +42 -0
  85. package/src/components/popover/MPopover.spec.ts +126 -0
  86. package/src/components/popover/MPopover.vue +36 -1
  87. package/src/components/quantityselector/MQuantitySelector.figma.ts +50 -0
  88. package/src/components/radio/MRadio.figma.ts +40 -0
  89. package/src/components/radiogroup/MRadioGroup.figma.ts +30 -0
  90. package/src/components/segmentedcontrol/MSegmentedControl.figma.ts +33 -0
  91. package/src/components/segmentedcontrol/MSegmentedControl.spec.ts +92 -0
  92. package/src/components/segmentedcontrol/MSegmentedControl.vue +61 -2
  93. package/src/components/select/MSelect.figma.ts +49 -0
  94. package/src/components/sidebar/MSidebar.figma.ts +28 -0
  95. package/src/components/sidebarexpandableitem/MSidebarExpandableItem.figma.ts +19 -0
  96. package/src/components/sidebarfooter/MSidebarFooter.figma.ts +21 -0
  97. package/src/components/sidebarheader/MSidebarHeader.figma.ts +18 -0
  98. package/src/components/sidebarnavitem/MSidebarNavItem.figma.ts +23 -0
  99. package/src/components/sidebarshortcutitem/MSidebarShortcutItem.figma.ts +20 -0
  100. package/src/components/starrating/MStarRating.figma.ts +35 -0
  101. package/src/components/starrating/MStarRating.spec.ts +19 -22
  102. package/src/components/starrating/MStarRating.vue +3 -2
  103. package/src/components/statusbadge/MStatusBadge.figma.ts +27 -0
  104. package/src/components/statusdot/MStatusDot.figma.ts +31 -0
  105. package/src/components/statusmessage/MStatusMessage.figma.ts +28 -0
  106. package/src/components/statusmessage/MStatusMessage.spec.ts +15 -0
  107. package/src/components/statusmessage/MStatusMessage.stories.ts +4 -0
  108. package/src/components/statusmessage/MStatusMessage.vue +7 -0
  109. package/src/components/statusmessage/README.md +2 -0
  110. package/src/components/statusnotification/MStatusNotification.figma.ts +29 -0
  111. package/src/components/stepperbottombar/MStepperBottomBar.figma.ts +20 -0
  112. package/src/components/steppercompact/MStepperCompact.figma.ts +21 -0
  113. package/src/components/stepperinline/MStepperInline.figma.ts +23 -0
  114. package/src/components/stepperstacked/MStepperStacked.figma.ts +23 -0
  115. package/src/components/tabs/MTabs.figma.ts +33 -0
  116. package/src/components/tabs/MTabs.vue +90 -4
  117. package/src/components/tabs/Mtabs.spec.ts +162 -0
  118. package/src/components/tag/MTag.figma.ts +26 -0
  119. package/src/components/tag/MTag.stories.ts +13 -3
  120. package/src/components/tag/MTag.vue +11 -1
  121. package/src/components/tag/README.md +6 -0
  122. package/src/components/textarea/MTextArea.figma.ts +28 -0
  123. package/src/components/textinput/MTextInput.figma.ts +51 -0
  124. package/src/components/tile/MTile.figma.ts +31 -0
  125. package/src/components/tileclickable/MTileClickable.figma.ts +31 -0
  126. package/src/components/tileexpandable/MTileExpandable.figma.ts +31 -0
  127. package/src/components/tileselectable/MTileSelectable.figma.ts +29 -0
  128. package/src/components/toaster/MToaster.figma.ts +25 -0
  129. package/src/components/toggle/MToggle.figma.ts +39 -0
  130. package/src/components/togglegroup/MToggleGroup.figma.ts +30 -0
  131. package/src/components/tooltip/MTooltip.figma.ts +29 -0
  132. package/src/main.ts +1 -0
@@ -1,9 +1,5 @@
1
1
  <template>
2
- <div
3
- class="mc-navigation-indicator"
4
- role="navigation"
5
- aria-label="Navigations steps"
6
- >
2
+ <nav class="mc-navigation-indicator" aria-label="Navigation steps">
7
3
  <ul class="mc-navigation-indicator__list">
8
4
  <li
9
5
  v-for="(_step, index) in steps"
@@ -11,14 +7,14 @@
11
7
  class="mc-navigation-indicator__item"
12
8
  >
13
9
  <button
10
+ type="button"
14
11
  :class="{
15
12
  'mc-navigation-indicator__button': true,
16
13
  'mc-navigation-indicator__button--active': active === index,
17
14
  }"
18
- aria-label="Navigation step button"
15
+ :aria-label="getStepAriaLabel(index)"
19
16
  :aria-current="active === index && 'step'"
20
17
  @click="setActiveStep(index)"
21
- @keydown="onKeydown($event, index)"
22
18
  ></button>
23
19
  </li>
24
20
  </ul>
@@ -43,14 +39,14 @@
43
39
  size="s"
44
40
  ghost
45
41
  @click="emit('action')"
46
- aria-label="Control button"
42
+ :aria-label="props.action === 'pause' ? 'Pause' : 'Resume'"
47
43
  >
48
44
  <template #icon>
49
45
  <component :is="actionIcon" />
50
46
  </template>
51
47
  </MIconButton>
52
48
  </template>
53
- </div>
49
+ </nav>
54
50
  </template>
55
51
 
56
52
  <script setup lang="ts">
@@ -120,10 +116,12 @@ function setActiveStep(step: number) {
120
116
  active.value = step;
121
117
  }
122
118
 
123
- function onKeydown(event: KeyboardEvent, step: number) {
124
- if (event.key === 'Enter') {
125
- setActiveStep(step);
119
+ function getStepAriaLabel(step: number) {
120
+ if (active.value === step) {
121
+ return `Current step, ${step + 1} of ${props.steps}`;
126
122
  }
123
+
124
+ return `Go to step ${step + 1} of ${props.steps}`;
127
125
  }
128
126
  </script>
129
127
 
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Code Connect mapping for MNumberBadge
3
+ * Links Figma Number badge (ADS2) to @mozaic-ds/vue
4
+ */
5
+ import figma, { html } from '@figma/code-connect/html';
6
+
7
+ figma.connect(
8
+ 'https://www.figma.com/design/Zyh9RyabNaqkjbuFWP9Aqj/%E2%9C%A8-Components--ADS2---Stable-version-?node-id=5-13889',
9
+ {
10
+ props: {
11
+ label: figma.string('Count (number only)'),
12
+ appearance: figma.enum('Appearance', {
13
+ Standard: 'standard',
14
+ Accent: 'accent',
15
+ Inverse: 'inverse',
16
+ Danger: 'danger',
17
+ }),
18
+ size: figma.enum('Size', {
19
+ 'S (16px)': 's',
20
+ 'M (24px)': 'm',
21
+ 'L (32px)': 'm',
22
+ }),
23
+ },
24
+ example: ({ label, appearance, size }) =>
25
+ html`<script setup>
26
+ import { MNumberBadge } from '@mozaic-ds/vue';
27
+ </script>
28
+
29
+ <MNumberBadge :label=${label} appearance=${appearance} size=${size}></MNumberBadge>`,
30
+ },
31
+ );
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Code Connect mapping for MOptionListbox
3
+ * Links Figma _option listbox / multi-select to @mozaic-ds/vue
4
+ */
5
+ import figma, { html } from '@figma/code-connect/html';
6
+
7
+ figma.connect(
8
+ 'https://www.figma.com/design/Zyh9RyabNaqkjbuFWP9Aqj/%E2%9C%A8-Components--ADS2---Stable-version-?node-id=20140-26198',
9
+ {
10
+ props: {
11
+ search: figma.boolean('Has a search'),
12
+ actions: figma.boolean('Has actions (select / unselect all)'),
13
+ hasSections: figma.enum('Has sections', {
14
+ False: false,
15
+ True: true,
16
+ }),
17
+ hasAdditionalInfo: figma.enum('Has additional info', {
18
+ False: false,
19
+ True: true,
20
+ }),
21
+ },
22
+ example: ({ search, actions }) =>
23
+ html`<script setup>
24
+ import { MOptionListbox } from '@mozaic-ds/vue';
25
+ </script>
26
+
27
+ <MOptionListbox
28
+ id="option-listbox"
29
+ v-model="selected"
30
+ multiple
31
+ search=${search}
32
+ actions=${actions}
33
+ :options="[{ label: 'Option 1', value: '1' }, { label: 'Option 2', value: '2' }]"
34
+ />`,
35
+ },
36
+ );
@@ -82,23 +82,23 @@
82
82
  "
83
83
  >
84
84
  <div class="mc-option-listbox__label">
85
- <slot v-if="item.type !== 'section'" name="optionPrefix" />
86
-
87
- <div class="mc-option-listbox__content">
88
- <span
89
- :class="
90
- item.type === 'section'
91
- ? 'mc-option-listbox__section-title'
92
- : 'mc-option-listbox__text'
93
- "
94
- >
95
- {{ item.label }}
96
- </span>
85
+ <slot name="item" v-bind="{ item }">
86
+ <div class="mc-option-listbox__content">
87
+ <span
88
+ :class="
89
+ item.type === 'section'
90
+ ? 'mc-option-listbox__section-title'
91
+ : 'mc-option-listbox__text'
92
+ "
93
+ >
94
+ {{ item.label }}
95
+ </span>
97
96
 
98
- <span v-if="item.content" class="mc-option-listbox__additional">
99
- {{ item.content }}
100
- </span>
101
- </div>
97
+ <span v-if="item.content" class="mc-option-listbox__additional">
98
+ {{ item.content }}
99
+ </span>
100
+ </div>
101
+ </slot>
102
102
 
103
103
  <div class="mc-option-listbox__spacer"></div>
104
104
 
@@ -137,7 +137,22 @@ import {
137
137
  Less20,
138
138
  Check20,
139
139
  } from '@mozaic-ds/icons-vue';
140
- import { debounce } from 'lodash';
140
+
141
+ function debounce<T extends (...args: unknown[]) => unknown>(
142
+ fn: T,
143
+ wait: number,
144
+ ): (...args: Parameters<T>) => void {
145
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
146
+
147
+ return function (this: unknown, ...args: Parameters<T>) {
148
+ if (timeoutId) {
149
+ clearTimeout(timeoutId);
150
+ }
151
+ timeoutId = setTimeout(() => {
152
+ fn.apply(this, args);
153
+ }, wait);
154
+ };
155
+ }
141
156
 
142
157
  /**
143
158
  * An Option Listbox is a customizable, accessible listbox component designed to power dropdowns and comboboxes with advanced selection capabilities. It supports single or multiple selection, optional search, grouped options with section headers, and full keyboard navigation.
@@ -228,9 +243,9 @@ const emit = defineEmits<{
228
243
 
229
244
  defineSlots<{
230
245
  /**
231
- * Use this slot to add a prefix to options.
246
+ * Use this slot to customize the content of each item.
232
247
  */
233
- optionPrefix: VNode;
248
+ item(props: { item: ListboxOption }): VNode;
234
249
  }>();
235
250
 
236
251
  const listboxEl = useTemplateRef('listboxEl');
@@ -24,7 +24,7 @@ An Option Listbox is a customizable, accessible listbox component designed to po
24
24
 
25
25
  | Name | Description |
26
26
  | --- | --- |
27
- | `optionPrefix` | Use this slot to add a prefix to options. |
27
+ | `item` | Use this slot to customize the content of each item. |
28
28
 
29
29
  ## Events
30
30
 
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Code Connect mapping for MOverlay
3
+ * Links Figma Overlay (ADS2) to @mozaic-ds/vue
4
+ */
5
+ import figma, { html } from '@figma/code-connect/html';
6
+
7
+ figma.connect(
8
+ 'https://www.figma.com/design/Zyh9RyabNaqkjbuFWP9Aqj/%E2%9C%A8-Components--ADS2---Stable-version-?node-id=6-19511',
9
+ {
10
+ props: {},
11
+ example: () =>
12
+ html`<script setup>
13
+ import { MOverlay } from '@mozaic-ds/vue';
14
+ </script>
15
+
16
+ <MOverlay is-visible dialog-label="Overlay">
17
+ <p>Overlay content</p>
18
+ </MOverlay>`,
19
+ },
20
+ );
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Code Connect mapping for MPageHeader
3
+ * Links Figma Page header (ADS2) to @mozaic-ds/vue
4
+ */
5
+ import figma, { html } from '@figma/code-connect/html';
6
+
7
+ figma.connect(
8
+ 'https://www.figma.com/design/Zyh9RyabNaqkjbuFWP9Aqj/%E2%9C%A8-Components--ADS2---Stable-version-?node-id=16419-62764',
9
+ {
10
+ props: {
11
+ title: figma.string('Title'),
12
+ shadow: figma.boolean('Has shadow'),
13
+ },
14
+ example: ({ title, shadow }) =>
15
+ html`<script setup>
16
+ import { MPageHeader } from '@mozaic-ds/vue';
17
+ </script>
18
+
19
+ <MPageHeader title=${title} shadow=${shadow} />`,
20
+ },
21
+ );
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Code Connect mapping for MPagination
3
+ * Links Figma Pagination (ADS2) to @mozaic-ds/vue
4
+ */
5
+ import figma, { html } from '@figma/code-connect/html';
6
+
7
+ figma.connect(
8
+ 'https://www.figma.com/design/Zyh9RyabNaqkjbuFWP9Aqj/%E2%9C%A8-Components--ADS2---Stable-version-?node-id=6-11558',
9
+ {
10
+ props: {
11
+ compact: figma.enum('Compact mode', {
12
+ True: true,
13
+ False: false,
14
+ }),
15
+ },
16
+ example: ({ compact }) =>
17
+ html`<script setup>
18
+ import { MPagination } from '@mozaic-ds/vue';
19
+ </script>
20
+
21
+ <MPagination
22
+ id="pagination-id"
23
+ :model-value="1"
24
+ :compact=${compact}
25
+ :options="[
26
+ { text: 'Page 1 of 99', value: 1 },
27
+ { text: 'Page 2 of 99', value: 2 },
28
+ { text: 'Page 99 of 99', value: 99 },
29
+ ]"
30
+ select-label="Select page"
31
+ aria-label="pagination"
32
+ ></MPagination>`,
33
+ },
34
+ );
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Code Connect mapping for MPasswordInput
3
+ * Links Figma _password input / base to @mozaic-ds/vue
4
+ */
5
+ import figma, { html } from '@figma/code-connect/html';
6
+
7
+ figma.connect(
8
+ 'https://www.figma.com/design/Zyh9RyabNaqkjbuFWP9Aqj/%E2%9C%A8-Components--ADS2---Stable-version-?node-id=6-29957',
9
+ {
10
+ props: {
11
+ isInvalid: figma.enum('Is invalid', {
12
+ True: true,
13
+ False: false,
14
+ }),
15
+ isClearable: figma.boolean('Is clearable'),
16
+ },
17
+ example: ({ isInvalid, isClearable }) =>
18
+ html`<script setup>
19
+ import { MPasswordInput } from '@mozaic-ds/vue';
20
+ </script>
21
+
22
+ <MPasswordInput
23
+ id="password-input-id"
24
+ :is-invalid=${isInvalid}
25
+ :is-clearable=${isClearable}
26
+ placeholder="Enter your password"
27
+ model-value=""
28
+ ></MPasswordInput>`,
29
+ },
30
+ );
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Code Connect mapping for MPhoneNumber
3
+ * Links Figma _phone number input / base to @mozaic-ds/vue
4
+ */
5
+ import figma, { html } from '@figma/code-connect/html';
6
+
7
+ figma.connect(
8
+ 'https://www.figma.com/design/Zyh9RyabNaqkjbuFWP9Aqj/%E2%9C%A8-Components--ADS2---Stable-version-?node-id=5022-21080',
9
+ {
10
+ props: {
11
+ size: figma.enum('Size', {
12
+ S: 's',
13
+ 'M (default)': 'm',
14
+ }),
15
+ disabled: figma.enum('State', {
16
+ Disabled: true,
17
+ Default: false,
18
+ Hovered: false,
19
+ Focused: false,
20
+ 'Read-only': false,
21
+ }),
22
+ readonly: figma.enum('State', {
23
+ 'Read-only': true,
24
+ Default: false,
25
+ Hovered: false,
26
+ Focused: false,
27
+ Disabled: false,
28
+ }),
29
+ isInvalid: figma.enum('Is invalid', {
30
+ True: true,
31
+ False: false,
32
+ }),
33
+ },
34
+ example: ({ size, disabled, readonly, isInvalid }) =>
35
+ html`<script setup>
36
+ import { MPhoneNumber } from '@mozaic-ds/vue';
37
+ </script>
38
+
39
+ <MPhoneNumber
40
+ id="phone-number-id"
41
+ size=${size}
42
+ disabled=${disabled}
43
+ readonly=${readonly}
44
+ :is-invalid=${isInvalid}
45
+ ></MPhoneNumber>`,
46
+ },
47
+ );
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Code Connect mapping for MPincode
3
+ * Links Figma Pincode input (ADS2) to @mozaic-ds/vue
4
+ */
5
+ import figma, { html } from '@figma/code-connect/html';
6
+
7
+ figma.connect(
8
+ 'https://www.figma.com/design/Zyh9RyabNaqkjbuFWP9Aqj/%E2%9C%A8-Components--ADS2---Stable-version-?node-id=12443-35984',
9
+ {
10
+ props: {
11
+ disabled: figma.enum('State', {
12
+ Disabled: true,
13
+ Default: false,
14
+ Hovered: false,
15
+ 'Read-only': false,
16
+ }),
17
+ readonly: figma.enum('State', {
18
+ 'Read-only': true,
19
+ Default: false,
20
+ Disabled: false,
21
+ Hovered: false,
22
+ }),
23
+ isInvalid: figma.enum('Is invalid', {
24
+ True: true,
25
+ False: false,
26
+ }),
27
+ },
28
+ example: ({ disabled, readonly, isInvalid }) =>
29
+ html`<script setup>
30
+ import { MPincode } from '@mozaic-ds/vue';
31
+ </script>
32
+
33
+ <MPincode
34
+ id="pincode-id"
35
+ disabled=${disabled}
36
+ readonly=${readonly}
37
+ :is-invalid=${isInvalid}
38
+ aria-label="Enter your code"
39
+ ></MPincode>`,
40
+ },
41
+ );
@@ -93,10 +93,7 @@ describe('MPincode component', () => {
93
93
  },
94
94
  });
95
95
 
96
- const inputs = wrapper.findAll('input');
97
- for (const input of inputs) {
98
- expect(input.classes()).toContain('is-invalid');
99
- }
96
+ expect(wrapper.classes()).toContain('is-invalid');
100
97
  });
101
98
 
102
99
  it('disables inputs when disabled is true', () => {
@@ -1,5 +1,14 @@
1
1
  <template>
2
- <div class="mc-pincode-input" @paste="onPaste">
2
+ <div
3
+ class="mc-pincode-input"
4
+ :class="{
5
+ 'mc-pincode-input': true,
6
+ 'mc-pincode-input__disabled': disabled,
7
+ 'mc-pincode-input__readonly': readonly,
8
+ 'is-invalid': isInvalid,
9
+ }"
10
+ @paste="onPaste"
11
+ >
3
12
  <input
4
13
  v-for="(digit, index) in otp"
5
14
  :key="index"
@@ -12,7 +21,6 @@
12
21
  autocomplete="one-time-code"
13
22
  :name="name || `pincode-${id}`"
14
23
  class="mc-pincode-input__control"
15
- :class="classObject"
16
24
  :disabled="disabled"
17
25
  :readonly="readonly"
18
26
  :value="digit"
@@ -25,13 +33,7 @@
25
33
  </template>
26
34
 
27
35
  <script setup lang="ts">
28
- import {
29
- ref,
30
- computed,
31
- watch,
32
- nextTick,
33
- type ComponentPublicInstance,
34
- } from 'vue';
36
+ import { ref, watch, nextTick, type ComponentPublicInstance } from 'vue';
35
37
  /**
36
38
  * A pincode input is a specialized input field used to enter short numeric codes, such as verification codes, security PINs, or authentication tokens. It typically separates each digit into individual fields to improve readability and ease of entry. This component is commonly used in two-factor authentication (2FA), password recovery, and secure access flows, ensuring a structured and user-friendly experience.<br><br> To put a label, requierement text, help text or to apply a valid or invalid message, the examples are available in the [Field section](/docs/form-elements-field--docs#input).
37
39
  */
@@ -71,12 +73,6 @@ const props = withDefaults(
71
73
  },
72
74
  );
73
75
 
74
- const classObject = computed(() => {
75
- return {
76
- 'is-invalid': props.isInvalid,
77
- };
78
- });
79
-
80
76
  const emit = defineEmits<{
81
77
  /**
82
78
  * Emits when the pincode value changes, updating the modelValue prop.
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Code Connect mapping for MPopover
3
+ * Links Figma Popover (ADS2) to @mozaic-ds/vue
4
+ */
5
+ import figma, { html } from '@figma/code-connect/html';
6
+
7
+ figma.connect(
8
+ 'https://www.figma.com/design/Zyh9RyabNaqkjbuFWP9Aqj/%E2%9C%A8-Components--ADS2---Stable-version-?node-id=9680-7268',
9
+ {
10
+ props: {
11
+ appearance: figma.enum('Appareance', {
12
+ Standard: 'standard',
13
+ Inverse: 'inverse',
14
+ }),
15
+ size: figma.enum('Size', {
16
+ 'S (default)': 's',
17
+ M: 'm',
18
+ L: 'l',
19
+ }),
20
+ pointer: figma.boolean('Has pointer'),
21
+ closable: figma.boolean('Is closable'),
22
+ },
23
+ example: ({ appearance, size, pointer, closable }) =>
24
+ html`<script setup>
25
+ import { MPopover, MButton } from '@mozaic-ds/vue';
26
+ </script>
27
+
28
+ <MPopover
29
+ id="popover-id"
30
+ title="Popover title"
31
+ description="Popover description"
32
+ :pointer=${pointer}
33
+ :closable=${closable}
34
+ appearance=${appearance}
35
+ size=${size}
36
+ >
37
+ <template #activator="{ id }">
38
+ <MButton :popovertarget="id">Open</MButton>
39
+ </template>
40
+ </MPopover>`,
41
+ },
42
+ );
@@ -103,4 +103,130 @@ describe('MPopover.vue', () => {
103
103
  expect(activator.exists()).toBe(true);
104
104
  expect(activator.attributes('popovertarget')).toBe(id);
105
105
  });
106
+
107
+ describe('Focus management (toggle)', () => {
108
+ it('focuses the first focusable element inside when opened', async () => {
109
+ const wrapper = mount(MPopover, {
110
+ attachTo: document.body,
111
+ props: { closable: true },
112
+ });
113
+
114
+ const popoverEl = wrapper.find('.mc-popover__wrapper').element;
115
+ const toggleEvent = new Event('toggle') as ToggleEvent;
116
+ Object.defineProperty(toggleEvent, 'newState', { value: 'open' });
117
+ popoverEl.dispatchEvent(toggleEvent);
118
+ await wrapper.vm.$nextTick();
119
+
120
+ const firstFocusable = popoverEl.querySelector<HTMLElement>(
121
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
122
+ );
123
+ expect(document.activeElement).toBe(firstFocusable);
124
+ wrapper.unmount();
125
+ });
126
+
127
+ it('focuses the popover container when opened without focusable content', async () => {
128
+ const wrapper = mount(MPopover, {
129
+ attachTo: document.body,
130
+ props: {
131
+ closable: false,
132
+ title: 'Information',
133
+ description: 'Read-only content',
134
+ },
135
+ });
136
+
137
+ const popoverEl = wrapper.find('.mc-popover__wrapper')
138
+ .element as HTMLElement;
139
+ const toggleEvent = new Event('toggle') as ToggleEvent;
140
+ Object.defineProperty(toggleEvent, 'newState', { value: 'open' });
141
+ popoverEl.dispatchEvent(toggleEvent);
142
+ await wrapper.vm.$nextTick();
143
+
144
+ expect(document.activeElement).toBe(popoverEl);
145
+ wrapper.unmount();
146
+ });
147
+
148
+ it('returns focus to the trigger when closed with focus inside the popover', async () => {
149
+ const wrapper = mount(MPopover, {
150
+ attachTo: document.body,
151
+ props: { closable: true },
152
+ slots: {
153
+ activator: (slotProps: { id: string }) =>
154
+ h('button', {
155
+ popovertarget: slotProps.id,
156
+ class: 'trigger-btn',
157
+ }),
158
+ },
159
+ });
160
+
161
+ // Simulate focus being on the close button (inside the popover)
162
+ const closeBtn = wrapper.findComponent(MIconButton).find('button')
163
+ .element as HTMLElement;
164
+ closeBtn.focus();
165
+
166
+ const popoverEl = wrapper.find('.mc-popover__wrapper').element;
167
+ const toggleEvent = new Event('toggle') as ToggleEvent;
168
+ Object.defineProperty(toggleEvent, 'newState', { value: 'closed' });
169
+ popoverEl.dispatchEvent(toggleEvent);
170
+ await wrapper.vm.$nextTick();
171
+
172
+ const trigger = wrapper.find('.trigger-btn').element as HTMLElement;
173
+ expect(document.activeElement).toBe(trigger);
174
+ wrapper.unmount();
175
+ });
176
+
177
+ it('returns focus to the trigger when closed with focus on document.body', async () => {
178
+ const wrapper = mount(MPopover, {
179
+ attachTo: document.body,
180
+ slots: {
181
+ activator: (slotProps: { id: string }) =>
182
+ h('button', {
183
+ popovertarget: slotProps.id,
184
+ class: 'trigger-btn',
185
+ }),
186
+ },
187
+ });
188
+
189
+ // Simulate browser moving focus to body after popover collapses
190
+ (document.activeElement as HTMLElement)?.blur?.();
191
+
192
+ const popoverEl = wrapper.find('.mc-popover__wrapper').element;
193
+ const toggleEvent = new Event('toggle') as ToggleEvent;
194
+ Object.defineProperty(toggleEvent, 'newState', { value: 'closed' });
195
+ popoverEl.dispatchEvent(toggleEvent);
196
+ await wrapper.vm.$nextTick();
197
+
198
+ const trigger = wrapper.find('.trigger-btn').element as HTMLElement;
199
+ expect(document.activeElement).toBe(trigger);
200
+ wrapper.unmount();
201
+ });
202
+
203
+ it('does not steal focus when closed by an outside click (focus already on another element)', async () => {
204
+ const wrapper = mount(MPopover, {
205
+ attachTo: document.body,
206
+ slots: {
207
+ activator: (slotProps: { id: string }) =>
208
+ h('button', {
209
+ popovertarget: slotProps.id,
210
+ class: 'trigger-btn',
211
+ }),
212
+ },
213
+ });
214
+
215
+ // Simulate user clicking an outside input
216
+ const outsideInput = document.createElement('input');
217
+ document.body.appendChild(outsideInput);
218
+ outsideInput.focus();
219
+
220
+ const popoverEl = wrapper.find('.mc-popover__wrapper').element;
221
+ const toggleEvent = new Event('toggle') as ToggleEvent;
222
+ Object.defineProperty(toggleEvent, 'newState', { value: 'closed' });
223
+ popoverEl.dispatchEvent(toggleEvent);
224
+ await wrapper.vm.$nextTick();
225
+
226
+ // Focus should remain on the outside input, not be stolen by the trigger
227
+ expect(document.activeElement).toBe(outsideInput);
228
+ outsideInput.remove();
229
+ wrapper.unmount();
230
+ });
231
+ });
106
232
  });