UNPKG

@coreui/vue-pro

Version:

UI Components Library for Vue.js

1,223 lines (1,130 loc) 40.7 kB
import { computed, defineComponent, h, PropType, provide, ref, useId, watch } from 'vue' import { CFormControlWrapper } from './../form/CFormControlWrapper' import { CConditionalTeleport } from '../conditional-teleport' import { CMultiSelectNativeSelect } from './CMultiSelectNativeSelect' import { CMultiSelectOptions } from './CMultiSelectOptions' import { CMultiSelectSelection } from './CMultiSelectSelection' import { useDropdownWithPopper } from '../../composables' import { getNextActiveElement, isEqual } from '../../utils' import { createOption, filterOptionsList, flattenOptionsArray, getOptionsList, isExternalSearch, isGlobalSearch, selectOptions, } from './utils' import type { Option, OptionsGroup, Search, SelectedOption } from './types' const CMultiSelect = defineComponent({ name: 'CMultiSelect', props: { /** * Allow users to create options if they are not in the list of options. * * @since 4.9.0 */ allowCreateOptions: Boolean, /** * A string that provides an accessible label for the cleaner button. This label is read by screen readers to describe the action associated with the button. * * @since 5.13.0 */ ariaCleanerLabel: { type: String, default: 'Clear all selections', }, /** * A string that provides an accessible label for the indicator button. This label is read by screen readers to describe the action associated with the button. * * @since 5.7.0 */ ariaIndicatorLabel: { type: String, default: 'Toggle dropdown', }, /** * Accessible label for the search input (when `search` is enabled). * * @since 5.19.0 */ ariaSearchLabel: { type: String, default: 'Search', }, /** * Accessible label prefix for a tag's delete button (selection type `tags`). The selected option's label is appended, so screen readers announce e.g. "Remove Angular". * * @since 5.19.0 */ ariaTagDeleteLabel: { type: String, default: 'Remove', }, /** * Enables selection cleaner element. * * @default true */ cleaner: { type: Boolean, default: true, }, /** * Appends the dropdown to a specific element. You can pass an HTML element or function that returns a single element. * * @since 5.7.0 */ container: { type: [Object, String] as PropType<HTMLElement | (() => HTMLElement) | string>, default: 'body', }, /** * Clear current search on selecting an item. * * @since 4.9.0 */ clearSearchOnSelect: Boolean, /** * Sets the select all button label shown once everything is selected. The button is a toggle: it shows `selectAllLabel` (and selects all) until everything is selected, then shows `deselectAllLabel` (and deselects all). * * @since 5.19.0 */ deselectAllLabel: { type: String, default: 'Deselect all', }, /** * Sets the deselect filtered button label (used with `selectAllMode="filtered"`). * * @since 5.19.0 */ deselectFilteredLabel: { type: String, default: 'Deselect filtered', }, /** * Toggle the disabled state for the component. */ disabled: Boolean, /** * Provide valuable, actionable feedback. * * @since 4.6.0 */ feedback: String, /** * Provide valuable, actionable feedback. * * @since 4.6.0 */ feedbackInvalid: String, /** * Provide valuable, actionable invalid feedback when using standard HTML form validation which applied two CSS pseudo-classes, `:invalid` and `:valid`. * * @since 4.6.0 */ feedbackValid: String, /** * Hide the dropdown header with the built-in select all button when a search filter leaves no matching options. Set to `false` to keep the select all button visible even when there are no results. * * @since 5.20.0 */ hideSelectAllOnSearchNoResults: { type: Boolean, default: true, }, /** * Set the id attribute for the native select element. * * **[Deprecated since v5.3.0]** The name attribute for the native select element is generated based on the `id` property: * - `<select name="\{id\}-multi-select" />` */ id: String, /** * Set component validation state to invalid. * * @since 4.6.0 */ invalid: Boolean, /** * Add a caption for a component. * * @since 4.6.0 */ label: String, /** * When set, the options list will have a loading style: loading spinner and reduced opacity. * * @since 4.9.0 */ loading: Boolean, /** * It specifies that multiple options can be selected at once. * * @default true */ multiple: { type: Boolean, default: true, }, /** * The name attribute for the select element. * * @since 5.3.0 */ name: String, /** * List of option elements. */ options: { type: Array as PropType<(Option | OptionsGroup)[]>, default: () => [], }, /** * Makes each options group label a toggle that selects or deselects all options in that group. With `optionsGroupsStyle="checkbox"` (and `multiple`) the label shows a tri-state checkbox indicator (`none` / `all` / `indeterminate`) reflecting the group's selection. * * @since 5.19.0 */ optionsGroupsSelectable: Boolean, /** * Sets the options group label style when `optionsGroupsSelectable` is enabled. * * @values 'checkbox', 'text' * @default 'checkbox' * @since 5.19.0 */ optionsGroupsStyle: { type: String, default: 'checkbox', validator: (value: string) => { return ['checkbox', 'text'].includes(value) }, }, /** * Sets maxHeight of options list. * * @default 'auto' */ optionsMaxHeight: { type: [Number, String], default: 'auto', }, /** * Sets option style. * * @values 'checkbox', 'text' * @default 'checkbox' */ optionsStyle: { type: String, default: 'checkbox', validator: (value: string) => { return ['checkbox', 'text'].includes(value) }, }, /** * Specifies a short hint that is visible in the search input. * * @default 'Select...'' */ placeholder: { type: String, default: 'Select...', }, /** * When it is present, it indicates that the user must choose a value before submitting the form. */ required: Boolean, /** * Determines whether the selected options should be cleared when the options list is updated. When set to true, any previously selected options will be reset whenever the options list undergoes a change. This ensures that outdated selections are not retained when new options are provided. * * @since 5.3.0 */ resetSelectionOnOptionsChange: Boolean, /** * The `search` prop determines how the search input element is enabled and behaves. It accepts multiple types to provide flexibility in configuring search behavior: * * - `true` : Enables the default search input element with standard behavior. * - `'external'`: Enables an external search mechanism, possibly integrating with external APIs or services. * - `'global'`: When set, the user can perform searches across the entire component, regardless of where their focus is within the component. * - `{ external?: boolean; global?: boolean }`: Allows for granular control over the search behavior by specifying individual properties. It is useful when you also want to use external and global search. */ search: { type: [Boolean, String, Object] as PropType<Search>, default: true, validator: (value: boolean | object | string) => { if (typeof value == 'boolean') { return true } if (typeof value == 'string') { return ['external', 'global'].includes(value) } if (typeof value === 'object' && value !== null) { // Ensure that all keys are either 'external' or 'global' const validKeys = new Set(['external', 'global']) const keys = Object.keys(value) const allKeysValid = keys.every((key) => validKeys.has(key)) if (!allKeysValid) { return false } // Ensure that all values corresponding to the keys are boolean const allValuesBoolean = keys.every( (key) => typeof value[key as keyof typeof value] === 'boolean' ) return allValuesBoolean } return false }, }, /** * Sets the label for no results when filtering. */ searchNoResultsLabel: { type: String, default: 'no items', }, /** * Enables select all button. * * @default true */ selectAll: { type: Boolean, default: true, }, /** * Sets the select all button label. * * @default 'Select all' */ selectAllLabel: { type: String, default: 'Select all', }, /** * Determines what the select all button operates on: all options (`'all'`) or only the currently filtered (search-visible) options (`'filtered'`). * * @values 'all', 'filtered' * @default 'all' * @since 5.19.0 */ selectAllMode: { type: String, default: 'all', validator: (value: string) => { return ['all', 'filtered'].includes(value) }, }, /** * Sets the select all button style. With `'checkbox'` (and `multiple`), the button shows a tri-state checkbox indicator (`none` / `all` / `indeterminate`) and toggles between selecting and deselecting. * * @values 'checkbox', 'text' * @default 'checkbox' * @since 5.19.0 */ selectAllStyle: { type: String, default: 'checkbox', validator: (value: string) => { return ['checkbox', 'text'].includes(value) }, }, /** * Sets the select filtered button label (used with `selectAllMode="filtered"`). * * @since 5.19.0 */ selectFilteredLabel: { type: String, default: 'Select filtered', }, /** * Sets the maximum number of options that can be selected. The select all button stays enabled and selects options up to the limit, then toggles to deselect all once the limit is reached. The `selectionLimit` event fires when a user tries to select more options than allowed. * * @since 5.19.0 */ selectionLimit: { type: Number, default: null, }, /** * Sets the selection style. * * @values 'counter', 'tags', 'text' * @default 'tags' */ selectionType: { type: String, default: 'tags', validator: (value: string) => { return ['counter', 'tags', 'text'].includes(value) }, }, /** * Sets the counter selection label. * * @default 'item(s) selected' */ selectionTypeCounterText: { type: String, default: 'item(s) selected', }, /** * Size the component small or large. * * @values 'sm', 'lg' */ size: { type: String, validator: (value: string) => { return ['sm', 'lg'].includes(value) }, }, /** * Generates dropdown menu using Teleport. * * @since 5.7.0 */ teleport: { type: [Boolean], default: false, }, /** * Add helper text to the component. * * @since 4.6.0 */ text: String, /** * Display validation feedback in a styled tooltip. * * @since 4.6.0 */ tooltipFeedback: Boolean, /** * Set component validation state to valid. * * @since 4.6.0 */ valid: Boolean, /** * Sets the initially selected values for the multi-select component. * * @since 5.11.0 */ value: [String, Number, Array] as PropType<string | number | (string | number)[]>, /** * Enable virtual scroller for the options list. * * @since 4.8.0 */ virtualScroller: Boolean, /** * Toggle the visibility of multi select dropdown. * * @default false */ visible: Boolean, /** * * Amount of visible items when virtualScroller is set to `true`. * * @since 4.8.0 */ visibleItems: { type: Number, default: 10, }, }, emits: [ /** * Execute a function when a user changes the selected option. [docs] */ 'change', /** * Execute a function when the filter value changed. * * @since 4.7.0 */ 'filterChange', /** * The callback is fired when the Multi Select component requests to be hidden. */ 'hide', /** * Execute a function when the user tries to select more options than allowed by `selectionLimit`. * * @since 5.19.0 */ 'selectionLimit', /** * The callback is fired when the Multi Select component requests to be shown. */ 'show', ], setup(props, { attrs, emit, slots }) { const multiSelectRef = ref<HTMLDivElement>() const nativeSelectRef = ref<HTMLSelectElement>() const searchRef = ref<HTMLInputElement>() const searchValue = ref('') const selected = ref<SelectedOption[]>([]) const userOptions = ref<Option[]>([]) const uniqueId = useId() const { dropdownMenuElement, dropdownRefElement, isOpen, closeDropdown, openDropdown, toggleDropdown, updatePopper, } = useDropdownWithPopper() provide('nativeSelectRef', nativeSelectRef) const filteredOptions = computed(() => { const source = isExternalSearch(props.search) ? [...props.options, ...filterOptionsList(searchValue.value, userOptions.value)] : filterOptionsList(searchValue.value, [...props.options, ...userOptions.value]) const flattened: (Option | OptionsGroup)[] = [] for (const option of source) { if (Array.isArray((option as OptionsGroup).options)) { flattened.push(option, ...((option as OptionsGroup).options as Option[])) } else { flattened.push(option) } } return flattened }) const flattenedOptions = computed(() => { return flattenOptionsArray(props.options).map((option) => { if (props.value && Array.isArray(props.value)) { return { ...option, selected: props.value.includes(option.value as string | number), } } if (props.value === option.value) { return { ...option, selected: true, } } return option }) }) const userOption = computed(() => { if ( props.allowCreateOptions && filteredOptions.value.some( (option) => option.label && option.label.toLowerCase() === searchValue.value.toLowerCase() ) ) { return false } return searchRef.value && createOption(String(searchValue.value), flattenedOptions.value) }) const selectableOptions = computed(() => flattenOptionsArray([...props.options, ...userOptions.value]).filter( (option) => !option.disabled ) ) const filteredSelectableOptions = computed(() => flattenOptionsArray( isExternalSearch(props.search) ? [...props.options, ...filterOptionsList(searchValue.value, userOptions.value)] : filterOptionsList(searchValue.value, [...props.options, ...userOptions.value]) ).filter((option) => !option.disabled) ) const dropdownOptions = computed(() => filteredOptions.value.length === 0 && props.allowCreateOptions ? userOption.value || [] : filteredOptions.value ) const hasSelectionLimit = computed( () => props.multiple && typeof props.selectionLimit === 'number' ) const isSelectionLimitReached = computed( () => hasSelectionLimit.value && selected.value.length >= (props.selectionLimit as number) ) const selectableTarget = (total: number) => hasSelectionLimit.value ? Math.min(total, props.selectionLimit as number) : total const selectAllScope = computed(() => { const selectedValues = new Set(selected.value.map((option) => option.value)) const countSelected = (scope: (Option | OptionsGroup)[]) => scope.filter( (option) => 'value' in option && selectedValues.has(option.value as string | number) ).length return props.selectAllMode === 'filtered' ? { selected: countSelected(filteredSelectableOptions.value), total: filteredSelectableOptions.value.length, } : { selected: countSelected(selectableOptions.value), total: selectableOptions.value.length, } }) const isAllSelected = computed(() => { const target = selectableTarget(selectAllScope.value.total) return target > 0 && selectAllScope.value.selected >= target }) const selectAllCheckboxState = computed(() => isAllSelected.value ? 'all' : selectAllScope.value.selected === 0 ? 'none' : 'indeterminate' ) const isFilteredScopeNarrowed = computed( () => props.selectAllMode === 'filtered' && filteredSelectableOptions.value.length < selectableOptions.value.length ) const selectAllButtonLabel = computed(() => isFilteredScopeNarrowed.value ? isAllSelected.value ? props.deselectFilteredLabel : props.selectFilteredLabel : isAllSelected.value ? props.deselectAllLabel : props.selectAllLabel ) const headerState = computed(() => { const selectedValues = new Set(selected.value.map((option) => option.value)) return { selected: selected.value.length, total: selectableOptions.value.length, filtered: filteredSelectableOptions.value.length, filteredSelected: filteredSelectableOptions.value.filter((option) => selectedValues.has(option.value as string | number) ).length, } }) const selectWithLimit = (scope: (Option | OptionsGroup)[]) => { if (!hasSelectionLimit.value) { selected.value = selectOptions(props.multiple, scope, selected.value) return } const remaining = (props.selectionLimit as number) - selected.value.length if (remaining <= 0) { emit('selectionLimit', props.selectionLimit) return } const selectedValues = new Set(selected.value.map((option) => option.value)) const toAdd = scope.filter( (option) => 'value' in option && !selectedValues.has(option.value as string | number) ) selected.value = selectOptions(props.multiple, toAdd.slice(0, remaining), selected.value) if (toAdd.length > remaining) { emit('selectionLimit', props.selectionLimit) } } watch( flattenedOptions, () => { if (props.resetSelectionOnOptionsChange) { selected.value = [] return } const _selected = flattenedOptions.value.filter( (option: Option | OptionsGroup) => option.selected === true ) const deselected = flattenedOptions.value.filter( (option: Option | OptionsGroup) => option.selected === false ) as Option[] if (_selected.length > 0 || deselected.length > 0) { const newSelectedValue = selectOptions( props.multiple, _selected, selected.value, deselected ) if (!isEqual(newSelectedValue, selected.value)) { selected.value = newSelectedValue } } }, { immediate: true } ) watch(selected, () => { nativeSelectRef.value?.dispatchEvent(new Event('change', { bubbles: true })) updatePopper() }) watch( () => props.visible, (visible) => { if (visible) { openDropdown() } else { closeDropdown() } }, { immediate: true, } ) watch(isOpen, () => { if (isOpen.value) { emit('show') if (props.teleport && dropdownMenuElement.value && dropdownRefElement.value) { dropdownMenuElement.value.style.minWidth = `${(dropdownRefElement.value as HTMLElement).offsetWidth}px` } searchRef.value?.focus() return } emit('hide') const active = document.activeElement const wrapper = multiSelectRef.value as HTMLElement | null const menu = dropdownMenuElement.value as HTMLElement | null if (wrapper?.contains(active) || menu?.contains(active)) { if (props.search && searchRef.value) { searchRef.value.focus() } else { ;(dropdownRefElement.value as HTMLElement | null)?.focus() } } searchValue.value = '' if (searchRef.value) { searchRef.value.value = '' } }) const handleSearchChange = (event: InputEvent) => { const target = event.target as HTMLInputElement searchValue.value = target.value.toLowerCase() emit('filterChange', target.value) } const handleSearchKeyDown = (event: KeyboardEvent) => { if ( (!isOpen.value && event.key.length === 1 && !event.ctrlKey && !event.metaKey) || event.key === 'ArrowDown' ) { openDropdown() } if ( event.key === 'ArrowDown' && dropdownMenuElement.value && searchRef.value && searchRef.value.value.length === searchRef.value.selectionStart ) { event.preventDefault() const items = getOptionsList(dropdownMenuElement.value) const target = event.target as HTMLDivElement getNextActiveElement( items, target, event.key === 'ArrowDown', !items.includes(target) ).focus() return } if (event.key === 'Enter' && searchValue.value && props.allowCreateOptions) { event.preventDefault() if (!userOption.value) { selected.value = [ ...selected.value, filteredOptions.value.find( (option) => String(option.label).toLowerCase() === searchValue.value.toLowerCase() ) as Option, ] } if (userOption.value) { selected.value = [...selected.value, ...userOption.value] userOptions.value = [...userOptions.value, ...userOption.value] } searchValue.value = '' if (searchRef.value) { searchRef.value.value = '' } return } if (searchValue.value.length > 0) { return } if (event.key === 'Backspace' || event.key === 'Delete') { const last = selected.value.findLast((option: Option) => !option.disabled) if (last) { selected.value = selected.value.filter((option: Option) => option.value !== last.value) } } } const handleTogglerKeyDown = (event: KeyboardEvent) => { if (!isOpen.value && (event.key === 'Enter' || event.key === 'ArrowDown')) { event.preventDefault() openDropdown() return } if (isOpen && dropdownMenuElement.value && event.key === 'ArrowDown') { event.preventDefault() const items = getOptionsList(dropdownMenuElement.value) const target = event.target as HTMLDivElement getNextActiveElement( items, target, event.key === 'ArrowDown', !items.includes(target) ).focus() } } const handleNativeSelectKeyDown = (event: KeyboardEvent) => { if (event.key === 'Tab' || event.key === 'Escape') { return } event.preventDefault() const isPrintable = event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey if ( !isOpen.value && (event.key === 'Enter' || event.key === 'ArrowDown' || (props.search && isPrintable)) ) { openDropdown() } if (props.search && searchRef.value) { searchRef.value.focus() if (isPrintable) { const nextValue = searchRef.value.value + event.key searchRef.value.value = nextValue searchValue.value = nextValue.toLowerCase() } } else { ;(dropdownRefElement.value as HTMLElement | null)?.focus() } } const handleGlobalSearch = (event: KeyboardEvent) => { if ( isGlobalSearch(props.search) && searchRef.value && (event.key.length === 1 || event.key === 'Backspace' || event.key === 'Delete') ) { searchRef.value.focus() } } const handleOnOptionClick = (option: Option) => { if (!props.multiple) { selected.value = [option] as SelectedOption[] closeDropdown() if (searchRef.value) { searchRef.value.value = '' } return } const alreadySelected = selected.value.some((_option) => _option.value === option.value) if (!alreadySelected && isSelectionLimitReached.value) { emit('selectionLimit', props.selectionLimit) return } if (option.custom && !userOptions.value.some((_option) => _option.value === option.value)) { userOptions.value = [...userOptions.value, option] } if (props.clearSearchOnSelect || option.custom) { searchValue.value = '' if (searchRef.value) { searchRef.value.value = '' searchRef.value.focus() } } selected.value = alreadySelected ? selected.value.filter((_option) => _option.value !== option.value) : ([...selected.value, option] as SelectedOption[]) } const handleSelectAll = () => { selectWithLimit(selectableOptions.value) } const handleDeselectAll = () => { selected.value = selected.value.filter((option) => option.disabled) } const handleSelectFiltered = () => { selectWithLimit(filteredSelectableOptions.value) } const handleDeselectFiltered = () => { const filteredValues = new Set(filteredSelectableOptions.value.map((option) => option.value)) selected.value = selected.value.filter( (option) => option.disabled || !filteredValues.has(option.value) ) } const handleSelectAllToggle = () => { if (props.selectAllMode === 'filtered') { isAllSelected.value ? handleDeselectFiltered() : handleSelectFiltered() } else { isAllSelected.value ? handleDeselectAll() : handleSelectAll() } } const handleGroupClick = (group: OptionsGroup) => { const selectedValues = new Set(selected.value.map((option) => option.value)) const groupOptions = (group.options ?? []).filter((option) => !option.disabled) const allSelected = groupOptions.length > 0 && groupOptions.every((option) => selectedValues.has(option.value as string | number)) if (allSelected) { const groupValues = new Set(groupOptions.map((option) => option.value)) selected.value = selected.value.filter( (option) => option.disabled || !groupValues.has(option.value) ) } else { selectWithLimit(groupOptions) } } const headerActions = { selectAll: handleSelectAll, deselectAll: handleDeselectAll, selectFiltered: handleSelectFiltered, deselectFiltered: handleDeselectFiltered, } return () => [ h( CFormControlWrapper, { ...(typeof attrs['aria-describedby'] === 'string' && { describedby: attrs['aria-describedby'], }), feedback: props.feedback, feedbackInvalid: props.feedbackInvalid, feedbackValid: props.feedbackValid, id: props.id ?? uniqueId, invalid: props.invalid, label: props.label, text: props.text, tooltipFeedback: props.tooltipFeedback, valid: props.valid, }, { default: () => [ h( 'div', { class: [ 'form-multi-select', { disabled: props.disabled, [`form-multi-select-${props.size}`]: props.size, 'is-invalid': props.invalid, 'is-valid': props.valid, show: isOpen.value, }, ], onKeydown: handleGlobalSearch, role: 'combobox', 'aria-haspopup': 'listbox', 'aria-expanded': isOpen.value, 'aria-controls': `multi-select-listbox-${uniqueId}`, ...(props.teleport && { 'aria-owns': `multi-select-listbox-${uniqueId}` }), ...(props.disabled && { 'aria-disabled': true }), ref: multiSelectRef, }, { default: () => [ h(CMultiSelectNativeSelect, { id: props.id ?? uniqueId, multiple: props.multiple, name: props.name ?? uniqueId, options: selected.value, required: props.required, value: props.multiple ? selected.value.map((option: SelectedOption) => option.value.toString()) : selected.value.map((option: SelectedOption) => option.value)[0], onChange: () => emit('change', selected.value), onKeydown: handleNativeSelectKeyDown, }), h( 'div', { class: 'form-multi-select-input-group', ...(!props.search && !props.disabled && { tabIndex: 0 }), onClick: () => { if (!props.disabled) { openDropdown() } }, onKeydown: handleTogglerKeyDown, ref: dropdownRefElement, }, { default: () => [ h( CMultiSelectSelection, { ariaTagDeleteLabel: props.ariaTagDeleteLabel, disabled: props.disabled, multiple: props.multiple, placeholder: props.placeholder, onRemove: (option: Option) => !props.disabled && handleOnOptionClick(option), search: props.search, selected: selected.value, selectionType: props.selectionType, selectionTypeCounterText: props.selectionTypeCounterText, }, { default: () => props.search ? h('input', { type: 'text', class: 'form-multi-select-search', disabled: props.disabled, id: `search-${props.id ?? uniqueId}`, name: `search-${props.name ?? uniqueId}`, autocomplete: 'off', 'aria-label': props.ariaSearchLabel, 'aria-autocomplete': 'list', 'aria-controls': `multi-select-listbox-${uniqueId}`, onInput: (event: InputEvent) => handleSearchChange(event), onKeydown: (event: KeyboardEvent) => handleSearchKeyDown(event), ...(selected.value.length === 0 && { placeholder: props.placeholder, }), ...(selected.value.length > 0 && props.selectionType === 'counter' && { placeholder: `${selected.value.length} ${props.selectionTypeCounterText}`, }), ...(selected.value.length > 0 && !props.multiple && { placeholder: selected.value.map( (option) => option.label )[0], }), ...(props.multiple && selected.value.length > 0 && props.selectionType !== 'counter' && { size: searchValue.value.length + 2, }), ref: searchRef, }) : selected.value.length === 0 && h( 'span', { class: 'form-multi-select-placeholder', }, { default: () => props.placeholder, } ), } ), h( 'div', { class: 'form-multi-select-buttons' }, { default: () => [ !props.disabled && props.cleaner && selected.value.length > 0 && h('button', { class: 'form-multi-select-cleaner', onClick: () => handleDeselectAll(), type: 'button', 'aria-label': props.ariaCleanerLabel, }), h('button', { class: 'form-multi-select-indicator', onClick: (event: Event) => { event.preventDefault() event.stopPropagation() if (!props.disabled) { toggleDropdown() } }, type: 'button', 'aria-label': props.ariaIndicatorLabel, ...(props.disabled && { tabIndex: -1 }), }), ], } ), ], } ), h( CConditionalTeleport, { container: props.container, teleport: props.teleport, }, { default: () => h( 'div', { class: [ 'form-multi-select-dropdown', { show: props.teleport && isOpen.value, }, ], id: `multi-select-listbox-${uniqueId}`, onKeydown: handleGlobalSearch, role: 'listbox', 'aria-labelledby': props.id ?? uniqueId, 'aria-multiselectable': props.multiple, ref: dropdownMenuElement, }, { default: () => [ (slots.header || (props.multiple && props.selectAll && !( props.hideSelectAllOnSearchNoResults && searchValue.value.length > 0 && dropdownOptions.value.length === 0 ))) && h('div', { class: 'form-multi-select-dropdown-header' }, [ slots.header ? h( 'div', { onClick: (event: MouseEvent) => event.stopPropagation(), }, slots.header({ state: headerState.value, actions: headerActions, }) ) : h( 'button', { class: [ 'form-multi-select-all', { 'form-multi-select-all-with-checkbox': props.selectAllStyle === 'checkbox', 'form-multi-selected': props.selectAllStyle === 'checkbox' && selectAllCheckboxState.value === 'all', 'form-multi-select-indeterminate': props.selectAllStyle === 'checkbox' && selectAllCheckboxState.value === 'indeterminate', }, ], onClick: () => handleSelectAllToggle(), type: 'button', }, selectAllButtonLabel.value ), ]), h(CMultiSelectOptions, { loading: props.loading, onGroupClick: (group: OptionsGroup) => handleGroupClick(group), onOptionClick: (option: Option) => handleOnOptionClick(option), options: dropdownOptions.value, optionsGroupsSelectable: props.optionsGroupsSelectable && props.multiple, optionsGroupsStyle: props.optionsGroupsStyle, optionsMaxHeight: props.optionsMaxHeight, optionsStyle: props.optionsStyle, scopedSlots: slots, searchNoResultsLabel: props.searchNoResultsLabel, selected: selected.value, virtualScroller: props.virtualScroller, visibleItems: props.visibleItems, }), ], } ), } ), ], } ), ], } ), ] }, }) export { CMultiSelect }