UNPKG

@mozaic-ds/vue

Version:

Mozaic-Vue is the Vue.js implementation of ADEO Design system

148 lines (127 loc) 3.87 kB
import { computed, nextTick, ref, watch, type Ref, type ShallowRef } from 'vue'; import type { ListboxOption } from './MOptionListbox.types'; type Props = { open?: boolean; id: string; }; type Deps = { filteredResults: Ref<ListboxOption[]>; optionsList: Readonly<ShallowRef<HTMLElement | null>>; isSelectable: (item: ListboxOption) => boolean; isOptionSelected: (item: ListboxOption) => boolean; toggleValue: (item: ListboxOption) => void; toggleSection: (item: ListboxOption) => void; }; export function useListboxNavigation( props: Props, emit: (on: 'open' | 'close') => void, deps: Deps, ) { const activeIndex = ref<number>(-1); const activeDescendantId = computed(() => activeIndex.value >= 0 ? `option-${props.id}-${activeIndex.value}` : undefined, ); function moveActive(delta: number) { if (!props.open || deps.filteredResults.value.length === 0) return; const total = deps.filteredResults.value.length; let nextIndex = activeIndex.value; for (let i = 0; i < total; i++) { nextIndex += delta; if (nextIndex < 0) nextIndex = total - 1; if (nextIndex >= total) nextIndex = 0; if (deps.isSelectable(deps.filteredResults.value[nextIndex])) { activeIndex.value = nextIndex; return; } } } function getFirstSelectableIndex() { return deps.filteredResults.value.findIndex((item) => deps.isSelectable(item), ); } function getLastSelectableIndex() { for (let i = deps.filteredResults.value.length - 1; i >= 0; i--) { if (deps.isSelectable(deps.filteredResults.value[i])) return i; } return -1; } function getSelectedOptionIndex() { return deps.filteredResults.value.findIndex( (item) => item.type !== 'section' && deps.isSelectable(item) && deps.isOptionSelected(item), ); } function getInitialActiveIndex(direction: 'forward' | 'backward') { const selectedIndex = getSelectedOptionIndex(); if (selectedIndex >= 0) return selectedIndex; return direction === 'backward' ? getLastSelectableIndex() : getFirstSelectableIndex(); } function selectActive() { const item = deps.filteredResults.value[activeIndex.value]; if (!item || !deps.isSelectable(item)) return; if (item.type === 'section') { deps.toggleSection(item); } else { deps.toggleValue(item); } } function scrollActiveOptionIntoView() { if (!props.open || activeIndex.value < 0) return; const activeOption = deps.optionsList.value?.querySelector<HTMLElement>( `#option-${props.id}-${activeIndex.value}`, ); activeOption?.scrollIntoView({ block: 'nearest', inline: 'nearest' }); } function handleKeydown(event: KeyboardEvent) { switch (event.key) { case 'ArrowDown': event.preventDefault(); if (!props.open) { emit('open'); activeIndex.value = getInitialActiveIndex('forward'); } else { moveActive(1); } break; case 'ArrowUp': event.preventDefault(); if (!props.open) { emit('open'); activeIndex.value = getInitialActiveIndex('backward'); } else { moveActive(-1); } break; case 'Enter': event.preventDefault(); if (!props.open) { emit('open'); activeIndex.value = getInitialActiveIndex('forward'); } else { selectActive(); } break; case 'Escape': event.preventDefault(); emit('close'); break; } } watch([activeIndex, () => props.open], async ([index, isOpen]) => { if (!isOpen || index < 0) return; await nextTick(); scrollActiveOptionIntoView(); }); return { activeIndex, activeDescendantId, handleKeydown, }; }