From 4fc6b406a6e499ea7dda27f1436f0280cda5aa95 Mon Sep 17 00:00:00 2001 From: gucal Date: Mon, 18 Mar 2024 10:22:48 +0300 Subject: [PATCH 1/3] #6171 Listbox - focusOnHover prop --- components/lib/listbox/ListBox.js | 98 ++++++++++++++++++++++++--- components/lib/listbox/ListBoxBase.js | 1 + components/lib/listbox/ListBoxItem.js | 2 + 3 files changed, 91 insertions(+), 10 deletions(-) diff --git a/components/lib/listbox/ListBox.js b/components/lib/listbox/ListBox.js index ba20a2f512..0793d04b20 100644 --- a/components/lib/listbox/ListBox.js +++ b/components/lib/listbox/ListBox.js @@ -19,7 +19,9 @@ export const ListBox = React.memo( const firstHiddenFocusableElement = React.useRef(null); const lastHiddenFocusableElement = React.useRef(null); const [startRangeIndex, setStartRangeIndex] = React.useState(-1); + const [focused, setFocused] = React.useState(false); const [filterValueState, setFilterValueState] = React.useState(''); + const [searchValue, setSearchValue] = React.useState(''); const elementRef = React.useRef(null); const virtualScrollerRef = React.useRef(null); const id = React.useRef(null); @@ -49,6 +51,16 @@ export const ListBox = React.memo( index !== -1 && setFocusedOptionIndex(index); }; + const onOptionMouseDown = (event, index) => { + changeFocusedOptionIndex(event, index); + }; + + const onOptionMouseMove = (event, index) => { + if (props.focusOnHover && focused) { + changeFocusedOptionIndex(event, index); + } + }; + const onOptionTouchEnd = () => { if (props.disabled) { return; @@ -150,6 +162,31 @@ export const ListBox = React.memo( return visibleOptions.findIndex((option) => isValidOption(option)); }; + const findLastSelectedOptionIndex = () => { + return hasSelectedOption() ? ObjectUtils.findLastIndex(visibleOptions, (option) => isValidSelectedOption(option)) : -1; + }; + + const findSelectedOptionIndex = () => { + if (hasSelectedOption()) { + if (props.multiple) { + for (let index = props.value.length - 1; index >= 0; index--) { + const value = props.value[index]; + const matchedOptionIndex = visibleOptions.findIndex((option) => isValidSelectedOption(option) && isEquals(value, getOptionValue(option))); + + if (matchedOptionIndex > -1) return matchedOptionIndex; + } + } else { + return visibleOptions.findIndex((option) => isValidSelectedOption(option)); + } + } + + return -1; + }; + + const findFirstSelectedOptionIndex = () => { + return hasSelectedOption() ? visibleOptions.findIndex((option) => isValidSelectedOption(option)) : -1; + }; + const findLastOptionIndex = () => { return ObjectUtils.findLastIndex(visibleOptions, (option) => isValidOption(option)); }; @@ -173,7 +210,7 @@ export const ListBox = React.memo( const findNearestSelectedOptionIndex = (index, firstCheckUp = false) => { let matchedOptionIndex = -1; - if (hasSelectedOption) { + if (hasSelectedOption()) { if (firstCheckUp) { matchedOptionIndex = findPrevSelectedOptionIndex(index); matchedOptionIndex = matchedOptionIndex === -1 ? findNextSelectedOptionIndex(index) : matchedOptionIndex; @@ -191,7 +228,7 @@ export const ListBox = React.memo( }; const searchOptions = (event, char) => { - searchValue = (searchValue || '') + char; + setSearchValue((searchValue || '') + char); let optionIndex = -1; @@ -217,19 +254,19 @@ export const ListBox = React.memo( } searchTimeout.current = setTimeout(() => { - searchValue = ''; + setSearchValue(''); searchTimeout.current = null; }, 500); }; const findNextSelectedOptionIndex = (index) => { - const matchedOptionIndex = hasSelectedOption && index < visibleOptions.length - 1 ? visibleOptions.slice(index + 1).findIndex((option) => isValidSelectedOption(option)) : -1; + const matchedOptionIndex = hasSelectedOption() && index < visibleOptions.length - 1 ? visibleOptions.slice(index + 1).findIndex((option) => isValidSelectedOption(option)) : -1; return matchedOptionIndex > -1 ? matchedOptionIndex + index + 1 : -1; }; const findPrevSelectedOptionIndex = (index) => { - const matchedOptionIndex = hasSelectedOption && index > 0 ? ObjectUtils.findLastIndex(visibleOptions.slice(0, index), (option) => isValidSelectedOption(option)) : -1; + const matchedOptionIndex = hasSelectedOption() && index > 0 ? ObjectUtils.findLastIndex(visibleOptions.slice(0, index), (option) => isValidSelectedOption(option)) : -1; return matchedOptionIndex > -1 ? matchedOptionIndex : -1; }; @@ -256,6 +293,12 @@ export const ListBox = React.memo( return selectedIndex < 0 ? findFirstOptionIndex() : selectedIndex; }; + const findLastFocusedOptionIndex = () => { + const selectedIndex = findLastSelectedOptionIndex(); + + return selectedIndex < 0 ? findLastOptionIndex() : selectedIndex; + }; + const changeFocusedOptionIndex = (event, index) => { if (focusedOptionIndex !== index) { setFocusedOptionIndex(index); @@ -355,7 +398,7 @@ export const ListBox = React.memo( event.preventDefault(); }; - const onKeyDown = (event) => { + const onListKeyDown = (event) => { const metaKey = event.metaKey || event.ctrlKey; switch (event.code) { @@ -426,7 +469,7 @@ export const ListBox = React.memo( if (element) { element.scrollIntoView({ block: 'nearest', inline: 'nearest', behavior: 'smooth' }); } else if (props.virtualScrollerOptions) { - virtualScrollerRef.current && virtualScrollerRef.current.scrollToIndex(index !== -1 ? index : props.focusedOptionIndex); + virtualScrollerRef.current && virtualScrollerRef.current.scrollToIndex(index !== -1 ? index : focusedOptionIndex); } }, 0); }; @@ -451,6 +494,15 @@ export const ListBox = React.memo( props.onFilter && props.onFilter({ filter: '' }); }; + const autoUpdateModel = (isFocus = focused) => { + if (props.selectOnFocus && props.autoOptionFocus && !hasSelectedOption() && !props.multiple && isFocus) { + const currentFocusOptionIndex = findFirstFocusedOptionIndex(); + + onOptionSelect(null, visibleOptions[currentFocusOptionIndex]); + setFocusedOptionIndex(currentFocusOptionIndex); + } + }; + const updateModel = (event, value) => { if (props.onChange) { props.onChange({ @@ -503,11 +555,15 @@ export const ListBox = React.memo( return list.findIndex((item) => ObjectUtils.equals(value, getOptionValue(item), key)); }; + const isEquals = (value1, value2) => { + return ObjectUtils.equals(value1, value2, equalityKey()); + }; + const isSelected = (option) => { const optionValue = getOptionValue(option); - const key = equalityKey(); - return props.multiple && props.value ? props.value.some((val) => ObjectUtils.equals(val, optionValue, key)) : ObjectUtils.equals(props.value, optionValue, key); + if (props.multiple) return (props.value || []).some((value) => isEquals(value, optionValue)); + else return isEquals(props.value, optionValue); }; const filter = (option) => { @@ -562,6 +618,19 @@ export const ListBox = React.memo( lastHiddenFocusableElement.current.tabIndex = -1; }; + const onListFocus = () => { + setFocused(true); + setFocusedOptionIndex(focusedOptionIndex !== -1 ? focusedOptionIndex : props.autoOptionFocus ? findFirstFocusedOptionIndex() : findSelectedOptionIndex()); + autoUpdateModel(true); + }; + + const onListBlur = (event) => { + setFocused(false); + setFocusedOptionIndex(-1); + setStartRangeIndex(-1); + setSearchValue(''); + }; + const getOptionGroupRenderKey = (optionGroup) => { return ObjectUtils.resolveFieldData(optionGroup, props.optionGroupLabel); }; @@ -658,6 +727,8 @@ export const ListBox = React.memo( style={style} template={props.itemTemplate} selected={isSelected(option)} + onOptionMouseDown={onOptionMouseDown} + onOptionMouseMove={onOptionMouseMove} onClick={onOptionSelect} index={j} focusedOptionIndex={focusedOptionIndex} @@ -706,6 +777,8 @@ export const ListBox = React.memo( key={optionKey} label={optionLabel} index={index} + onOptionMouseDown={onOptionMouseDown} + onOptionMouseMove={onOptionMouseMove} focusedOptionIndex={focusedOptionIndex} option={option} style={style} @@ -761,6 +834,9 @@ export const ListBox = React.memo( role: 'listbox', tabIndex: '-1', 'aria-multiselectable': props.multiple, + onFocus: onListFocus, + onBlur: onListBlur, + onKeyDown: onListKeyDown, ...ariaProps }, ptCallbacks.ptm('list') @@ -782,7 +858,9 @@ export const ListBox = React.memo( role: 'listbox', 'aria-multiselectable': props.multiple, tabIndex: '-1', - onKeyDown: onKeyDown, + onFocus: onListFocus, + onBlur: onListBlur, + onKeyDown: onListKeyDown, ...ariaProps }, ptCallbacks.ptm('list') diff --git a/components/lib/listbox/ListBoxBase.js b/components/lib/listbox/ListBoxBase.js index 8fdaf18801..71ea6e4863 100644 --- a/components/lib/listbox/ListBoxBase.js +++ b/components/lib/listbox/ListBoxBase.js @@ -89,6 +89,7 @@ export const ListBoxBase = ComponentBase.extend({ filterTemplate: null, filterValue: null, selectOnFocus: false, + focusOnHover: true, id: null, itemTemplate: null, invalid: false, diff --git a/components/lib/listbox/ListBoxItem.js b/components/lib/listbox/ListBoxItem.js index 2165d21b48..55b7925db7 100644 --- a/components/lib/listbox/ListBoxItem.js +++ b/components/lib/listbox/ListBoxItem.js @@ -63,6 +63,8 @@ export const ListBoxItem = React.memo((props) => { onFocus: onFocus, onBlur: onBlur, tabIndex: '-1', + onMouseDown: (event) => props.onOptionMouseDown(event, props.index), + onMouseMove: (event) => props.onOptionMouseMove(event, props.index), 'aria-label': props.label, key: props.optionKey, role: 'option', From eb3dd8220d5bd3c009b99c9968a194403dbbdd26 Mon Sep 17 00:00:00 2001 From: gucal Date: Mon, 18 Mar 2024 10:23:12 +0300 Subject: [PATCH 2/3] Update listbox d.ts --- components/lib/listbox/listbox.d.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/components/lib/listbox/listbox.d.ts b/components/lib/listbox/listbox.d.ts index 2561c3e440..6f35d5ad4c 100755 --- a/components/lib/listbox/listbox.d.ts +++ b/components/lib/listbox/listbox.d.ts @@ -399,6 +399,21 @@ export interface ListBoxProps extends Omit Date: Mon, 18 Mar 2024 17:22:33 +0300 Subject: [PATCH 3/3] Remove duplicate types --- components/lib/listbox/ListBoxBase.js | 1 - components/lib/listbox/listbox.d.ts | 10 ---------- 2 files changed, 11 deletions(-) diff --git a/components/lib/listbox/ListBoxBase.js b/components/lib/listbox/ListBoxBase.js index 71ea6e4863..6b6c7b2643 100644 --- a/components/lib/listbox/ListBoxBase.js +++ b/components/lib/listbox/ListBoxBase.js @@ -88,7 +88,6 @@ export const ListBoxBase = ComponentBase.extend({ filterPlaceholder: null, filterTemplate: null, filterValue: null, - selectOnFocus: false, focusOnHover: true, id: null, itemTemplate: null, diff --git a/components/lib/listbox/listbox.d.ts b/components/lib/listbox/listbox.d.ts index 6f35d5ad4c..14f79129fe 100755 --- a/components/lib/listbox/listbox.d.ts +++ b/components/lib/listbox/listbox.d.ts @@ -339,16 +339,6 @@ export interface ListBoxProps extends Omit