Skip to content

Commit

Permalink
fix(defaultProps): only select if there are items and one is highligh…
Browse files Browse the repository at this point in the history
…ted (#1467)
  • Loading branch information
silviuaavram committed Mar 10, 2023
1 parent 7471fba commit 108c1ba
Show file tree
Hide file tree
Showing 7 changed files with 425 additions and 46 deletions.
2 changes: 1 addition & 1 deletion src/hooks/useCombobox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -950,7 +950,7 @@ described below.
- `Alt+ArrowUp`: If the menu is open, it will close it and will select the item
that was highlighted.
- `CharacterKey`: Will change the `inputValue` according to the value visible in
the `<input>`. `Backspace` or `Space` triggere the same event.
the `<input>`. `Backspace` or `Space` trigger the same event.
- `End`: If the menu is open, it will highlight the last item in the list.
- `Home`: If the menu is open, it will highlight the first item in the list.
- `PageUp`: If the menu is open, it will move the highlight the item 10
Expand Down
183 changes: 182 additions & 1 deletion src/hooks/useCombobox/__tests__/getInputProps.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,40 @@ describe('getInputProps', () => {
expect(getInput()).toHaveValue('california')
})

test('on change should remove the highlightedIndex', async () => {
renderCombobox({initialHighlightedIndex: 2})

await changeInputValue('california')

expect(getInput()).toHaveAttribute('aria-activedescendant', '')
})

test('on change should reset to defaultHighlightedIndex', async () => {
const defaultHighlightedIndex = 2
renderCombobox({defaultHighlightedIndex})

await changeInputValue('a')

expect(getInput()).toHaveAttribute(
'aria-activedescendant',
defaultIds.getItemId(defaultHighlightedIndex),
)

await keyDownOnInput('{ArrowDown}')

expect(getInput()).toHaveAttribute(
'aria-activedescendant',
defaultIds.getItemId(defaultHighlightedIndex + 1),
)

await changeInputValue('a')

expect(getInput()).toHaveAttribute(
'aria-activedescendant',
defaultIds.getItemId(defaultHighlightedIndex),
)
})

describe('on key down', () => {
describe('arrow up', () => {
test('it prevents the default event behavior', () => {
Expand Down Expand Up @@ -550,6 +584,61 @@ describe('getInputProps', () => {
)
})

test('with Alt selects highlighted item and resets to user defaults', async () => {
const defaultHighlightedIndex = 2
renderCombobox({
defaultHighlightedIndex,
defaultIsOpen: true,
})
const input = getInput()

await keyDownOnInput('{Alt>}{ArrowUp}{/Alt}')

expect(input).toHaveValue(items[defaultHighlightedIndex])
expect(getItems()).toHaveLength(items.length)
expect(input).toHaveAttribute(
'aria-activedescendant',
defaultIds.getItemId(defaultHighlightedIndex),
)
})

test('with Alt closes the menu without resetting to user defaults if no item is highlighted', async () => {
const defaultHighlightedIndex = 2
const initialSelectedItem = items[0]
renderCombobox({
defaultHighlightedIndex,
defaultIsOpen: true,
initialSelectedItem,
})
const input = getInput()

await mouseMoveItemAtIndex(defaultHighlightedIndex)
await mouseLeaveItemAtIndex(defaultHighlightedIndex)
await keyDownOnInput('{Alt>}{ArrowUp}{/Alt}')

expect(input).toHaveValue(initialSelectedItem)
expect(getItems()).toHaveLength(0)
expect(input).toHaveAttribute('aria-activedescendant', '')
})

test('with Alt closes the menu without resetting to user defaults if lhe list is empty', async () => {
const defaultHighlightedIndex = 2
const initialSelectedItem = items[0]
renderCombobox({
defaultHighlightedIndex,
defaultIsOpen: true,
initialSelectedItem,
items: [],
})
const input = getInput()

await keyDownOnInput('{Alt>}{ArrowUp}{/Alt}')

expect(input).toHaveValue(initialSelectedItem)
expect(getItems()).toHaveLength(0)
expect(input).toHaveAttribute('aria-activedescendant', '')
})

test('will continue from 0 to last item', async () => {
renderCombobox({
isOpen: true,
Expand Down Expand Up @@ -848,7 +937,7 @@ describe('getInputProps', () => {

await keyDownOnInput('{Escape}') // focus input and close the menu.
renderSpy.mockClear()

await keyDownOnInput('{Escape}')

expect(renderSpy).toHaveBeenCalledTimes(0) // no re-render
Expand Down Expand Up @@ -917,6 +1006,43 @@ describe('getInputProps', () => {
)
})

test('enter closes the menu without resetting to user defaults if no item is highlighted', async () => {
const defaultHighlightedIndex = 2
const initialSelectedItem = items[0]
renderCombobox({
defaultHighlightedIndex,
defaultIsOpen: true,
initialSelectedItem,
})
const input = getInput()

await mouseMoveItemAtIndex(defaultHighlightedIndex)
await mouseLeaveItemAtIndex(defaultHighlightedIndex)
await keyDownOnInput('{Enter}')

expect(input).toHaveValue(initialSelectedItem)
expect(getItems()).toHaveLength(0)
expect(input).toHaveAttribute('aria-activedescendant', '')
})

test('enter closes the menu without resetting to user defaults if the list is empty', async () => {
const defaultHighlightedIndex = 2
const initialSelectedItem = items[0]
renderCombobox({
defaultHighlightedIndex,
defaultIsOpen: true,
initialSelectedItem,
items: [],
})
const input = getInput()

await keyDownOnInput('{Enter}')

expect(input).toHaveValue(initialSelectedItem)
expect(getItems()).toHaveLength(0)
expect(input).toHaveAttribute('aria-activedescendant', '')
})

test('enter while IME composing will not select highlighted item', async () => {
const initialHighlightedIndex = 2
renderCombobox({
Expand Down Expand Up @@ -1016,6 +1142,61 @@ describe('getInputProps', () => {
expect(getInput()).toHaveValue(items[initialHighlightedIndex])
})

test('tab closes the menu if there is no highlighted item', async () => {
const defaultHighlightedIndex = 2
const initialSelectedItem = items[0]

renderCombobox(
{
defaultHighlightedIndex,
defaultIsOpen: true,
initialSelectedItem,
},
ui => {
return (
<>
{ui}
<div tabIndex={0}>Second element</div>
</>
)
},
)

await mouseMoveItemAtIndex(defaultHighlightedIndex)
await mouseLeaveItemAtIndex(defaultHighlightedIndex)
await tab()

expect(getItems()).toHaveLength(0)
expect(getInput()).toHaveValue(initialSelectedItem)
})

test('tab closes the menu if there is no items', async () => {
const defaultHighlightedIndex = 2
const initialSelectedItem = items[0]

renderCombobox(
{
defaultHighlightedIndex,
defaultIsOpen: true,
initialSelectedItem,
items: [],
},
ui => {
return (
<>
{ui}
<div tabIndex={0}>Second element</div>
</>
)
},
)

await tab()

expect(getItems()).toHaveLength(0)
expect(getInput()).toHaveValue(initialSelectedItem)
})

test('shift+tab it closes the menu', async () => {
const initialHighlightedIndex = 2
renderCombobox(
Expand Down
28 changes: 9 additions & 19 deletions src/hooks/useCombobox/reducer.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import {getHighlightedIndexOnOpen, getDefaultValue} from '../utils'
import {
getHighlightedIndexOnOpen,
getDefaultValue,
getChangesOnSelection,
} from '../utils'
import {getNextWrappingIndex, getNextNonDisabledIndex} from '../../utils'
import commonReducer from '../reducer'
import * as stateChangeTypes from './stateChangeTypes'
Expand Down Expand Up @@ -46,16 +50,7 @@ export default function downshiftUseComboboxReducer(state, action) {
case stateChangeTypes.InputKeyDownArrowUp:
if (state.isOpen) {
if (altKey) {
changes = {
isOpen: getDefaultValue(props, 'isOpen'),
highlightedIndex: getDefaultValue(props, 'highlightedIndex'),
...(state.highlightedIndex >= 0 && {
selectedItem: props.items[state.highlightedIndex],
inputValue: props.itemToString(
props.items[state.highlightedIndex],
),
}),
}
changes = getChangesOnSelection(props, state.highlightedIndex)
} else {
changes = {
highlightedIndex: getNextWrappingIndex(
Expand All @@ -80,14 +75,8 @@ export default function downshiftUseComboboxReducer(state, action) {
}
break
case stateChangeTypes.InputKeyDownEnter:
changes = {
isOpen: getDefaultValue(props, 'isOpen'),
highlightedIndex: getDefaultValue(props, 'highlightedIndex'),
...(state.highlightedIndex >= 0 && {
selectedItem: props.items[state.highlightedIndex],
inputValue: props.itemToString(props.items[state.highlightedIndex]),
}),
}
changes = getChangesOnSelection(props, state.highlightedIndex)

break
case stateChangeTypes.InputKeyDownEscape:
changes = {
Expand Down Expand Up @@ -148,6 +137,7 @@ export default function downshiftUseComboboxReducer(state, action) {
isOpen: false,
highlightedIndex: -1,
...(state.highlightedIndex >= 0 &&
props.items?.length &&
action.selectItem && {
selectedItem: props.items[state.highlightedIndex],
inputValue: props.itemToString(props.items[state.highlightedIndex]),
Expand Down
10 changes: 4 additions & 6 deletions src/hooks/useCombobox/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from '../utils'
import {ControlledPropUpdatedSelectedItem} from './stateChangeTypes'

function getInitialState(props) {
export function getInitialState(props) {
const initialState = getInitialStateCommon(props)
const {selectedItem} = initialState
let {inputValue} = initialState
Expand Down Expand Up @@ -86,7 +86,7 @@ const propTypes = {
* @param {Object} props The hook props.
* @returns {Array} An array with the state and an action dispatcher.
*/
function useControlledReducer(reducer, initialState, props) {
export function useControlledReducer(reducer, initialState, props) {
const previousSelectedItemRef = useRef()
const [state, dispatch] = useEnhancedReducer(reducer, initialState, props)

Expand All @@ -111,17 +111,15 @@ function useControlledReducer(reducer, initialState, props) {
}

// eslint-disable-next-line import/no-mutable-exports
let validatePropTypes = noop
export let validatePropTypes = noop
/* istanbul ignore next */
if (process.env.NODE_ENV !== 'production') {
validatePropTypes = (options, caller) => {
PropTypes.checkPropTypes(propTypes, options, 'prop', caller.name)
}
}

const defaultProps = {
export const defaultProps = {
...defaultPropsCommon,
getA11yStatusMessage,
}

export {validatePropTypes, useControlledReducer, getInitialState, defaultProps}
Loading

0 comments on commit 108c1ba

Please sign in to comment.