diff --git a/docusaurus/pages/useMultipleCombobox.js b/docusaurus/pages/useMultipleCombobox.js index 14f791bc..d33c7949 100644 --- a/docusaurus/pages/useMultipleCombobox.js +++ b/docusaurus/pages/useMultipleCombobox.js @@ -125,7 +125,7 @@ export default function DropdownMultipleCombobox() { style={{padding: '4px', cursor: 'pointer'}} onClick={e => { e.stopPropagation() - removeSelectedItem(null) + removeSelectedItem(selectedItemForRender) }} > ✕ diff --git a/src/hooks/__tests__/utils.test.js b/src/hooks/__tests__/utils.test.js index 7ef5bc55..5fb8544b 100644 --- a/src/hooks/__tests__/utils.test.js +++ b/src/hooks/__tests__/utils.test.js @@ -5,6 +5,7 @@ import { getDefaultValue, useMouseAndTouchTracker, getItemAndIndex, + isDropdownsStateEqual, } from '../utils' describe('utils', () => { @@ -152,4 +153,37 @@ describe('utils', () => { }) }) }) + + describe('isDropdownsStateEqual', () => { + test('is true when each property is equal', () => { + const selectedItem = 'hello' + const prevState = { + highlightedIndex: 2, + isOpen: true, + selectedItem, + inputValue: selectedItem, + } + const newState = { + ...prevState, + } + + expect(isDropdownsStateEqual(prevState, newState)).toBe(true) + }) + + test('is false when at least one property is not equal', () => { + const selectedItem = {value: 'hello'} + const prevState = { + highlightedIndex: 2, + isOpen: true, + selectedItem, + inputValue: selectedItem, + } + const newState = { + ...prevState, + selectedItem: {...selectedItem}, + } + + expect(isDropdownsStateEqual(prevState, newState)).toBe(false) + }) + }) }) diff --git a/src/hooks/useCombobox/index.js b/src/hooks/useCombobox/index.js index 04bd6048..a1d4c82d 100644 --- a/src/hooks/useCombobox/index.js +++ b/src/hooks/useCombobox/index.js @@ -11,6 +11,7 @@ import { useElementIds, getItemAndIndex, getInitialValue, + isDropdownsStateEqual } from '../utils' import { getInitialState, @@ -43,6 +44,7 @@ function useCombobox(userProps = {}) { downshiftUseComboboxReducer, props, getInitialState, + isDropdownsStateEqual ) const {isOpen, highlightedIndex, selectedItem, inputValue} = state diff --git a/src/hooks/useCombobox/utils.js b/src/hooks/useCombobox/utils.js index 6f41c205..ab75e226 100644 --- a/src/hooks/useCombobox/utils.js +++ b/src/hooks/useCombobox/utils.js @@ -58,14 +58,16 @@ const propTypes = { * @param {Function} reducer Reducer function from downshift. * @param {Object} props The hook props, also passed to createInitialState. * @param {Function} createInitialState Function that returns the initial state. + * @param {Function} isStateEqual Function that checks if a previous state is equal to the next. * @returns {Array} An array with the state and an action dispatcher. */ -export function useControlledReducer(reducer, props, createInitialState) { +export function useControlledReducer(reducer, props, createInitialState, isStateEqual) { const previousSelectedItemRef = useRef() const [state, dispatch] = useEnhancedReducer( reducer, props, createInitialState, + isStateEqual ) // ToDo: if needed, make same approach as selectedItemChanged from Downshift. diff --git a/src/hooks/useMultipleSelection/index.js b/src/hooks/useMultipleSelection/index.js index 87b32002..42fdbcc4 100644 --- a/src/hooks/useMultipleSelection/index.js +++ b/src/hooks/useMultipleSelection/index.js @@ -14,6 +14,7 @@ import { defaultProps, isKeyDownOperationPermitted, validatePropTypes, + isStateEqual } from './utils' import downshiftMultipleSelectionReducer from './reducer' import * as stateChangeTypes from './stateChangeTypes' @@ -40,6 +41,7 @@ function useMultipleSelection(userProps = {}) { downshiftMultipleSelectionReducer, props, getInitialState, + isStateEqual ) const {activeIndex, selectedItems} = state diff --git a/src/hooks/useMultipleSelection/utils.js b/src/hooks/useMultipleSelection/utils.js index 8988d91d..13affd02 100644 --- a/src/hooks/useMultipleSelection/utils.js +++ b/src/hooks/useMultipleSelection/utils.js @@ -95,6 +95,21 @@ function getA11yRemovalMessage(selectionParameters) { return `${itemToStringLocal(removedSelectedItem)} has been removed.` } +/** + * Check if a state is equal for taglist, by comparing active index and selected items. + * Used by useSelect and useCombobox. + * + * @param {Object} prevState + * @param {Object} newState + * @returns {boolean} Wheather the states are deeply equal. + */ +function isStateEqual(prevState, newState) { + return ( + prevState.selectedItems === newState.selectedItems && + prevState.activeIndex === newState.activeIndex + ) +} + const propTypes = { ...commonPropTypes, selectedItems: PropTypes.array, @@ -133,4 +148,5 @@ export { getDefaultValue, getInitialState, isKeyDownOperationPermitted, + isStateEqual, } diff --git a/src/hooks/useSelect/index.js b/src/hooks/useSelect/index.js index 506ec613..81d30994 100644 --- a/src/hooks/useSelect/index.js +++ b/src/hooks/useSelect/index.js @@ -12,6 +12,7 @@ import { useMouseAndTouchTracker, getItemAndIndex, getInitialValue, + isDropdownsStateEqual, } from '../utils' import { callAllEventHandlers, @@ -46,9 +47,9 @@ function useSelect(userProps = {}) { downshiftSelectReducer, props, getInitialState, + isDropdownsStateEqual, ) const {isOpen, highlightedIndex, selectedItem, inputValue} = state - // Element efs. const toggleButtonRef = useRef(null) const menuRef = useRef(null) diff --git a/src/hooks/utils.js b/src/hooks/utils.js index 3c6165c9..ea97d3f3 100644 --- a/src/hooks/utils.js +++ b/src/hooks/utils.js @@ -189,9 +189,10 @@ function useLatestRef(val) { * @param {Function} reducer Reducer function from downshift. * @param {Object} props The hook props, also passed to createInitialState. * @param {Function} createInitialState Function that returns the initial state. + * @param {Function} isStateEqual Function that checks if a previous state is equal to the next. * @returns {Array} An array with the state and an action dispatcher. */ -function useEnhancedReducer(reducer, props, createInitialState) { +function useEnhancedReducer(reducer, props, createInitialState, isStateEqual) { const prevStateRef = useRef() const actionRef = useRef() const enhancedReducer = useCallback( @@ -219,7 +220,12 @@ function useEnhancedReducer(reducer, props, createInitialState) { const action = actionRef.current useEffect(() => { - if (action && prevStateRef.current && prevStateRef.current !== state) { + const shouldCallOnChangeProps = + action && + prevStateRef.current && + !isStateEqual(prevStateRef.current, state) + + if (shouldCallOnChangeProps) { callOnChangeProps( action, getState(prevStateRef.current, action.props), @@ -228,7 +234,7 @@ function useEnhancedReducer(reducer, props, createInitialState) { } prevStateRef.current = state - }, [state, props, action]) + }, [state, action, isStateEqual]) return [state, dispatchWithProps] } @@ -240,13 +246,20 @@ function useEnhancedReducer(reducer, props, createInitialState) { * @param {Function} reducer Reducer function from downshift. * @param {Object} props The hook props, also passed to createInitialState. * @param {Function} createInitialState Function that returns the initial state. + * @param {Function} isStateEqual Function that checks if a previous state is equal to the next. * @returns {Array} An array with the state and an action dispatcher. */ -function useControlledReducer(reducer, props, createInitialState) { +function useControlledReducer( + reducer, + props, + createInitialState, + isStateEqual, +) { const [state, dispatch] = useEnhancedReducer( reducer, props, createInitialState, + isStateEqual, ) return [getState(state, props), dispatch] @@ -585,6 +598,23 @@ function getChangesOnSelection(props, highlightedIndex, inputValue = true) { } } +/** + * Check if a state is equal for dropdowns, by comparing isOpen, inputValue, highlightedIndex and selected item. + * Used by useSelect and useCombobox. + * + * @param {Object} prevState + * @param {Object} newState + * @returns {boolean} Wheather the states are deeply equal. + */ +function isDropdownsStateEqual(prevState, newState) { + return ( + prevState.isOpen === newState.isOpen && + prevState.inputValue === newState.inputValue && + prevState.highlightedIndex === newState.highlightedIndex && + prevState.selectedItem === newState.selectedItem + ) +} + // Shared between all exports. const commonPropTypes = { environment: PropTypes.shape({ @@ -646,6 +676,7 @@ export { getItemAndIndex, useElementIds, getChangesOnSelection, + isDropdownsStateEqual, commonDropdownPropTypes, commonPropTypes, }