@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
@@ -10,10 +10,13 @@
10
10
  >
11
11
  <div
12
12
  :id="id"
13
+ ref="popoverRef"
13
14
  class="mc-popover__wrapper"
14
15
  popover
16
+ tabindex="-1"
15
17
  :aria-labelledby="title && `${id}-title`"
16
18
  :aria-describedby="description && `${id}-description`"
19
+ @toggle="onToggle"
17
20
  >
18
21
  <div class="mc-popover__content">
19
22
  <div v-if="title || description" class="mc-popover__headings">
@@ -58,7 +61,7 @@
58
61
  </template>
59
62
 
60
63
  <script setup lang="ts">
61
- import { useId, type VNode } from 'vue';
64
+ import { useId, useTemplateRef, nextTick, type VNode } from 'vue';
62
65
  import { Cross20 } from '@mozaic-ds/icons-vue';
63
66
  import MIconButton from '../iconbutton/MIconButton.vue';
64
67
  /**
@@ -124,6 +127,38 @@ defineSlots<{
124
127
  }>();
125
128
 
126
129
  const id = useId();
130
+ const popoverRef = useTemplateRef('popoverRef');
131
+
132
+ function onToggle(event: ToggleEvent) {
133
+ if (event.newState === 'open') {
134
+ nextTick(() => {
135
+ const focusable = popoverRef.value?.querySelector<HTMLElement>(
136
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
137
+ );
138
+ if (focusable) {
139
+ focusable.focus();
140
+ } else {
141
+ popoverRef.value?.focus();
142
+ }
143
+ });
144
+ } else {
145
+ const activeEl = document.activeElement as HTMLElement | null;
146
+ const closedFromInside =
147
+ !activeEl ||
148
+ activeEl === document.body ||
149
+ popoverRef.value?.contains(activeEl);
150
+
151
+ if (closedFromInside) {
152
+ const triggers = document.querySelectorAll<HTMLElement>(
153
+ `[popovertarget="${id}"]:not(.mc-popover__close)`,
154
+ );
155
+ const trigger = Array.from(triggers).find(
156
+ (el) => !popoverRef.value?.contains(el),
157
+ );
158
+ trigger?.focus();
159
+ }
160
+ }
161
+ }
127
162
  </script>
128
163
 
129
164
  <style lang="scss" scoped>
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Code Connect mapping for MQuantitySelector
3
+ * Links Figma _quantity selector / 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-29697',
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 { MQuantitySelector } from '@mozaic-ds/vue';
37
+ </script>
38
+
39
+ <MQuantitySelector
40
+ id="quantity-selector-id"
41
+ size=${size}
42
+ disabled=${disabled}
43
+ readonly=${readonly}
44
+ :is-invalid=${isInvalid}
45
+ :model-value="1"
46
+ :min="0"
47
+ :max="10"
48
+ ></MQuantitySelector>`,
49
+ },
50
+ );
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Code Connect mapping for MRadio
3
+ * Links Figma Radio button (stand-alone) (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-29235',
9
+ {
10
+ props: {
11
+ modelValue: figma.enum('Is checked', {
12
+ True: true,
13
+ False: false,
14
+ }),
15
+ disabled: figma.enum('State', {
16
+ Disabled: true,
17
+ Default: false,
18
+ Hovered: false,
19
+ Focused: false,
20
+ 'Read-only': false,
21
+ }),
22
+ isInvalid: figma.enum('is invalid', {
23
+ True: true,
24
+ False: false,
25
+ }),
26
+ },
27
+ example: ({ modelValue, disabled, isInvalid }) =>
28
+ html`<script setup>
29
+ import { MRadio } from '@mozaic-ds/vue';
30
+ </script>
31
+
32
+ <MRadio
33
+ id="radio-id"
34
+ label="Radio button Label"
35
+ :model-value=${modelValue}
36
+ disabled=${disabled}
37
+ :is-invalid=${isInvalid}
38
+ ></MRadio>`,
39
+ },
40
+ );
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Code Connect mapping for MRadioGroup
3
+ * Links Figma Radio button group (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-29119',
9
+ {
10
+ props: {
11
+ inline: figma.enum('Layout', {
12
+ 'Stacked (default)': false,
13
+ Inline: true,
14
+ }),
15
+ },
16
+ example: ({ inline }) =>
17
+ html`<script setup>
18
+ import { MRadioGroup } from '@mozaic-ds/vue';
19
+ </script>
20
+
21
+ <MRadioGroup
22
+ name="group"
23
+ :options="[
24
+ { id: '1', label: 'Option 1', value: '1' },
25
+ { id: '2', label: 'Option 2', value: '2' },
26
+ ]"
27
+ inline=${inline}
28
+ />`,
29
+ },
30
+ );
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Code Connect mapping for MSegmentedControl
3
+ * Links Figma Segmented control (ADS2) to @mozaic-ds/vue
4
+ * Props from figma_get_component (Figma Console MCP) + MSegmentedControl.stories.ts
5
+ */
6
+ import figma, { html } from '@figma/code-connect/html';
7
+
8
+ figma.connect(
9
+ 'https://www.figma.com/design/Zyh9RyabNaqkjbuFWP9Aqj/%E2%9C%A8-Components--ADS2---Stable-version-?node-id=6-26997',
10
+ {
11
+ props: {
12
+ size: figma.enum('Size', {
13
+ S: 's',
14
+ M: 'm',
15
+ }),
16
+ full: figma.enum('Full width', {
17
+ True: true,
18
+ False: false,
19
+ }),
20
+ },
21
+ example: ({ size, full }) =>
22
+ html`<script setup>
23
+ import { MSegmentedControl } from '@mozaic-ds/vue';
24
+ </script>
25
+
26
+ <MSegmentedControl
27
+ size=${size}
28
+ full=${full}
29
+ :segments="[{ id: 'label1', label: 'Label' }, { id: 'label2', label: 'Label' }]"
30
+ model-value="label1"
31
+ ></MSegmentedControl>`,
32
+ },
33
+ );
@@ -1,5 +1,6 @@
1
1
  import { mount } from '@vue/test-utils';
