diff --git a/src/components/forms/ComboBox/ComboBox.test.tsx b/src/components/forms/ComboBox/ComboBox.test.tsx
index 4902821ae9..1e7ace1ec6 100644
--- a/src/components/forms/ComboBox/ComboBox.test.tsx
+++ b/src/components/forms/ComboBox/ComboBox.test.tsx
@@ -170,12 +170,14 @@ describe('ComboBox component', () => {
)
const firstItem = getByTestId('combo-box-option-list').children[0]
+ const inputEl = getByTestId('combo-box-input')
userEvent.click(getByTestId('combo-box-toggle'))
expect(firstItem).toBeVisible()
expect(firstItem).not.toHaveFocus()
expect(firstItem).toHaveClass('usa-combo-box__list-option--focused')
+ expect(inputEl).toHaveAttribute('aria-activedescendant', firstItem.id)
})
it('highlights the default value when opening the menu, when one exists', () => {
@@ -249,144 +251,148 @@ describe('ComboBox component', () => {
expect(getByTestId('combo-box-option-list')).not.toBeVisible()
})
- it('renders select with custom props if passed in', () => {
- const { getByTestId } = render(
-
- )
- const comboBoxSelect = getByTestId('combo-box-select')
- expect(comboBoxSelect).toHaveAttribute('required')
- expect(comboBoxSelect).toHaveAttribute('role', 'testing')
- })
+ describe('with custom props', () => {
+ it('renders select with custom props if passed in', () => {
+ const { getByTestId } = render(
+
+ )
+ const comboBoxSelect = getByTestId('combo-box-select')
+ expect(comboBoxSelect).toHaveAttribute('required')
+ expect(comboBoxSelect).toHaveAttribute('role', 'testing')
+ })
- it('renders input with custom props if passed in', () => {
- const { getByTestId } = render(
-
- )
+ it('renders input with custom props if passed in', () => {
+ const { getByTestId } = render(
+
+ )
- const comboBoxInput = getByTestId('combo-box-input')
- expect(comboBoxInput).toHaveAttribute('required')
- expect(comboBoxInput).toHaveAttribute('type', 'url')
- })
+ const comboBoxInput = getByTestId('combo-box-input')
+ expect(comboBoxInput).toHaveAttribute('required')
+ expect(comboBoxInput).toHaveAttribute('type', 'url')
+ })
- it('allows a custom input onChange handler to be called', () => {
- const mockOnInputChange = jest.fn()
+ it('allows a custom input onChange handler to be called', () => {
+ const mockOnInputChange = jest.fn()
- const { getByTestId } = render(
-
- )
+ const { getByTestId } = render(
+
+ )
- const input = getByTestId('combo-box-input')
- userEvent.type(input, 'x')
- expect(mockOnInputChange).toHaveBeenCalled()
- })
+ const input = getByTestId('combo-box-input')
+ userEvent.type(input, 'x')
+ expect(mockOnInputChange).toHaveBeenCalled()
+ })
- it('renders list with custom props if passed in', () => {
- const { getByTestId } = render(
-
- )
+ it('renders list with custom props if passed in', () => {
+ const { getByTestId } = render(
+
+ )
- const comboBoxOptionList = getByTestId('combo-box-option-list')
- expect(comboBoxOptionList).toHaveAttribute(
- 'aria-labelledby',
- 'test-label-id'
- )
+ const comboBoxOptionList = getByTestId('combo-box-option-list')
+ expect(comboBoxOptionList).toHaveAttribute(
+ 'aria-labelledby',
+ 'test-label-id'
+ )
+ })
})
- it('scrolls options list to the very top when the menu opens if nothing is selected', () => {
- const { getByTestId } = render(
-
- )
+ describe('scrolling to a focused option', () => {
+ it('scrolls options list to the very top when the menu opens if nothing is selected', () => {
+ const { getByTestId } = render(
+
+ )
- const listEl = getByTestId('combo-box-option-list')
- jest.spyOn(listEl, 'offsetHeight', 'get').mockReturnValue(205)
- listEl.scrollTop = 2000 // Scroll list 2000px down
+ const listEl = getByTestId('combo-box-option-list')
+ jest.spyOn(listEl, 'offsetHeight', 'get').mockReturnValue(205)
+ listEl.scrollTop = 2000 // Scroll list 2000px down
- userEvent.click(getByTestId('combo-box-toggle'))
+ userEvent.click(getByTestId('combo-box-toggle'))
- expect(listEl.scrollTop).toEqual(0)
- })
+ expect(listEl.scrollTop).toEqual(0)
+ })
- it('scrolls down to the selected option when the list is opened', () => {
- const { getByTestId } = render(
-
- )
+ it('scrolls down to the selected option when the list is opened', () => {
+ const { getByTestId } = render(
+
+ )
- const mango = getByTestId('combo-box-option-mango')
- const listEl = getByTestId('combo-box-option-list')
+ const mango = getByTestId('combo-box-option-mango')
+ const listEl = getByTestId('combo-box-option-list')
- jest.spyOn(mango, 'offsetTop', 'get').mockReturnValue(1365)
- jest.spyOn(mango, 'offsetHeight', 'get').mockReturnValue(39)
- jest.spyOn(listEl, 'offsetHeight', 'get').mockReturnValue(205)
- listEl.scrollTop = 0 // Scroll list to the top
+ jest.spyOn(mango, 'offsetTop', 'get').mockReturnValue(1365)
+ jest.spyOn(mango, 'offsetHeight', 'get').mockReturnValue(39)
+ jest.spyOn(listEl, 'offsetHeight', 'get').mockReturnValue(205)
+ listEl.scrollTop = 0 // Scroll list to the top
- userEvent.click(getByTestId('combo-box-toggle'))
- expect(mango).toHaveClass(
- 'usa-combo-box__list-option--focused usa-combo-box__list-option--selected'
- )
+ userEvent.click(getByTestId('combo-box-toggle'))
+ expect(mango).toHaveClass(
+ 'usa-combo-box__list-option--focused usa-combo-box__list-option--selected'
+ )
- expect(listEl.scrollTop).toEqual(1199)
- })
+ expect(listEl.scrollTop).toEqual(1199)
+ })
- it('scrolls up to the selected option when the list is opened', () => {
- const { getByTestId } = render(
-
- )
+ it('scrolls up to the selected option when the list is opened', () => {
+ const { getByTestId } = render(
+
+ )
- const mango = getByTestId('combo-box-option-mango')
- const listEl = getByTestId('combo-box-option-list')
+ const mango = getByTestId('combo-box-option-mango')
+ const listEl = getByTestId('combo-box-option-list')
- jest.spyOn(mango, 'offsetTop', 'get').mockReturnValue(1365)
- jest.spyOn(mango, 'offsetHeight', 'get').mockReturnValue(39)
- jest.spyOn(listEl, 'offsetHeight', 'get').mockReturnValue(205)
- listEl.scrollTop = 2292 // Scroll list 2292px down
+ jest.spyOn(mango, 'offsetTop', 'get').mockReturnValue(1365)
+ jest.spyOn(mango, 'offsetHeight', 'get').mockReturnValue(39)
+ jest.spyOn(listEl, 'offsetHeight', 'get').mockReturnValue(205)
+ listEl.scrollTop = 2292 // Scroll list 2292px down
- userEvent.click(getByTestId('combo-box-toggle'))
- expect(mango).toHaveClass(
- 'usa-combo-box__list-option--focused usa-combo-box__list-option--selected'
- )
+ userEvent.click(getByTestId('combo-box-toggle'))
+ expect(mango).toHaveClass(
+ 'usa-combo-box__list-option--focused usa-combo-box__list-option--selected'
+ )
- expect(listEl.scrollTop).toEqual(1365)
+ expect(listEl.scrollTop).toEqual(1365)
+ })
})
describe('filtering', () => {
@@ -749,6 +755,11 @@ describe('ComboBox component', () => {
fireEvent.blur(getByTestId('combo-box-input'))
expect(getByTestId('combo-box-input')).toHaveValue('')
+ expect(getByTestId('combo-box-input')).toHaveAttribute(
+ 'aria-activedescendant',
+ ''
+ )
+ expect(getByTestId('combo-box-option-list')).not.toBeVisible()
})
describe('keyboard actions', () => {
@@ -942,7 +953,12 @@ describe('ComboBox component', () => {
key: 'ArrowDown',
})
- expect(getByTestId('combo-box-option-apricot')).toHaveFocus()
+ const focusedOption = getByTestId('combo-box-option-apricot')
+ expect(focusedOption).toHaveFocus()
+ expect(getByTestId('combo-box-input')).toHaveAttribute(
+ 'aria-activedescendant',
+ focusedOption.id
+ )
})
it('focuses the previous option when up arrow is pressed', () => {
@@ -967,7 +983,38 @@ describe('ComboBox component', () => {
key: 'ArrowUp',
})
- expect(getByTestId('combo-box-option-apricot')).toHaveFocus()
+ const focusedOption = getByTestId('combo-box-option-apricot')
+ expect(focusedOption).toHaveFocus()
+ expect(getByTestId('combo-box-input')).toHaveAttribute(
+ 'aria-activedescendant',
+ focusedOption.id
+ )
+ })
+
+ it('opens the menu and focuses on the selected option when down arrow is pressed in the input', () => {
+ const { getByTestId } = render(
+
+ )
+
+ const comboBoxInput = getByTestId('combo-box-input')
+ userEvent.click(comboBoxInput)
+ fireEvent.keyDown(comboBoxInput, {
+ key: 'ArrowDown',
+ })
+
+ expect(getByTestId('combo-box-option-list')).toBeVisible()
+ const focusedOption = getByTestId('combo-box-option-watermelon')
+ expect(focusedOption).toHaveFocus()
+ expect(getByTestId('combo-box-input')).toHaveAttribute(
+ 'aria-activedescendant',
+ focusedOption.id
+ )
})
it('opens the menu when down arrow is pressed in the input', () => {
@@ -987,7 +1034,12 @@ describe('ComboBox component', () => {
})
expect(getByTestId('combo-box-option-list')).toBeVisible()
- expect(getByTestId('combo-box-option-apple')).toHaveFocus()
+ const focusedOption = getByTestId('combo-box-option-apple')
+ expect(focusedOption).toHaveFocus()
+ expect(getByTestId('combo-box-input')).toHaveAttribute(
+ 'aria-activedescendant',
+ focusedOption.id
+ )
})
it('closes the menu when the first option is focused and arrow up is pressed', () => {
@@ -1011,6 +1063,7 @@ describe('ComboBox component', () => {
expect(getByTestId('combo-box-option-list')).not.toBeVisible()
expect(comboBoxInput).toHaveFocus()
+ expect(comboBoxInput).toHaveAttribute('aria-activedescendant', '')
})
it('does not change focus when last option is focused and down arrow is pressed', () => {
@@ -1090,7 +1143,7 @@ describe('ComboBox component', () => {
expect(getByTestId('combo-box-option-mangosteen')).toHaveFocus()
})
- it('does not close menu when an option is selected and the first option is focused and up arrow is pressed', () => {
+ it('closes the menu when an option is selected and the first option is focused and up arrow is pressed', () => {
const { getByTestId } = render(
{
key: 'ArrowUp',
})
- expect(appleOption).toHaveFocus()
+ expect(getByTestId('combo-box-input')).toHaveFocus()
expect(getByTestId('combo-box-input')).toHaveAttribute(
'aria-expanded',
- 'true'
+ 'false'
)
- expect(getByTestId('combo-box-option-list')).toBeVisible()
+ expect(getByTestId('combo-box-option-list')).not.toBeVisible()
})
it('hides the clear button when typing in the input, but does not clear the selection', () => {
@@ -1202,7 +1255,8 @@ describe('ComboBox component', () => {
expect(comboBoxInput).toHaveValue('Apple')
})
- xit('focuses the input when an option is focused and shift-tab is pressed', () => {
+ // TODO
+ it.skip('focuses the input when an option is focused and shift-tab is pressed', () => {
const { getByTestId } = render(
{
fireEvent.blur(input)
expect(input).toHaveAttribute('aria-expanded', 'false')
+ expect(input).toHaveAttribute('aria-activedescendant', '')
expect(getByTestId('combo-box-option-list')).not.toBeVisible()
})
@@ -1302,6 +1357,7 @@ describe('ComboBox component', () => {
fireEvent.blur(blackberryOption)
expect(input).toHaveAttribute('aria-expanded', 'false')
+ expect(input).toHaveAttribute('aria-activedescendant', '')
expect(getByTestId('combo-box-option-list')).not.toBeVisible()
})
@@ -1325,6 +1381,7 @@ describe('ComboBox component', () => {
fireEvent.click(getByTestId('combo-box-toggle'))
expect(input).toHaveAttribute('aria-expanded', 'false')
+ expect(input).toHaveAttribute('aria-activedescendant', '')
expect(optionList).not.toBeVisible()
})
@@ -1424,6 +1481,7 @@ describe('ComboBox component', () => {
const yuzuOption = getByTestId('combo-box-option-yuzu')
const blackberryOption = getByTestId('combo-box-option-blackberry')
+ const inputEl = getByTestId('combo-box-input')
userEvent.click(getByTestId('combo-box-toggle'))
userEvent.hover(blackberryOption)
@@ -1431,12 +1489,19 @@ describe('ComboBox component', () => {
expect(blackberryOption).toHaveClass(
'usa-combo-box__list-option--focused'
)
+ expect(blackberryOption).toHaveFocus()
+ expect(inputEl).toHaveAttribute(
+ 'aria-activedescendant',
+ blackberryOption.id
+ )
userEvent.hover(yuzuOption)
expect(blackberryOption).not.toHaveClass(
'usa-combo-box__list-option--focused'
)
expect(yuzuOption).toHaveClass('usa-combo-box__list-option--focused')
+ expect(yuzuOption).toHaveFocus()
+ expect(inputEl).toHaveAttribute('aria-activedescendant', yuzuOption.id)
})
it('clears focus when clicking outside of the component', () => {
@@ -1458,6 +1523,10 @@ describe('ComboBox component', () => {
userEvent.click(getByTestId('outside'))
expect(getByTestId('combo-box-input')).not.toHaveFocus()
+ expect(getByTestId('combo-box-input')).toHaveAttribute(
+ 'aria-activedescendant',
+ ''
+ )
})
})
@@ -1514,6 +1583,108 @@ describe('ComboBox component', () => {
})
})
+ it('updates the status text when the list is opened with options', () => {
+ const { getByTestId } = render(
+
+ )
+
+ const status = screen.getByRole('status')
+
+ // open options list
+ fireEvent.click(getByTestId('combo-box-input'))
+
+ expect(status).toHaveTextContent(
+ `${fruitOptions.length} results available.`
+ )
+ })
+
+ it('updates the status text when the list is opened with filtered options', () => {
+ const { getByTestId } = render(
+
+ )
+
+ const status = screen.getByRole('status')
+
+ const input = getByTestId('combo-box-input')
+ userEvent.type(input, 'a')
+
+ expect(status).toHaveTextContent(`43 results available.`)
+ })
+
+ it('updates the status text when the list is opened with no options', () => {
+ const { getByTestId } = render(
+
+ )
+
+ const status = screen.getByRole('status')
+
+ // open options list
+ fireEvent.click(getByTestId('combo-box-input'))
+
+ expect(status).toHaveTextContent(`No results.`)
+ })
+
+ it('updates the status text when the list is closed', () => {
+ const { getByTestId } = render(
+
+ )
+
+ const status = screen.getByRole('status')
+
+ // open options list
+ fireEvent.click(getByTestId('combo-box-input'))
+
+ expect(status).toHaveTextContent(
+ `${fruitOptions.length} results available.`
+ )
+
+ fireEvent.blur(getByTestId('combo-box-input'))
+
+ expect(getByTestId('combo-box-option-list')).not.toBeVisible()
+ expect(status).toBeEmptyDOMElement()
+ })
+
+ it('updates the status text when an option is selected', () => {
+ const { getByTestId } = render(
+
+ )
+
+ const status = screen.getByRole('status')
+
+ userEvent.type(getByTestId('combo-box-input'), 'Banana')
+
+ expect(status).toHaveTextContent(`1 result available.`)
+
+ userEvent.type(getByTestId('combo-box-input'), '{enter}')
+
+ expect(status).toBeEmptyDOMElement()
+ })
+
it('allows the assistive hint to be customized', () => {
const { getByTestId } = render(
= state.filteredOptions.length) {
dispatch({ type: ActionTypes.FOCUS_OPTION, option: lastOption })
@@ -358,6 +357,12 @@ export const ComboBox = forwardRef(
const listID = `${id}--list`
const assistiveHintID = `${id}--assistiveHint`
+ const focusedItemIndex = state.focusedOption
+ ? state.filteredOptions.findIndex((i) => i === state.focusedOption)
+ : -1
+ const focusedItemId =
+ focusedItemIndex > -1 && `${listID}--option-${focusedItemIndex}`
+
return (
@@ -483,7 +489,9 @@ export const ComboBox = forwardRef(
) : null}
-
+
+ {state.statusText}
+
1 ? 's' : ''
+ } available.`,
+ }
+
+ if (optionsToDisplay.length < 1) {
+ newState.statusText = 'No results.'
}
if (disableFiltering || !state.selectedOption) {
@@ -123,20 +132,29 @@ export const useComboBox = (
return newState
}
- case ActionTypes.OPEN_LIST:
+ case ActionTypes.OPEN_LIST: {
+ const statusText = state.filteredOptions.length
+ ? `${state.filteredOptions.length} result${
+ state.filteredOptions.length > 1 ? 's' : ''
+ } available.`
+ : 'No results.'
+
return {
...state,
isOpen: true,
focusMode: FocusMode.Input,
focusedOption:
state.selectedOption || state.focusedOption || optionsList[0],
+ statusText,
}
+ }
case ActionTypes.CLOSE_LIST: {
const newState = {
...state,
isOpen: false,
focusMode: FocusMode.Input,
focusedOption: undefined,
+ statusText: '',
}
if (state.filteredOptions.length === 0) {
@@ -151,13 +169,21 @@ export const useComboBox = (
return newState
}
- case ActionTypes.FOCUS_OPTION:
+ case ActionTypes.FOCUS_OPTION: {
+ const statusText = state.filteredOptions.length
+ ? `${state.filteredOptions.length} result${
+ state.filteredOptions.length > 1 ? 's' : ''
+ } available.`
+ : 'No results.'
+
return {
...state,
isOpen: true,
focusedOption: action.option,
focusMode: FocusMode.Item,
+ statusText,
}
+ }
case ActionTypes.CLEAR:
return {
...state,
@@ -167,6 +193,7 @@ export const useComboBox = (
selectedOption: undefined,
filteredOptions: optionsList,
focusedOption: optionsList[0],
+ statusText: '',
}
case ActionTypes.BLUR: {
const newState = {
@@ -174,6 +201,7 @@ export const useComboBox = (
isOpen: false,
focusMode: FocusMode.None,
filteredOptions: optionsList,
+ statusText: '',
}
if (!state.selectedOption) {
@@ -195,6 +223,7 @@ export const useComboBox = (
selectedOption: undefined,
filteredOptions: optionsList,
focusedOption: undefined,
+ statusText: '',
}
}
default: