@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.
- package/dist/mozaic-vue.css +2 -1
- package/dist/mozaic-vue.d.ts +258 -137
- package/dist/mozaic-vue.js +14054 -10878
- package/dist/mozaic-vue.js.map +1 -1
- package/dist/mozaic-vue.umd.cjs +7 -25
- package/dist/mozaic-vue.umd.cjs.map +1 -1
- package/package.json +22 -11
- package/src/components/BrandPresets.mdx +2 -2
- package/src/components/Migration.mdx +651 -0
- package/src/components/accordionlist/MAccordionList.figma.ts +43 -0
- package/src/components/accordionlistitem/MAccordionListItem.figma.ts +27 -0
- package/src/components/accordionlistitem/MAccordionListItem.spec.ts +22 -3
- package/src/components/accordionlistitem/MAccordionListItem.vue +38 -28
- package/src/components/actionbottombar/MActionBottomBar.figma.ts +24 -0
- package/src/components/actionlistbox/MActionListbox.figma.ts +30 -0
- package/src/components/avatar/MAvatar.figma.ts +31 -0
- package/src/components/breadcrumb/MBreadcrumb.figma.ts +31 -0
- package/src/components/builtinmenu/MBuiltInMenu.figma.ts +23 -0
- package/src/components/builtinmenu/MBuiltInMenu.spec.ts +30 -1
- package/src/components/builtinmenu/MBuiltInMenu.vue +26 -17
- package/src/components/builtinmenu/README.md +2 -0
- package/src/components/button/MButton.figma.ts +41 -0
- package/src/components/callout/MCallout.figma.ts +29 -0
- package/src/components/callout/MCallout.spec.ts +35 -0
- package/src/components/callout/MCallout.vue +22 -4
- package/src/components/callout/README.md +2 -0
- package/src/components/carousel/MCarousel.figma.ts +32 -0
- package/src/components/checkbox/MCheckbox.figma.ts +45 -0
- package/src/components/checkboxgroup/MCheckboxGroup.figma.ts +30 -0
- package/src/components/checklistmenu/MCheckListMenu.figma.ts +29 -0
- package/src/components/checklistmenu/MCheckListMenu.spec.ts +12 -1
- package/src/components/checklistmenu/MCheckListMenu.vue +6 -0
- package/src/components/checklistmenu/README.md +2 -0
- package/src/components/circularprogressbar/MCircularProgressbar.figma.ts +31 -0
- package/src/components/combobox/MCombobox.figma.ts +48 -0
- package/src/components/combobox/MCombobox.spec.ts +1 -1
- package/src/components/combobox/MCombobox.vue +18 -9
- package/src/components/combobox/README.md +2 -2
- package/src/components/container/MContainer.figma.ts +30 -0
- package/src/components/datatable/DataTable.stories.ts +277 -0
- package/src/components/datatable/DataTableCells.stories.ts +251 -0
- package/src/components/datatable/DataTableEmpty.stories.ts +102 -0
- package/src/components/datatable/DataTableExpandable.stories.ts +95 -0
- package/src/components/datatable/DataTableNested.stories.ts +96 -0
- package/src/components/datatable/DataTableSelectable.stories.ts +124 -0
- package/src/components/datatable/DataTableSortable.stories.ts +164 -0
- package/src/components/datatable/MDataTable.types.ts +54 -0
- package/src/components/datatable/assets/styles.scss +10 -0
- package/src/components/datatable/datatable.mdx +63 -0
- package/src/components/datatable/tools/data.js +8 -0
- package/src/components/datatable/tools/data.json +2018 -0
- package/src/components/datatable/utils.js +19 -0
- package/src/components/datepicker/MDatepicker.figma.ts +20 -0
- package/src/components/divider/MDivider.figma.ts +30 -0
- package/src/components/drawer/MDrawer.figma.ts +37 -0
- package/src/components/drawer/README.md +1 -1
- package/src/components/field/MField.figma.ts +30 -0
- package/src/components/fileuploader/MFileUploader.figma.ts +23 -0
- package/src/components/fileuploaderitem/MFileUploaderItem.figma.ts +27 -0
- package/src/components/flag/MFlag.figma.ts +26 -0
- package/src/components/iconbutton/MIconButton.figma.ts +54 -0
- package/src/components/kpiitem/MKpiItem.figma.ts +33 -0
- package/src/components/linearprogressbarbuffer/MLinearProgressbarBuffer.figma.ts +31 -0
- package/src/components/linearprogressbarpercentage/MLinearProgressbarPercentage.figma.ts +26 -0
- package/src/components/link/MLink.figma.ts +32 -0
- package/src/components/loader/MLoader.figma.ts +30 -0
- package/src/components/loadingoverlay/MLoadingOverlay.figma.ts +18 -0
- package/src/components/modal/MModal.figma.ts +27 -0
- package/src/components/navigationindicator/MNavigationIndicator.figma.ts +24 -0
- package/src/components/navigationindicator/MNavigationIndicator.spec.ts +75 -18
- package/src/components/navigationindicator/MNavigationIndicator.vue +10 -12
- package/src/components/numberbadge/MNumberBadge.figma.ts +31 -0
- package/src/components/optionListbox/MOptionListbox.figma.ts +36 -0
- package/src/components/optionListbox/MOptionListbox.vue +34 -19
- package/src/components/optionListbox/README.md +1 -1
- package/src/components/overlay/MOverlay.figma.ts +20 -0
- package/src/components/pageheader/MPageHeader.figma.ts +21 -0
- package/src/components/pagination/MPagination.figma.ts +34 -0
- package/src/components/passwordinput/MPasswordInput.figma.ts +30 -0
- package/src/components/phonenumber/MPhoneNumber.figma.ts +47 -0
- package/src/components/pincode/MPincode.figma.ts +41 -0
- package/src/components/pincode/MPincode.spec.ts +1 -4
- package/src/components/pincode/MPincode.vue +11 -15
- package/src/components/popover/MPopover.figma.ts +42 -0
- package/src/components/popover/MPopover.spec.ts +126 -0
- package/src/components/popover/MPopover.vue +36 -1
- package/src/components/quantityselector/MQuantitySelector.figma.ts +50 -0
- package/src/components/radio/MRadio.figma.ts +40 -0
- package/src/components/radiogroup/MRadioGroup.figma.ts +30 -0
- package/src/components/segmentedcontrol/MSegmentedControl.figma.ts +33 -0
- package/src/components/segmentedcontrol/MSegmentedControl.spec.ts +92 -0
- package/src/components/segmentedcontrol/MSegmentedControl.vue +61 -2
- package/src/components/select/MSelect.figma.ts +49 -0
- package/src/components/sidebar/MSidebar.figma.ts +28 -0
- package/src/components/sidebarexpandableitem/MSidebarExpandableItem.figma.ts +19 -0
- package/src/components/sidebarfooter/MSidebarFooter.figma.ts +21 -0
- package/src/components/sidebarheader/MSidebarHeader.figma.ts +18 -0
- package/src/components/sidebarnavitem/MSidebarNavItem.figma.ts +23 -0
- package/src/components/sidebarshortcutitem/MSidebarShortcutItem.figma.ts +20 -0
- package/src/components/starrating/MStarRating.figma.ts +35 -0
- package/src/components/starrating/MStarRating.spec.ts +19 -22
- package/src/components/starrating/MStarRating.vue +3 -2
- package/src/components/statusbadge/MStatusBadge.figma.ts +27 -0
- package/src/components/statusdot/MStatusDot.figma.ts +31 -0
- package/src/components/statusmessage/MStatusMessage.figma.ts +28 -0
- package/src/components/statusmessage/MStatusMessage.spec.ts +15 -0
- package/src/components/statusmessage/MStatusMessage.stories.ts +4 -0
- package/src/components/statusmessage/MStatusMessage.vue +7 -0
- package/src/components/statusmessage/README.md +2 -0
- package/src/components/statusnotification/MStatusNotification.figma.ts +29 -0
- package/src/components/stepperbottombar/MStepperBottomBar.figma.ts +20 -0
- package/src/components/steppercompact/MStepperCompact.figma.ts +21 -0
- package/src/components/stepperinline/MStepperInline.figma.ts +23 -0
- package/src/components/stepperstacked/MStepperStacked.figma.ts +23 -0
- package/src/components/tabs/MTabs.figma.ts +33 -0
- package/src/components/tabs/MTabs.vue +90 -4
- package/src/components/tabs/Mtabs.spec.ts +162 -0
- package/src/components/tag/MTag.figma.ts +26 -0
- package/src/components/tag/MTag.stories.ts +13 -3
- package/src/components/tag/MTag.vue +11 -1
- package/src/components/tag/README.md +6 -0
- package/src/components/textarea/MTextArea.figma.ts +28 -0
- package/src/components/textinput/MTextInput.figma.ts +51 -0
- package/src/components/tile/MTile.figma.ts +31 -0
- package/src/components/tileclickable/MTileClickable.figma.ts +31 -0
- package/src/components/tileexpandable/MTileExpandable.figma.ts +31 -0
- package/src/components/tileselectable/MTileSelectable.figma.ts +29 -0
- package/src/components/toaster/MToaster.figma.ts +25 -0
- package/src/components/toggle/MToggle.figma.ts +39 -0
- package/src/components/togglegroup/MToggleGroup.figma.ts +30 -0
- package/src/components/tooltip/MTooltip.figma.ts +29 -0
- 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
|
|
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 {
|
|
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
|
+
);
|