2
2
  import { describe, it, expect } from 'vitest';
3
+ import { nextTick } from 'vue';
3
4
  import MSegmentedControl from './MSegmentedControl.vue';
4
5
 
5
6
  describe('MSegmentedControl.vue', () => {
@@ -145,4 +146,95 @@ describe('MSegmentedControl.vue', () => {
145
146
  expect(button.attributes('role')).toBe('radio');
146
147
  });
147
148
  });
149
+
150
+ it('forwards attrs to the radiogroup so callers can name the group', () => {
151
+ const wrapper = mount(MSegmentedControl, {
152
+ props: { segments },
153
+ attrs: { 'aria-label': 'View options' },
154
+ });
155
+ expect(wrapper.find('[role="radiogroup"]').attributes('aria-label')).toBe(
156
+ 'View options',
157
+ );
158
+ });
159
+
160
+ describe('Keyboard navigation (radiogroup pattern)', () => {
161
+ it('selected button has tabindex="0", others have tabindex="-1"', () => {
162
+ const wrapper = mount(MSegmentedControl, {
163
+ props: { segments, modelValue: '2' },
164
+ });
165
+ const buttons = wrapper.findAll('button');
166
+ expect(buttons[0].attributes('tabindex')).toBe('-1');
167
+ expect(buttons[1].attributes('tabindex')).toBe('0');
168
+ expect(buttons[2].attributes('tabindex')).toBe('-1');
169
+ });
170
+
171
+ it('keeps the first segment tabbable when modelValue does not match any segment', () => {
172
+ const wrapper = mount(MSegmentedControl, {
173
+ props: { segments, modelValue: 'unknown' },
174
+ });
175
+ const buttons = wrapper.findAll('button');
176
+ expect(buttons[0].attributes('tabindex')).toBe('0');
177
+ expect(buttons[1].attributes('tabindex')).toBe('-1');
178
+ expect(buttons[2].attributes('tabindex')).toBe('-1');
179
+ expect(buttons[0].attributes('aria-checked')).toBe('false');
180
+ });
181
+
182
+ it('ArrowRight selects and focuses the next segment', async () => {
183
+ const wrapper = mount(MSegmentedControl, {
184
+ attachTo: document.body,
185
+ props: { segments, modelValue: '1' },
186
+ });
187
+ const buttons = wrapper.findAll('button');
188
+ await buttons[0].element.focus();
189
+ await buttons[0].trigger('keydown', { key: 'ArrowRight' });
190
+ await nextTick();
191
+ expect(wrapper.emitted('update:modelValue')![0]).toEqual(['2']);
192
+ expect(document.activeElement).toBe(buttons[1].element);
193
+ wrapper.unmount();
194
+ });
195
+
196
+ it('ArrowLeft selects and focuses the previous segment', async () => {
197
+ const wrapper = mount(MSegmentedControl, {
198
+ attachTo: document.body,
199
+ props: { segments, modelValue: '2' },
200
+ });
201
+ const buttons = wrapper.findAll('button');
202
+ await buttons[1].trigger('keydown', { key: 'ArrowLeft' });
203
+ expect(wrapper.emitted('update:modelValue')![0]).toEqual(['1']);
204
+ wrapper.unmount();
205
+ });
206
+
207
+ it('ArrowRight wraps around from last to first segment', async () => {
208
+ const wrapper = mount(MSegmentedControl, {
209
+ attachTo: document.body,
210
+ props: { segments, modelValue: '3' },
211
+ });
212
+ const buttons = wrapper.findAll('button');
213
+ await buttons[2].trigger('keydown', { key: 'ArrowRight' });
214
+ expect(wrapper.emitted('update:modelValue')![0]).toEqual(['1']);
215
+ wrapper.unmount();
216
+ });
217
+
218
+ it('Home selects the first segment', async () => {
219
+ const wrapper = mount(MSegmentedControl, {
220
+ attachTo: document.body,
221
+ props: { segments, modelValue: '3' },
222
+ });
223
+ const buttons = wrapper.findAll('button');
224
+ await buttons[2].trigger('keydown', { key: 'Home' });
225
+ expect(wrapper.emitted('update:modelValue')![0]).toEqual(['1']);
226
+ wrapper.unmount();
227
+ });
228
+
229
+ it('End selects the last segment', async () => {
230
+ const wrapper = mount(MSegmentedControl, {
231
+ attachTo: document.body,
232
+ props: { segments, modelValue: '1' },
233
+ });
234
+ const buttons = wrapper.findAll('button');
235
+ await buttons[0].trigger('keydown', { key: 'End' });
236
+ expect(wrapper.emitted('update:modelValue')![0]).toEqual(['3']);
237
+ wrapper.unmount();
238
+ });
239
+ });
148
240
  });
@@ -1,8 +1,14 @@
1
1
  <template>
2
- <div class="mc-segmented-control" :class="classObject" role="radiogroup">
2
+ <div
3
+ class="mc-segmented-control"
4
+ :class="classObject"
5
+ role="radiogroup"
6
+ v-bind="$attrs"
7
+ >
3
8
  <button
4
9
  v-for="(segment, index) in segments"
5
10
  :key="`segment-${index}`"
11
+ :ref="(el) => setButtonRef(el, index)"
6
12
  type="button"
7
13
  class="mc-segmented-control__segment"
8
14
  :class="{
@@ -13,7 +19,9 @@
13
19
  }"
14
20
  :aria-checked="isSegmentSelected(index, segment.id)"
15
21
  role="radio"
22
+ :tabindex="index === selectedIndex ? 0 : -1"
16
23
  @click="onClickSegment(index, segment.id)"
24
+ @keydown="onKeydown($event, index)"
17
25
  >
18
26
  {{ segment.label }}
19
27
  </button>
@@ -21,7 +29,13 @@
21
29
  </template>
22
30
 
23
31
  <script setup lang="ts">
24
- import { computed, ref, watch } from 'vue';
32
+ import {
33
+ computed,
34
+ nextTick,
35
+ ref,
36
+ watch,
37
+ type ComponentPublicInstance,
38
+ } from 'vue';
25
39
  /**
26
40
  * A Segmented Control allows users to switch between multiple options or views within a single container. It provides a compact and efficient way to toggle between sections without requiring a dropdown or separate navigation. Segmented Controls are commonly used in filters, tabbed navigation, and content selection to enhance user interaction and accessibility.
27
41
  */
