Skip to content

Commit

Permalink
feat(useSelect): improve highlight by character keys algorithm (#1456)
Browse files Browse the repository at this point in the history
  • Loading branch information
silviuaavram committed Jan 4, 2023
1 parent 99bd9d9 commit d822530
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 10 deletions.
72 changes: 71 additions & 1 deletion src/hooks/useSelect/__tests__/getToggleButtonProps.test.js
Expand Up @@ -463,7 +463,7 @@ describe('getToggleButtonProps', () => {
})

test('should highlight the first item that starts with the keys typed in rapid succession', async () => {
const chars = ['c', 'a']
const chars = ['m', 'e']
const expectedItemId = defaultIds.getItemId(
getItemIndexByCharacter(chars.join('')),
)
Expand All @@ -480,6 +480,50 @@ describe('getToggleButtonProps', () => {
expect(getItems()).toHaveLength(items.length)
})

test('should highlight the second item that starts with the keys typed in rapid succession, with first valid item initially highlighted', async () => {
const chars = ['m', 'e']
const firstValidItemIndex = getItemIndexByCharacter(chars.join(''))
const expectedItemId = defaultIds.getItemId(
getItemIndexByCharacter(chars.join(''), firstValidItemIndex + 1),
)
// Mendelevium is already highlighted before typing chars.
renderSelect({initialHighlightedIndex: firstValidItemIndex})

await keyDownOnToggleButton(chars[0])
reactAct(() => jest.advanceTimersByTime(200))
await keyDownOnToggleButton(chars[1])

// highlight should go on Meitnerium which is the second in the list.
expect(getToggleButton()).toHaveAttribute(
'aria-activedescendant',
expectedItemId,
)
expect(getItems()).toHaveLength(items.length)
})

test('should highlight the first item that starts with the keys typed in rapid succession, with first valid item initially highlighted, but made invalid after third character', async () => {
const chars = ['m', 'e', 'n']
const firstValidItemIndex = getItemIndexByCharacter(chars.join(''))
const expectedItemId = defaultIds.getItemId(firstValidItemIndex)
// Mendelevium is already highlighted before typing chars.
renderSelect({initialHighlightedIndex: firstValidItemIndex})

await keyDownOnToggleButton(chars[0])
reactAct(() => jest.advanceTimersByTime(200))
await keyDownOnToggleButton(chars[1])
reactAct(() => jest.advanceTimersByTime(200))
// Meitnerium is highlighted right now, as we typed "me".
await keyDownOnToggleButton(chars[2])
reactAct(() => jest.advanceTimersByTime(200))

// now we go back to Mendelevium, since we also typed "n".
expect(getToggleButton()).toHaveAttribute(
'aria-activedescendant',
expectedItemId,
)
expect(getItems()).toHaveLength(items.length)
})

test('should become first character after timeout passes', async () => {
const chars = ['c', 'a', 'l']
const expectedItemId = defaultIds.getItemId(
Expand All @@ -492,6 +536,7 @@ describe('getToggleButtonProps', () => {
await keyDownOnToggleButton(chars[1])
reactAct(() => jest.runAllTimers())
await keyDownOnToggleButton(chars[2])
reactAct(() => jest.advanceTimersByTime(200))

expect(getToggleButton()).toHaveAttribute(
'aria-activedescendant',
Expand All @@ -500,6 +545,31 @@ describe('getToggleButtonProps', () => {
expect(getItems()).toHaveLength(items.length)
})

test('should account space character as search query', async () => {
const itemToSelectIndex = 3
const itemsWithSpaces = ['1 2 3', '4 3 2', '2 1 3', '1 2 4']
const itemToSelect = itemsWithSpaces[itemToSelectIndex]
const expectedItemId = defaultIds.getItemId(itemToSelectIndex)
renderSelect({items: itemsWithSpaces})

const toggleButton = getToggleButton()

// should highlight "1 2 3" until we pass the last character, "4".
for (let index = 0; index < itemToSelect.length; index++) {
// eslint-disable-next-line no-await-in-loop
await keyDownOnToggleButton(itemToSelect[index])

reactAct(() => jest.advanceTimersByTime(200))
}

expect(toggleButton).toHaveAttribute(
'aria-activedescendant',
expectedItemId,
)
expect(getItems()).toHaveLength(itemsWithSpaces.length)
expect(toggleButton).toHaveTextContent('Elements')
})

/* Here we just want to make sure the keys cleanup works. */
test('should not go to the second option starting with the key if timeout did not pass', async () => {
const char = 'l'
Expand Down
21 changes: 16 additions & 5 deletions src/hooks/useSelect/index.js
Expand Up @@ -247,11 +247,22 @@ function useSelect(userProps = {}) {
' '(event) {
event.preventDefault()

dispatch({
type: latest.current.state.isOpen
? stateChangeTypes.ToggleButtonKeyDownSpaceButton
: stateChangeTypes.ToggleButtonClick,
})
const currentState = latest.current.state

if (!currentState.isOpen) {
dispatch({type: stateChangeTypes.ToggleButtonClick})
return
}

if (currentState.inputValue) {
dispatch({
type: stateChangeTypes.ToggleButtonKeyDownCharacter,
key: ' ',
getItemNodeFromIndex,
})
} else {
dispatch({type: stateChangeTypes.ToggleButtonKeyDownSpaceButton})
}
},
}),
[dispatch, getItemNodeFromIndex, latest],
Expand Down
9 changes: 5 additions & 4 deletions src/hooks/useSelect/utils.ts
Expand Up @@ -14,14 +14,15 @@ export function getItemIndexByCharacterKey<Item>({
const lowerCasedKeysSoFar = keysSoFar.toLowerCase()

for (let index = 0; index < items.length; index++) {
const offsetIndex = (index + highlightedIndex + 1) % items.length
// if we already have a search query in progress, we also consider the current highlighted item.
const offsetIndex =
(index + highlightedIndex + (keysSoFar.length < 2 ? 1 : 0)) % items.length

const item = items[offsetIndex]

if (
item !== undefined &&
itemToString(item)
.toLowerCase()
.startsWith(lowerCasedKeysSoFar)
itemToString(item).toLowerCase().startsWith(lowerCasedKeysSoFar)
) {
const element = getItemNodeFromIndex(offsetIndex)

Expand Down

0 comments on commit d822530

Please sign in to comment.