Skip to content

Commit

Permalink
feat(useCombobox): selectedItemChanged support (#1480)
Browse files Browse the repository at this point in the history
  • Loading branch information
silviuaavram committed Mar 10, 2023
1 parent 108c1ba commit 9b3199a
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 12 deletions.
8 changes: 8 additions & 0 deletions src/hooks/useCombobox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,14 @@ reset or when an item is selected.
Pass a string that sets the content of the input when downshift is reset or when
an item is selected.

### selectedItemChanged

> `function(prevItem: any, item: any)` | defaults to:
> `(prevItem, item) => (prevItem !== item)`
Used to determine if the new `selectedItem` has changed compared to the previous
`selectedItem` and properly update Downshift's internal state.

### getA11yStatusMessage

> `function({/* see below */})` | default messages provided in English
Expand Down
105 changes: 105 additions & 0 deletions src/hooks/useCombobox/__tests__/props.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,111 @@ describe('props', () => {
})
})

describe('selectedItemChanged', () => {
test('props update of selectedItem will update inputValue state with default selectedItemChanged referential equality check', () => {
const selectedItem = {id: 1, value: 'wow'}
const newSelectedItem = {id: 1, value: 'not wow'}
function itemToString(item) {
return item.value
}
const stateReducer = jest
.fn()
.mockImplementation((_state, {changes}) => changes)

const {rerender} = renderCombobox({
stateReducer,
itemToString,
selectedItem,
})

expect(stateReducer).toHaveBeenCalledTimes(1)
expect(stateReducer).toHaveBeenCalledWith(
{
inputValue: itemToString(selectedItem),
selectedItem,
highlightedIndex: -1,
isOpen: false,
},
expect.objectContaining({
type: useCombobox.stateChangeTypes.ControlledPropUpdatedSelectedItem,
changes: {
inputValue: itemToString(selectedItem),
selectedItem,
highlightedIndex: -1,
isOpen: false,
},
}),
)

stateReducer.mockClear()
rerender({
stateReducer,
selectedItem: newSelectedItem,
itemToString
})

expect(stateReducer).toHaveBeenCalledTimes(1)
expect(stateReducer).toHaveBeenCalledWith(
{
inputValue: itemToString(selectedItem),
selectedItem: newSelectedItem,
highlightedIndex: -1,
isOpen: false,
},
expect.objectContaining({
changes: {
inputValue: itemToString(newSelectedItem),
selectedItem: newSelectedItem,
highlightedIndex: -1,
isOpen: false,
},
type: useCombobox.stateChangeTypes.ControlledPropUpdatedSelectedItem,
}),
)
expect(getInput()).toHaveValue(itemToString(newSelectedItem))
})

test('props update of selectedItem will not update inputValue state', () => {
const selectedItem = {id: 1, value: 'wow'}
const newSelectedItem = {id: 1, value: 'not wow'}
function itemToString(item) {
return item.value
}
const selectedItemChanged = jest.fn().mockReturnValue((prev, next) => prev.id !== next.id)
const stateReducer = jest
.fn()
.mockImplementation((_state, {changes}) => changes)

const {rerender} = renderCombobox({
selectedItemChanged,
stateReducer,
selectedItem,
itemToString
})

expect(getInput()).toHaveValue(itemToString(selectedItem))
expect(selectedItemChanged).toHaveBeenCalledTimes(1)
expect(selectedItemChanged).toHaveBeenCalledWith(undefined, selectedItem)

stateReducer.mockReset()
selectedItemChanged.mockReset()
rerender({
stateReducer,
itemToString,
selectedItem: newSelectedItem,
selectedItemChanged,
})

expect(selectedItemChanged).toHaveBeenCalledTimes(1)
expect(selectedItemChanged).toHaveBeenCalledWith(
selectedItem,
newSelectedItem,
)
expect(stateReducer).not.toHaveBeenCalled()
expect(getInput()).toHaveValue(itemToString(selectedItem))
})
})

describe('getA11ySelectionMessage', () => {
beforeEach(jest.useFakeTimers)
afterEach(() => {
Expand Down
34 changes: 22 additions & 12 deletions src/hooks/useCombobox/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export function getInitialState(props) {
const propTypes = {
items: PropTypes.array.isRequired,
itemToString: PropTypes.func,
selectedItemChanged: PropTypes.func,
getA11yStatusMessage: PropTypes.func,
getA11ySelectionMessage: PropTypes.func,
highlightedIndex: PropTypes.number,
Expand Down Expand Up @@ -92,20 +93,28 @@ export function useControlledReducer(reducer, initialState, props) {

// ToDo: if needed, make same approach as selectedItemChanged from Downshift.
useEffect(() => {
if (isControlledProp(props, 'selectedItem')) {
if (previousSelectedItemRef.current !== props.selectedItem) {
dispatch({
type: ControlledPropUpdatedSelectedItem,
inputValue: props.itemToString(props.selectedItem),
})
}
if (!isControlledProp(props, 'selectedItem')) {
return
}

previousSelectedItemRef.current =
state.selectedItem === previousSelectedItemRef.current
? props.selectedItem
: state.selectedItem
if (
props.selectedItemChanged(
previousSelectedItemRef.current,
props.selectedItem,
)
) {
dispatch({
type: ControlledPropUpdatedSelectedItem,
inputValue: props.itemToString(props.selectedItem),
})
}
}, [props.selectedItem, state.selectedItem])

previousSelectedItemRef.current =
state.selectedItem === previousSelectedItemRef.current
? props.selectedItem
: state.selectedItem
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state.selectedItem, props.selectedItem])

return [getState(state, props), dispatch]
}
Expand All @@ -121,5 +130,6 @@ if (process.env.NODE_ENV !== 'production') {

export const defaultProps = {
...defaultPropsCommon,
selectedItemChanged: (prevItem, item) => prevItem !== item,
getA11yStatusMessage,
}
1 change: 1 addition & 0 deletions typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,7 @@ export enum UseComboboxStateChangeTypes {
export interface UseComboboxProps<Item> {
items: Item[]
itemToString?: (item: Item | null) => string
selectedItemChanged?: (prevItem: Item, item: Item) => boolean
getA11yStatusMessage?: (options: A11yStatusMessageOptions<Item>) => string
getA11ySelectionMessage?: (options: A11yStatusMessageOptions<Item>) => string
highlightedIndex?: number
Expand Down

0 comments on commit 9b3199a

Please sign in to comment.