From 2072631cdad88f161d03858995832c8a8fba6e84 Mon Sep 17 00:00:00 2001 From: mertsincan Date: Tue, 23 Jan 2024 13:08:06 +0000 Subject: [PATCH] Fixed #5130 - Add focusOnHover props to some components --- components/lib/api/FilterService.js | 12 +- components/lib/autocomplete/AutoComplete.d.ts | 5 + components/lib/autocomplete/AutoComplete.vue | 36 +++--- .../lib/autocomplete/BaseAutoComplete.vue | 4 + .../lib/cascadeselect/BaseCascadeSelect.vue | 4 + .../lib/cascadeselect/CascadeSelect.d.ts | 5 + .../lib/cascadeselect/CascadeSelect.vue | 76 ++++++++----- .../lib/cascadeselect/CascadeSelectSub.vue | 11 +- components/lib/dropdown/BaseDropdown.vue | 4 + components/lib/dropdown/Dropdown.d.ts | 5 + components/lib/dropdown/Dropdown.vue | 76 ++++++++----- components/lib/listbox/BaseListbox.vue | 4 + components/lib/listbox/Listbox.d.ts | 5 + components/lib/listbox/Listbox.vue | 70 ++++++++---- .../lib/multiselect/BaseMultiSelect.vue | 4 + components/lib/multiselect/MultiSelect.d.ts | 5 + components/lib/multiselect/MultiSelect.vue | 103 +++++++++++------ .../lib/virtualscroller/VirtualScroller.vue | 104 ++++++++++-------- 18 files changed, 350 insertions(+), 183 deletions(-) diff --git a/components/lib/api/FilterService.js b/components/lib/api/FilterService.js index 1fb8b05cc..534b5491b 100644 --- a/components/lib/api/FilterService.js +++ b/components/lib/api/FilterService.js @@ -30,7 +30,7 @@ const FilterService = { }, filters: { startsWith(value, filter, filterLocale) { - if (filter === undefined || filter === null || filter.trim() === '') { + if (filter === undefined || filter === null || filter === '') { return true; } @@ -44,7 +44,7 @@ const FilterService = { return stringValue.slice(0, filterValue.length) === filterValue; }, contains(value, filter, filterLocale) { - if (filter === undefined || filter === null || (typeof filter === 'string' && filter.trim() === '')) { + if (filter === undefined || filter === null || filter === '') { return true; } @@ -58,7 +58,7 @@ const FilterService = { return stringValue.indexOf(filterValue) !== -1; }, notContains(value, filter, filterLocale) { - if (filter === undefined || filter === null || (typeof filter === 'string' && filter.trim() === '')) { + if (filter === undefined || filter === null || filter === '') { return true; } @@ -72,7 +72,7 @@ const FilterService = { return stringValue.indexOf(filterValue) === -1; }, endsWith(value, filter, filterLocale) { - if (filter === undefined || filter === null || filter.trim() === '') { + if (filter === undefined || filter === null || filter === '') { return true; } @@ -86,7 +86,7 @@ const FilterService = { return stringValue.indexOf(filterValue, stringValue.length - filterValue.length) !== -1; }, equals(value, filter, filterLocale) { - if (filter === undefined || filter === null || (typeof filter === 'string' && filter.trim() === '')) { + if (filter === undefined || filter === null || filter === '') { return true; } @@ -98,7 +98,7 @@ const FilterService = { else return ObjectUtils.removeAccents(value.toString()).toLocaleLowerCase(filterLocale) == ObjectUtils.removeAccents(filter.toString()).toLocaleLowerCase(filterLocale); }, notEquals(value, filter, filterLocale) { - if (filter === undefined || filter === null || (typeof filter === 'string' && filter.trim() === '')) { + if (filter === undefined || filter === null || filter === '') { return false; } diff --git a/components/lib/autocomplete/AutoComplete.d.ts b/components/lib/autocomplete/AutoComplete.d.ts index 14d24a845..5bf2ae40d 100755 --- a/components/lib/autocomplete/AutoComplete.d.ts +++ b/components/lib/autocomplete/AutoComplete.d.ts @@ -447,6 +447,11 @@ export interface AutoCompleteProps { * @defaultValue false */ selectOnFocus?: boolean | undefined; + /** + * When enabled, the focus is placed on the hovered option. + * @defaultValue true + */ + focusOnHover?: boolean | undefined; /** * Locale to use in searching. The default locale is the host environment's current locale. */ diff --git a/components/lib/autocomplete/AutoComplete.vue b/components/lib/autocomplete/AutoComplete.vue index e2e9eceff..f038bedc4 100755 --- a/components/lib/autocomplete/AutoComplete.vue +++ b/components/lib/autocomplete/AutoComplete.vue @@ -197,11 +197,11 @@ export default { overlay: null, virtualScroller: null, searchTimeout: null, - focusOnHover: false, dirty: false, data() { return { id: this.$attrs.id, + clicked: false, focused: false, focusedOptionIndex: -1, focusedMultipleOptionIndex: -1, @@ -298,6 +298,7 @@ export default { this.$emit('before-hide'); this.dirty = isFocus; this.overlayVisible = false; + this.clicked = false; this.focusedOptionIndex = -1; isFocus && DomHandler.focus(this.$refs.focusInput); @@ -319,8 +320,12 @@ export default { this.dirty = true; this.focused = true; - this.focusedOptionIndex = this.focusedOptionIndex !== -1 ? this.focusedOptionIndex : this.overlayVisible && this.autoOptionFocus ? this.findFirstFocusedOptionIndex() : -1; - this.overlayVisible && this.scrollInView(this.focusedOptionIndex); + + if (this.overlayVisible) { + this.focusedOptionIndex = this.focusedOptionIndex !== -1 ? this.focusedOptionIndex : this.overlayVisible && this.autoOptionFocus ? this.findFirstFocusedOptionIndex() : -1; + this.scrollInView(this.focusedOptionIndex); + } + this.$emit('focus', event); }, onBlur(event) { @@ -394,6 +399,8 @@ export default { default: break; } + + this.clicked = false; }, onInput(event) { if (this.searchTimeout) { @@ -479,6 +486,8 @@ export default { } }, onContainerClick(event) { + this.clicked = true; + if (this.disabled || this.searching || this.loading || this.isInputClicked(event) || this.isDropdownClicked(event)) { return; } @@ -545,7 +554,7 @@ export default { return; } - const optionIndex = this.focusedOptionIndex !== -1 ? this.findNextOptionIndex(this.focusedOptionIndex) : this.findFirstFocusedOptionIndex(); + const optionIndex = this.focusedOptionIndex !== -1 ? this.findNextOptionIndex(this.focusedOptionIndex) : this.clicked ? this.findFirstOptionIndex() : this.findFirstFocusedOptionIndex(); this.changeFocusedOptionIndex(event, optionIndex); @@ -564,7 +573,7 @@ export default { this.overlayVisible && this.hide(); event.preventDefault(); } else { - const optionIndex = this.focusedOptionIndex !== -1 ? this.findPrevOptionIndex(this.focusedOptionIndex) : this.findLastFocusedOptionIndex(); + const optionIndex = this.focusedOptionIndex !== -1 ? this.findPrevOptionIndex(this.focusedOptionIndex) : this.clicked ? this.findLastOptionIndex() : this.findLastFocusedOptionIndex(); this.changeFocusedOptionIndex(event, optionIndex); @@ -618,6 +627,7 @@ export default { }, onEnterKey(event) { if (!this.overlayVisible) { + this.focusedOptionIndex = -1; // reset this.onArrowDownKey(event); } else { if (this.focusedOptionIndex !== -1) { @@ -838,16 +848,16 @@ export default { } }, scrollInView(index = -1) { - const id = index !== -1 ? `${this.id}_${index}` : this.focusedOptionId; - const element = DomHandler.findSingle(this.list, `li[id="${id}"]`); + this.$nextTick(() => { + 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: 'start' }); - } else if (!this.virtualScrollerDisabled) { - setTimeout(() => { + if (element) { + element.scrollIntoView && element.scrollIntoView({ block: 'nearest', inline: 'start' }); + } else if (!this.virtualScrollerDisabled) { this.virtualScroller && this.virtualScroller.scrollToIndex(index !== -1 ? index : this.focusedOptionIndex); - }, 0); - } + } + }); }, autoUpdateModel() { if ((this.selectOnFocus || this.autoHighlight) && this.autoOptionFocus && !this.hasSelectedOption) { diff --git a/components/lib/autocomplete/BaseAutoComplete.vue b/components/lib/autocomplete/BaseAutoComplete.vue index 779f5bfb9..df9b9e647 100644 --- a/components/lib/autocomplete/BaseAutoComplete.vue +++ b/components/lib/autocomplete/BaseAutoComplete.vue @@ -133,6 +133,10 @@ export default { type: Boolean, default: false }, + focusOnHover: { + type: Boolean, + default: true + }, searchLocale: { type: String, default: undefined diff --git a/components/lib/cascadeselect/BaseCascadeSelect.vue b/components/lib/cascadeselect/BaseCascadeSelect.vue index 33556e5e7..abd4f72e7 100644 --- a/components/lib/cascadeselect/BaseCascadeSelect.vue +++ b/components/lib/cascadeselect/BaseCascadeSelect.vue @@ -72,6 +72,10 @@ export default { type: Boolean, default: false }, + focusOnHover: { + type: Boolean, + default: true + }, searchLocale: { type: String, default: undefined diff --git a/components/lib/cascadeselect/CascadeSelect.d.ts b/components/lib/cascadeselect/CascadeSelect.d.ts index a565736a7..c7d1ee9e3 100644 --- a/components/lib/cascadeselect/CascadeSelect.d.ts +++ b/components/lib/cascadeselect/CascadeSelect.d.ts @@ -343,6 +343,11 @@ export interface CascadeSelectProps { * @defaultValue false */ selectOnFocus?: boolean | undefined; + /** + * When enabled, the focus is placed on the hovered option. + * @defaultValue true + */ + focusOnHover?: boolean | undefined; /** * Locale to use in searching. The default locale is the host environment's current locale. */ diff --git a/components/lib/cascadeselect/CascadeSelect.vue b/components/lib/cascadeselect/CascadeSelect.vue index 998184732..6126c5435 100644 --- a/components/lib/cascadeselect/CascadeSelect.vue +++ b/components/lib/cascadeselect/CascadeSelect.vue @@ -62,6 +62,7 @@ :optionGroupLabel="optionGroupLabel" :optionGroupChildren="optionGroupChildren" @option-change="onOptionChange" + @option-focus-change="onOptionFocusChange" :pt="pt" :unstyled="unstyled" /> @@ -95,10 +96,10 @@ export default { overlay: null, searchTimeout: null, searchValue: null, - focusOnHover: false, data() { return { id: this.$attrs.id, + clicked: false, focused: false, focusedOptionInfo: { index: -1, level: 0, parentKey: '' }, activeOptionPath: [], @@ -169,9 +170,9 @@ export default { if (this.hasSelectedOption && ObjectUtils.isNotEmpty(this.activeOptionPath)) { const processedOption = this.activeOptionPath[this.activeOptionPath.length - 1]; - this.focusedOptionInfo = { index: this.autoOptionFocus ? processedOption.index : -1, level: processedOption.level, parentKey: processedOption.parentKey }; + this.focusedOptionInfo = { index: processedOption.index, level: processedOption.level, parentKey: processedOption.parentKey }; } else { - this.focusedOptionInfo = { index: this.autoOptionFocus ? this.findFirstFocusedOptionIndex() : -1, level: 0, parentKey: '' }; + this.focusedOptionInfo = { index: this.autoOptionFocus ? this.findFirstFocusedOptionIndex() : this.findSelectedOptionIndex(), level: 0, parentKey: '' }; } isFocus && DomHandler.focus(this.$refs.focusInput); @@ -180,6 +181,7 @@ export default { const _hide = () => { this.$emit('before-hide'); this.overlayVisible = false; + this.clicked = false; this.activeOptionPath = []; this.focusedOptionInfo = { index: -1, level: 0, parentKey: '' }; @@ -272,6 +274,8 @@ export default { break; } + + this.clicked = false; }, onOptionChange(event) { const { originalEvent, processedOption, isFocus, isHide } = event; @@ -285,12 +289,21 @@ export default { activeOptionPath.push(processedOption); - this.focusedOptionInfo = originalEvent?.type === 'click' ? { index: -1, level, parentKey } : { index, level, parentKey }; + this.focusedOptionInfo = { index, level, parentKey }; this.activeOptionPath = activeOptionPath; grouped ? this.onOptionGroupSelect(originalEvent, processedOption) : this.onOptionSelect(originalEvent, processedOption, isHide); isFocus && DomHandler.focus(this.$refs.focusInput); }, + onOptionFocusChange(event) { + if (this.focusOnHover) { + const { originalEvent, processedOption } = event; + const { index, level, parentKey } = processedOption; + + this.focusedOptionInfo = { index, level, parentKey }; + this.changeFocusedOptionIndex(originalEvent, index); + } + }, onOptionSelect(event, processedOption, isHide = true) { const value = this.getOptionValue(processedOption?.option); @@ -312,6 +325,7 @@ export default { DomHandler.focus(this.$refs.focusInput); } + this.clicked = true; this.$emit('click', event); }, onOverlayClick(event) { @@ -331,11 +345,14 @@ export default { } }, onArrowDownKey(event) { - const optionIndex = this.focusedOptionInfo.index !== -1 ? this.findNextOptionIndex(this.focusedOptionInfo.index) : this.findFirstFocusedOptionIndex(); + if (!this.overlayVisible) { + this.show(); + } else { + const optionIndex = this.focusedOptionInfo.index !== -1 ? this.findNextOptionIndex(this.focusedOptionInfo.index) : this.clicked ? this.findFirstOptionIndex() : this.findFirstFocusedOptionIndex(); - this.changeFocusedOptionIndex(event, optionIndex); + this.changeFocusedOptionIndex(event, optionIndex); + } - !this.overlayVisible && this.show(); event.preventDefault(); }, onArrowUpKey(event) { @@ -350,7 +367,7 @@ export default { this.overlayVisible && this.hide(); event.preventDefault(); } else { - const optionIndex = this.focusedOptionInfo.index !== -1 ? this.findPrevOptionIndex(this.focusedOptionInfo.index) : this.findLastFocusedOptionIndex(); + const optionIndex = this.focusedOptionInfo.index !== -1 ? this.findPrevOptionIndex(this.focusedOptionInfo.index) : this.clicked ? this.findLastOptionIndex() : this.findLastFocusedOptionIndex(); this.changeFocusedOptionIndex(event, optionIndex); @@ -412,6 +429,7 @@ export default { }, onEnterKey(event) { if (!this.overlayVisible) { + this.focusedOptionInfo.index !== -1; // reset this.onArrowDownKey(event); } else { if (this.focusedOptionInfo.index !== -1) { @@ -595,23 +613,25 @@ export default { let optionIndex = -1; let matched = false; - if (this.focusedOptionInfo.index !== -1) { - optionIndex = this.visibleOptions.slice(this.focusedOptionInfo.index).findIndex((processedOption) => this.isOptionMatched(processedOption)); - optionIndex = optionIndex === -1 ? this.visibleOptions.slice(0, this.focusedOptionInfo.index).findIndex((processedOption) => this.isOptionMatched(processedOption)) : optionIndex + this.focusedOptionInfo.index; - } else { - optionIndex = this.visibleOptions.findIndex((processedOption) => this.isOptionMatched(processedOption)); - } + if (ObjectUtils.isNotEmpty(this.searchValue)) { + if (this.focusedOptionInfo.index !== -1) { + optionIndex = this.visibleOptions.slice(this.focusedOptionInfo.index).findIndex((processedOption) => this.isOptionMatched(processedOption)); + optionIndex = optionIndex === -1 ? this.visibleOptions.slice(0, this.focusedOptionInfo.index).findIndex((processedOption) => this.isOptionMatched(processedOption)) : optionIndex + this.focusedOptionInfo.index; + } else { + optionIndex = this.visibleOptions.findIndex((processedOption) => this.isOptionMatched(processedOption)); + } - if (optionIndex !== -1) { - matched = true; - } + if (optionIndex !== -1) { + matched = true; + } - if (optionIndex === -1 && this.focusedOptionInfo.index === -1) { - optionIndex = this.findFirstFocusedOptionIndex(); - } + if (optionIndex === -1 && this.focusedOptionInfo.index === -1) { + optionIndex = this.findFirstFocusedOptionIndex(); + } - if (optionIndex !== -1) { - this.changeFocusedOptionIndex(event, optionIndex); + if (optionIndex !== -1) { + this.changeFocusedOptionIndex(event, optionIndex); + } } if (this.searchTimeout) { @@ -636,12 +656,14 @@ export default { } }, scrollInView(index = -1) { - const id = index !== -1 ? `${this.id}_${index}` : this.focusedOptionId; - const element = DomHandler.findSingle(this.list, `li[id="${id}"]`); + this.$nextTick(() => { + 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: 'start' }); - } + if (element) { + element.scrollIntoView && element.scrollIntoView({ block: 'nearest', inline: 'start' }); + } + }); }, autoUpdateModel() { if (this.selectOnFocus && this.autoOptionFocus && !this.hasSelectedOption) { diff --git a/components/lib/cascadeselect/CascadeSelectSub.vue b/components/lib/cascadeselect/CascadeSelectSub.vue index c4904d45b..917abb5a1 100644 --- a/components/lib/cascadeselect/CascadeSelectSub.vue +++ b/components/lib/cascadeselect/CascadeSelectSub.vue @@ -17,7 +17,7 @@ :data-p-focus="isOptionFocused(processedOption)" :data-p-disabled="isOptionDisabled(processedOption)" > -
+
{{ getOptionLabelToRender(processedOption) }}