@mozaic-ds/vue 2.14.0 → 2.16.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 +1 -1
- package/dist/mozaic-vue.d.ts +1582 -500
- package/dist/mozaic-vue.js +8020 -3218
- package/dist/mozaic-vue.js.map +1 -1
- package/dist/mozaic-vue.umd.cjs +24 -5
- package/dist/mozaic-vue.umd.cjs.map +1 -1
- package/package.json +6 -4
- package/src/components/DarkMode.mdx +115 -0
- package/src/components/actionlistbox/MActionListbox.spec.ts +20 -10
- package/src/components/actionlistbox/MActionListbox.stories.ts +15 -8
- package/src/components/actionlistbox/MActionListbox.vue +15 -12
- package/src/components/actionlistbox/README.md +2 -1
- package/src/components/avatar/MAvatar.stories.ts +1 -1
- package/src/components/breadcrumb/MBreadcrumb.vue +2 -2
- package/src/components/button/README.md +2 -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 +277 -0
- package/src/components/combobox/README.md +52 -0
- package/src/components/field/MField.stories.ts +105 -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/pageheader/MPageHeader.spec.ts +12 -12
- package/src/components/pageheader/MPageHeader.stories.ts +9 -1
- package/src/components/pageheader/MPageHeader.vue +3 -6
- package/src/components/segmentedcontrol/MSegmentedControl.spec.ts +57 -25
- package/src/components/segmentedcontrol/MSegmentedControl.stories.ts +6 -19
- package/src/components/segmentedcontrol/MSegmentedControl.vue +27 -13
- package/src/components/segmentedcontrol/README.md +6 -3
- package/src/components/select/MSelect.vue +4 -3
- package/src/components/sidebar/stories/DefaultCase.stories.vue +2 -2
- package/src/components/sidebar/stories/README.md +8 -0
- package/src/components/sidebar/stories/WithExpandOnly.stories.vue +1 -1
- package/src/components/sidebar/stories/WithProfileInfoOnly.stories.vue +2 -2
- package/src/components/sidebar/stories/WithSingleLevel.stories.vue +2 -2
- package/src/components/stepperinline/MStepperInline.spec.ts +63 -28
- package/src/components/stepperinline/MStepperInline.stories.ts +18 -10
- package/src/components/stepperinline/MStepperInline.vue +24 -10
- package/src/components/stepperinline/README.md +6 -2
- 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.stories.ts +18 -0
- package/src/components/tabs/MTabs.vue +30 -14
- package/src/components/tabs/Mtabs.spec.ts +56 -10
- package/src/components/tabs/README.md +6 -3
- package/src/components/textinput/MTextInput.vue +13 -1
- package/src/components/textinput/README.md +15 -1
- package/src/components/tileclickable/README.md +1 -1
- package/src/main.ts +10 -2
|
@@ -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 v-if="item.type !== 'section'" name="optionPrefix" />
|
|
86
|
+
|
|
87
|
+
<div class="mc-option-listbox__content">
|
|
88
|
+
<span
|
|
89
|
+
:class="
|
|
90
|
+
item.type === 'section'
|
|
91
|
+
? 'mc-option-listbox__section-title'
|
|
92
|
+
: 'mc-option-listbox__text'
|
|
93
|
+
"
|
|
94
|
+
>
|
|
95
|
+
{{ item.label }}
|
|
96
|
+
</span>
|
|
97
|
+
|
|
98
|
+
<span v-if="item.content" class="mc-option-listbox__additional">
|
|
99
|
+
{{ item.content }}
|
|
100
|
+
</span>
|
|
101
|
+
</div>
|
|
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 add a prefix to options.
|
|
232
|
+
*/
|
|
233
|
+
optionPrefix: 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
|
+
| `optionPrefix` | Use this slot to add a prefix to options. |
|
|
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
|
+
```
|
|
@@ -98,9 +98,9 @@ describe('MPageHeader', () => {
|
|
|
98
98
|
},
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
-
expect(
|
|
102
|
-
|
|
103
|
-
)
|
|
101
|
+
expect(wrapper.findComponent({ name: 'MStatusBadge' }).exists()).toBe(
|
|
102
|
+
true,
|
|
103
|
+
);
|
|
104
104
|
});
|
|
105
105
|
|
|
106
106
|
it('does not render status badge if statusLabel is missing', () => {
|
|
@@ -111,9 +111,9 @@ describe('MPageHeader', () => {
|
|
|
111
111
|
},
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
-
expect(
|
|
115
|
-
|
|
116
|
-
)
|
|
114
|
+
expect(wrapper.findComponent({ name: 'MStatusBadge' }).exists()).toBe(
|
|
115
|
+
false,
|
|
116
|
+
);
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
it('renders extra info when provided', () => {
|
|
@@ -124,9 +124,9 @@ describe('MPageHeader', () => {
|
|
|
124
124
|
},
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
-
expect(
|
|
128
|
-
|
|
129
|
-
)
|
|
127
|
+
expect(wrapper.find('.mc-page-header__extra-info').text()).toBe(
|
|
128
|
+
'Details',
|
|
129
|
+
);
|
|
130
130
|
});
|
|
131
131
|
|
|
132
132
|
it('renders info wrapper only when status or extraInfo exists', () => {
|
|
@@ -134,9 +134,9 @@ describe('MPageHeader', () => {
|
|
|
134
134
|
props: { title: 'My Page' },
|
|
135
135
|
});
|
|
136
136
|
|
|
137
|
-
expect(
|
|
138
|
-
|
|
139
|
-
)
|
|
137
|
+
expect(wrapper.find('.mc-page-header__info-wrapper').exists()).toBe(
|
|
138
|
+
false,
|
|
139
|
+
);
|
|
140
140
|
});
|
|
141
141
|
});
|
|
142
142
|
});
|
|
@@ -63,7 +63,15 @@ const meta: Meta<typeof MPageHeader> = {
|
|
|
63
63
|
`,
|
|
64
64
|
},
|
|
65
65
|
render: (args) => ({
|
|
66
|
-
components: {
|
|
66
|
+
components: {
|
|
67
|
+
MPageHeader,
|
|
68
|
+
MTabs,
|
|
69
|
+
MIconButton,
|
|
70
|
+
Search24,
|
|
71
|
+
HelpCircle24,
|
|
72
|
+
Notification24,
|
|
73
|
+
MSelect,
|
|
74
|
+
},
|
|
67
75
|
setup() {
|
|
68
76
|
const handleBackButtonClick = action('back');
|
|
69
77
|
const handleMenuClick = action('toggle-menu');
|
|
@@ -23,10 +23,7 @@
|
|
|
23
23
|
{{ title }}
|
|
24
24
|
</span>
|
|
25
25
|
|
|
26
|
-
<div
|
|
27
|
-
v-if="status || extraInfo"
|
|
28
|
-
class="mc-page-header__info-wrapper"
|
|
29
|
-
>
|
|
26
|
+
<div v-if="status || extraInfo" class="mc-page-header__info-wrapper">
|
|
30
27
|
<MStatusBadge
|
|
31
28
|
v-if="statusLabel && status"
|
|
32
29
|
:label="statusLabel"
|
|
@@ -55,13 +52,13 @@
|
|
|
55
52
|
</MIconButton>
|
|
56
53
|
|
|
57
54
|
<div class="mc-page-header__actions-content">
|
|
58
|
-
<slot name="actions"/>
|
|
55
|
+
<slot name="actions" />
|
|
59
56
|
</div>
|
|
60
57
|
</div>
|
|
61
58
|
</div>
|
|
62
59
|
|
|
63
60
|
<div class="mc-page-header__tabs">
|
|
64
|
-
<slot name="tabs"/>
|
|
61
|
+
<slot name="tabs" />
|
|
65
62
|
</div>
|
|
66
63
|
</div>
|
|
67
64
|
</template>
|