Refactor #5426 - For Autocomplete

pull/5507/head
tugcekucukoglu 2024-03-18 13:01:24 +03:00
parent 770c8d4271
commit 52813c0848
3 changed files with 72 additions and 45 deletions

View File

@ -9,9 +9,9 @@
*/ */
import { HTMLAttributes, InputHTMLAttributes, TransitionProps, VNode } from 'vue'; import { HTMLAttributes, InputHTMLAttributes, TransitionProps, VNode } from 'vue';
import { ComponentHooks } from '../basecomponent'; import { ComponentHooks } from '../basecomponent';
import { ButtonPassThroughOptions } from '../button'; import { InputTextPassThroughOptions } from '../inputtext';
import { PassThroughOptions } from '../passthrough'; import { PassThroughOptions } from '../passthrough';
import { ClassComponent, GlobalComponentConstructor, PassThrough, HintedString } from '../ts-helpers'; import { ClassComponent, GlobalComponentConstructor, HintedString, Nullable, PassThrough } from '../ts-helpers';
import { VirtualScrollerItemOptions, VirtualScrollerPassThroughOptionType, VirtualScrollerProps } from '../virtualscroller'; import { VirtualScrollerItemOptions, VirtualScrollerPassThroughOptionType, VirtualScrollerProps } from '../virtualscroller';
export declare type AutoCompletePassThroughOptionType = AutoCompletePassThroughAttributes | ((options: AutoCompletePassThroughMethodOptions) => AutoCompletePassThroughAttributes | string) | string | null | undefined; export declare type AutoCompletePassThroughOptionType = AutoCompletePassThroughAttributes | ((options: AutoCompletePassThroughMethodOptions) => AutoCompletePassThroughAttributes | string) | string | null | undefined;
@ -143,9 +143,10 @@ export interface AutoCompletePassThroughOptions {
*/ */
root?: AutoCompletePassThroughOptionType; root?: AutoCompletePassThroughOptionType;
/** /**
* Used to pass attributes to the input's DOM element. * Used to pass attributes to the InputText component.
* @see {@link InputTextPassThroughOptions}
*/ */
input?: AutoCompletePassThroughOptionType; input?: InputTextPassThroughOptions<AutoCompleteSharedPassThroughMethodOptions>;
/** /**
* Used to pass attributes to the container's DOM element. * Used to pass attributes to the container's DOM element.
*/ */
@ -171,10 +172,9 @@ export interface AutoCompletePassThroughOptions {
*/ */
loadingIcon?: AutoCompletePassThroughOptionType; loadingIcon?: AutoCompletePassThroughOptionType;
/** /**
* Used to pass attributes to the Button component. * Used to pass attributes to the dropdown button component.
* @see {@link ButtonPassThroughOptions}
*/ */
dropdownButton?: ButtonPassThroughOptions<AutoCompleteSharedPassThroughMethodOptions>; dropdownButton?: AutoCompletePassThroughOptionType;
/** /**
* Used to pass attributes to the panel's DOM element. * Used to pass attributes to the panel's DOM element.
*/ */
@ -241,12 +241,12 @@ export interface AutoCompleteState {
focused: boolean; focused: boolean;
/** /**
* Current focused item index as a number. * Current focused item index as a number.
* @defaultvalue -1 * @defaultValue -1
*/ */
focusedOptionIndex: number; focusedOptionIndex: number;
/** /**
* Current focused item index as a number. * Current focused item index as a number.
* @defaultvalue -1 * @defaultValue -1
*/ */
focusedMultipleOptionIndex: number; focusedMultipleOptionIndex: number;
/** /**
@ -259,6 +259,11 @@ export interface AutoCompleteState {
* @defaultValue false * @defaultValue false
*/ */
searching: boolean; searching: boolean;
/**
* Value of the token input as a string.
* @defaultValue null
*/
multipleInputValue: Nullable<string>;
} }
/** /**
@ -687,6 +692,16 @@ export interface AutoCompleteSlots {
*/ */
class: string; class: string;
}): VNode[]; }): VNode[];
/**
* Custom dropdown button template.
*/
dropdownbutton(scope: {
/**
* Toggle function.
* @param {Event} event - Browser event
*/
toggleCallback: (event: Event) => void;
}): VNode[];
} }
/** /**

View File

@ -1,6 +1,6 @@
<template> <template>
<div ref="container" :class="cx('root')" :style="sx('root')" @click="onContainerClick" v-bind="ptmi('root')"> <div ref="container" :class="cx('root')" :style="sx('root')" @click="onContainerClick" v-bind="ptmi('root')">
<input <ACInputText
v-if="!multiple" v-if="!multiple"
ref="focusInput" ref="focusInput"
:id="inputId" :id="inputId"
@ -11,6 +11,8 @@
:placeholder="placeholder" :placeholder="placeholder"
:tabindex="!disabled ? tabindex : -1" :tabindex="!disabled ? tabindex : -1"
:disabled="disabled" :disabled="disabled"
:invalid="invalid"
:variant="variant"
autocomplete="off" autocomplete="off"
role="combobox" role="combobox"
:aria-label="ariaLabel" :aria-label="ariaLabel"
@ -20,13 +22,14 @@
:aria-expanded="overlayVisible" :aria-expanded="overlayVisible"
:aria-controls="id + '_list'" :aria-controls="id + '_list'"
:aria-activedescendant="focused ? focusedOptionId : undefined" :aria-activedescendant="focused ? focusedOptionId : undefined"
:aria-invalid="invalid || undefined"
@focus="onFocus" @focus="onFocus"
@blur="onBlur" @blur="onBlur"
@keydown="onKeyDown" @keydown="onKeyDown"
@input="onInput" @input="onInput"
@change="onChange" @change="onChange"
v-bind="{ ...inputProps, ...ptm('input') }" :unstyled="unstyled"
v-bind="inputProps"
:pt="ptm('input')"
/> />
<ul <ul
v-if="multiple" v-if="multiple"
@ -61,15 +64,18 @@
</slot> </slot>
</li> </li>
<li :class="cx('inputToken')" role="option" v-bind="ptm('inputToken')"> <li :class="cx('inputToken')" role="option" v-bind="ptm('inputToken')">
<input <ACInputText
ref="focusInput" ref="focusInput"
:id="inputId" :id="inputId"
type="text"
:style="inputStyle" :style="inputStyle"
:class="inputClass" :class="inputClass"
:value="multipleInputValue"
:placeholder="placeholder" :placeholder="placeholder"
:tabindex="!disabled ? tabindex : -1" :tabindex="!disabled ? tabindex : -1"
:disabled="disabled" :disabled="disabled"
:invalid="invalid"
:variant="variant"
:unstyled="unstyled"
autocomplete="off" autocomplete="off"
role="combobox" role="combobox"
:aria-label="ariaLabel" :aria-label="ariaLabel"
@ -79,13 +85,13 @@
:aria-expanded="overlayVisible" :aria-expanded="overlayVisible"
:aria-controls="id + '_list'" :aria-controls="id + '_list'"
:aria-activedescendant="focused ? focusedOptionId : undefined" :aria-activedescendant="focused ? focusedOptionId : undefined"
:aria-invalid="invalid || undefined"
@focus="onFocus" @focus="onFocus"
@blur="onBlur" @blur="onBlur"
@keydown="onKeyDown" @keydown="onKeyDown"
@input="onInput" @input="onInput"
@change="onChange" @change="onChange"
v-bind="{ ...inputProps, ...ptm('input') }" v-bind="inputProps"
:pt="ptm('input')"
/> />
</li> </li>
</ul> </ul>
@ -93,13 +99,13 @@
<i v-if="loadingIcon" :class="['pi-spin', cx('loadingIcon'), loadingIcon]" aria-hidden="true" v-bind="ptm('loadingIcon')" /> <i v-if="loadingIcon" :class="['pi-spin', cx('loadingIcon'), loadingIcon]" aria-hidden="true" v-bind="ptm('loadingIcon')" />
<SpinnerIcon v-else :class="[cx('loadingIcon'), loadingIcon]" spin aria-hidden="true" v-bind="ptm('loadingIcon')" /> <SpinnerIcon v-else :class="[cx('loadingIcon'), loadingIcon]" spin aria-hidden="true" v-bind="ptm('loadingIcon')" />
</slot> </slot>
<Button v-if="dropdown" ref="dropdownButton" type="button" tabindex="-1" :class="[cx('dropdownButton'), dropdownClass]" :disabled="disabled" aria-hidden="true" @click="onDropdownClick" :unstyled="unstyled" :pt="ptm('dropdownButton')"> <slot name="dropdownbutton" :toggleCallback="(event) => onDropdownClick(event)">
<template #icon> <button v-if="dropdown" ref="dropdownButton" type="button" tabindex="-1" :class="[cx('dropdownButton'), dropdownClass]" :disabled="disabled" aria-hidden="true" @click="onDropdownClick" v-bind="ptm('dropdownButton')">
<slot name="dropdownicon" :class="dropdownIcon"> <slot name="dropdownicon" :class="dropdownIcon">
<component :is="dropdownIcon ? 'span' : 'ChevronDownIcon'" :class="dropdownIcon" v-bind="ptm('dropdownButton')['icon']" data-pc-section="dropdownicon" /> <component :is="dropdownIcon ? 'span' : 'ChevronDownIcon'" :class="dropdownIcon" v-bind="ptm('dropdownButtonIcon')" />
</slot> </slot>
</template> </button>
</Button> </slot>
<span role="status" aria-live="polite" class="p-hidden-accessible" v-bind="ptm('hiddenSearchResult')" :data-p-hidden-accessible="true"> <span role="status" aria-live="polite" class="p-hidden-accessible" v-bind="ptm('hiddenSearchResult')" :data-p-hidden-accessible="true">
{{ searchResultMessageText }} {{ searchResultMessageText }}
</span> </span>
@ -166,10 +172,10 @@
</template> </template>
<script> <script>
import Button from 'primevue/button';
import ChevronDownIcon from 'primevue/icons/chevrondown'; import ChevronDownIcon from 'primevue/icons/chevrondown';
import SpinnerIcon from 'primevue/icons/spinner'; import SpinnerIcon from 'primevue/icons/spinner';
import TimesCircleIcon from 'primevue/icons/timescircle'; import TimesCircleIcon from 'primevue/icons/timescircle';
import InputText from 'primevue/inputtext';
import OverlayEventBus from 'primevue/overlayeventbus'; import OverlayEventBus from 'primevue/overlayeventbus';
import Portal from 'primevue/portal'; import Portal from 'primevue/portal';
import Ripple from 'primevue/ripple'; import Ripple from 'primevue/ripple';
@ -197,7 +203,8 @@ export default {
focusedOptionIndex: -1, focusedOptionIndex: -1,
focusedMultipleOptionIndex: -1, focusedMultipleOptionIndex: -1,
overlayVisible: false, overlayVisible: false,
searching: false searching: false,
multipleInputValue: null
}; };
}, },
watch: { watch: {
@ -279,8 +286,7 @@ export default {
this.dirty = true; this.dirty = true;
this.overlayVisible = true; this.overlayVisible = true;
this.focusedOptionIndex = this.focusedOptionIndex !== -1 ? this.focusedOptionIndex : this.autoOptionFocus ? this.findFirstFocusedOptionIndex() : -1; this.focusedOptionIndex = this.focusedOptionIndex !== -1 ? this.focusedOptionIndex : this.autoOptionFocus ? this.findFirstFocusedOptionIndex() : -1;
isFocus && DomHandler.focus(this.$refs.focusInput.$el);
isFocus && DomHandler.focus(this.$refs.focusInput);
}, },
hide(isFocus) { hide(isFocus) {
const _hide = () => { const _hide = () => {
@ -290,7 +296,7 @@ export default {
this.clicked = false; this.clicked = false;
this.focusedOptionIndex = -1; this.focusedOptionIndex = -1;
isFocus && DomHandler.focus(this.$refs.focusInput); isFocus && DomHandler.focus(this.$refs.focusInput.$el);
}; };
setTimeout(() => { setTimeout(() => {
@ -400,6 +406,8 @@ export default {
if (!this.multiple) { if (!this.multiple) {
this.updateModel(event, query); this.updateModel(event, query);
} else {
this.multipleInputValue = query;
} }
if (query.length === 0) { if (query.length === 0) {
@ -423,7 +431,7 @@ export default {
// when forceSelection is on, prevent called twice onOptionSelect() // when forceSelection is on, prevent called twice onOptionSelect()
if (this.visibleOptions && !this.multiple) { if (this.visibleOptions && !this.multiple) {
const matchedValue = this.visibleOptions.find((option) => this.isOptionMatched(option, this.$refs.focusInput.value || '')); const matchedValue = this.visibleOptions.find((option) => this.isOptionMatched(option, this.$refs.focusInput.$el.value || ''));
if (matchedValue !== undefined) { if (matchedValue !== undefined) {
valid = true; valid = true;
@ -432,7 +440,7 @@ export default {
} }
if (!valid) { if (!valid) {
this.$refs.focusInput.value = ''; this.$refs.focusInput.$el.value = '';
this.$emit('clear'); this.$emit('clear');
!this.multiple && this.updateModel(event, null); !this.multiple && this.updateModel(event, null);
} }
@ -482,7 +490,7 @@ export default {
} }
if (!this.overlay || !this.overlay.contains(event.target)) { if (!this.overlay || !this.overlay.contains(event.target)) {
DomHandler.focus(this.$refs.focusInput); DomHandler.focus(this.$refs.focusInput.$el);
} }
}, },
onDropdownClick(event) { onDropdownClick(event) {
@ -491,8 +499,8 @@ export default {
if (this.overlayVisible) { if (this.overlayVisible) {
this.hide(true); this.hide(true);
} else { } else {
DomHandler.focus(this.$refs.focusInput); DomHandler.focus(this.$refs.focusInput.$el);
query = this.$refs.focusInput.value; query = this.$refs.focusInput.$el.value;
if (this.dropdownMode === 'blank') this.search(event, '', 'dropdown'); if (this.dropdownMode === 'blank') this.search(event, '', 'dropdown');
else if (this.dropdownMode === 'current') this.search(event, query, 'dropdown'); else if (this.dropdownMode === 'current') this.search(event, query, 'dropdown');
@ -504,7 +512,7 @@ export default {
const value = this.getOptionValue(option); const value = this.getOptionValue(option);
if (this.multiple) { if (this.multiple) {
this.$refs.focusInput.value = ''; this.multipleInputValue = null;
if (!this.isSelected(option)) { if (!this.isSelected(option)) {
this.updateModel(event, [...(this.modelValue || []), value]); this.updateModel(event, [...(this.modelValue || []), value]);
@ -641,7 +649,7 @@ export default {
}, },
onBackspaceKey(event) { onBackspaceKey(event) {
if (this.multiple) { if (this.multiple) {
if (ObjectUtils.isNotEmpty(this.modelValue) && !this.$refs.focusInput.value) { if (ObjectUtils.isNotEmpty(this.modelValue) && !this.$refs.focusInput.$el.value) {
const removedValue = this.modelValue[this.modelValue.length - 1]; const removedValue = this.modelValue[this.modelValue.length - 1];
const newValue = this.modelValue.slice(0, -1); const newValue = this.modelValue.slice(0, -1);
@ -660,7 +668,7 @@ export default {
if (this.focusedMultipleOptionIndex > this.modelValue.length - 1) { if (this.focusedMultipleOptionIndex > this.modelValue.length - 1) {
this.focusedMultipleOptionIndex = -1; this.focusedMultipleOptionIndex = -1;
DomHandler.focus(this.$refs.focusInput); DomHandler.focus(this.$refs.focusInput.$el);
} }
}, },
onBackspaceKeyOnMultiple(event) { onBackspaceKeyOnMultiple(event) {
@ -693,7 +701,7 @@ export default {
ZIndexUtils.clear(el); ZIndexUtils.clear(el);
}, },
alignOverlay() { alignOverlay() {
let target = this.multiple ? this.$refs.multiContainer : this.$refs.focusInput; let target = this.multiple ? this.$refs.multiContainer : this.$refs.focusInput.$el;
if (this.appendTo === 'self') { if (this.appendTo === 'self') {
DomHandler.relativePosition(this.overlay, target); DomHandler.relativePosition(this.overlay, target);
@ -757,10 +765,10 @@ export default {
}, },
isInputClicked(event) { isInputClicked(event) {
if (this.multiple) return event.target === this.$refs.multiContainer || this.$refs.multiContainer.contains(event.target); if (this.multiple) return event.target === this.$refs.multiContainer || this.$refs.multiContainer.contains(event.target);
else return event.target === this.$refs.focusInput; else return event.target === this.$refs.focusInput.$el;
}, },
isDropdownClicked(event) { isDropdownClicked(event) {
return this.$refs.dropdownButton ? event.target === this.$refs.dropdownButton || this.$refs.dropdownButton.$el.contains(event.target) : false; return this.$refs.dropdownButton ? event.target === this.$refs.dropdownButton || this.$refs.dropdownButton.contains(event.target) : false;
}, },
isOptionMatched(option, value) { isOptionMatched(option, value) {
return this.isValidOption(option) && this.getOptionLabel(option)?.toLocaleLowerCase(this.searchLocale) === value.toLocaleLowerCase(this.searchLocale); return this.isValidOption(option) && this.getOptionLabel(option)?.toLocaleLowerCase(this.searchLocale) === value.toLocaleLowerCase(this.searchLocale);
@ -824,7 +832,7 @@ export default {
this.updateModel(event, value); this.updateModel(event, value);
this.$emit('item-unselect', { originalEvent: event, value: removedOption }); this.$emit('item-unselect', { originalEvent: event, value: removedOption });
this.dirty = true; this.dirty = true;
DomHandler.focus(this.$refs.focusInput); DomHandler.focus(this.$refs.focusInput.$el);
}, },
changeFocusedOptionIndex(event, index) { changeFocusedOptionIndex(event, index) {
if (this.focusedOptionIndex !== index) { if (this.focusedOptionIndex !== index) {
@ -935,7 +943,7 @@ export default {
} }
}, },
components: { components: {
Button: Button, ACInputText: InputText,
VirtualScroller: VirtualScroller, VirtualScroller: VirtualScroller,
Portal: Portal, Portal: Portal,
ChevronDownIcon: ChevronDownIcon, ChevronDownIcon: ChevronDownIcon,

View File

@ -19,20 +19,24 @@ const classes = {
'p-overlay-open': instance.overlayVisible 'p-overlay-open': instance.overlayVisible
} }
], ],
input: ({ props, instance }) => [ input: ({ props }) => [
'p-autocomplete-input p-inputtext p-component', 'p-autocomplete-input',
{ {
'p-autocomplete-dd-input': props.dropdown, 'p-autocomplete-dd-input': props.dropdown
'p-variant-filled': props.variant ? props.variant === 'filled' : instance.$primevue.config.inputStyle === 'filled'
} }
], ],
container: ({ props, instance }) => [ container: ({ props, instance }) => [
'p-autocomplete-multiple-container p-component p-inputtext', 'p-autocomplete-multiple-container',
{ {
'p-variant-filled': props.variant ? props.variant === 'filled' : instance.$primevue.config.inputStyle === 'filled' 'p-variant-filled': props.variant ? props.variant === 'filled' : instance.$primevue.config.inputStyle === 'filled'
} }
], ],
token: ({ instance, i }) => ['p-autocomplete-token', { 'p-focus': instance.focusedMultipleOptionIndex === i }], token: ({ instance, i }) => [
'p-autocomplete-token',
{
'p-focus': instance.focusedMultipleOptionIndex === i
}
],
tokenLabel: 'p-autocomplete-token-label', tokenLabel: 'p-autocomplete-token-label',
removeTokenIcon: 'p-autocomplete-token-icon', removeTokenIcon: 'p-autocomplete-token-icon',
inputToken: 'p-autocomplete-input-token', inputToken: 'p-autocomplete-input-token',