UNPKG

@coreui/vue-pro

Version:

UI Components Library for Vue.js

183 lines (162 loc) 6 kB
import { defineComponent, h, PropType } from 'vue' import { CElementCover } from '../element-cover' import { CVirtualScroller } from '../virtual-scroller' import { getNextSibling, getPreviousSibling } from '../../utils' import type { Option, OptionsGroup } from './types' import { getOptionLabel, highlightSubstring, isOptionDisabled, isOptionSelected } from './utils' const CAutocompleteOptions = defineComponent({ name: 'CAutocompleteOptions', props: { highlightOptionsOnSearch: Boolean, loading: Boolean, options: { type: Array as PropType<(Option | OptionsGroup)[]>, required: true, }, optionsMaxHeight: [Number, String] as PropType<number | string>, scopedSlots: Object, searchNoResultsLabel: [Boolean, String] as PropType<boolean | string>, searchValue: String, selected: [Object, String, null] as PropType<Option | null>, virtualScroller: Boolean, visible: Boolean, visibleItems: { type: Number, default: 10, }, }, emits: ['optionClick'], setup(props, { emit }) { const handleKeyDown = (event: KeyboardEvent, option: Option) => { if (event.code === 'Space' || event.key === 'Enter') { event.preventDefault() emit('optionClick', option) } if (event.key === 'Down' || event.key === 'ArrowDown') { event.preventDefault() const target = event.target as HTMLElement const next = getNextSibling( target, '.autocomplete-option:not(.disabled):not(:disabled)' ) as HTMLElement | null if (next) { next.focus() } } if (event.key === 'Up' || event.key === 'ArrowUp') { event.preventDefault() const target = event.target as HTMLElement const prev = getPreviousSibling( target, '.autocomplete-option:not(.disabled):not(:disabled)' ) as HTMLElement | null if (prev) { prev.focus() } } if (event.key === 'Home' || event.key === 'End') { event.preventDefault() const target = event.target as HTMLElement const container = target.closest('.autocomplete-options') as HTMLElement | null if (!container) { return } const first = event.key === 'Home' const focusEdgeOption = () => { const options = container.querySelectorAll<HTMLElement>( '.autocomplete-option:not(.disabled):not(:disabled)' ) if (options.length > 0) { options[first ? 0 : options.length - 1].focus() } } if (props.virtualScroller) { // The virtualized viewport only mounts a window of options — scroll it to // the edge so the true first/last options render, then focus after the // window has re-rendered (scroll event → state update → commit). container.scrollTop = first ? 0 : container.scrollHeight requestAnimationFrame(() => requestAnimationFrame(focusEdgeOption)) return } focusEdgeOption() } } const createOption = (option: Option, index: number) => h( 'div', { class: [ 'autocomplete-option', { disabled: isOptionDisabled(option), selected: isOptionSelected(option, props.selected || null), }, ], key: index, onClick: () => emit('optionClick', option), onKeydown: (event: KeyboardEvent) => handleKeyDown(event, option), onMousedown: (event: MouseEvent) => event.preventDefault(), role: 'option', 'aria-selected': isOptionSelected(option, props.selected || null) ? 'true' : 'false', ...(isOptionDisabled(option) && { 'aria-disabled': 'true' }), tabindex: 0, ...(props.highlightOptionsOnSearch && !props.scopedSlots?.['options'] && { innerHTML: highlightSubstring(getOptionLabel(option), props.searchValue), }), }, props.highlightOptionsOnSearch ? undefined : props.scopedSlots && props.scopedSlots['options'] ? h(props.scopedSlots['options'], { option: option }) : getOptionLabel(option) ) const createOptions = (options: (Option | OptionsGroup)[]) => { if (options.length === 0 && props.searchNoResultsLabel) { return h( 'div', { class: 'autocomplete-options-empty', role: 'status' }, props.searchNoResultsLabel ) } return options.map((option: Option | OptionsGroup, index: number) => { if (typeof option !== 'string' && 'options' in option) { return h('div', { key: index }, [ h('div', { class: 'autocomplete-optgroup-label' }, [ props.scopedSlots && props.scopedSlots['options-groups'] ? h(props.scopedSlots['options-groups'], { option: option }) : option.label, ]), ...(option.options?.map((opt: Option, idx: number) => createOption(opt, idx)) || []), ]) } return createOption(option as Option, index) }) } return () => [ props.visible && props.virtualScroller && props.options.length > 0 ? h( CVirtualScroller, { class: 'autocomplete-options', visibleItems: props.visibleItems, }, { default: () => createOptions(props.options), } ) : h( 'div', { class: 'autocomplete-options', ...(props.optionsMaxHeight !== 'auto' && { style: { maxHeight: props.optionsMaxHeight, overflow: 'scroll' }, }), }, createOptions(props.options) ), props.loading && h(CElementCover), ] }, }) export { CAutocompleteOptions }