@mozaic-ds/vue 2.19.1 → 2.20.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 (88) hide show
  1. package/dist/mozaic-vue.css +1 -1
  2. package/dist/mozaic-vue.d.ts +512 -296
  3. package/dist/mozaic-vue.js +1952 -1678
  4. package/dist/mozaic-vue.js.map +1 -1
  5. package/dist/mozaic-vue.umd.cjs +6 -6
  6. package/dist/mozaic-vue.umd.cjs.map +1 -1
  7. package/package.json +6 -3
  8. package/src/components/Catalog.scss +84 -0
  9. package/src/components/Catalog.stories.ts +146 -0
  10. package/src/components/GettingStarted.mdx +258 -2
  11. package/src/components/Migration.mdx +4135 -532
  12. package/src/components/Welcome.mdx +1 -1
  13. package/src/components/carousel/MCarousel.spec.ts +174 -55
  14. package/src/components/carousel/MCarousel.vue +106 -33
  15. package/src/components/checkboxgroup/MCheckboxGroup.vue +2 -9
  16. package/src/components/combobox/MCombobox.spec.ts +144 -0
  17. package/src/components/combobox/MCombobox.stories.ts +2 -1
  18. package/src/components/combobox/MCombobox.vue +68 -38
  19. package/src/components/combobox/README.md +6 -0
  20. package/src/components/datatable/DataTable.stories.ts +2 -2
  21. package/src/components/datatable/DataTableCells.stories.ts +1 -0
  22. package/src/components/datatable/DataTableEmpty.stories.ts +1 -0
  23. package/src/components/datatable/DataTableExpandable.stories.ts +1 -0
  24. package/src/components/datatable/DataTableNested.stories.ts +1 -0
  25. package/src/components/datatable/DataTablePageable.stories.ts +194 -0
  26. package/src/components/datatable/DataTableSelectable.stories.ts +1 -0
  27. package/src/components/datatable/DataTableSortable.stories.ts +1 -0
  28. package/src/components/datepicker/MDatepicker.stories.ts +2 -0
  29. package/src/components/datepicker/MDatepicker.vue +17 -11
  30. package/src/components/divider/README.md +2 -0
  31. package/src/components/drawer/MDrawer.vue +7 -5
  32. package/src/components/link/MLink.stories.ts +18 -0
  33. package/src/components/link/MLink.vue +20 -4
  34. package/src/components/link/README.md +2 -1
  35. package/src/components/optionListbox/MOptionListbox.spec.ts +78 -1
  36. package/src/components/optionListbox/MOptionListbox.types.ts +7 -0
  37. package/src/components/optionListbox/MOptionListbox.vue +31 -217
  38. package/src/components/optionListbox/README.md +2 -0
  39. package/src/components/optionListbox/use-listbox-navigation.composable.ts +147 -0
  40. package/src/components/optionListbox/use-listbox-selection.composable.ts +149 -0
  41. package/src/components/pageheader/MPageHeader.spec.ts +17 -3
  42. package/src/components/pageheader/MPageHeader.stories.ts +37 -41
  43. package/src/components/pageheader/MPageHeader.vue +26 -16
  44. package/src/components/pageheader/README.md +2 -1
  45. package/src/components/pageheadercontext/MPageHeaderContext.spec.ts +155 -0
  46. package/src/components/pageheadercontext/MPageHeaderContext.vue +126 -0
  47. package/src/components/pageheadercontext/README.md +33 -0
  48. package/src/components/pageheaderfeature/MPageHeaderFeature.spec.ts +39 -0
  49. package/src/components/pageheaderfeature/MPageHeaderFeature.vue +40 -0
  50. package/src/components/pageheaderfeature/README.md +17 -0
  51. package/src/components/radiogroup/MRadioGroup.stories.ts +6 -0
  52. package/src/components/radiogroup/MRadioGroup.vue +2 -9
  53. package/src/components/sidebar/MSidebar.const.ts +2 -0
  54. package/src/components/sidebar/MSidebar.stories.ts +33 -0
  55. package/src/components/sidebar/MSidebar.vue +10 -1
  56. package/src/components/sidebar/README.md +1 -0
  57. package/src/components/sidebar/stories/DefaultCase.stories.vue +12 -11
  58. package/src/components/sidebar/use-floating-item.composable.ts +1 -1
  59. package/src/components/sidebarexpandableitem/MSidebarExpandableItem.vue +13 -7
  60. package/src/components/sidebarexpandableitem/README.md +2 -1
  61. package/src/components/sidebarfooter/MSidebarFooter.vue +16 -9
  62. package/src/components/sidebarfooter/_MSidebarFooterMenu.vue +10 -2
  63. package/src/components/sidebarnavitem/MSidebarNavItem.vue +36 -5
  64. package/src/components/sidebarnavitem/README.md +1 -0
  65. package/src/components/sidebarshortcutitem/MSidebarShortcutItem.vue +24 -3
  66. package/src/components/sidebarshortcutitem/README.md +3 -1
  67. package/src/components/sidebarshortcuts/MSidebarShortcuts.vue +6 -1
  68. package/src/components/sidebarshortcuts/README.md +12 -0
  69. package/src/components/statusbadge/MStatusBadge.stories.ts +5 -1
  70. package/src/components/statusbadge/MStatusBadge.vue +7 -1
  71. package/src/components/statusbadge/MstatusBadge.spec.ts +31 -0
  72. package/src/components/statusbadge/README.md +1 -0
  73. package/src/components/statusmessage/MStatusMessage.spec.ts +4 -4
  74. package/src/components/statusmessage/MStatusMessage.vue +5 -10
  75. package/src/components/statusnotification/MStatusNotification.spec.ts +9 -9
  76. package/src/components/statusnotification/MStatusNotification.vue +9 -8
  77. package/src/components/tabs/MTabs.stories.ts +30 -1
  78. package/src/components/tabs/MTabs.vue +12 -3
  79. package/src/components/tabs/Mtabs.spec.ts +19 -0
  80. package/src/components/tabs/README.md +1 -0
  81. package/src/components/tileselectable/MTileSelectable.spec.ts +69 -0
  82. package/src/components/tileselectable/MTileSelectable.stories.ts +8 -2
  83. package/src/components/tileselectable/MTileSelectable.vue +18 -1
  84. package/src/components/tileselectable/README.md +1 -0
  85. package/src/main.ts +3 -0
  86. package/src/utils/debounce.ts +15 -0
  87. package/src/components/BrandPresets.mdx +0 -146
  88. package/src/components/DarkMode.mdx +0 -115