@@ -71,6 +85,17 @@ const classObject = computed(() => {
71
85
 
72
86
  const modelValue = ref<string | number | undefined>(props.modelValue);
73
87
 
88
+ const selectedIndex = computed(() => {
89
+ const index = props.segments.findIndex((segment, segmentIndex) => {
90
+ const value =
91
+ typeof props.modelValue === 'number' ? segmentIndex : segment.id;
92
+
93
+ return modelValue.value === value;
94
+ });
95
+
96
+ return index >= 0 ? index : 0;
97
+ });
98
+
74
99
  watch(
75
100
  () => props.modelValue,
76
101
  (newVal) => {
@@ -93,6 +118,40 @@ const isSegmentSelected = (index: number, id?: string) => {
93
118
  return modelValue.value === value;
94
119
  };
95
120
 
121
+ const buttonRefs = ref<(HTMLButtonElement | null)[]>([]);
122
+
123
+ function setButtonRef(
124
+ el: Element | ComponentPublicInstance | null,
125
+ index: number,
126
+ ) {
127
+ buttonRefs.value[index] = el as HTMLButtonElement | null;
128
+ }
129
+
130
+ function onKeydown(event: KeyboardEvent, index: number) {
131
+ let nextIndex: number | null = null;
132
+
133
+ if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
134
+ event.preventDefault();
135
+ nextIndex = (index + 1) % props.segments.length;
136
+ } else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {
137
+ event.preventDefault();
138
+ nextIndex = (index - 1 + props.segments.length) % props.segments.length;
139
+ } else if (event.key === 'Home') {
140
+ event.preventDefault();
141
+ nextIndex = 0;
142
+ } else if (event.key === 'End') {
143
+ event.preventDefault();
144
+ nextIndex = props.segments.length - 1;
145
+ }
146
+
147
+ if (nextIndex !== null) {
148
+ onClickSegment(nextIndex, props.segments[nextIndex].id);
149
+ nextTick(() => buttonRefs.value[nextIndex!]?.focus());
150
+ }
151
+ }
152
+
153
+ defineOptions({ inheritAttrs: false });
154
+
96
155
  const emit = defineEmits<{
97
156
  /**
98
157
  * Emits when the selected segment changes, updating the modelValue prop.
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Code Connect mapping for MSelect
3
+ * Links Figma _select / 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=4924-17972',
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 { MSelect } from '@mozaic-ds/vue';
37
+ </script>
38
+
39
+ <MSelect
40
+ id="select-id"
41
+ size=${size}
42
+ disabled=${disabled}
43
+ readonly=${readonly}
44
+ :is-invalid=${isInvalid}
45
+ placeholder="Choose an option"
46
+ :options="[{ text: 'Option 1', value: 'option1' }, { text: 'Option 2', value: 'option2' }]"
47
+ ></MSelect>`,
48
+ },
49
+ );
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Code Connect mapping for MSidebar
3
+ * Links Figma Sidebar (desktop only) (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=11697-6444',
9
+ {
10
+ props: {
11
+ modelValue: figma.enum('is expanded', {
12
+ True: true,
13
+ False: false,
14
+ }),
15
+ },
16
+ example: ({ modelValue }) =>
17
+ html`<script setup>
18
+ import { MSidebar } from '@mozaic-ds/vue';
19
+ </script>
20
+
21
+ <MSidebar :model-value=${modelValue}>
22
+ <template #header>Header</template>
23
+ <template #shortcuts>Shortcuts</template>
24
+ <template #nav>Nav items</template>
25
+ <template #footer>Footer</template>
26
+ </MSidebar>`,
27
+ },
28
+ );
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Code Connect mapping for MSidebarExpandableItem
3
+ * Links Figma _sidebar / section menu item (multiple levels) 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=11697-6356',
9
+ {
10
+ example: () =>
11
+ html`<script setup>
12
+ import { MSidebarExpandableItem, MSidebarNavItem } from '@mozaic-ds/vue';
13
+ </script>
14
+
15
+ <MSidebarExpandableItem label="Section" menu-label="Section">
16
+ <MSidebarNavItem label="Sub item" />
17
+ </MSidebarExpandableItem>`,
18
+ },
19
+ );
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Code Connect mapping for MSidebarFooter
3
+ * Links Figma _sidebar / footer 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=11880-28442',
9
+ {
10
+ props: {
11
+ expendable: figma.boolean('Is expandable'),
12
+ hasProfileInfo: figma.boolean('Has profile info'),
13
+ },
14
+ example: ({ expendable }) =>
15
+ html`<script setup>
16
+ import { MSidebarFooter } from '@mozaic-ds/vue';
17
+ </script>
18
+
19
+ <MSidebarFooter title="User name" subtitle="user@example.com" :expendable=${expendable} />`,
20
+ },
21
+ );
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Code Connect mapping for MSidebarHeader
3
+ * Links Figma _sidebar / header 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=11910-12842',
9
+ {
10
+ props: {},
11
+ example: () =>
12
+ html`<script setup>
13
+ import { MSidebarHeader } from '@mozaic-ds/vue';
14
+ </script>
15
+
16
+ <MSidebarHeader title="App name" logo="/logo.png" />`,
17
+ },
18
+ );
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Code Connect mapping for MSidebarNavItem
3
+ * Links Figma _sidebar / section menu item (single level) 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=18213-20397',
9
+ {
10
+ props: {
11
+ active: figma.enum('Is selected', {
12
+ False: false,
13
+ True: true,
14
+ }),
15
+ },
16
+ example: ({ active }) =>
17
+ html`<script setup>
18
+ import { MSidebarNavItem } from '@mozaic-ds/vue';
19
+ </script>
20
+
21
+ <MSidebarNavItem label="Menu item" active=${active} />`,
22
+ },
23
+ );
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Code Connect mapping for MSidebarShortcutItem
3
+ * Links Figma _sidebar / shortcut item 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=11697-6376',
9
+ {
10
+ props: {
11
+ label: figma.string('Text'),
12
+ },
13
+ example: ({ label }) =>
14
+ html`<script setup>
15
+ import { MSidebarShortcutItem } from '@mozaic-ds/vue';
16
+ </script>
17
+
18
+ <MSidebarShortcutItem label=${label} href="#" />`,
19
+ },
20
+ );
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Code Connect mapping for MStarRating
3
+ * Links Figma Star Rating to @mozaic-ds/vue
4
+ * Figma component (node 5:14389) exposes: Type, Size, Text only.
5
+ */
6
+ import figma, { html } from '@figma/code-connect/html';
7
+
8
+ figma.connect(
9
+ 'https://www.figma.com/design/Zyh9RyabNaqkjbuFWP9Aqj/%E2%9C%A8-Components--ADS2---Stable-version-?node-id=5-14389',
10
+ {
11
+ props: {
12
+ text: figma.string('Text'),
13
+ size: figma.enum('Size', {
14
+ 'S (20px)': 's',
15
+ 'M (24px)': 'm',
16
+ 'L (32px)': 'l',
17
+ }),
18
+ type: figma.enum('Type', {
19
+ 'Stars only (read-only or as input)': 'stars',
20
+ 'Additional info': 'additionalInfo',
21
+ Link: 'link',
22
+ }),
23
+ },
24
+ example: ({ text, size }) =>
25
+ html`<script setup>
26
+ import { MStarRating } from '@mozaic-ds/vue';
27
+ </script>
28
+
29
+ <MStarRating
30
+ :model-value="3.5"
31
+ size=${size}
32
+ text=${text}
33
+ ></MStarRating>`,
34
+ },
35
+ );