Refactor #3965 - For Listbox

pull/3997/head
Tuğçe Küçükoğlu 2023-05-25 16:53:42 +03:00
parent 42b054864a
commit b6b02ca075
3 changed files with 212 additions and 138 deletions

View File

@ -0,0 +1,163 @@
<script>
import BaseComponent from 'primevue/basecomponent';
import { useStyle } from 'primevue/usestyle';
const styles = `
.p-listbox-list-wrapper {
overflow: auto;
}
.p-listbox-list {
list-style-type: none;
margin: 0;
padding: 0;
}
.p-listbox-item {
cursor: pointer;
position: relative;
overflow: hidden;
}
.p-listbox-item-group {
cursor: auto;
}
.p-listbox-filter-container {
position: relative;
}
.p-listbox-filter-icon {
position: absolute;
top: 50%;
margin-top: -0.5rem;
}
.p-listbox-filter {
width: 100%;
}
`;
const classes = {
root: ({ instance, props }) => [
'p-listbox p-component',
{
'p-focus': instance.focused,
'p-disabled': props.disabled
}
],
hiddenFirstFocusableEl: 'p-hidden-accessible p-hidden-focusable',
header: 'p-listbox-header',
filterContainer: 'p-listbox-filter-container',
filterInput: 'p-listbox-filter p-inputtext p-component',
filterIcon: 'p-listbox-filter-icon',
hiddenFilterResult: 'p-hidden-accessible',
wrapper: 'p-listbox-list-wrapper',
list: 'p-listbox-list',
itemGroup: 'p-listbox-item-group',
item: ({ instance, option, index, getItemOptions }) => [
'p-listbox-item',
{
'p-highlight': instance.isSelected(option),
'p-focus': instance.focusedOptionIndex === instance.getOptionIndex(index, getItemOptions),
'p-disabled': instance.isOptionDisabled(option)
}
],
emptyMessage: 'p-listbox-empty-message',
hiddenEmptyMessage: 'p-hidden-accessible',
hiddenSelectedMessage: 'p-hidden-accessible',
hiddenLastFocusableEl: 'p-hidden-accessible p-hidden-focusable'
};
const { load: loadStyle, unload: unloadStyle } = useStyle(styles, { id: 'primevue_listbox_style', manual: true });
export default {
name: 'BaseListbox',
extends: BaseComponent,
props: {
modelValue: null,
options: Array,
optionLabel: null,
optionValue: null,
optionDisabled: null,
optionGroupLabel: null,
optionGroupChildren: null,
listStyle: null,
disabled: Boolean,
dataKey: null,
multiple: Boolean,
metaKeySelection: Boolean,
filter: Boolean,
filterPlaceholder: String,
filterLocale: String,
filterMatchMode: {
type: String,
default: 'contains'
},
filterFields: {
type: Array,
default: null
},
filterInputProps: null,
virtualScrollerOptions: {
type: Object,
default: null
},
autoOptionFocus: {
type: Boolean,
default: true
},
selectOnFocus: {
type: Boolean,
default: false
},
filterMessage: {
type: String,
default: null
},
selectionMessage: {
type: String,
default: null
},
emptySelectionMessage: {
type: String,
default: null
},
emptyFilterMessage: {
type: String,
default: null
},
emptyMessage: {
type: String,
default: null
},
filterIcon: {
type: String,
default: undefined
},
tabindex: {
type: Number,
default: 0
},
'aria-label': {
type: String,
default: null
},
'aria-labelledby': {
type: String,
default: null
}
},
css: {
classes
},
watch: {
isUnstyled: {
immediate: true,
handler(newValue) {
!newValue && loadStyle();
}
}
}
};
</script>

View File