@@ -1,4 +1,13 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
1
+ import {
2
+ describe,
3
+ it,
4
+ expect,
5
+ vi,
6
+ beforeAll,
7
+ afterAll,
8
+ beforeEach,
9
+ afterEach,
10
+ } from 'vitest';
2
11
  import { mount, VueWrapper } from '@vue/test-utils';
3
12
  import { nextTick } from 'vue';
4
13
  import MOptionListbox from './MOptionListbox.vue';
@@ -68,6 +77,34 @@ const optionsWithSections: ListboxOption[] = [
68
77
  { label: 'Carrot', value: 'carrot' },
69
78
  ];
70
79
 
80
+ let originalScrollIntoViewDescriptor: PropertyDescriptor | undefined;
81
+ const scrollIntoViewMock = vi.fn();
82
+
83
+ beforeAll(() => {
84
+ originalScrollIntoViewDescriptor = Object.getOwnPropertyDescriptor(
85
+ HTMLElement.prototype,
86
+ 'scrollIntoView',
87
+ );
88
+
89
+ Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', {
90
+ configurable: true,
91
+ value: scrollIntoViewMock,
92
+ });
93
+ });
94
+
95
+ afterAll(() => {
96
+ if (originalScrollIntoViewDescriptor) {
97
+ Object.defineProperty(
98
+ HTMLElement.prototype,
99
+ 'scrollIntoView',
100
+ originalScrollIntoViewDescriptor,
101
+ );
102
+ } else {
103
+ delete (HTMLElement.prototype as { scrollIntoView?: unknown })
104
+ .scrollIntoView;
105
+ }
106
+ });
107
+
71
108
  function mountListbox(props: Record<string, unknown> = {}): VueWrapper {
72
109
  return mount(MOptionListbox, {
73
110
  props: {
@@ -389,6 +426,7 @@ describe('MOptionListbox – search / filtering', () => {
389
426
  describe('MOptionListbox – keyboard navigation', () => {
390
427
  beforeEach(() => {
391
428
  vi.useFakeTimers();
429
+ scrollIntoViewMock.mockClear();
392
430
  });
393
431
 
394
432
  afterEach(() => {
@@ -455,6 +493,21 @@ describe('MOptionListbox – keyboard navigation', () => {
455
493
  expect(wrapper.emitted('open')).toBeDefined();
456
494
  });
457
495
 
496
+ it('ArrowDown when closed sets activeIndex to selected option', async () => {
497
+ const wrapper = mountListbox({
498
+ search: true,
499
+ open: false,
500
+ modelValue: 'banana',
501
+ });
502
+ const input = wrapper.find('input');
503
+
504
+ await input.trigger('keydown', { key: 'ArrowDown' });
505
+ await nextTick();
506
+
507
+ const items = wrapper.findAll('.mc-option-listbox__item');
508
+ expect(items[1].classes()).toContain('mc-option-listbox__item--active');
509
+ });
510
+
458
511
  it('ArrowUp when closed emits open and sets activeIndex to last item', async () => {
459
512
  const wrapper = mountListbox({ search: true, open: false });
460
513
  const input = wrapper.find('input');
@@ -474,6 +527,30 @@ describe('MOptionListbox – keyboard navigation', () => {
474
527
  const items = wrapper.findAll('.mc-option-listbox__item');
475
528
  expect(items[2].classes()).toContain('mc-option-listbox__item--active');
476
529
  });
530
+
531
+ it('scrolls the active option into view during ArrowDown navigation', async () => {
532
+ const wrapper = mountListbox({ search: true, open: true });
533
+ const input = wrapper.find('input');
534
+
535
+ await input.trigger('keydown', { key: 'ArrowDown' });
536
+ await nextTick();
537
+
538
+ expect(scrollIntoViewMock).toHaveBeenCalledTimes(1);
539
+ expect(scrollIntoViewMock).toHaveBeenCalledWith({
540
+ block: 'nearest',
541
+ inline: 'nearest',
542
+ });
543
+ });
544
+
545
+ it('does not scroll active option when listbox is closed', async () => {
546
+ const wrapper = mountListbox({ search: true, open: false });
547
+ const input = wrapper.find('input');
548
+
549
+ await input.trigger('keydown', { key: 'ArrowDown' });
550
+ await nextTick();
551
+
552
+ expect(scrollIntoViewMock).not.toHaveBeenCalled();
553
+ });
477
554
  });
478
555
 
479
556
  describe('MOptionListbox – exposed API', () => {
@@ -0,0 +1,7 @@
1
+ export type ListboxOption = {
2
+ label: string;
3
+ content?: string;
4
+ value?: string | number;
5
+ disabled?: boolean;
6
+ type?: 'option' | 'section';
7
+ };
@@ -1,8 +1,5 @@
1
1
  <template>
2
- <div
3
- ref="listboxEl"
4
- class="mc-option-listbox mc-listbox__content mc-combobox__listbox"
5
- >
2
+ <div ref="listboxEl" class="mc-option-listbox">
6
3
  <template v-if="search">
7
4
  <div class="mc-option-listbox__search">
8
5
  <MTextInput
@@ -42,6 +39,7 @@
42
39
  </template>
43
40
 
44
41
  <ul
42
+ ref="optionsList"
45
43
  class="mc-option-listbox__list"
46
44
  role="listbox"
47
45
  :id="`listbox-${id}`"
@@ -128,7 +126,7 @@
128
126
  </template>
129
127
 
130
128
  <script setup lang="ts">
131
- import { computed, ref, useTemplateRef, watch, type VNode } from 'vue';
129
+ import { ref, useTemplateRef, watch, type VNode } from 'vue';
132
130
  import MButton from '../button/MButton.vue';
133
131
  import MTextInput from '../textinput/MTextInput.vue';
134
132
  import {
@@ -137,34 +135,16 @@ import {
137
135
  Less20,
138
136
  Check20,
139
137
  } from '@mozaic-ds/icons-vue';
138
+ import { debounce } from '../../utils/debounce';
139
+ import { useListboxSelection } from './use-listbox-selection.composable';
140
+ import { useListboxNavigation } from './use-listbox-navigation.composable';
141
+ import type { ListboxOption } from './MOptionListbox.types';
140
142
 
141
- function debounce<T extends (...args: unknown[]) => unknown>(
142
- fn: T,
143
- wait: number,
144
- ): (...args: Parameters<T>) => void {
145
- let timeoutId: ReturnType<typeof setTimeout> | null = null;
146
-
147
- return function (this: unknown, ...args: Parameters<T>) {
148
- if (timeoutId) {
149
- clearTimeout(timeoutId);
150
- }
151
- timeoutId = setTimeout(() => {
152
- fn.apply(this, args);
153
- }, wait);
154
- };
155
- }
143
+ export type { ListboxOption };
156
144
 
157
145
  /**
158
146
  * 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.
159
147
  */
160
- export type ListboxOption = {
161
- label: string;
162
- content?: string;
163
- value?: string | number;
164
- disabled?: boolean;
165
- type?: 'option' | 'section';
166
- };
167
-
168
148
  const props = withDefaults(
169
149
  defineProps<{
170
150
  /**
@@ -249,28 +229,35 @@ defineSlots<{
249
229
  }>();
250
230
 
251
231
  const listboxEl = useTemplateRef('listboxEl');
232
+ const optionsList = useTemplateRef('optionsList');
252
233
  const textInput = useTemplateRef('textInput');
253
234
 
254
- const activeIndex = ref<number>(-1);
255
-
256
235
  const searchText = ref('');
257
-
258
236
  const filteredResults = ref<ListboxOption[]>(props.options);
259
237
 
260
- const selection = computed({
261
- get() {
262
- return props.modelValue;
263
- },
264
- set(value: string | number | null | (string | number)[]) {
265
- emit('update:modelValue', value);
266
- },
267
- });
238
+ const {
239
+ isSelectable,
240
+ isOptionSelected,
241
+ isSectionSelected,
242
+ isIndeterminate,
243
+ toggleValue,
244
+ toggleSection,
245
+ selectAll,
246
+ clearSelection,
247
+ } = useListboxSelection(props, emit);
268
248
 
269
- const activeDescendantId = computed(() => {
270
- return activeIndex.value >= 0
271
- ? `option-${props.id}-${activeIndex.value}`
272
- : undefined;
273
- });
249
+ const { activeIndex, activeDescendantId, handleKeydown } = useListboxNavigation(
250
+ props,
251
+ (on) => (on === 'open' ? emit('open') : emit('close')),
252
+ {
253
+ filteredResults,
254
+ optionsList,
255
+ isSelectable,
256
+ isOptionSelected,
257
+ toggleValue,
258
+ toggleSection,
259
+ },
260
+ );
274
261
 
275
262
  const updateFilteredResults = debounce(() => {
276
263
  const search = searchText.value.toLowerCase().trim();
@@ -287,179 +274,6 @@ const updateFilteredResults = debounce(() => {
287
274
  activeIndex.value = filteredResults.value.length ? 0 : -1;
288
275
  }, 200);
289
276
 
290
- const sectionMap = computed(() => {
291
- const map = new Map<string, ListboxOption[]>();
292
- let currentSection: ListboxOption | null = null;
293
-
294
- props.options.forEach((option) => {
295
- if (option.type === 'section') {
296
- currentSection = option;
297
- map.set(currentSection?.value?.toString() || currentSection.label, []);
298
- } else if (currentSection) {
299
- map
300
- .get(currentSection?.value?.toString() || currentSection.label)!
301
- .push(option);
302
- }
303
- });
304
-
305
- return map;
306
- });
307
-
308
- function toggleSection(item: ListboxOption) {
309
- if (!props.checkableSections || !props.multiple) return;
310
-
311
- const sectionItems =
312
- sectionMap.value.get(item.value?.toString() || item.label) || [];
313
- const selectedItems = selection.value as (string | number)[];
314
-
315
- if (isSectionSelected(item)) {
316
- selection.value = selectedItems.filter(
317
- (opt) => !sectionItems.find((item) => item.value === opt),
318
- );
319
- } else {
320
- selection.value = [
321
- ...selectedItems,
322
- ...sectionItems
323
- .filter((opt) => !selectedItems.includes(opt.value!))
324
- .map((item) => item.value!),
325
- ];
326
- }
327
- }
328
-
329
- function toggleValue(item?: ListboxOption) {
330
- if (!item || !isSelectable(item)) return;
331
-
332
- if (Array.isArray(selection.value)) {
333
- if (isOptionSelected(item)) {
334
- selection.value = (selection.value as (string | number)[]).filter(
335
- (el) => el !== item.value,
336
- );
337
- } else {
338
- selection.value = [...selection.value, item.value!];
339
- }
340
- } else {
341
- selection.value = isOptionSelected(item) ? null : item.value!;
342
- }
343
- }
344
-
345
- function selectAll() {
346
- selection.value = [
347
- ...props.options
348
- .filter(
349
- (option) =>
350
- !!option.value && !option.disabled && option.type !== 'section',
351
- )
352
- .map((item) => item.value!),
353
- ];
354
- }
355
-
356
- function clearSelection() {
357
- if (Array.isArray(selection.value)) {
358
- selection.value = [];
359
- } else {
360
- selection.value = null;
361
- }
362
- }
363
-
364
- function isSelectable(item: ListboxOption) {
365
- return (
366
- (item.type !== 'section' && !item.disabled) ||
367
- (item.type === 'section' && props.checkableSections && props.multiple)
368
- );
369
- }
370
-
371
- function isSectionSelected(item: ListboxOption) {
372
- if (!props.checkableSections || !props.multiple) return false;
373
-
374
- const sectionItems =
375
- sectionMap.value.get(item.value?.toString() || item.label) || [];
376
- const selectedItems = selection.value as (string | number)[];
377
-
378
- return sectionItems.every((opt) => selectedItems.includes(opt.value!));
379
- }
380
-
381
- function isOptionSelected(item: ListboxOption) {
382
- if (!item.value) return false;
383
-
384
- if (Array.isArray(selection.value)) {
385
- return (selection.value as (string | number)[])?.includes(item.value!);
386
- } else {
387
- return item.value === selection.value;
388
- }
389
- }
390
-
391
- function isIndeterminate(item: ListboxOption) {
392
- const section = sectionMap.value.get(item.value?.toString() || item.label);
393
- return (
394
- section?.some((option) => isOptionSelected(option)) &&
395
- !section?.every((option) => isOptionSelected(option))
396
- );
397
- }
398
-
399
- function moveActive(delta: number) {
400
- if (!props.open || filteredResults.value.length === 0) return;
401
-
402
- let nextIndex = activeIndex.value + delta;
403
-
404
- if (nextIndex < 0) nextIndex = filteredResults.value.length - 1;
405
- if (nextIndex >= filteredResults.value.length) nextIndex = 0;
406
-
407
- while (!isSelectable(filteredResults.value[nextIndex])) {
408
- nextIndex += delta > 0 ? 1 : -1;
409
- if (nextIndex < 0) nextIndex = filteredResults.value.length - 1;
410
- if (nextIndex >= filteredResults.value.length) nextIndex = 0;
411
- }
412
-
413
- activeIndex.value = nextIndex;
414
- }
415
-
416
- function selectActive() {
417
- const item = filteredResults.value[activeIndex.value];
418
- if (!item || !isSelectable(item)) return;
419
-
420
- if (item.type === 'section') {
421
- toggleSection(item);
422
- } else {
423
- toggleValue(item);
424
- }
425
- }
426
-
427
- function handleKeydown(event: KeyboardEvent) {
428
- switch (event.key) {
429
- case 'ArrowDown':
430
- event.preventDefault();
431
- if (!props.open) {
432
- emit('open');
433
- activeIndex.value = 0;
434
- } else {
435
- moveActive(1);
436
- }
437
- break;
438
- case 'ArrowUp':
439
- event.preventDefault();
440
- if (!props.open) {
441
- emit('open');
442
- activeIndex.value = filteredResults.value.length - 1;
443
- } else {
444
- moveActive(-1);
445
- }
446
- break;
447
- case 'Enter':
448
- event.preventDefault();
449
- if (!props.open) {
450
- emit('open');
451
- activeIndex.value = 0;
452
- } else {
453
- selectActive();
454
- }
455
- break;
456
- case 'Escape':
457
- event.preventDefault();
458
- emit('close');
459
- break;
460
- }
461
- }
462
-
463
277
  watch(
464
278
  () => props.open,
465
279
  (v) => {
@@ -53,11 +53,13 @@ style MOptionListbox fill:#008240,stroke:#333,stroke-width:4px
53
53
  ### Used By
54
54
 
55
55
  - [MCombobox](../combobox)
56
+ - [MPageHeaderContext](../pageheadercontext)
56
57
 
57
58
  ### Graph
58
59
 
59
60
  ```mermaid
60
61
  graph TD;
61
62
  MCombobox --> MOptionListbox
63
+ MPageHeaderContext --> MOptionListbox
62
64
  style MOptionListbox fill:#008240,stroke:#333,stroke-width:4px
63
65
  ```
@@ -0,0 +1,147 @@
1
+ import { computed, nextTick, ref, watch, type Ref, type ShallowRef } from 'vue';
2
+ import type { ListboxOption } from './MOptionListbox.types';
3
+
4
+ type Props = {
5
+ open?: boolean;
6
+ id: string;
7
+ };
8
+
9
+ type Deps = {
10
+ filteredResults: Ref<ListboxOption[]>;
11
+ optionsList: Readonly<ShallowRef<HTMLElement | null>>;
12
+ isSelectable: (item: ListboxOption) => boolean;
13
+ isOptionSelected: (item: ListboxOption) => boolean;
14
+ toggleValue: (item: ListboxOption) => void;
15
+ toggleSection: (item: ListboxOption) => void;
16
+ };
17
+
18
+ export function useListboxNavigation(
19
+ props: Props,
20
+ emit: (on: 'open' | 'close') => void,
21
+ deps: Deps,
22
+ ) {
23
+ const activeIndex = ref<number>(-1);
24
+
25
+ const activeDescendantId = computed(() =>
26
+ activeIndex.value >= 0
27
+ ? `option-${props.id}-${activeIndex.value}`
28
+ : undefined,
29
+ );
30
+
31
+ function moveActive(delta: number) {
32
+ if (!props.open || deps.filteredResults.value.length === 0) return;
33
+
34
+ const total = deps.filteredResults.value.length;
35
+ let nextIndex = activeIndex.value;
36
+ for (let i = 0; i < total; i++) {
37
+ nextIndex += delta;
38
+ if (nextIndex < 0) nextIndex = total - 1;
39
+ if (nextIndex >= total) nextIndex = 0;
40
+ if (deps.isSelectable(deps.filteredResults.value[nextIndex])) {
41
+ activeIndex.value = nextIndex;
42
+ return;
43
+ }
44
+ }
45
+ }
46
+
47
+ function getFirstSelectableIndex() {
48
+ return deps.filteredResults.value.findIndex((item) =>
49
+ deps.isSelectable(item),
50
+ );
51
+ }
52
+
53
+ function getLastSelectableIndex() {
54
+ for (let i = deps.filteredResults.value.length - 1; i >= 0; i--) {
55
+ if (deps.isSelectable(deps.filteredResults.value[i])) return i;
56
+ }
57
+ return -1;
58
+ }
59
+
60
+ function getSelectedOptionIndex() {
61
+ return deps.filteredResults.value.findIndex(
62
+ (item) =>
63
+ item.type !== 'section' &&
64
+ deps.isSelectable(item) &&
65
+ deps.isOptionSelected(item),
66
+ );
67
+ }
68
+
69
+ function getInitialActiveIndex(direction: 'forward' | 'backward') {
70
+ const selectedIndex = getSelectedOptionIndex();
71
+
72
+ if (selectedIndex >= 0) return selectedIndex;
73
+
74
+ return direction === 'backward'
75
+ ? getLastSelectableIndex()
76
+ : getFirstSelectableIndex();
77
+ }
78
+
79
+ function selectActive() {
80
+ const item = deps.filteredResults.value[activeIndex.value];
81
+ if (!item || !deps.isSelectable(item)) return;
82
+
83
+ if (item.type === 'section') {
84
+ deps.toggleSection(item);
85
+ } else {
86
+ deps.toggleValue(item);
87
+ }
88
+ }
89
+
90
+ function scrollActiveOptionIntoView() {
91
+ if (!props.open || activeIndex.value < 0) return;
92
+
93
+ const activeOption = deps.optionsList.value?.querySelector<HTMLElement>(
94
+ `#option-${props.id}-${activeIndex.value}`,
95
+ );
96
+
97
+ activeOption?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
98
+ }
99
+
100
+ function handleKeydown(event: KeyboardEvent) {
101
+ switch (event.key) {
102
+ case 'ArrowDown':
103
+ event.preventDefault();
104
+ if (!props.open) {
105
+ emit('open');
106
+ activeIndex.value = getInitialActiveIndex('forward');
107
+ } else {
108
+ moveActive(1);
109
+ }
110
+ break;
111
+ case 'ArrowUp':
112
+ event.preventDefault();
113
+ if (!props.open) {
114
+ emit('open');
115
+ activeIndex.value = getInitialActiveIndex('backward');
116
+ } else {
117
+ moveActive(-1);
118
+ }
119
+ break;
120
+ case 'Enter':
121
+ event.preventDefault();
122
+ if (!props.open) {
123
+ emit('open');
124
+ activeIndex.value = getInitialActiveIndex('forward');
125
+ } else {
126
+ selectActive();
127
+ }
128
+ break;
129
+ case 'Escape':
130
+ event.preventDefault();
131
+ emit('close');
132
+ break;
133
+ }
134
+ }
135
+
136
+ watch([activeIndex, () => props.open], async ([index, isOpen]) => {
137
+ if (!isOpen || index < 0) return;
138
+ await nextTick();
139
+ scrollActiveOptionIntoView();
140
+ });
141
+
142
+ return {
143
+ activeIndex,
144
+ activeDescendantId,
145
+ handleKeydown,
146
+ };
147
+ }