Fixed #2819 - Improve Listbox implementation for Accessibility
parent
a96a133a98
commit
a67befc047
|
@ -101,6 +101,42 @@ const ListboxProps = [
|
||||||
default: "null",
|
default: "null",
|
||||||
description: "Fields used when filtering the options, defaults to optionLabel."
|
description: "Fields used when filtering the options, defaults to optionLabel."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "filterInputProps",
|
||||||
|
type: "object",
|
||||||
|
default: "null",
|
||||||
|
description: "Uses to pass all properties of the HTMLInputElement to the filter input inside the component."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "virtualScrollerOptions",
|
||||||
|
type: "object",
|
||||||
|
default: "null",
|
||||||
|
description: "Whether to use the virtualScroller feature. The properties of VirtualScroller component can be used like an object in it."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "autoOptionFocus",
|
||||||
|
type: "boolean",
|
||||||
|
default: "true",
|
||||||
|
description: "Whether to focus on the first visible or selected element."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "filterMessage",
|
||||||
|
type: "string",
|
||||||
|
default: "{0} results are available",
|
||||||
|
description: "Text to be displayed in hidden accessible field when filtering returns any results. Defaults to value from PrimeVue locale configuration."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "selectionMessage",
|
||||||
|
type: "string",
|
||||||
|
default: "{0} items selected",
|
||||||
|
description: "Text to be displayed in hidden accessible field when options are selected. Defaults to value from PrimeVue locale configuration."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "emptySelectionMessage",
|
||||||
|
type: "string",
|
||||||
|
default: "No selected item",
|
||||||
|
description: "Text to be displayed in hidden accessible field when any option is not selected. Defaults to value from PrimeVue locale configuration."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "emptyFilterMessage",
|
name: "emptyFilterMessage",
|
||||||
type: "string",
|
type: "string",
|
||||||
|
@ -112,6 +148,24 @@ const ListboxProps = [
|
||||||
type: "string",
|
type: "string",
|
||||||
default: "No results found",
|
default: "No results found",
|
||||||
description: "Text to display when there are no options available. Defaults to value from PrimeVue locale configuration."
|
description: "Text to display when there are no options available. Defaults to value from PrimeVue locale configuration."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tabindex",
|
||||||
|
type: "number",
|
||||||
|
default: "0",
|
||||||
|
description: "Index of the element in tabbing order."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ariaLabel",
|
||||||
|
type: "string",
|
||||||
|
default: "null",
|
||||||
|
description: "Defines a string value that labels an interactive element."
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name: "ariaLabelledby",
|
||||||
|
type: "string",
|
||||||
|
default: "null",
|
||||||
|
description: "Identifier of the underlying input element."
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -132,6 +186,28 @@ const ListboxEvents = [
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "focus",
|
||||||
|
description: "Callback to invoke when component receives focus.",
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
name: "event",
|
||||||
|
type: "object",
|
||||||
|
description: "Browser event"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "blur",
|
||||||
|
description: "Callback to invoke when component loses focus.",
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
name: "event",
|
||||||
|
type: "object",
|
||||||
|
description: "Browser event"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "filter",
|
name: "filter",
|
||||||
description: "Callback to invoke on filter input.",
|
description: "Callback to invoke on filter input.",
|
||||||
|
@ -147,12 +223,6 @@ const ListboxEvents = [
|
||||||
description: "Filter value"
|
description: "Filter value"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "virtualScrollerOptions",
|
|
||||||
type: "object",
|
|
||||||
default: "null",
|
|
||||||
description: "Whether to use the virtualScroller feature. The properties of VirtualScroller component can be used like an object in it."
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -3,13 +3,13 @@ import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
|
||||||
import { VirtualScrollerProps, VirtualScrollerItemOptions } from '../virtualscroller';
|
import { VirtualScrollerProps, VirtualScrollerItemOptions } from '../virtualscroller';
|
||||||
|
|
||||||
|
|
||||||
type ListboxOptionLabelType = string | ((data: any) => string) | undefined;
|
type ListboxOptionLabelType = string | ((data: any) => string) | undefined;
|
||||||
|
|
||||||
type ListboxOptionValueType = string | ((data: any) => any) | undefined;
|
type ListboxOptionValueType = string | ((data: any) => any) | undefined;
|
||||||
|
|
||||||
type ListboxOptionDisabledType = string | ((data: any) => boolean) | undefined;
|
type ListboxOptionDisabledType = string | ((data: any) => boolean) | undefined;
|
||||||
|
|
||||||
type ListboxOptionChildrenType = string | ((data: any) => any[]) | undefined;
|
type ListboxOptionChildrenType = string | ((data: any) => any[]) | undefined;
|
||||||
|
|
||||||
type ListboxFilterMatchModeType = 'contains' | 'startsWith' | 'endsWith' | undefined;
|
type ListboxFilterMatchModeType = 'contains' | 'startsWith' | 'endsWith' | undefined;
|
||||||
|
|
||||||
|
@ -108,6 +108,35 @@ export interface ListboxProps {
|
||||||
* Fields used when filtering the options, defaults to optionLabel.
|
* Fields used when filtering the options, defaults to optionLabel.
|
||||||
*/
|
*/
|
||||||
filterFields?: string[] | undefined;
|
filterFields?: string[] | undefined;
|
||||||
|
/**
|
||||||
|
* Uses to pass all properties of the HTMLInputElement to the filter input inside the component.
|
||||||
|
*/
|
||||||
|
filterInputProps?: HTMLInputElement | undefined;
|
||||||
|
/**
|
||||||
|
* Whether to use the virtualScroller feature. The properties of VirtualScroller component can be used like an object in it.
|
||||||
|
* @see VirtualScroller.VirtualScrollerProps
|
||||||
|
*/
|
||||||
|
virtualScrollerOptions?: VirtualScrollerProps;
|
||||||
|
/**
|
||||||
|
* Whether to focus on the first visible or selected element.
|
||||||
|
* Default value is true.
|
||||||
|
*/
|
||||||
|
autoOptionFocus?: boolean | undefined;
|
||||||
|
/**
|
||||||
|
* Text to be displayed in hidden accessible field when filtering returns any results. Defaults to value from PrimeVue locale configuration.
|
||||||
|
* Default value is '{0} results are available'.
|
||||||
|
*/
|
||||||
|
filterMessage?: string | undefined;
|
||||||
|
/**
|
||||||
|
* Text to be displayed in hidden accessible field when options are selected. Defaults to value from PrimeVue locale configuration.
|
||||||
|
* Default value is '{0} items selected'.
|
||||||
|
*/
|
||||||
|
selectionMessage?: string | undefined;
|
||||||
|
/**
|
||||||
|
* Text to be displayed in hidden accessible field when any option is not selected. Defaults to value from PrimeVue locale configuration.
|
||||||
|
* Default value is 'No selected item'.
|
||||||
|
*/
|
||||||
|
emptySelectionMessage?: string | undefined;
|
||||||
/**
|
/**
|
||||||
* Text to display when filtering does not return any results. Defaults to value from PrimeVue locale configuration.
|
* Text to display when filtering does not return any results. Defaults to value from PrimeVue locale configuration.
|
||||||
* Default value is 'No results found'.
|
* Default value is 'No results found'.
|
||||||
|
@ -119,10 +148,17 @@ export interface ListboxProps {
|
||||||
*/
|
*/
|
||||||
emptyMessage?: string | undefined;
|
emptyMessage?: string | undefined;
|
||||||
/**
|
/**
|
||||||
* Whether to use the virtualScroller feature. The properties of VirtualScroller component can be used like an object in it.
|
* Index of the element in tabbing order.
|
||||||
* @see VirtualScroller.VirtualScrollerProps
|
|
||||||
*/
|
*/
|
||||||
virtualScrollerOptions?: VirtualScrollerProps;
|
tabindex?: number | string | undefined;
|
||||||
|
/**
|
||||||
|
* Defines a string value that labels an interactive element.
|
||||||
|
*/
|
||||||
|
ariaLabel?: string | undefined;
|
||||||
|
/**
|
||||||
|
* Identifier of the underlying input element.
|
||||||
|
*/
|
||||||
|
ariaLabelledby?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ListboxSlots {
|
export interface ListboxSlots {
|
||||||
|
@ -238,6 +274,16 @@ export declare type ListboxEmits = {
|
||||||
* @param {ListboxChangeEvent} event - Custom change event.
|
* @param {ListboxChangeEvent} event - Custom change event.
|
||||||
*/
|
*/
|
||||||
'change': (event: ListboxChangeEvent) => void;
|
'change': (event: ListboxChangeEvent) => void;
|
||||||
|
/**
|
||||||
|
* Callback to invoke when the component receives focus.
|
||||||
|
* @param {Event} event - Browser event.
|
||||||
|
*/
|
||||||
|
'focus': (event: Event) => void;
|
||||||
|
/**
|
||||||
|
* Callback to invoke when the component loses focus.
|
||||||
|
* @param {Event} event - Browser event.
|
||||||
|
*/
|
||||||
|
'blur': (event: Event) => void;
|
||||||
/**
|
/**
|
||||||
* Callback to invoke on filter input.
|
* Callback to invoke on filter input.
|
||||||
* @param {ListboxFilterEvent} event - Custom filter event.
|
* @param {ListboxFilterEvent} event - Custom filter event.
|
||||||
|
|
|
@ -1,60 +1,68 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="p-listbox p-component">
|
<div :id="id" :class="containerClass" @focusout="onFocusout">
|
||||||
|
<a ref="firstHiddenFocusableElement" role="presentation" aria-hidden="true" class="p-hidden-accessible p-hidden-focusable" :tabindex="!disabled ? tabindex : -1" @focus="onFirstHiddenFocus"></a>
|
||||||
<slot name="header" :value="modelValue" :options="visibleOptions"></slot>
|
<slot name="header" :value="modelValue" :options="visibleOptions"></slot>
|
||||||
<div class="p-listbox-header" v-if="filter">
|
<div v-if="filter" class="p-listbox-header">
|
||||||
<div class="p-listbox-filter-container">
|
<div class="p-listbox-filter-container">
|
||||||
<input type="text" class="p-listbox-filter p-inputtext p-component" v-model="filterValue" :placeholder="filterPlaceholder" @input="onFilterChange">
|
<input ref="filterInput" type="text" class="p-listbox-filter p-inputtext p-component" v-model="filterValue" :placeholder="filterPlaceholder"
|
||||||
|
role="searchbox" autocomplete="off" :aria-owns="id + '_list'" :aria-activedescendant="focusedOptionId" :tabindex="!disabled && !focused ? tabindex : -1"
|
||||||
|
@input="onFilterChange" @blur="onFilterBlur" @keydown="onFilterKeyDown" v-bind="filterInputProps">
|
||||||
<span class="p-listbox-filter-icon pi pi-search"></span>
|
<span class="p-listbox-filter-icon pi pi-search"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<span role="status" aria-live="polite" class="p-hidden-accessible">
|
||||||
|
{{filterResultMessageText}}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-listbox-list-wrapper" :style="listStyle">
|
<div ref="listWrapper" class="p-listbox-list-wrapper" :style="listStyle">
|
||||||
<VirtualScroller :ref="virtualScrollerRef" v-bind="virtualScrollerOptions" :style="listStyle" :items="visibleOptions" :disabled="virtualScrollerDisabled">
|
<VirtualScroller :ref="virtualScrollerRef" v-bind="virtualScrollerOptions" :style="listStyle" :items="visibleOptions" :tabindex="-1" :disabled="virtualScrollerDisabled">
|
||||||
<template v-slot:content="{ styleClass, contentRef, items, getItemOptions, contentStyle }">
|
<template v-slot:content="{ styleClass, contentRef, items, getItemOptions, contentStyle, itemSize }">
|
||||||
<ul :ref="contentRef" :class="['p-listbox-list', styleClass]" :style="contentStyle" role="listbox" aria-multiselectable="multiple">
|
<ul :ref="(el) => listRef(el, contentRef)" :id="id + '_list'" :class="['p-listbox-list', styleClass]" :style="contentStyle" :tabindex="-1" role="listbox"
|
||||||
<template v-if="!optionGroupLabel">
|
:aria-multiselectable="multiple" :aria-label="ariaLabel" :aria-labelledby="ariaLabelledby" :aria-activedescendant="focused ? focusedOptionId : undefined" :aria-disabled="disabled"
|
||||||
<li v-for="(option, i) of items" :tabindex="isOptionDisabled(option) ? null : '0'" :class="['p-listbox-item', {'p-highlight': isSelected(option), 'p-disabled': isOptionDisabled(option)}]" v-ripple
|
@focus="onListFocus" @blur="onListBlur" @keydown="onListKeyDown">
|
||||||
:key="getOptionRenderKey(option)" @click="onOptionSelect($event, option)" @touchend="onOptionTouchEnd()" @keydown="onOptionKeyDown($event, option)" role="option" :aria-label="getOptionLabel(option)" :aria-selected="isSelected(option)" >
|
<template v-for="(option, i) of items" :key="getOptionRenderKey(option, getOptionIndex(i, getItemOptions))">
|
||||||
<slot name="option" :option="option" :index="getOptionIndex(i, getItemOptions)">{{getOptionLabel(option)}} </slot>
|
<li v-if="isOptionGroup(option)" :id="id + '_' + getOptionIndex(i, getItemOptions)" :style="{height: itemSize ? itemSize + 'px' : undefined}" class="p-listbox-item-group" role="option">
|
||||||
|
<slot name="optiongroup" :option="option.optionGroup" :index="getOptionIndex(i, getItemOptions)">{{getOptionGroupLabel(option.optionGroup)}}</slot>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
<li v-else v-ripple :id="id + '_' + getOptionIndex(i, getItemOptions)" :style="{height: itemSize ? itemSize + 'px' : undefined}"
|
||||||
<template v-else>
|
:class="['p-listbox-item', {'p-highlight': isSelected(option), 'p-focus': focusedOptionIndex === getOptionIndex(i, getItemOptions), 'p-disabled': isOptionDisabled(option)}]"
|
||||||
<template v-for="(optionGroup, i) of items" :key="getOptionGroupRenderKey(optionGroup)">
|
role="option" :aria-label="getOptionLabel(option)" :aria-selected="isSelected(option)" :aria-disabled="isOptionDisabled(option)" :aria-setsize="ariaSetSize" :aria-posinset="getAriaPosInset(getOptionIndex(i, getItemOptions))"
|
||||||
<li class="p-listbox-item-group">
|
@click="onOptionSelect($event, option, getOptionIndex(i, getItemOptions))" @mousemove="onOptionMouseMove($event, getOptionIndex(i, getItemOptions))" @touchend="onOptionTouchEnd()">
|
||||||
<slot name="optiongroup" :option="optionGroup" :index="getOptionIndex(i, getItemOptions)">{{getOptionGroupLabel(optionGroup)}}</slot>
|
|
||||||
</li>
|
|
||||||
<li v-for="(option, i) of getOptionGroupChildren(optionGroup)" :tabindex="isOptionDisabled(option) ? null : '0'" :class="['p-listbox-item', {'p-highlight': isSelected(option), 'p-disabled': isOptionDisabled(option)}]" v-ripple
|
|
||||||
:key="getOptionRenderKey(option)" @click="onOptionSelect($event, option)" @touchend="onOptionTouchEnd()" @keydown="onOptionKeyDown($event, option)" role="option" :aria-label="getOptionLabel(option)" :aria-selected="isSelected(option)" >
|
|
||||||
<slot name="option" :option="option" :index="getOptionIndex(i, getItemOptions)">{{getOptionLabel(option)}}</slot>
|
<slot name="option" :option="option" :index="getOptionIndex(i, getItemOptions)">{{getOptionLabel(option)}}</slot>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
<li v-if="filterValue && (!items || (items && items.length === 0))" class="p-listbox-empty-message" role="option">
|
||||||
<li v-if="filterValue && (!items || (items && items.length === 0))" class="p-listbox-empty-message">
|
|
||||||
<slot name="emptyfilter">{{emptyFilterMessageText}}</slot>
|
<slot name="emptyfilter">{{emptyFilterMessageText}}</slot>
|
||||||
</li>
|
</li>
|
||||||
<li v-else-if="(!options || (options && options.length === 0))" class="p-listbox-empty-message">
|
<li v-else-if="(!options || (options && options.length === 0))" class="p-listbox-empty-message" role="option">
|
||||||
<slot name="empty">{{emptyMessageText}}</slot>
|
<slot name="empty">{{emptyMessageText}}</slot>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<span v-if="(!options || (options && options.length === 0))" role="status" aria-live="polite" class="p-hidden-accessible">
|
||||||
|
{{emptyMessageText}}
|
||||||
|
</span>
|
||||||
|
<span role="status" aria-live="polite" class="p-hidden-accessible">
|
||||||
|
{{selectedMessageText}}
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:loader="{ options }" v-if="$slots.loader">
|
<template v-if="$slots.loader" v-slot:loader="{ options }">
|
||||||
<slot name="loader" :options="options"></slot>
|
<slot name="loader" :options="options"></slot>
|
||||||
</template>
|
</template>
|
||||||
</VirtualScroller>
|
</VirtualScroller>
|
||||||
</div>
|
</div>
|
||||||
<slot name="footer" :value="modelValue" :options="visibleOptions"></slot>
|
<slot name="footer" :value="modelValue" :options="visibleOptions"></slot>
|
||||||
|
<a ref="lastHiddenFocusableElement" role="presentation" aria-hidden="true" class="p-hidden-accessible p-hidden-focusable" :tabindex="!disabled ? tabindex : -1" @focus="onLastHiddenFocus"></a>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {ObjectUtils} from 'primevue/utils';
|
import {DomHandler,ObjectUtils,UniqueComponentId} from 'primevue/utils';
|
||||||
import {DomHandler} from 'primevue/utils';
|
|
||||||
import {FilterService} from 'primevue/api';
|
import {FilterService} from 'primevue/api';
|
||||||
import Ripple from 'primevue/ripple';
|
import Ripple from 'primevue/ripple';
|
||||||
import VirtualScroller from 'primevue/virtualscroller';
|
import VirtualScroller from 'primevue/virtualscroller';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Listbox',
|
name: 'Listbox',
|
||||||
emits: ['update:modelValue', 'change', 'filter'],
|
emits: ['update:modelValue', 'change', 'focus', 'blur', 'filter'],
|
||||||
props: {
|
props: {
|
||||||
modelValue: null,
|
modelValue: null,
|
||||||
options: Array,
|
options: Array,
|
||||||
|
@ -79,6 +87,27 @@ export default {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
filterInputProps: null,
|
||||||
|
virtualScrollerOptions: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
autoOptionFocus: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
filterMessage: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
selectionMessage: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
emptySelectionMessage: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
emptyFilterMessage: {
|
emptyFilterMessage: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
|
@ -87,17 +116,44 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
virtualScrollerOptions: {
|
tabindex: {
|
||||||
type: Object,
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
ariaLabel: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
ariaLabelledby: {
|
||||||
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
optionTouched: false,
|
list: null,
|
||||||
virtualScroller: null,
|
virtualScroller: null,
|
||||||
|
optionTouched: false,
|
||||||
|
startRangeIndex: -1,
|
||||||
|
searchTimeout: null,
|
||||||
|
searchValue: '',
|
||||||
|
selectOnFocus: false,
|
||||||
|
focusOnHover: false,
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
filterValue: null
|
id: UniqueComponentId(),
|
||||||
};
|
filterValue: null,
|
||||||
|
focused: false,
|
||||||
|
focusedOptionIndex: -1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
options() {
|
||||||
|
this.autoUpdateModel();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.id = this.$attrs.id || this.id;
|
||||||
|
|
||||||
|
this.autoUpdateModel();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getOptionIndex(index, fn) {
|
getOptionIndex(index, fn) {
|
||||||
|
@ -109,32 +165,128 @@ export default {
|
||||||
getOptionValue(option) {
|
getOptionValue(option) {
|
||||||
return this.optionValue ? ObjectUtils.resolveFieldData(option, this.optionValue) : option;
|
return this.optionValue ? ObjectUtils.resolveFieldData(option, this.optionValue) : option;
|
||||||
},
|
},
|
||||||
getOptionRenderKey(option) {
|
getOptionRenderKey(option, index) {
|
||||||
return this.dataKey ? ObjectUtils.resolveFieldData(option, this.dataKey) : this.getOptionLabel(option);
|
return (this.dataKey ? ObjectUtils.resolveFieldData(option, this.dataKey) : this.getOptionLabel(option)) + '_' + index;
|
||||||
},
|
},
|
||||||
isOptionDisabled(option) {
|
isOptionDisabled(option) {
|
||||||
return this.optionDisabled ? ObjectUtils.resolveFieldData(option, this.optionDisabled) : false;
|
return this.optionDisabled ? ObjectUtils.resolveFieldData(option, this.optionDisabled) : false;
|
||||||
},
|
},
|
||||||
getOptionGroupRenderKey(optionGroup) {
|
|
||||||
return ObjectUtils.resolveFieldData(optionGroup, this.optionGroupLabel);
|
|
||||||
},
|
|
||||||
getOptionGroupLabel(optionGroup) {
|
getOptionGroupLabel(optionGroup) {
|
||||||
return ObjectUtils.resolveFieldData(optionGroup, this.optionGroupLabel);
|
return ObjectUtils.resolveFieldData(optionGroup, this.optionGroupLabel);
|
||||||
},
|
},
|
||||||
getOptionGroupChildren(optionGroup) {
|
getOptionGroupChildren(optionGroup) {
|
||||||
return ObjectUtils.resolveFieldData(optionGroup, this.optionGroupChildren);
|
return ObjectUtils.resolveFieldData(optionGroup, this.optionGroupChildren);
|
||||||
},
|
},
|
||||||
onOptionSelect(event, option) {
|
getAriaPosInset(index) {
|
||||||
|
return (this.optionGroupLabel ? index - this.visibleOptions.slice(0, index).filter(option => this.isOptionGroup(option)).length : index) + 1;
|
||||||
|
},
|
||||||
|
onFirstHiddenFocus() {
|
||||||
|
this.list.focus();
|
||||||
|
|
||||||
|
const firstFocusableEl = DomHandler.getFirstFocusableElement(this.$el, ':not(.p-hidden-focusable)');
|
||||||
|
this.$refs.lastHiddenFocusableElement.tabIndex = ObjectUtils.isEmpty(firstFocusableEl) ? -1 : undefined;
|
||||||
|
this.$refs.firstHiddenFocusableElement.tabIndex = -1;
|
||||||
|
},
|
||||||
|
onLastHiddenFocus(event) {
|
||||||
|
const relatedTarget = event.relatedTarget;
|
||||||
|
|
||||||
|
if (relatedTarget === this.list) {
|
||||||
|
const firstFocusableEl = DomHandler.getFirstFocusableElement(this.$el, ':not(.p-hidden-focusable)');
|
||||||
|
firstFocusableEl && firstFocusableEl.focus();
|
||||||
|
this.$refs.firstHiddenFocusableElement.tabIndex = undefined;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.$refs.firstHiddenFocusableElement.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$refs.lastHiddenFocusableElement.tabIndex = -1;
|
||||||
|
},
|
||||||
|
onFocusout(event) {
|
||||||
|
if (!this.$el.contains(event.relatedTarget)) {
|
||||||
|
this.$refs.lastHiddenFocusableElement.tabIndex = this.$refs.firstHiddenFocusableElement.tabIndex = undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onListFocus(event) {
|
||||||
|
this.focused = true;
|
||||||
|
this.focusedOptionIndex = this.autoOptionFocus ? this.findFirstFocusedOptionIndex() : -1;
|
||||||
|
this.$emit('focus', event);
|
||||||
|
},
|
||||||
|
onListBlur(event) {
|
||||||
|
this.focused = false;
|
||||||
|
this.focusedOptionIndex = this.startRangeIndex = -1;
|
||||||
|
this.searchValue = '';
|
||||||
|
this.$emit('blur', event);
|
||||||
|
},
|
||||||
|
onListKeyDown(event) {
|
||||||
|
switch (event.code) {
|
||||||
|
case 'ArrowDown':
|
||||||
|
this.onArrowDownKey(event);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'ArrowUp':
|
||||||
|
this.onArrowUpKey(event);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Home':
|
||||||
|
this.onHomeKey(event);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'End':
|
||||||
|
this.onEndKey(event);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'PageDown':
|
||||||
|
this.onPageDownKey(event);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'PageUp':
|
||||||
|
this.onPageUpKey(event);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Enter':
|
||||||
|
case 'Space':
|
||||||
|
this.onSpaceKey(event);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Tab':
|
||||||
|
//NOOP
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'ShiftLeft':
|
||||||
|
case 'ShiftRight':
|
||||||
|
this.onShiftKey(event);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
if (event.code === 'KeyA' && this.multiple && (event.metaKey || event.ctrlKey)) {
|
||||||
|
const value = this.visibleOptions.filter(option => this.isValidOption(option)).map(option => this.getOptionValue(option));
|
||||||
|
this.updateModel(event, value);
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ObjectUtils.isPrintableCharacter(event.key)) {
|
||||||
|
this.searchOptions(event, event.key);
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onOptionSelect(event, option, index = -1) {
|
||||||
if (this.disabled || this.isOptionDisabled(option)) {
|
if (this.disabled || this.isOptionDisabled(option)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.multiple)
|
this.multiple ? this.onOptionSelectMultiple(event, option) : this.onOptionSelectSingle(event, option);
|
||||||
this.onOptionSelectMultiple(event, option);
|
|
||||||
else
|
|
||||||
this.onOptionSelectSingle(event, option);
|
|
||||||
|
|
||||||
this.optionTouched = false;
|
this.optionTouched = false;
|
||||||
|
index !== -1 && (this.focusedOptionIndex = index);
|
||||||
|
},
|
||||||
|
onOptionMouseMove(event, index) {
|
||||||
|
if (this.focusOnHover) {
|
||||||
|
this.changeFocusedOptionIndex(event, index);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onOptionTouchEnd() {
|
onOptionTouchEnd() {
|
||||||
if (this.disabled) {
|
if (this.disabled) {
|
||||||
|
@ -174,7 +326,6 @@ export default {
|
||||||
},
|
},
|
||||||
onOptionSelectMultiple(event, option) {
|
onOptionSelectMultiple(event, option) {
|
||||||
let selected = this.isSelected(option);
|
let selected = this.isSelected(option);
|
||||||
let valueChanged = false;
|
|
||||||
let value = null;
|
let value = null;
|
||||||
let metaSelection = this.optionTouched ? false : this.metaKeySelection;
|
let metaSelection = this.optionTouched ? false : this.metaKeySelection;
|
||||||
|
|
||||||
|
@ -182,133 +333,327 @@ export default {
|
||||||
let metaKey = (event.metaKey || event.ctrlKey);
|
let metaKey = (event.metaKey || event.ctrlKey);
|
||||||
|
|
||||||
if (selected) {
|
if (selected) {
|
||||||
if(metaKey)
|
value = metaKey ? this.removeOption(option) : [this.getOptionValue(option)];
|
||||||
value = this.removeOption(option);
|
|
||||||
else
|
|
||||||
value = [this.getOptionValue(option)];
|
|
||||||
|
|
||||||
valueChanged = true;
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
value = (metaKey) ? this.modelValue || [] : [];
|
value = (metaKey) ? this.modelValue || [] : [];
|
||||||
value = [...value, this.getOptionValue(option)];
|
value = [...value, this.getOptionValue(option)];
|
||||||
valueChanged = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (selected)
|
value = selected ? this.removeOption(option) : [...this.modelValue || [], this.getOptionValue(option)];
|
||||||
value = this.removeOption(option);
|
|
||||||
else
|
|
||||||
value = [...this.modelValue || [], this.getOptionValue(option)];
|
|
||||||
|
|
||||||
valueChanged = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(valueChanged) {
|
this.updateModel(event, value);
|
||||||
|
},
|
||||||
|
onOptionSelectRange(event, start = -1, end = -1) {
|
||||||
|
start === -1 && (start = this.findNearestSelectedOptionIndex(end, true));
|
||||||
|
end === -1 && (end = this.findNearestSelectedOptionIndex(start));
|
||||||
|
|
||||||
|
if (start !== -1 && end !== -1) {
|
||||||
|
const rangeStart = Math.min(start, end);
|
||||||
|
const rangeEnd = Math.max(start, end);
|
||||||
|
const value = this.visibleOptions.slice(rangeStart, rangeEnd + 1).filter(option => this.isValidOption(option)).map(option => this.getOptionValue(option));
|
||||||
|
|
||||||
this.updateModel(event, value);
|
this.updateModel(event, value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isSelected(option) {
|
onFilterChange(event) {
|
||||||
let selected = false;
|
this.$emit('filter', {originalEvent: event, value: event.target.value});
|
||||||
let optionValue = this.getOptionValue(option);
|
this.focusedOptionIndex = this.startRangeIndex = -1;
|
||||||
|
},
|
||||||
|
onFilterBlur() {
|
||||||
|
this.focusedOptionIndex = this.startRangeIndex = -1;
|
||||||
|
},
|
||||||
|
onFilterKeyDown(event) {
|
||||||
|
switch (event.code) {
|
||||||
|
case 'ArrowDown':
|
||||||
|
this.onArrowDownKey(event);
|
||||||
|
break;
|
||||||
|
|
||||||
if (this.multiple) {
|
case 'ArrowUp':
|
||||||
if (this.modelValue) {
|
this.onArrowUpKey(event);
|
||||||
for (let val of this.modelValue) {
|
break;
|
||||||
if (ObjectUtils.equals(val, optionValue, this.equalityKey)) {
|
|
||||||
selected = true;
|
case 'ArrowLeft':
|
||||||
|
case 'ArrowRight':
|
||||||
|
this.onArrowLeftKey(event, true);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Home':
|
||||||
|
this.onHomeKey(event, true);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'End':
|
||||||
|
this.onEndKey(event, true);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Enter':
|
||||||
|
this.onEnterKey(event);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'ShiftLeft':
|
||||||
|
case 'ShiftRight':
|
||||||
|
this.onShiftKey(event);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
onArrowDownKey(event) {
|
||||||
}
|
const optionIndex = this.focusedOptionIndex !== -1 ? this.findNextOptionIndex(this.focusedOptionIndex) : this.findFirstFocusedOptionIndex();
|
||||||
else {
|
|
||||||
selected = ObjectUtils.equals(this.modelValue, optionValue, this.equalityKey);
|
if (this.multiple && event.shiftKey) {
|
||||||
|
this.onOptionSelectRange(event, this.startRangeIndex, optionIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
return selected;
|
this.changeFocusedOptionIndex(event, optionIndex);
|
||||||
|
event.preventDefault();
|
||||||
|
},
|
||||||
|
onArrowUpKey(event) {
|
||||||
|
const optionIndex = this.focusedOptionIndex !== -1 ? this.findPrevOptionIndex(this.focusedOptionIndex) : this.findLastFocusedOptionIndex();
|
||||||
|
|
||||||
|
if (this.multiple && event.shiftKey) {
|
||||||
|
this.onOptionSelectRange(event, optionIndex, this.startRangeIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.changeFocusedOptionIndex(event, optionIndex);
|
||||||
|
event.preventDefault();
|
||||||
|
},
|
||||||
|
onArrowLeftKey(event, pressedInInputText = false) {
|
||||||
|
pressedInInputText && (this.focusedOptionIndex = -1);
|
||||||
|
},
|
||||||
|
onHomeKey(event, pressedInInputText = false) {
|
||||||
|
if (pressedInInputText) {
|
||||||
|
event.currentTarget.setSelectionRange(0, 0);
|
||||||
|
this.focusedOptionIndex = -1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let metaKey = event.metaKey || event.ctrlKey;
|
||||||
|
let optionIndex = this.findFirstOptionIndex();
|
||||||
|
|
||||||
|
if (this.multiple && event.shiftKey && metaKey) {
|
||||||
|
this.onOptionSelectRange(event, optionIndex, this.startRangeIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.changeFocusedOptionIndex(event, optionIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
},
|
||||||
|
onEndKey(event, pressedInInputText = false) {
|
||||||
|
if (pressedInInputText) {
|
||||||
|
const target = event.currentTarget;
|
||||||
|
const len = target.value.length;
|
||||||
|
target.setSelectionRange(len, len);
|
||||||
|
this.focusedOptionIndex = -1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let metaKey = event.metaKey || event.ctrlKey;
|
||||||
|
let optionIndex = this.findLastOptionIndex();
|
||||||
|
|
||||||
|
if (this.multiple && event.shiftKey && metaKey) {
|
||||||
|
this.onOptionSelectRange(event, this.startRangeIndex, optionIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.changeFocusedOptionIndex(event, optionIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
},
|
||||||
|
onPageUpKey(event) {
|
||||||
|
this.scrollInView(0);
|
||||||
|
event.preventDefault();
|
||||||
|
},
|
||||||
|
onPageDownKey(event) {
|
||||||
|
this.scrollInView(this.visibleOptions.length - 1);
|
||||||
|
event.preventDefault();
|
||||||
|
},
|
||||||
|
onEnterKey(event) {
|
||||||
|
if (this.focusedOptionIndex !== -1) {
|
||||||
|
if (this.multiple && event.shiftKey)
|
||||||
|
this.onOptionSelectRange(event, this.focusedOptionIndex);
|
||||||
|
else
|
||||||
|
this.onOptionSelect(event, this.visibleOptions[this.focusedOptionIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
},
|
||||||
|
onSpaceKey(event) {
|
||||||
|
this.onEnterKey(event);
|
||||||
|
},
|
||||||
|
onShiftKey() {
|
||||||
|
this.startRangeIndex = this.focusedOptionIndex;
|
||||||
|
},
|
||||||
|
isOptionGroup(option) {
|
||||||
|
return this.optionGroupLabel && option.optionGroup && option.group;
|
||||||
|
},
|
||||||
|
isOptionMatched(option) {
|
||||||
|
return this.isValidOption(option) && this.getOptionLabel(option).toLocaleLowerCase(this.filterLocale).startsWith(this.searchValue.toLocaleLowerCase(this.filterLocale));
|
||||||
|
},
|
||||||
|
isValidOption(option) {
|
||||||
|
return option && !(this.isOptionDisabled(option) || option.optionGroup);
|
||||||
|
},
|
||||||
|
isValidSelectedOption(option) {
|
||||||
|
return this.isValidOption(option) && this.isSelected(option);
|
||||||
|
},
|
||||||
|
isSelected(option) {
|
||||||
|
const optionValue = this.getOptionValue(option);
|
||||||
|
|
||||||
|
if (this.multiple)
|
||||||
|
return (this.modelValue || []).some(value => ObjectUtils.equals(value, optionValue, this.equalityKey));
|
||||||
|
else
|
||||||
|
return ObjectUtils.equals(this.modelValue, optionValue, this.equalityKey);
|
||||||
|
},
|
||||||
|
findFirstOptionIndex() {
|
||||||
|
return this.visibleOptions.findIndex(option => this.isValidOption(option));
|
||||||
|
},
|
||||||
|
findLastOptionIndex() {
|
||||||
|
return this.visibleOptions.findLastIndex(option => this.isValidOption(option));
|
||||||
|
},
|
||||||
|
findNextOptionIndex(index) {
|
||||||
|
const matchedOptionIndex = index < (this.visibleOptions.length - 1) ? this.visibleOptions.slice(index + 1).findIndex(option => this.isValidOption(option)) : -1;
|
||||||
|
return matchedOptionIndex > -1 ? matchedOptionIndex + index + 1 : index;
|
||||||
|
},
|
||||||
|
findPrevOptionIndex(index) {
|
||||||
|
const matchedOptionIndex = index > 0 ? this.visibleOptions.slice(0, index).findLastIndex(option => this.isValidOption(option)) : -1;
|
||||||
|
return matchedOptionIndex > -1 ? matchedOptionIndex : index;
|
||||||
|
},
|
||||||
|
findFirstSelectedOptionIndex() {
|
||||||
|
return this.hasSelectedOption ? this.visibleOptions.findIndex(option => this.isValidSelectedOption(option)) : -1;
|
||||||
|
},
|
||||||
|
findLastSelectedOptionIndex() {
|
||||||
|
return this.hasSelectedOption ? this.visibleOptions.findLastIndex(option => this.isValidSelectedOption(option)) : -1;
|
||||||
|
},
|
||||||
|
findNextSelectedOptionIndex(index) {
|
||||||
|
const matchedOptionIndex = this.hasSelectedOption && index < (this.visibleOptions.length - 1) ? this.visibleOptions.slice(index + 1).findIndex(option => this.isValidSelectedOption(option)) : -1;
|
||||||
|
return matchedOptionIndex > -1 ? matchedOptionIndex + index + 1 : -1;
|
||||||
|
},
|
||||||
|
findPrevSelectedOptionIndex(index) {
|
||||||
|
const matchedOptionIndex = this.hasSelectedOption && index > 0 ? this.visibleOptions.slice(0, index).findLastIndex(option => this.isValidSelectedOption(option)) : -1;
|
||||||
|
return matchedOptionIndex > -1 ? matchedOptionIndex : -1;
|
||||||
|
},
|
||||||
|
findNearestSelectedOptionIndex(index, firstCheckUp = false) {
|
||||||
|
let matchedOptionIndex = -1;
|
||||||
|
|
||||||
|
if (this.hasSelectedOption) {
|
||||||
|
if (firstCheckUp) {
|
||||||
|
matchedOptionIndex = this.findPrevSelectedOptionIndex(index);
|
||||||
|
matchedOptionIndex = matchedOptionIndex === -1 ? this.findNextSelectedOptionIndex(index) : matchedOptionIndex;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
matchedOptionIndex = this.findNextSelectedOptionIndex(index);
|
||||||
|
matchedOptionIndex = matchedOptionIndex === -1 ? this.findPrevSelectedOptionIndex(index) : matchedOptionIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchedOptionIndex > -1 ? matchedOptionIndex : index;
|
||||||
|
},
|
||||||
|
findFirstFocusedOptionIndex() {
|
||||||
|
const selectedIndex = this.findFirstSelectedOptionIndex();
|
||||||
|
return selectedIndex < 0 ? this.findFirstOptionIndex() : selectedIndex;
|
||||||
|
},
|
||||||
|
findLastFocusedOptionIndex() {
|
||||||
|
const selectedIndex = this.findLastSelectedOptionIndex();
|
||||||
|
return selectedIndex < 0 ? this.findLastOptionIndex() : selectedIndex;
|
||||||
|
},
|
||||||
|
searchOptions(event, char) {
|
||||||
|
this.searchValue = (this.searchValue || '') + char;
|
||||||
|
|
||||||
|
let optionIndex = -1;
|
||||||
|
if (this.focusedOptionIndex !== -1) {
|
||||||
|
optionIndex = this.visibleOptions.slice(this.focusedOptionIndex).findIndex(option => this.isOptionMatched(option));
|
||||||
|
optionIndex = optionIndex === -1 ? this.visibleOptions.slice(0, this.focusedOptionIndex).findIndex(option => this.isOptionMatched(option)) : optionIndex + this.focusedOptionIndex;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
optionIndex = this.visibleOptions.findIndex(option => this.isOptionMatched(option));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (optionIndex === -1 && this.focusedOptionIndex === -1) {
|
||||||
|
optionIndex = this.findFirstFocusedOptionIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (optionIndex !== -1) {
|
||||||
|
this.changeFocusedOptionIndex(event, optionIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.searchTimeout) {
|
||||||
|
clearTimeout(this.searchTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.searchTimeout = setTimeout(() => {
|
||||||
|
this.searchValue = '';
|
||||||
|
this.searchTimeout = null;
|
||||||
|
}, 500);
|
||||||
},
|
},
|
||||||
removeOption(option) {
|
removeOption(option) {
|
||||||
return this.modelValue.filter(val => !ObjectUtils.equals(val, this.getOptionValue(option), this.equalityKey));
|
return this.modelValue.filter(val => !ObjectUtils.equals(val, this.getOptionValue(option), this.equalityKey));
|
||||||
},
|
},
|
||||||
|
changeFocusedOptionIndex(event, index) {
|
||||||
|
if (this.focusedOptionIndex !== index) {
|
||||||
|
this.focusedOptionIndex = index;
|
||||||
|
this.scrollInView();
|
||||||
|
|
||||||
|
if (this.selectOnFocus && !this.multiple) {
|
||||||
|
this.updateModel(event, this.getOptionValue(this.visibleOptions[index]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scrollInView(index = -1) {
|
||||||
|
const id = index !== -1 ? `${this.id}_${index}` : this.focusedOptionId;
|
||||||
|
const element = DomHandler.findSingle(this.list, `li[id="${id}"]`);
|
||||||
|
if (element) {
|
||||||
|
element.scrollIntoView && element.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
||||||
|
}
|
||||||
|
else if (!this.virtualScrollerDisabled) {
|
||||||
|
this.virtualScroller && this.virtualScroller.scrollToIndex(index !== -1 ? index : this.focusedOptionIndex);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
autoUpdateModel() {
|
||||||
|
if (this.selectOnFocus && this.autoOptionFocus && !this.hasSelectedOption) {
|
||||||
|
this.focusedOptionIndex = this.findFirstFocusedOptionIndex();
|
||||||
|
const value = this.getOptionValue(this.visibleOptions[this.focusedOptionIndex]);
|
||||||
|
this.updateModel(null, this.multiple ? [value] : value);
|
||||||
|
}
|
||||||
|
},
|
||||||
updateModel(event, value) {
|
updateModel(event, value) {
|
||||||
this.$emit('update:modelValue', value);
|
this.$emit('update:modelValue', value);
|
||||||
this.$emit('change', {originalEvent: event, value: value});
|
this.$emit('change', { originalEvent: event, value });
|
||||||
},
|
},
|
||||||
onOptionKeyDown(event, option) {
|
listRef(el, contentRef) {
|
||||||
let item = event.currentTarget;
|
this.list = el;
|
||||||
|
contentRef && contentRef(el); // For VirtualScroller
|
||||||
switch (event.which) {
|
|
||||||
//down
|
|
||||||
case 40:
|
|
||||||
var nextItem = this.findNextItem(item);
|
|
||||||
if(nextItem) {
|
|
||||||
nextItem.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
break;
|
|
||||||
|
|
||||||
//up
|
|
||||||
case 38:
|
|
||||||
var prevItem = this.findPrevItem(item);
|
|
||||||
if(prevItem) {
|
|
||||||
prevItem.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
break;
|
|
||||||
|
|
||||||
//enter
|
|
||||||
case 13:
|
|
||||||
this.onOptionSelect(event, option);
|
|
||||||
event.preventDefault();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
findNextItem(item) {
|
|
||||||
let nextItem = item.nextElementSibling;
|
|
||||||
|
|
||||||
if (nextItem)
|
|
||||||
return DomHandler.hasClass(nextItem, 'p-disabled') || DomHandler.hasClass(nextItem, 'p-listbox-item-group') ? this.findNextItem(nextItem) : nextItem;
|
|
||||||
else
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
findPrevItem(item) {
|
|
||||||
let prevItem = item.previousElementSibling;
|
|
||||||
|
|
||||||
if (prevItem)
|
|
||||||
return DomHandler.hasClass(prevItem, 'p-disabled') || DomHandler.hasClass(prevItem, 'p-listbox-item-group') ? this.findPrevItem(prevItem) : prevItem;
|
|
||||||
else
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
onFilterChange(event) {
|
|
||||||
this.$emit('filter', {originalEvent: event, value: event.target.value});
|
|
||||||
},
|
},
|
||||||
virtualScrollerRef(el) {
|
virtualScrollerRef(el) {
|
||||||
this.virtualScroller = el;
|
this.virtualScroller = el;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
containerClass() {
|
||||||
|
return ['p-listbox p-component', {
|
||||||
|
'p-focus': this.focused,
|
||||||
|
'p-disabled': this.disabled
|
||||||
|
}];
|
||||||
|
},
|
||||||
visibleOptions() {
|
visibleOptions() {
|
||||||
if (this.filterValue) {
|
let options = this.options || [];
|
||||||
|
|
||||||
if (this.optionGroupLabel) {
|
if (this.optionGroupLabel) {
|
||||||
let filteredGroups = [];
|
options = options.reduce((result, option, index) => {
|
||||||
for (let optgroup of this.options) {
|
result.push({ optionGroup: option, group: true, groupIndex: index });
|
||||||
let filteredSubOptions = FilterService.filter(this.getOptionGroupChildren(optgroup), this.searchFields, this.filterValue, this.filterMatchMode, this.filterLocale);
|
|
||||||
if (filteredSubOptions && filteredSubOptions.length) {
|
let optionGroupChildren = this.getOptionGroupChildren(option);
|
||||||
filteredGroups.push({...optgroup, ...{items: filteredSubOptions}});
|
optionGroupChildren && optionGroupChildren.forEach(o => result.push(o));
|
||||||
}
|
|
||||||
}
|
return result;
|
||||||
return filteredGroups
|
}, []);
|
||||||
}
|
|
||||||
else {
|
|
||||||
return FilterService.filter(this.options, this.searchFields, this.filterValue, 'contains', this.filterLocale);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return this.options;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return this.filterValue ? FilterService.filter(options, this.searchFields, this.filterValue, this.filterMatchMode, this.filterLocale) : options;
|
||||||
|
},
|
||||||
|
hasSelectedOption() {
|
||||||
|
return ObjectUtils.isNotEmpty(this.modelValue);
|
||||||
},
|
},
|
||||||
equalityKey() {
|
equalityKey() {
|
||||||
return this.optionValue ? null : this.dataKey;
|
return this.optionValue ? null : this.dataKey;
|
||||||
|
@ -316,12 +661,33 @@ export default {
|
||||||
searchFields() {
|
searchFields() {
|
||||||
return this.filterFields || [this.optionLabel];
|
return this.filterFields || [this.optionLabel];
|
||||||
},
|
},
|
||||||
|
filterResultMessageText() {
|
||||||
|
return ObjectUtils.isNotEmpty(this.visibleOptions) ? this.filterMessageText.replaceAll('{0}', this.visibleOptions.length) : this.emptyFilterMessageText;
|
||||||
|
},
|
||||||
|
filterMessageText() {
|
||||||
|
return this.filterMessage || this.$primevue.config.locale.searchMessage;
|
||||||
|
},
|
||||||
emptyFilterMessageText() {
|
emptyFilterMessageText() {
|
||||||
return this.emptyFilterMessage || this.$primevue.config.locale.emptyFilterMessage;
|
return this.emptyFilterMessage || this.$primevue.config.locale.emptySearchMessage || this.$primevue.config.locale.emptyFilterMessage;
|
||||||
},
|
},
|
||||||
emptyMessageText() {
|
emptyMessageText() {
|
||||||
return this.emptyMessage || this.$primevue.config.locale.emptyMessage;
|
return this.emptyMessage || this.$primevue.config.locale.emptyMessage;
|
||||||
},
|
},
|
||||||
|
selectionMessageText() {
|
||||||
|
return this.selectionMessage || this.$primevue.config.locale.selectionMessage;
|
||||||
|
},
|
||||||
|
emptySelectionMessageText() {
|
||||||
|
return this.emptySelectionMessage || this.$primevue.config.locale.emptySelectionMessage;
|
||||||
|
},
|
||||||
|
selectedMessageText() {
|
||||||
|
return ObjectUtils.isNotEmpty(this.modelValue) ? this.selectionMessageText.replaceAll('{0}', this.multiple ? this.modelValue.length : '1') : this.emptySelectionMessageText;
|
||||||
|
},
|
||||||
|
focusedOptionId() {
|
||||||
|
return this.focusedOptionIndex !== -1 ? `${this.id}_${this.focusedOptionIndex}` : null;
|
||||||
|
},
|
||||||
|
ariaSetSize() {
|
||||||
|
return this.visibleOptions.filter(option => !this.isOptionGroup(option)).length;
|
||||||
|
},
|
||||||
virtualScrollerDisabled() {
|
virtualScrollerDisabled() {
|
||||||
return !this.virtualScrollerOptions;
|
return !this.virtualScrollerOptions;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue