@mozaic-ds/vue 2.15.0 → 2.17.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 +1040 -408
- package/dist/mozaic-vue.js +17183 -6742
- package/dist/mozaic-vue.js.map +1 -1
- package/dist/mozaic-vue.umd.cjs +26 -6
- package/dist/mozaic-vue.umd.cjs.map +1 -1
- package/package.json +11 -5
- package/src/components/BrandPresets.mdx +2 -2
- package/src/components/ComponentsMapping.mdx +98 -0
- package/src/components/accordionlist/MAccordionList.figma.ts +43 -0
- package/src/components/accordionlistitem/MAccordionListItem.figma.ts +27 -0
- package/src/components/actionbottombar/MActionBottomBar.figma.ts +24 -0
- package/src/components/actionlistbox/MActionListbox.figma.ts +30 -0
- package/src/components/actionlistbox/MActionListbox.spec.ts +14 -0
- package/src/components/actionlistbox/MActionListbox.stories.ts +15 -8
- package/src/components/actionlistbox/MActionListbox.vue +13 -1
- package/src/components/actionlistbox/README.md +2 -1
- 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/button/MButton.figma.ts +41 -0
- package/src/components/button/README.md +2 -0
- package/src/components/callout/MCallout.figma.ts +29 -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/circularprogressbar/MCircularProgressbar.figma.ts +31 -0
- package/src/components/combobox/MCombobox.figma.ts +48 -0
- package/src/components/combobox/MCombobox.spec.ts +246 -0
- package/src/components/combobox/MCombobox.stories.ts +190 -0
- package/src/components/combobox/MCombobox.vue +286 -0
- package/src/components/combobox/README.md +52 -0
- 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 +62 -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/field/MField.stories.ts +105 -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/numberbadge/MNumberBadge.figma.ts +31 -0
- package/src/components/optionListbox/MOptionListbox.figma.ts +36 -0
- package/src/components/optionListbox/MOptionListbox.spec.ts +527 -0
- package/src/components/optionListbox/MOptionListbox.vue +470 -0
- package/src/components/optionListbox/README.md +63 -0
- 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/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/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/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/stepperstacked/MStepperStacked.spec.ts +162 -0
- package/src/components/stepperstacked/MStepperStacked.stories.ts +57 -0
- package/src/components/stepperstacked/MStepperStacked.vue +106 -0
- package/src/components/stepperstacked/README.md +15 -0
- package/src/components/tabs/MTabs.figma.ts +33 -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/textinput/MTextInput.vue +13 -1
- package/src/components/textinput/README.md +15 -1
- 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
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
ref="listboxEl"
|
|
4
|
+
class="mc-option-listbox mc-listbox__content mc-combobox__listbox"
|
|
5
|
+
>
|
|
6
|
+
<template v-if="search">
|
|
7
|
+
<div class="mc-option-listbox__search">
|
|
8
|
+
<MTextInput
|
|
9
|
+
ref="textInput"
|
|
10
|
+
v-model="searchText"
|
|
11
|
+
role="combobox"
|
|
12
|
+
:id="`search-${id}`"
|
|
13
|
+
size="s"
|
|
14
|
+
:placeholder="searchPlaceholder"
|
|
15
|
+
autocomplete="off"
|
|
16
|
+
:aria-expanded="open"
|
|
17
|
+
:aria-controls="`listbox-${id}`"
|
|
18
|
+
aria-autocomplete="list"
|
|
19
|
+
:aria-activedescendant="activeDescendantId"
|
|
20
|
+
@input="updateFilteredResults"
|
|
21
|
+
@keydown="handleKeydown"
|
|
22
|
+
>
|
|
23
|
+
<template #icon>
|
|
24
|
+
<Search24 />
|
|
25
|
+
</template>
|
|
26
|
+
</MTextInput>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<hr class="mc-option-listbox__separator" />
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<template v-if="multiple && actions">
|
|
33
|
+
<div class="mc-option-listbox__actions">
|
|
34
|
+
<MButton appearance="accent" ghost size="s" @click="selectAll">
|
|
35
|
+
{{ selectLabel }}
|
|
36
|
+
</MButton>
|
|
37
|
+
<MButton appearance="standard" ghost size="s" @click="clearSelection">
|
|
38
|
+
{{ clearLabel }}
|
|
39
|
+
</MButton>
|
|
40
|
+
</div>
|
|
41
|
+
<hr class="mc-option-listbox__separator" />
|
|
42
|
+
</template>
|
|
43
|
+
|
|
44
|
+
<ul
|
|
45
|
+
class="mc-option-listbox__list"
|
|
46
|
+
role="listbox"
|
|
47
|
+
:id="`listbox-${id}`"
|
|
48
|
+
:tabindex="-1"
|
|
49
|
+
aria-label="Suggestions"
|
|
50
|
+
:aria-multiselectable="multiple"
|
|
51
|
+
>
|
|
52
|
+
<li
|
|
53
|
+
v-for="(item, index) in filteredResults"
|
|
54
|
+
:key="index"
|
|
55
|
+
:id="`option-${id}-${index}`"
|
|
56
|
+
:class="{
|
|
57
|
+
'mc-option-listbox__item': true,
|
|
58
|
+
'mc-option-listbox__item--section': item.type === 'section',
|
|
59
|
+
'mc-option-listbox__item--readonly': readonly,
|
|
60
|
+
'mc-option-listbox__item--disabled': item.disabled,
|
|
61
|
+
'mc-option-listbox__item--selectable': isSelectable(item),
|
|
62
|
+
'mc-option-listbox__item--active': activeIndex === index,
|
|
63
|
+
'mc-option-listbox__item--selected':
|
|
64
|
+
item.type === 'section'
|
|
65
|
+
? isSectionSelected(item) || isIndeterminate(item)
|
|
66
|
+
: isOptionSelected(item),
|
|
67
|
+
}"
|
|
68
|
+
v-bind="
|
|
69
|
+
item.type === 'section' && !checkableSections
|
|
70
|
+
? {
|
|
71
|
+
role: 'presentation',
|
|
72
|
+
}
|
|
73
|
+
: {
|
|
74
|
+
role: 'option',
|
|
75
|
+
['aria-disabled']: item.disabled,
|
|
76
|
+
['aria-selected']: isOptionSelected(item),
|
|
77
|
+
onClick: () =>
|
|
78
|
+
item.type === 'section'
|
|
79
|
+
? toggleSection(item)
|
|
80
|
+
: toggleValue(item),
|
|
81
|
+
}
|
|
82
|
+
"
|
|
83
|
+
>
|
|
84
|
+
<div class="mc-option-listbox__label">
|
|
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>
|
|
96
|
+
|
|
97
|
+
<span v-if="item.content" class="mc-option-listbox__additional">
|
|
98
|
+
{{ item.content }}
|
|
99
|
+
</span>
|
|
100
|
+
</div>
|
|
101
|
+
</slot>
|
|
102
|
+
|
|
103
|
+
<div class="mc-option-listbox__spacer"></div>
|
|
104
|
+
|
|
105
|
+
<template v-if="isSelectable(item)">
|
|
106
|
+
<span
|
|
107
|
+
v-if="item.type === 'section'"
|
|
108
|
+
class="mc-option-listbox__checkbox"
|
|
109
|
+
>
|
|
110
|
+
<component :is="isIndeterminate(item) ? Less20 : Check20" />
|
|
111
|
+
</span>
|
|
112
|
+
|
|
113
|
+
<template v-else>
|
|
114
|
+
<span v-if="multiple" class="mc-option-listbox__checkbox">
|
|
115
|
+
<Check20 />
|
|
116
|
+
</span>
|
|
117
|
+
|
|
118
|
+
<CheckCircleFilled24
|
|
119
|
+
v-else
|
|
120
|
+
class="mc-option-listbox__selection-icon"
|
|
121
|
+
/>
|
|
122
|
+
</template>
|
|
123
|
+
</template>
|
|
124
|
+
</div>
|
|
125
|
+
</li>
|
|
126
|
+
</ul>
|
|
127
|
+
</div>
|
|
128
|
+
</template>
|
|
129
|
+
|
|
130
|
+
<script setup lang="ts">
|
|
131
|
+
import { computed, ref, useTemplateRef, watch, type VNode } from 'vue';
|
|
132
|
+
import MButton from '../button/MButton.vue';
|
|
133
|
+
import MTextInput from '../textinput/MTextInput.vue';
|
|
134
|
+
import {
|
|
135
|
+
CheckCircleFilled24,
|
|
136
|
+
Search24,
|
|
137
|
+
Less20,
|
|
138
|
+
Check20,
|
|
139
|
+
} from '@mozaic-ds/icons-vue';
|
|
140
|
+
import { debounce } from 'lodash';
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 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.
|
|
144
|
+
*/
|
|
145
|
+
export type ListboxOption = {
|
|
146
|
+
label: string;
|
|
147
|
+
content?: string;
|
|
148
|
+
value?: string | number;
|
|
149
|
+
disabled?: boolean;
|
|
150
|
+
type?: 'option' | 'section';
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const props = withDefaults(
|
|
154
|
+
defineProps<{
|
|
155
|
+
/**
|
|
156
|
+
* The current selected value(s) of the listbox.
|
|
157
|
+
*/
|
|
158
|
+
modelValue: string | number | null | (string | number)[];
|
|
159
|
+
/**
|
|
160
|
+
* Unique identifier for the listbox.
|
|
161
|
+
*/
|
|
162
|
+
id: string;
|
|
163
|
+
/**
|
|
164
|
+
* Whether the listbox is open.
|
|
165
|
+
*/
|
|
166
|
+
open?: boolean;
|
|
167
|
+
/**
|
|
168
|
+
* Enable multiple selection.
|
|
169
|
+
*/
|
|
170
|
+
multiple?: boolean;
|
|
171
|
+
/**
|
|
172
|
+
* Make the listbox read-only.
|
|
173
|
+
*/
|
|
174
|
+
readonly?: boolean;
|
|
175
|
+
/**
|
|
176
|
+
* Show a search input above the options.
|
|
177
|
+
*/
|
|
178
|
+
search?: boolean;
|
|
179
|
+
/**
|
|
180
|
+
* Show select all / clear buttons when multiple.
|
|
181
|
+
*/
|
|
182
|
+
actions?: boolean;
|
|
183
|
+
/**
|
|
184
|
+
* Enable checkable section headers.
|
|
185
|
+
*/
|
|
186
|
+
checkableSections?: boolean;
|
|
187
|
+
/**
|
|
188
|
+
* Placeholder text for the search input.
|
|
189
|
+
*/
|
|
190
|
+
searchPlaceholder?: string;
|
|
191
|
+
/**
|
|
192
|
+
* Label for the "Select all" button.
|
|
193
|
+
*/
|
|
194
|
+
selectLabel?: string;
|
|
195
|
+
/**
|
|
196
|
+
* Label for the "Clear selection" button.
|
|
197
|
+
*/
|
|
198
|
+
clearLabel?: string;
|
|
199
|
+
/**
|
|
200
|
+
* Array of options and sections to display in the listbox.
|
|
201
|
+
*/
|
|
202
|
+
options: Array<ListboxOption>;
|
|
203
|
+
}>(),
|
|
204
|
+
{
|
|
205
|
+
searchPlaceholder: 'Find an option...',
|
|
206
|
+
selectLabel: 'Select all',
|
|
207
|
+
clearLabel: 'Clear',
|
|
208
|
+
},
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const emit = defineEmits<{
|
|
212
|
+
/**
|
|
213
|
+
* Emits when the selected value changes.
|
|
214
|
+
*/
|
|
215
|
+
(
|
|
216
|
+
on: 'update:modelValue',
|
|
217
|
+
value: string | number | null | (string | number)[],
|
|
218
|
+
): void;
|
|
219
|
+
/**
|
|
220
|
+
* Emits when the listbox should open.
|
|
221
|
+
*/
|
|
222
|
+
(on: 'open'): void;
|
|
223
|
+
/**
|
|
224
|
+
* Emits when the listbox should close.
|
|
225
|
+
*/
|
|
226
|
+
(on: 'close'): void;
|
|
227
|
+
}>();
|
|
228
|
+
|
|
229
|
+
defineSlots<{
|
|
230
|
+
/**
|
|
231
|
+
* Use this slot to customize the content of each item.
|
|
232
|
+
*/
|
|
233
|
+
item(props: { item: ListboxOption }): VNode;
|
|
234
|
+
}>();
|
|
235
|
+
|
|
236
|
+
const listboxEl = useTemplateRef('listboxEl');
|
|
237
|
+
const textInput = useTemplateRef('textInput');
|
|
238
|
+
|
|
239
|
+
const activeIndex = ref<number>(-1);
|
|
240
|
+
|
|
241
|
+
const searchText = ref('');
|
|
242
|
+
|
|
243
|
+
const filteredResults = ref<ListboxOption[]>(props.options);
|
|
244
|
+
|
|
245
|
+
const selection = computed({
|
|
246
|
+
get() {
|
|
247
|
+
return props.modelValue;
|
|
248
|
+
},
|
|
249
|
+
set(value: string | number | null | (string | number)[]) {
|
|
250
|
+
emit('update:modelValue', value);
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const activeDescendantId = computed(() => {
|
|
255
|
+
return activeIndex.value >= 0
|
|
256
|
+
? `option-${props.id}-${activeIndex.value}`
|
|
257
|
+
: undefined;
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const updateFilteredResults = debounce(() => {
|
|
261
|
+
const search = searchText.value.toLowerCase().trim();
|
|
262
|
+
|
|
263
|
+
if (!search) {
|
|
264
|
+
filteredResults.value = props.options;
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
filteredResults.value = props.options.filter((option) =>
|
|
269
|
+
option.label.toLowerCase().includes(search),
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
activeIndex.value = filteredResults.value.length ? 0 : -1;
|
|
273
|
+
}, 200);
|
|
274
|
+
|
|
275
|
+
const sectionMap = computed(() => {
|
|
276
|
+
const map = new Map<string, ListboxOption[]>();
|
|
277
|
+
let currentSection: ListboxOption | null = null;
|
|
278
|
+
|
|
279
|
+
props.options.forEach((option) => {
|
|
280
|
+
if (option.type === 'section') {
|
|
281
|
+
currentSection = option;
|
|
282
|
+
map.set(currentSection?.value?.toString() || currentSection.label, []);
|
|
283
|
+
} else if (currentSection) {
|
|
284
|
+
map
|
|
285
|
+
.get(currentSection?.value?.toString() || currentSection.label)!
|
|
286
|
+
.push(option);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
return map;
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
function toggleSection(item: ListboxOption) {
|
|
294
|
+
if (!props.checkableSections || !props.multiple) return;
|
|
295
|
+
|
|
296
|
+
const sectionItems =
|
|
297
|
+
sectionMap.value.get(item.value?.toString() || item.label) || [];
|
|
298
|
+
const selectedItems = selection.value as (string | number)[];
|
|
299
|
+
|
|
300
|
+
if (isSectionSelected(item)) {
|
|
301
|
+
selection.value = selectedItems.filter(
|
|
302
|
+
(opt) => !sectionItems.find((item) => item.value === opt),
|
|
303
|
+
);
|
|
304
|
+
} else {
|
|
305
|
+
selection.value = [
|
|
306
|
+
...selectedItems,
|
|
307
|
+
...sectionItems
|
|
308
|
+
.filter((opt) => !selectedItems.includes(opt.value!))
|
|
309
|
+
.map((item) => item.value!),
|
|
310
|
+
];
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function toggleValue(item?: ListboxOption) {
|
|
315
|
+
if (!item || !isSelectable(item)) return;
|
|
316
|
+
|
|
317
|
+
if (Array.isArray(selection.value)) {
|
|
318
|
+
if (isOptionSelected(item)) {
|
|
319
|
+
selection.value = (selection.value as (string | number)[]).filter(
|
|
320
|
+
(el) => el !== item.value,
|
|
321
|
+
);
|
|
322
|
+
} else {
|
|
323
|
+
selection.value = [...selection.value, item.value!];
|
|
324
|
+
}
|
|
325
|
+
} else {
|
|
326
|
+
selection.value = isOptionSelected(item) ? null : item.value!;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function selectAll() {
|
|
331
|
+
selection.value = [
|
|
332
|
+
...props.options
|
|
333
|
+
.filter(
|
|
334
|
+
(option) =>
|
|
335
|
+
!!option.value && !option.disabled && option.type !== 'section',
|
|
336
|
+
)
|
|
337
|
+
.map((item) => item.value!),
|
|
338
|
+
];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function clearSelection() {
|
|
342
|
+
if (Array.isArray(selection.value)) {
|
|
343
|
+
selection.value = [];
|
|
344
|
+
} else {
|
|
345
|
+
selection.value = null;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function isSelectable(item: ListboxOption) {
|
|
350
|
+
return (
|
|
351
|
+
(item.type !== 'section' && !item.disabled) ||
|
|
352
|
+
(item.type === 'section' && props.checkableSections && props.multiple)
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function isSectionSelected(item: ListboxOption) {
|
|
357
|
+
if (!props.checkableSections || !props.multiple) return false;
|
|
358
|
+
|
|
359
|
+
const sectionItems =
|
|
360
|
+
sectionMap.value.get(item.value?.toString() || item.label) || [];
|
|
361
|
+
const selectedItems = selection.value as (string | number)[];
|
|
362
|
+
|
|
363
|
+
return sectionItems.every((opt) => selectedItems.includes(opt.value!));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function isOptionSelected(item: ListboxOption) {
|
|
367
|
+
if (!item.value) return false;
|
|
368
|
+
|
|
369
|
+
if (Array.isArray(selection.value)) {
|
|
370
|
+
return (selection.value as (string | number)[])?.includes(item.value!);
|
|
371
|
+
} else {
|
|
372
|
+
return item.value === selection.value;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function isIndeterminate(item: ListboxOption) {
|
|
377
|
+
const section = sectionMap.value.get(item.value?.toString() || item.label);
|
|
378
|
+
return (
|
|
379
|
+
section?.some((option) => isOptionSelected(option)) &&
|
|
380
|
+
!section?.every((option) => isOptionSelected(option))
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function moveActive(delta: number) {
|
|
385
|
+
if (!props.open || filteredResults.value.length === 0) return;
|
|
386
|
+
|
|
387
|
+
let nextIndex = activeIndex.value + delta;
|
|
388
|
+
|
|
389
|
+
if (nextIndex < 0) nextIndex = filteredResults.value.length - 1;
|
|
390
|
+
if (nextIndex >= filteredResults.value.length) nextIndex = 0;
|
|
391
|
+
|
|
392
|
+
while (!isSelectable(filteredResults.value[nextIndex])) {
|
|
393
|
+
nextIndex += delta > 0 ? 1 : -1;
|
|
394
|
+
if (nextIndex < 0) nextIndex = filteredResults.value.length - 1;
|
|
395
|
+
if (nextIndex >= filteredResults.value.length) nextIndex = 0;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
activeIndex.value = nextIndex;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function selectActive() {
|
|
402
|
+
const item = filteredResults.value[activeIndex.value];
|
|
403
|
+
if (!item || !isSelectable(item)) return;
|
|
404
|
+
|
|
405
|
+
if (item.type === 'section') {
|
|
406
|
+
toggleSection(item);
|
|
407
|
+
} else {
|
|
408
|
+
toggleValue(item);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
413
|
+
switch (event.key) {
|
|
414
|
+
case 'ArrowDown':
|
|
415
|
+
event.preventDefault();
|
|
416
|
+
if (!props.open) {
|
|
417
|
+
emit('open');
|
|
418
|
+
activeIndex.value = 0;
|
|
419
|
+
} else {
|
|
420
|
+
moveActive(1);
|
|
421
|
+
}
|
|
422
|
+
break;
|
|
423
|
+
case 'ArrowUp':
|
|
424
|
+
event.preventDefault();
|
|
425
|
+
if (!props.open) {
|
|
426
|
+
emit('open');
|
|
427
|
+
activeIndex.value = filteredResults.value.length - 1;
|
|
428
|
+
} else {
|
|
429
|
+
moveActive(-1);
|
|
430
|
+
}
|
|
431
|
+
break;
|
|
432
|
+
case 'Enter':
|
|
433
|
+
event.preventDefault();
|
|
434
|
+
if (!props.open) {
|
|
435
|
+
emit('open');
|
|
436
|
+
activeIndex.value = 0;
|
|
437
|
+
} else {
|
|
438
|
+
selectActive();
|
|
439
|
+
}
|
|
440
|
+
break;
|
|
441
|
+
case 'Escape':
|
|
442
|
+
event.preventDefault();
|
|
443
|
+
emit('close');
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
watch(
|
|
449
|
+
() => props.open,
|
|
450
|
+
(v) => {
|
|
451
|
+
if (v) {
|
|
452
|
+
setTimeout(() => {
|
|
453
|
+
textInput.value?.focus();
|
|
454
|
+
}, 50);
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
defineExpose({
|
|
460
|
+
handleKeydown,
|
|
461
|
+
toggleValue,
|
|
462
|
+
clearSelection,
|
|
463
|
+
listboxEl,
|
|
464
|
+
activeIndex,
|
|
465
|
+
});
|
|
466
|
+
</script>
|
|
467
|
+
|
|
468
|
+
<style lang="scss">
|
|
469
|
+
@use '@mozaic-ds/styles/components/option-listbox';
|
|
470
|
+
</style>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# MOptionListbox
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## Props
|
|
7
|
+
|
|
8
|
+
| Name | Description | Type | Default |
|
|
9
|
+
| --- | --- | --- | --- |
|
|
10
|
+
| `modelValue*` | The current selected value(s) of the listbox. | `string` `number` `(string` `number)[]` `null` | - |
|
|
11
|
+
| `id*` | Unique identifier for the listbox. | `string` | - |
|
|
12
|
+
| `open` | Whether the listbox is open. | `boolean` | - |
|
|
13
|
+
| `multiple` | Enable multiple selection. | `boolean` | - |
|
|
14
|
+
| `readonly` | Make the listbox read-only. | `boolean` | - |
|
|
15
|
+
| `search` | Show a search input above the options. | `boolean` | - |
|
|
16
|
+
| `actions` | Show select all / clear buttons when multiple. | `boolean` | - |
|
|
17
|
+
| `checkableSections` | Enable checkable section headers. | `boolean` | - |
|
|
18
|
+
| `searchPlaceholder` | Placeholder text for the search input. | `string` | `"Find an option..."` |
|
|
19
|
+
| `selectLabel` | Label for the "Select all" button. | `string` | `"Select all"` |
|
|
20
|
+
| `clearLabel` | Label for the "Clear selection" button. | `string` | `"Clear"` |
|
|
21
|
+
| `options*` | Array of options and sections to display in the listbox. | `ListboxOption[]` | - |
|
|
22
|
+
|
|
23
|
+
## Slots
|
|
24
|
+
|
|
25
|
+
| Name | Description |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| `item` | Use this slot to customize the content of each item. |
|
|
28
|
+
|
|
29
|
+
## Events
|
|
30
|
+
|
|
31
|
+
| Name | Description | Type |
|
|
32
|
+
| --- | --- | --- |
|
|
33
|
+
| `update:modelValue` | - | `[value: string` `number` `(string` `number)[]` `null]` |
|
|
34
|
+
| `close` | Emits when the listbox should close. | [] |
|
|
35
|
+
| `open` | Emits when the selected value changes. / ( on: 'update:modelValue', value: string | number | null | (string | number)[], ): void; /** Emits when the listbox should open. | [] |
|
|
36
|
+
|
|
37
|
+
## Dependencies
|
|
38
|
+
|
|
39
|
+
### Depends on
|
|
40
|
+
|
|
41
|
+
- [MButton](../button)
|
|
42
|
+
- [MTextInput](../textinput)
|
|
43
|
+
|
|
44
|
+
### Graph
|
|
45
|
+
|
|
46
|
+
```mermaid
|
|
47
|
+
graph TD;
|
|
48
|
+
MOptionListbox --> MButton
|
|
49
|
+
MOptionListbox --> MTextInput
|
|
50
|
+
style MOptionListbox fill:#008240,stroke:#333,stroke-width:4px
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Used By
|
|
54
|
+
|
|
55
|
+
- [MCombobox](../combobox)
|
|
56
|
+
|
|
57
|
+
### Graph
|
|
58
|
+
|
|
59
|
+
```mermaid
|
|
60
|
+
graph TD;
|
|
61
|
+
MCombobox --> MOptionListbox
|
|
62
|
+
style MOptionListbox fill:#008240,stroke:#333,stroke-width:4px
|
|
63
|
+
```
|
|
@@ -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
|
+
);
|