@ -316,6 +316,11 @@ export interface ListboxProps {
* @type {ListboxPassThroughOptions} * @type {ListboxPassThroughOptions}
*/ */
pt?: ListboxPassThroughOptions; pt?: ListboxPassThroughOptions;
/**
* When enabled, it removes component related styles in the core.
* @defaultValue false
*/
unstyled?: boolean;
} }
/** /**

View File

@ -1,14 +1,25 @@
<template> <template>
<div :id="id" :class="containerClass" @focusout="onFocusout" v-bind="ptm('root')"> <div :id="id" :class="cx('root')" @focusout="onFocusout" v-bind="ptm('root')">
<span ref="firstHiddenFocusableElement" role="presentation" aria-hidden="true" class="p-hidden-accessible p-hidden-focusable" :tabindex="!disabled ? tabindex : -1" @focus="onFirstHiddenFocus" v-bind="ptm('hiddenFirstFocusableEl')"></span> <span
ref="firstHiddenFocusableElement"
role="presentation"
aria-hidden="true"
:class="cx('hiddenFirstFocusableEl')"
:style="sx('hiddenAccessible', isUnstyled)"
:tabindex="!disabled ? tabindex : -1"
@focus="onFirstHiddenFocus"
v-bind="ptm('hiddenFirstFocusableEl')"
:data-p-hidden-accessible="true"
:data-p-hidden-focusable="true"
></span>
<slot name="header" :value="modelValue" :options="visibleOptions"></slot> <slot name="header" :value="modelValue" :options="visibleOptions"></slot>
<div v-if="filter" class="p-listbox-header" v-bind="ptm('header')"> <div v-if="filter" :class="cx('header')" v-bind="ptm('header')">
<div class="p-listbox-filter-container" v-bind="ptm('filterContainer')"> <div :class="cx('filterContainer')" v-bind="ptm('filterContainer')">
<input <input
ref="filterInput" ref="filterInput"
v-model="filterValue" v-model="filterValue"
type="text" type="text"
class="p-listbox-filter p-inputtext p-component" :class="cx('filterInput')"
:placeholder="filterPlaceholder" :placeholder="filterPlaceholder"
role="searchbox" role="searchbox"
autocomplete="off" autocomplete="off"
@ -21,21 +32,21 @@
v-bind="{ ...filterInputProps, ...ptm('filterInput') }" v-bind="{ ...filterInputProps, ...ptm('filterInput') }"
/> />
<slot name="filtericon"> <slot name="filtericon" :class="cx('filterIcon')">
<component :is="filterIcon ? 'span' : 'SearchIcon'" :class="['p-listbox-filter-icon', filterIcon]" v-bind="ptm('filterIcon')" /> <component :is="filterIcon ? 'span' : 'SearchIcon'" :class="[cx('filterIcon'), filterIcon]" v-bind="ptm('filterIcon')" />
</slot> </slot>
</div> </div>
<span role="status" aria-live="polite" class="p-hidden-accessible" v-bind="ptm('hiddenFilterResult')"> <span role="status" aria-live="polite" :class="cx('hiddenFilterResult')" :style="sx('hiddenAccessible', isUnstyled)" v-bind="ptm('hiddenFilterResult')" :data-p-hidden-accessible="true">
{{ filterResultMessageText }} {{ filterResultMessageText }}
</span> </span>
</div> </div>
<div ref="listWrapper" class="p-listbox-list-wrapper" :style="listStyle" v-bind="ptm('wrapper')"> <div ref="listWrapper" :class="cx('wrapper')" :style="listStyle" v-bind="ptm('wrapper')">
<VirtualScroller :ref="virtualScrollerRef" v-bind="{ ...virtualScrollerOptions, ...ptm('virtualScroller') }" :style="listStyle" :items="visibleOptions" :tabindex="-1" :disabled="virtualScrollerDisabled"> <VirtualScroller :ref="virtualScrollerRef" v-bind="{ ...virtualScrollerOptions, ...ptm('virtualScroller') }" :style="listStyle" :items="visibleOptions" :tabindex="-1" :disabled="virtualScrollerDisabled">
<template v-slot:content="{ styleClass, contentRef, items, getItemOptions, contentStyle, itemSize }"> <template v-slot:content="{ styleClass, contentRef, items, getItemOptions, contentStyle, itemSize }">
<ul <ul
:ref="(el) => listRef(el, contentRef)" :ref="(el) => listRef(el, contentRef)"
:id="id + '_list'" :id="id + '_list'"
:class="['p-listbox-list', styleClass]" :class="[cx('list'), styleClass]"
:style="contentStyle" :style="contentStyle"
:tabindex="-1" :tabindex="-1"
role="listbox" role="listbox"
@ -50,7 +61,7 @@
v-bind="ptm('list')" v-bind="ptm('list')"
> >
<template v-for="(option, i) of items" :key="getOptionRenderKey(option, getOptionIndex(i, getItemOptions))"> <template v-for="(option, i) of items" :key="getOptionRenderKey(option, getOptionIndex(i, getItemOptions))">
<li v-if="isOptionGroup(option)" :id="id + '_' + getOptionIndex(i, getItemOptions)" :style="{ height: itemSize ? itemSize + 'px' : undefined }" class="p-listbox-item-group" role="option" v-bind="ptm('itemGroup')"> <li v-if="isOptionGroup(option)" :id="id + '_' + getOptionIndex(i, getItemOptions)" :style="{ height: itemSize ? itemSize + 'px' : undefined }" :class="cx('itemGroup')" role="option" v-bind="ptm('itemGroup')">
<slot name="optiongroup" :option="option.optionGroup" :index="getOptionIndex(i, getItemOptions)">{{ getOptionGroupLabel(option.optionGroup) }}</slot> <slot name="optiongroup" :option="option.optionGroup" :index="getOptionIndex(i, getItemOptions)">{{ getOptionGroupLabel(option.optionGroup) }}</slot>
</li> </li>
<li <li
@ -58,7 +69,7 @@
:id="id + '_' + getOptionIndex(i, getItemOptions)" :id="id + '_' + getOptionIndex(i, getItemOptions)"
v-ripple v-ripple
:style="{ height: itemSize ? itemSize + 'px' : undefined }" :style="{ height: itemSize ? itemSize + 'px' : undefined }"
:class="['p-listbox-item', { 'p-highlight': isSelected(option), 'p-focus': focusedOptionIndex === getOptionIndex(i, getItemOptions), 'p-disabled': isOptionDisabled(option) }]" :class="cx('item', { option, index: i, getItemOptions })"
role="option" role="option"
:aria-label="getOptionLabel(option)" :aria-label="getOptionLabel(option)"
:aria-selected="isSelected(option)" :aria-selected="isSelected(option)"
@ -70,14 +81,17 @@
@mousemove="onOptionMouseMove($event, getOptionIndex(i, getItemOptions))" @mousemove="onOptionMouseMove($event, getOptionIndex(i, getItemOptions))"
@touchend="onOptionTouchEnd()" @touchend="onOptionTouchEnd()"
v-bind="getPTOptions(option, getItemOptions, i, 'item')" v-bind="getPTOptions(option, getItemOptions, i, 'item')"
:data-p-highlight="isSelected(option)"
:data-p-focused="focusedOptionIndex === getOptionIndex(index, getItemOptions)"
:data-p-disabled="isOptionDisabled(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>
<li v-if="filterValue && (!items || (items && items.length === 0))" class="p-listbox-empty-message" role="option" v-bind="ptm('emptyMessage')"> <li v-if="filterValue && (!items || (items && items.length === 0))" :class="cx('p-listbox-empty-message')" role="option" v-bind="ptm('emptyMessage')">
<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" role="option" v-bind="ptm('emptyMessage')"> <li v-else-if="!options || (options && options.length === 0)" :class="cx('p-listbox-empty-message')" role="option" v-bind="ptm('emptyMessage')">
<slot name="empty">{{ emptyMessageText }}</slot> <slot name="empty">{{ emptyMessageText }}</slot>
</li> </li>
</ul> </ul>
@ -88,102 +102,39 @@
</VirtualScroller> </VirtualScroller>
</div> </div>
<slot name="footer" :value="modelValue" :options="visibleOptions"></slot> <slot name="footer" :value="modelValue" :options="visibleOptions"></slot>
<span v-if="!options || (options && options.length === 0)" role="status" aria-live="polite" class="p-hidden-accessible" v-bind="ptm('emptyMessage')"> <span v-if="!options || (options && options.length === 0)" role="status" aria-live="polite" :class="cx('hiddenEmptyMessage')" :style="sx('hiddenAccessible', isUnstyled)" v-bind="ptm('hiddenEmptyMessage')" :data-p-hidden-accessible="true">
{{ emptyMessageText }} {{ emptyMessageText }}
</span> </span>
<span role="status" aria-live="polite" class="p-hidden-accessible" v-bind="ptm('hiddenSelectedMessage')"> <span role="status" aria-live="polite" :class="cx('hiddenSelectedMessage')" :style="sx('hiddenAccessible', isUnstyled)" v-bind="ptm('hiddenSelectedMessage')" :data-p-hidden-accessible="true">
{{ selectedMessageText }} {{ selectedMessageText }}
</span> </span>
<span ref="lastHiddenFocusableElement" role="presentation" aria-hidden="true" class="p-hidden-accessible p-hidden-focusable" :tabindex="!disabled ? tabindex : -1" @focus="onLastHiddenFocus" v-bind="ptm('hiddenLastFocusableEl')"></span> <span
ref="lastHiddenFocusableElement"
role="presentation"
aria-hidden="true"
:class="cx('hiddenLastFocusableEl')"
:style="sx('hiddenAccessible', isUnstyled)"
:tabindex="!disabled ? tabindex : -1"
@focus="onLastHiddenFocus"
v-bind="ptm('hiddenLastFocusableEl')"
:data-p-hidden-accessible="true"
:data-p-hidden-focusable="true"
></span>
</div> </div>
</template> </template>
<script> <script>
import { FilterService } from 'primevue/api'; import { FilterService } from 'primevue/api';
import BaseComponent from 'primevue/basecomponent';
import SearchIcon from 'primevue/icons/search'; import SearchIcon from 'primevue/icons/search';
import Ripple from 'primevue/ripple'; import Ripple from 'primevue/ripple';
import { DomHandler, ObjectUtils, UniqueComponentId } from 'primevue/utils'; import { DomHandler, ObjectUtils, UniqueComponentId } from 'primevue/utils';
import VirtualScroller from 'primevue/virtualscroller'; import VirtualScroller from 'primevue/virtualscroller';
import BaseListbox from './BaseListbox.vue';
export default { export default {
name: 'Listbox', name: 'Listbox',
extends: BaseComponent, extends: BaseListbox,
emits: ['update:modelValue', 'change', 'focus', 'blur', 'filter'], emits: ['update:modelValue', 'change', 'focus', 'blur', 'filter'],
props: {
modelValue: null,
options: Array,
optionLabel: null,
optionValue: null,
optionDisabled: null,
optionGroupLabel: null,
optionGroupChildren: null,
listStyle: null,
disabled: Boolean,
dataKey: null,
multiple: Boolean,
metaKeySelection: Boolean,
filter: Boolean,
filterPlaceholder: String,
filterLocale: String,
filterMatchMode: {
type: String,
default: 'contains'
},
filterFields: {
type: Array,
default: null
},
filterInputProps: null,
virtualScrollerOptions: {
type: Object,
default: null
},
autoOptionFocus: {
type: Boolean,
default: true
},
selectOnFocus: {
type: Boolean,
default: false
},
filterMessage: {
type: String,
default: null
},
selectionMessage: {
type: String,
default: null
},
emptySelectionMessage: {
type: String,
default: null
},
emptyFilterMessage: {
type: String,
default: null
},
emptyMessage: {
type: String,
default: null
},
filterIcon: {
type: String,
default: undefined
},
tabindex: {
type: Number,
default: 0
},
'aria-label': {
type: String,
default: null
},
'aria-labelledby': {
type: String,
default: null
}
},
list: null, list: null,
virtualScroller: null, virtualScroller: null,
optionTouched: false, optionTouched: false,
@ -712,15 +663,6 @@ export default {
} }
}, },
computed: { computed: {
containerClass() {
return [
'p-listbox p-component',
{
'p-focus': this.focused,
'p-disabled': this.disabled
}
];
},
visibleOptions() { visibleOptions() {
const options = this.optionGroupLabel ? this.flatOptions(this.options) : this.options || []; const options = this.optionGroupLabel ? this.flatOptions(this.options) : this.options || [];
@ -775,39 +717,3 @@ export default {
} }
}; };
</script> </script>
<style>
.p-listbox-list-wrapper {
overflow: auto;
}
.p-listbox-list {
list-style-type: none;
margin: 0;
padding: 0;
}
.p-listbox-item {
cursor: pointer;
position: relative;
overflow: hidden;
}
.p-listbox-item-group {
cursor: auto;
}
.p-listbox-filter-container {
position: relative;
}
.p-listbox-filter-icon {
position: absolute;
top: 50%;
margin-top: -0.5rem;
}
.p-listbox-filter {
width: 100%;
}
</style>