Skip to content

Commit 7471fba

Browse files
authored
fix(hooks): correctly compute item and index in getter function (#1473)
1 parent c6c0af5 commit 7471fba

File tree

8 files changed

+75
-54
lines changed

8 files changed

+75
-54
lines changed

src/hooks/__tests__/utils.test.js

+25-11
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import {renderHook} from '@testing-library/react-hooks'
22
import {
3-
getItemIndex,
43
defaultProps,
54
getInitialValue,
65
getDefaultValue,
76
useMouseAndTouchTracker,
7+
getItemAndIndex,
88
} from '../utils'
99

1010
describe('utils', () => {
@@ -15,21 +15,35 @@ describe('utils', () => {
1515
})
1616
})
1717

18-
describe('getItemIndex', () => {
19-
test('returns -1 if no items', () => {
20-
const index = getItemIndex(undefined, {}, [])
21-
expect(index).toBe(-1)
18+
describe('getItemAndIndex', () => {
19+
test('returns arguments if passed as defined', () => {
20+
expect(getItemAndIndex({}, 5, [])).toEqual([{}, 5])
2221
})
2322

24-
test('returns index if passed', () => {
25-
const index = getItemIndex(5, {}, [])
26-
expect(index).toBe(5)
23+
test('throws an error when item and index are not passed', () => {
24+
const errorMessage = 'Pass either item or index to the item getter prop!'
25+
26+
expect(() =>
27+
getItemAndIndex(undefined, undefined, [1, 2, 3], errorMessage),
28+
).toThrowError(errorMessage)
29+
})
30+
31+
test('returns index if item is passed', () => {
32+
const item = {}
33+
34+
expect(getItemAndIndex(item, undefined, [{x: 1}, item, {x: 2}])).toEqual([
35+
item,
36+
1,
37+
])
2738
})
2839

29-
test('returns index of item', () => {
40+
test('returns item if index is passed', () => {
41+
const index = 2
3042
const item = {x: 2}
31-
const index = getItemIndex(undefined, item, [{x: 1}, item, {x: 2}])
32-
expect(index).toBe(1)
43+
expect(getItemAndIndex(undefined, 2, [{x: 1}, {x: 3}, item])).toEqual([
44+
item,
45+
index,
46+
])
3347
})
3448
})
3549

src/hooks/useCombobox/__tests__/getItemProps.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ describe('getItemProps', () => {
1717
const {result} = renderUseCombobox()
1818

1919
expect(result.current.getItemProps).toThrowError(
20-
'Pass either item or item index in getItemProps!',
20+
'Pass either item or index to getItemProps!',
2121
)
2222
})
2323

src/hooks/useCombobox/index.js

+12-10
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ import {useRef, useEffect, useCallback, useMemo} from 'react'
33
import {isPreact, isReactNative} from '../../is.macro'
44
import {handleRefs, normalizeArrowKey, callAllEventHandlers} from '../../utils'
55
import {
6-
getItemIndex,
76
useA11yMessageSetter,
87
useMouseAndTouchTracker,
98
useGetterPropsCalledChecker,
109
useLatestRef,
1110
useScrollIntoView,
1211
useControlPropsValidator,
1312
useElementIds,
13+
getItemAndIndex,
1414
} from '../utils'
1515
import {
1616
getInitialState,
@@ -284,8 +284,8 @@ function useCombobox(userProps = {}) {
284284

285285
const getItemProps = useCallback(
286286
({
287-
item,
288-
index,
287+
item: itemProp,
288+
index: indexProp,
289289
refKey = 'ref',
290290
ref,
291291
onMouseMove,
@@ -296,10 +296,12 @@ function useCombobox(userProps = {}) {
296296
...rest
297297
} = {}) => {
298298
const {props: latestProps, state: latestState} = latest.current
299-
const itemIndex = getItemIndex(index, item, latestProps.items)
300-
if (itemIndex < 0) {
301-
throw new Error('Pass either item or item index in getItemProps!')
302-
}
299+
const [, index] = getItemAndIndex(
300+
itemProp,
301+
indexProp,
302+
latestProps.items,
303+
'Pass either item or index to getItemProps!',
304+
)
303305

304306
const onSelectKey = isReactNative
305307
? /* istanbul ignore next (react-native) */ 'onPress'
@@ -330,13 +332,13 @@ function useCombobox(userProps = {}) {
330332
return {
331333
[refKey]: handleRefs(ref, itemNode => {
332334
if (itemNode) {
333-
itemRefs.current[elementIds.getItemId(itemIndex)] = itemNode
335+
itemRefs.current[elementIds.getItemId(index)] = itemNode
334336
}
335337
}),
336338
disabled,
337339
role: 'option',
338-
'aria-selected': `${itemIndex === latestState.highlightedIndex}`,
339-
id: elementIds.getItemId(itemIndex),
340+
'aria-selected': `${index === latestState.highlightedIndex}`,
341+
id: elementIds.getItemId(index),
340342
...(!disabled && {
341343
[onSelectKey]: callAllEventHandlers(
342344
customClickHandler,

src/hooks/useMultipleSelection/__tests__/getSelectedItemProps.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ describe('getSelectedItemProps', () => {
1818
const {result} = renderUseMultipleSelection()
1919

2020
expect(result.current.getSelectedItemProps).toThrowError(
21-
'Pass either selectedItem or index in getSelectedItemProps!',
21+
'Pass either item or index to getSelectedItemProps!',
2222
)
2323
})
2424

src/hooks/useMultipleSelection/index.js

+9-12
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import setStatus from '../../set-a11y-status'
44
import {handleRefs, callAllEventHandlers, normalizeArrowKey} from '../../utils'
55
import {
66
useControlledReducer,
7-
getItemIndex,
87
useGetterPropsCalledChecker,
98
useLatestRef,
109
useControlPropsValidator,
10+
getItemAndIndex,
1111
} from '../utils'
1212
import {
1313
getInitialState,
@@ -159,21 +159,18 @@ function useMultipleSelection(userProps = {}) {
159159
ref,
160160
onClick,
161161
onKeyDown,
162-
selectedItem,
163-
index,
162+
selectedItem: selectedItemProp,
163+
index: indexProp,
164164
...rest
165165
} = {}) => {
166166
const {state: latestState} = latest.current
167-
const itemIndex = getItemIndex(
168-
index,
169-
selectedItem,
167+
const [, index] = getItemAndIndex(
168+
selectedItemProp,
169+
indexProp,
170170
latestState.selectedItems,
171+
'Pass either item or index to getSelectedItemProps!',
171172
)
172-
if (itemIndex < 0) {
173-
throw new Error(
174-
'Pass either selectedItem or index in getSelectedItemProps!',
175-
)
176-
}
173+
const isFocusable = index > -1 && index === latestState.activeIndex
177174

178175
const selectedItemHandleClick = () => {
179176
dispatch({
@@ -194,7 +191,7 @@ function useMultipleSelection(userProps = {}) {
194191
selectedItemRefs.current.push(selectedItemNode)
195192
}
196193
}),
197-
tabIndex: index === latestState.activeIndex ? 0 : -1,
194+
tabIndex: isFocusable ? 0 : -1,
198195
onClick: callAllEventHandlers(onClick, selectedItemHandleClick),
199196
onKeyDown: callAllEventHandlers(onKeyDown, selectedItemHandleKeyDown),
200197
...rest,

src/hooks/useSelect/__tests__/getItemProps.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ describe('getItemProps', () => {
1717
const {result} = renderUseSelect()
1818

1919
expect(result.current.getItemProps).toThrowError(
20-
'Pass either item or item index in getItemProps!',
20+
'Pass either item or index to getItemProps!',
2121
)
2222
})
2323

src/hooks/useSelect/index.js

+10-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {useRef, useEffect, useCallback, useMemo} from 'react'
22
import {
3-
getItemIndex,
3+
getItemAndIndex,
44
isAcceptedCharacterKey,
55
useControlledReducer,
66
getInitialState,
@@ -456,8 +456,12 @@ function useSelect(userProps = {}) {
456456
...rest
457457
} = {}) => {
458458
const {state: latestState, props: latestProps} = latest.current
459-
const item = itemProp ?? items[indexProp]
460-
const index = getItemIndex(indexProp, item, latestProps.items)
459+
const [item, index] = getItemAndIndex(
460+
itemProp,
461+
indexProp,
462+
latestProps.items,
463+
'Pass either item or index to getItemProps!',
464+
)
461465

462466
const itemHandleMouseMove = () => {
463467
if (index === latestState.highlightedIndex) {
@@ -477,18 +481,14 @@ function useSelect(userProps = {}) {
477481
})
478482
}
479483

480-
const itemIndex = getItemIndex(index, item, latestProps.items)
481-
if (itemIndex < 0) {
482-
throw new Error('Pass either item or item index in getItemProps!')
483-
}
484484
const itemProps = {
485485
disabled,
486486
role: 'option',
487487
'aria-selected': `${item === selectedItem}`,
488-
id: elementIds.getItemId(itemIndex),
488+
id: elementIds.getItemId(index),
489489
[refKey]: handleRefs(ref, itemNode => {
490490
if (itemNode) {
491-
itemRefs.current[elementIds.getItemId(itemIndex)] = itemNode
491+
itemRefs.current[elementIds.getItemId(index)] = itemNode
492492
}
493493
}),
494494
...rest,
@@ -510,7 +510,7 @@ function useSelect(userProps = {}) {
510510

511511
return itemProps
512512
},
513-
[latest, items, selectedItem, elementIds, shouldScrollRef, dispatch],
513+
[latest, selectedItem, elementIds, shouldScrollRef, dispatch],
514514
)
515515

516516
return {

src/hooks/utils.js

+16-8
Original file line numberDiff line numberDiff line change
@@ -112,14 +112,22 @@ function useElementIds({
112112
return elementIdsRef.current
113113
}
114114

115-
function getItemIndex(index, item, items) {
116-
if (index !== undefined) {
117-
return index
118-
}
119-
if (items.length === 0) {
120-
return -1
115+
function getItemAndIndex(itemProp, indexProp, items, errorMessage) {
116+
let item, index
117+
118+
if (itemProp === undefined) {
119+
if (indexProp === undefined) {
120+
throw new Error(errorMessage)
121+
}
122+
123+
item = items[indexProp]
124+
index = indexProp
125+
} else {
126+
index = indexProp === undefined ? items.indexOf(itemProp) : indexProp
127+
item = itemProp
121128
}
122-
return items.indexOf(item)
129+
130+
return [item, index]
123131
}
124132

125133
function itemToString(item) {
@@ -532,6 +540,6 @@ export {
532540
useLatestRef,
533541
capitalizeString,
534542
isAcceptedCharacterKey,
535-
getItemIndex,
543+
getItemAndIndex,
536544
useElementIds,
537545
}

0 commit comments

Comments
 (0)