diff --git a/packages/react/__tests__/src/components/Combobox/index.js b/packages/react/__tests__/src/components/Combobox/index.js
deleted file mode 100644
index 0ea3c6782..000000000
--- a/packages/react/__tests__/src/components/Combobox/index.js
+++ /dev/null
@@ -1,969 +0,0 @@
-import React from 'react';
-import { act } from 'react-dom/test-utils';
-import { mount } from 'enzyme';
-import { spy } from 'sinon';
-import {
- default as Combobox,
- ComboboxGroup,
- ComboboxOption
-} from 'src/components/Combobox';
-
-const simulateKeyDown =
- (wrapper, key) =>
- (event = {}) => {
- wrapper.simulate('keydown', { key, ...event });
- wrapper.update();
- };
-
-// Utility function for checking if the ComboboxListbox is currently open
-const listboxIsOpen = (wrapper) => (isOpen) => {
- const Listbox = wrapper.find('ul[role="listbox"]');
- const input = wrapper.find('[role="combobox"]');
- expect(input.prop('aria-expanded')).toEqual(isOpen);
- if (isOpen) {
- expect(Listbox.prop('className')).toContain('Combobox__listbox--open');
- } else {
- expect(Listbox.prop('className')).not.toContain('Combobox__listbox--open');
- }
-};
-
-// Utility function for checking for active element for the given Combobox Option of a Combobox component
-const optionIsActive = (wrapper) => (index) => {
- const combobox = wrapper.find('input[role="combobox"]');
- const options = wrapper.find(
- '[role="listbox"] .ComboboxOption[role="option"]'
- );
- const activeOption = options.at(index);
- expect(combobox.prop('aria-activedescendant')).toBeTruthy();
- expect(combobox.prop('aria-activedescendant')).toEqual(
- activeOption.prop('id')
- );
- expect(activeOption.hasClass('ComboboxOption--active')).toBeTruthy();
- options.forEach(
- (option, index) =>
- index !== index &&
- expect(option.hasClass('ComboboxOption--active')).toBeFalsy()
- );
-};
-
-// Utility function for checking for active element for the given Combobox Option of a Combobox component
-const optionIsSelected = (wrapper) => (index) => {
- const options = wrapper.find(
- '[role="listbox"] .ComboboxOption[role="option"]'
- );
- const selectedOption = options.at(index);
- expect(selectedOption.prop('aria-selected')).toEqual(true);
- options.forEach(
- (option, index) =>
- index !== index && expect(option.prop('aria-selected')).toEqual(false)
- );
-};
-
-test('should render combobox with options', () => {
- const options = [
- { value: 'Apple', label: 'Apple' },
- { value: 'Banana', label: 'Banana' },
- { value: 'Cantaloupe', label: 'Cantaloupe' }
- ];
- const wrapper = mount();
- expect(wrapper.find(Combobox).exists()).toBeTruthy();
- expect(wrapper.find(ComboboxOption).at(0).text()).toEqual('Apple');
- expect(wrapper.find(ComboboxOption).at(1).text()).toEqual('Banana');
- expect(wrapper.find(ComboboxOption).at(2).text()).toEqual('Cantaloupe');
-});
-
-test('should render combobox with children', () => {
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
- expect(wrapper.find(Combobox).exists()).toBeTruthy();
- expect(wrapper.find(ComboboxOption).at(0).text()).toEqual('Apple');
- expect(wrapper.find(ComboboxOption).at(1).text()).toEqual('Banana');
- expect(wrapper.find(ComboboxOption).at(2).text()).toEqual('Cantaloupe');
-});
-
-test('should render combobox options with description', () => {
- const options = [
- {
- value: 'Apple',
- label: 'Apple',
- description:
- 'A crispy orb of deliciousness that comes in colors from "stoplight red" to "alien green".'
- },
- {
- value: 'Banana',
- label: 'Banana',
- description:
- 'This curvy wonder brings the tropics to your taste buds with its sunny disposition.'
- },
- {
- value: 'Cantaloupe',
- label: 'Cantaloupe',
- description: 'A bumpy, juicy, orb with moist deliciousness inside.'
- }
- ];
- const wrapper = mount();
- expect(wrapper.find(Combobox).exists()).toBeTruthy();
- expect(wrapper.find('.ComboboxOption__description').at(0).text()).toEqual(
- options[0].description
- );
- expect(wrapper.find('.ComboboxOption__description').at(1).text()).toEqual(
- options[1].description
- );
- expect(wrapper.find('.ComboboxOption__description').at(2).text()).toEqual(
- options[2].description
- );
-});
-
-test('should render combobox children with description', () => {
- const options = [
- {
- value: 'Apple',
- label: 'Apple',
- description:
- 'A crispy orb of deliciousness that comes in colors from "stoplight red" to "alien green".'
- },
- {
- value: 'Banana',
- label: 'Banana',
- description:
- 'This curvy wonder brings the tropics to your taste buds with its sunny disposition.'
- },
- {
- value: 'Cantaloupe',
- label: 'Cantaloupe',
- description: 'A bumpy, juicy, orb with moist deliciousness inside.'
- }
- ];
- const children = options.map(({ value, label, description }, index) => (
-
- {label}
-
- ));
- const wrapper = mount({children});
- expect(wrapper.find(Combobox).exists()).toBeTruthy();
- expect(wrapper.find('.ComboboxOption__description').at(0).text()).toEqual(
- options[0].description
- );
- expect(wrapper.find('.ComboboxOption__description').at(1).text()).toEqual(
- options[1].description
- );
- expect(wrapper.find('.ComboboxOption__description').at(2).text()).toEqual(
- options[2].description
- );
-});
-
-test('should render combobox with groups', () => {
- const wrapper = mount(
-
-
- Apple
- Banana
- Cantaloupe
-
-
- Artichoke
- Broccoli
- Carrots
-
-
- );
-
- const group1 = wrapper.find(ComboboxGroup).at(0);
- const group2 = wrapper.find(ComboboxGroup).at(1);
-
- expect(wrapper.find(Combobox).exists()).toBeTruthy();
- expect(group1.exists()).toBeTruthy();
- expect(group2.exists()).toBeTruthy();
- expect(group1.find('ul').prop('role')).toEqual('group');
- expect(group2.find('ul').prop('role')).toEqual('group');
- expect(group1.find('ul').prop('aria-labelledby')).toBeTruthy();
- expect(group2.find('ul').prop('aria-labelledby')).toBeTruthy();
- expect(group1.find(ComboboxOption).at(0).text()).toEqual('Apple');
- expect(group1.find(ComboboxOption).at(1).text()).toEqual('Banana');
- expect(group1.find(ComboboxOption).at(2).text()).toEqual('Cantaloupe');
- expect(group2.find(ComboboxOption).at(0).text()).toEqual('Artichoke');
- expect(group2.find(ComboboxOption).at(1).text()).toEqual('Broccoli');
- expect(group2.find(ComboboxOption).at(2).text()).toEqual('Carrots');
-});
-
-test('should render required combobox', () => {
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
-
- expect(wrapper.find('[role="combobox"]').prop('required')).toEqual(true);
- expect(wrapper.find('.Field__required-text').exists()).toBeTruthy();
- expect(wrapper.find('.Field__required-text').text()).toEqual('Required');
-});
-
-test('should render combobox with error', () => {
- const errorId = 'combo-error';
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
-
- expect(wrapper.find(`#${errorId}`).exists()).toBeTruthy();
- expect(wrapper.find(`#${errorId}`).text()).toEqual(
- 'You forgot to choose a value.'
- );
- expect(
- wrapper.find('input').getDOMNode().getAttribute('aria-describedby')
- ).toBe(`other-id ${errorId}`);
-});
-
-test('should open combobox listbox on click', () => {
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
-
- const assertListboxIsOpen = listboxIsOpen(wrapper);
-
- assertListboxIsOpen(false);
- wrapper.find('[role="combobox"]').simulate('click');
- assertListboxIsOpen(true);
-});
-
-test('should focus combobox input on click', () => {
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
-
- const assertListboxIsOpen = listboxIsOpen(wrapper);
-
- assertListboxIsOpen(false);
- const onFocus = spy(
- wrapper.find('[role="combobox"]').getElement().ref.current,
- 'focus'
- );
- expect(onFocus.notCalled).toBeTruthy();
- wrapper
- .find('[role="combobox"]')
- .simulate('click', { target: document.body });
- assertListboxIsOpen(true);
- expect(onFocus.calledOnce).toBeTruthy();
-});
-
-test('should allow an input ref to be passed to the combobox', () => {
- const inputRef = React.createRef();
- const wrapper = mount(
-
- Apple
-
- );
-
- expect(inputRef.current).toBeTruthy();
- expect(inputRef.current).toEqual(wrapper.find('input').getDOMNode());
-});
-
-test('should open combobox listbox on focus', () => {
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
-
- const assertListboxIsOpen = listboxIsOpen(wrapper);
-
- assertListboxIsOpen(false);
- wrapper.find('[role="combobox"]').simulate('focus');
- assertListboxIsOpen(true);
-});
-
-test('should open combobox listbox on keypress', () => {
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
-
- const assertListboxIsOpen = listboxIsOpen(wrapper);
-
- assertListboxIsOpen(false);
- wrapper.find('[role="combobox"]').simulate('keydown', { key: 'ArrowDown' });
- assertListboxIsOpen(true);
-});
-
-test('should close combobox listbox on "esc" keypress', () => {
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
-
- const assertListboxIsOpen = listboxIsOpen(wrapper);
-
- assertListboxIsOpen(false);
- wrapper.find('[role="combobox"]').simulate('keydown', { key: 'ArrowDown' });
- wrapper.find('[role="combobox"]').simulate('keydown', { key: 'A' });
- assertListboxIsOpen(true);
- wrapper.find('[role="combobox"]').simulate('keydown', { key: 'Escape' });
- assertListboxIsOpen(false);
-});
-
-test('should not open combobox listbox on "enter" keypress', () => {
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
-
- const assertListboxIsOpen = listboxIsOpen(wrapper);
-
- assertListboxIsOpen(false);
- wrapper.find('[role="combobox"]').simulate('keydown', { key: 'Enter' });
- assertListboxIsOpen(false);
-});
-
-test('should close combobox listbox on "blur"', () => {
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
-
- const assertListboxIsOpen = listboxIsOpen(wrapper);
-
- assertListboxIsOpen(false);
- wrapper.find('[role="combobox"]').simulate('focus');
- assertListboxIsOpen(true);
- wrapper.find('[role="combobox"]').simulate('blur');
- assertListboxIsOpen(false);
-});
-
-test('should close combobox listbox when selecting option via click', () => {
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
-
- const assertListboxIsOpen = listboxIsOpen(wrapper);
-
- assertListboxIsOpen(false);
- wrapper.find('[role="combobox"]').simulate('focus');
- assertListboxIsOpen(true);
- wrapper.find('li[role="option"]').at(0).simulate('click');
- assertListboxIsOpen(false);
-});
-
-test('should prevent default on combobox listbox option via mousedown', () => {
- const preventDefault = spy();
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
-
- const assertListboxIsOpen = listboxIsOpen(wrapper);
-
- assertListboxIsOpen(false);
- wrapper.find('[role="combobox"]').simulate('focus');
- assertListboxIsOpen(true);
- expect(preventDefault.notCalled).toBeTruthy();
- wrapper
- .find('li[role="option"]')
- .at(0)
- .simulate('mousedown', { preventDefault });
- expect(preventDefault.calledOnce).toBeTruthy();
-});
-
-test('should close combobox listbox when selecting option via keypress', () => {
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
-
- const assertListboxIsOpen = listboxIsOpen(wrapper);
- const combobox = wrapper.find('[role="combobox"]');
-
- assertListboxIsOpen(false);
- combobox.simulate('focus');
- assertListboxIsOpen(true);
- combobox.simulate('keydown', { key: 'ArrowDown' });
- act(() => {
- // we need to manually fire active change for the listbox
- wrapper
- .find('Listbox')
- .props()
- .onActiveChange({
- element: {
- getAttribute: () => 'id'
- }
- });
- });
- combobox.simulate('keydown', { key: 'Enter' });
- assertListboxIsOpen(false);
-});
-
-test('should set aria-activedescendent for active combobox options', () => {
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
-
- const assertListboxIsOpen = listboxIsOpen(wrapper);
- const combobox = wrapper.find('[role="combobox"]');
- // Note: Combobox forwards events to Listbox via dispatchEvent, but this doesn't
- // work correctly within jsdom/enzyme so we fire the events directly on listbox
- const simulateArrowDownKeypress = simulateKeyDown(
- wrapper.find('ul[role="listbox"]'),
- 'ArrowDown'
- );
- const assertOptionIsActive = optionIsActive(wrapper);
-
- expect(combobox.prop('aria-activedescendant')).toBeFalsy();
- combobox.simulate('focus');
- assertListboxIsOpen(true);
- simulateArrowDownKeypress();
- assertOptionIsActive(0);
- simulateArrowDownKeypress();
- assertOptionIsActive(1);
- simulateArrowDownKeypress();
- assertOptionIsActive(2);
-});
-
-test('should prevent default event on home/end keypress', () => {
- const preventDefault = spy();
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
-
- const combobox = wrapper.find('[role="combobox"]');
- expect(preventDefault.notCalled).toBeTruthy();
- combobox.simulate('keydown', { key: 'Home', preventDefault });
- expect(preventDefault.callCount).toEqual(1);
- combobox.simulate('keydown', { key: 'End', preventDefault });
- expect(preventDefault.callCount).toEqual(2);
- combobox.simulate('keydown', { key: 'ArrowDown', preventDefault });
- expect(preventDefault.callCount).toEqual(2);
-});
-
-test('should call onActiveChange when active option changes', () => {
- const onActiveChange = spy();
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
-
- const assertListboxIsOpen = listboxIsOpen(wrapper);
- const combobox = wrapper.find('[role="combobox"]');
- // Note: Combobox forwards events to Listbox via dispatchEvent, but this doesn't
- // work correctly within jsdom/enzyme so we fire the events directly on listbox
- const simulateArrowDownKeypress = simulateKeyDown(
- wrapper.find('ul[role="listbox"]'),
- 'ArrowDown'
- );
-
- combobox.simulate('focus');
- assertListboxIsOpen(true);
- expect(onActiveChange.notCalled).toBeTruthy();
- simulateArrowDownKeypress();
- expect(onActiveChange.callCount).toEqual(1);
- expect(onActiveChange.getCall(0).firstArg.value).toEqual('Apple');
- simulateArrowDownKeypress();
- expect(onActiveChange.callCount).toEqual(2);
- expect(onActiveChange.getCall(1).firstArg.value).toEqual('Banana');
- simulateArrowDownKeypress();
- expect(onActiveChange.callCount).toEqual(3);
- expect(onActiveChange.getCall(2).firstArg.value).toEqual('Cantaloupe');
-});
-
-test('should set input value to empty string on open with selected option', () => {
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
-
- const assertListboxIsOpen = listboxIsOpen(wrapper);
- const combobox = wrapper.find('[role="combobox"]');
-
- combobox.simulate('focus');
- assertListboxIsOpen(true);
- wrapper.find('[role="option"]').at(1).simulate('click');
- combobox.simulate('blur');
- assertListboxIsOpen(false);
- combobox.simulate('focus');
- expect(wrapper.find('input[role="combobox"]').prop('value')).toEqual('');
-});
-
-test('should restore input value to selected value on close with selected option', () => {
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
-
- const assertListboxIsOpen = listboxIsOpen(wrapper);
- const combobox = wrapper.find('[role="combobox"]');
-
- combobox.simulate('focus');
- assertListboxIsOpen(true);
- wrapper.find('[role="option"]').at(1).simulate('click');
- combobox.simulate('blur');
- assertListboxIsOpen(false);
- combobox.simulate('focus');
- combobox.simulate('blur');
- expect(wrapper.find('input[role="combobox"]').prop('value')).toEqual(
- 'Banana'
- );
-});
-
-test('should handle selection with "click" event', () => {
- const onSelectionChange = spy();
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
-
- const assertListboxIsOpen = listboxIsOpen(wrapper);
- const assertOptionIsSelected = optionIsSelected(wrapper);
- const combobox = wrapper.find('[role="combobox"]');
-
- combobox.simulate('focus');
- assertListboxIsOpen(true);
- expect(onSelectionChange.notCalled).toBeTruthy();
- wrapper.find('[role="option"]').at(1).simulate('click');
- wrapper.update();
- expect(onSelectionChange.callCount).toEqual(1);
- expect(onSelectionChange.firstCall.firstArg.value).toEqual('Banana');
- assertOptionIsSelected(0);
- combobox.simulate('click');
- wrapper.find('[role="option"]').at(2).simulate('click');
- expect(onSelectionChange.secondCall.firstArg.value).toEqual('Cantaloupe');
- assertOptionIsSelected(0);
-});
-
-test('should handle selection with "enter" keydown event', () => {
- const onSelectionChange = spy();
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
-
- const assertListboxIsOpen = listboxIsOpen(wrapper);
- const assertOptionIsSelected = optionIsSelected(wrapper);
- const combobox = wrapper.find('[role="combobox"]');
-
- // Note: Combobox forwards events to Listbox via dispatchEvent, but this doesn't
- // work correctly within jsdom/enzyme so we fire the events directly on listbox
- const simulateArrowDownKeypress = simulateKeyDown(
- wrapper.find('ul[role="listbox"]'),
- 'ArrowDown'
- );
- const simulateEnterKeypress = simulateKeyDown(
- wrapper.find('ul[role="listbox"]'),
- 'Enter'
- );
-
- combobox.simulate('focus');
- assertListboxIsOpen(true);
- expect(onSelectionChange.notCalled).toBeTruthy();
- simulateArrowDownKeypress();
- simulateEnterKeypress();
- expect(onSelectionChange.callCount).toEqual(1);
- expect(onSelectionChange.firstCall.firstArg.value).toEqual('Apple');
- combobox.simulate('click');
- assertOptionIsSelected(0);
- simulateArrowDownKeypress();
- simulateEnterKeypress();
- expect(onSelectionChange.secondCall.firstArg.value).toEqual('Banana');
- combobox.simulate('click');
- assertOptionIsSelected(1);
- simulateArrowDownKeypress();
- simulateEnterKeypress();
-});
-
-test('should always render all options when autocomplete="none"', () => {
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
-
- const assertListboxIsOpen = listboxIsOpen(wrapper);
- const combobox = wrapper.find('[role="combobox"]');
-
- combobox.simulate('focus');
- assertListboxIsOpen(true);
- combobox.simulate('change', { target: { value: 'a' } });
- wrapper.update();
- expect(wrapper.find('.ComboboxOption[role="option"]').length).toEqual(3);
- combobox.simulate('change', { target: { value: 'ap' } });
- wrapper.update();
- expect(wrapper.find('.ComboboxOption[role="option"]').length).toEqual(3);
- combobox.simulate('change', { target: { value: 'apple' } });
- wrapper.update();
- expect(wrapper.find('.ComboboxOption[role="option"]').length).toEqual(3);
-});
-
-test('should render matching options when autocomplete="manual"', () => {
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
-
- const assertListboxIsOpen = listboxIsOpen(wrapper);
- const combobox = wrapper.find('[role="combobox"]');
-
- combobox.simulate('focus');
- assertListboxIsOpen(true);
- combobox.simulate('change', { target: { value: 'a' } });
- wrapper.update();
- expect(wrapper.find('.ComboboxOption[role="option"]').length).toEqual(3);
- combobox.simulate('change', { target: { value: 'ap' } });
- wrapper.update();
- expect(wrapper.find('.ComboboxOption[role="option"]').length).toEqual(1);
- expect(wrapper.find('.ComboboxOption[role="option"]').text()).toEqual(
- 'Apple'
- );
-});
-
-test('should render results not found when no options match when autocomplete="manual"', () => {
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
-
- const assertListboxIsOpen = listboxIsOpen(wrapper);
- const combobox = wrapper.find('[role="combobox"]');
-
- combobox.simulate('focus');
- assertListboxIsOpen(true);
- combobox.simulate('change', { target: { value: 'x' } });
- wrapper.update();
- expect(wrapper.find('.ComboboxOption[role="option"]').length).toEqual(0);
- expect(wrapper.find('.ComboboxListbox__empty').text()).toEqual(
- 'No results found.'
- );
-});
-
-test('should render results not found render function when no options match when autocomplete="manual"', () => {
- const wrapper = mount(
- (
- Yo, no results found here.
- )}
- >
- Apple
- Banana
- Cantaloupe
-
- );
-
- const assertListboxIsOpen = listboxIsOpen(wrapper);
- const combobox = wrapper.find('[role="combobox"]');
-
- combobox.simulate('focus');
- assertListboxIsOpen(true);
- combobox.simulate('change', { target: { value: 'x' } });
- wrapper.update();
- expect(wrapper.find('.ComboboxOption[role="option"]').length).toEqual(0);
- expect(wrapper.find('.no-results').text()).toEqual(
- 'Yo, no results found here.'
- );
-});
-
-test('should render results not found render component when no options match when autocomplete="manual"', () => {
- const wrapper = mount(
- Yo, no results found here.
- }
- >
- Apple
- Banana
- Cantaloupe
-
- );
-
- const assertListboxIsOpen = listboxIsOpen(wrapper);
- const combobox = wrapper.find('[role="combobox"]');
-
- combobox.simulate('focus');
- assertListboxIsOpen(true);
- combobox.simulate('change', { target: { value: 'x' } });
- wrapper.update();
- expect(wrapper.find('.ComboboxOption[role="option"]').length).toEqual(0);
- expect(wrapper.find('.no-results').text()).toEqual(
- 'Yo, no results found here.'
- );
-});
-
-test('should render matching options when autocomplete="automatic"', () => {
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
-
- const assertListboxIsOpen = listboxIsOpen(wrapper);
- const combobox = wrapper.find('[role="combobox"]');
-
- combobox.simulate('focus');
- assertListboxIsOpen(true);
- combobox.simulate('change', { target: { value: 'a' } });
- wrapper.update();
- expect(wrapper.find('.ComboboxOption[role="option"]').length).toEqual(3);
- combobox.simulate('change', { target: { value: 'ap' } });
- wrapper.update();
- expect(wrapper.find('.ComboboxOption[role="option"]').length).toEqual(1);
- expect(wrapper.find('.ComboboxOption[role="option"]').text()).toEqual(
- 'Apple'
- );
-});
-
-test('should render results not found when no options match when autocomplete="automatic"', () => {
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
-
- const assertListboxIsOpen = listboxIsOpen(wrapper);
- const combobox = wrapper.find('[role="combobox"]');
-
- combobox.simulate('focus');
- assertListboxIsOpen(true);
- combobox.simulate('change', { target: { value: 'x' } });
- wrapper.update();
- expect(wrapper.find('.ComboboxOption[role="option"]').length).toEqual(0);
- expect(wrapper.find('.ComboboxListbox__empty').text()).toEqual(
- 'No results found.'
- );
-});
-
-test('should set selected value to active descendent when autocomplete="automatic" loses focus', () => {
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
-
- const assertListboxIsOpen = listboxIsOpen(wrapper);
- const combobox = wrapper.find('[role="combobox"]');
- // Note: Combobox forwards events to Listbox via dispatchEvent, but this doesn't
- // work correctly within jsdom/enzyme so we fire the events directly on listbox
- const simulateArrowDownKeypress = simulateKeyDown(
- wrapper.find('ul[role="listbox"]'),
- 'ArrowDown'
- );
-
- combobox.simulate('focus');
- assertListboxIsOpen(true);
- // Note: first active item should already be "Apple", but since dispatchEvent is
- // used it's not triggered correctly in enzyme so we fire an initial event to
- // make the first value active
- simulateArrowDownKeypress();
- simulateArrowDownKeypress();
- combobox.simulate('blur');
- expect(wrapper.find('[role="combobox"]').prop('value')).toEqual('Banana');
-});
-
-test('should use id from props when set', () => {
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
- expect(wrapper.find('.Combobox').prop('id')).toEqual('this-is-a-combobox');
-});
-
-test('should set selected value with "defaultValue" prop', () => {
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
-
- const assertListboxIsOpen = listboxIsOpen(wrapper);
- const combobox = wrapper.find('[role="combobox"]');
- const assertOptionIsSelected = optionIsSelected(wrapper);
-
- expect(wrapper.find('input[role="combobox"]').prop('value')).toEqual(
- 'Banana'
- );
-
- combobox.simulate('focus');
- assertListboxIsOpen(true);
- assertOptionIsSelected(1);
-});
-
-test('should set selected value with "value" prop', () => {
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
-
- const assertListboxIsOpen = listboxIsOpen(wrapper);
- const combobox = wrapper.find('[role="combobox"]');
- const assertOptionIsSelected = optionIsSelected(wrapper);
-
- expect(wrapper.find('input[role="combobox"]').prop('value')).toEqual(
- 'Banana'
- );
-
- combobox.simulate('focus');
- assertListboxIsOpen(true);
- assertOptionIsSelected(1);
-});
-
-test('should not render hidden input when name is not provided', () => {
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
-
- expect(wrapper.find('input[type="hidden"]').exists()).toBeFalsy();
-});
-
-test('should render hidden input with value from text contents of ComboboxOption', () => {
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
-
- expect(wrapper.find('input[type="hidden"]').prop('value')).toEqual('Banana');
-});
-
-test('should render hidden input with value from value from ComboboxOption', () => {
- const wrapper = mount(
-
- 🍎
- 🍌
- 🍈
-
- );
-
- expect(wrapper.find('input[type="hidden"]').prop('value')).toEqual('Banana');
-});
-
-test('should render hidden input with value from formValue from ComboboxOption', () => {
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
-
- expect(wrapper.find('input[type="hidden"]').prop('value')).toEqual('2');
-});
-
-test('should support portal element for combobox listbos', () => {
- const element = document.createElement('div');
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
- expect(wrapper.find('Portal').exists()).toBeTruthy();
- expect(element.querySelector('ul[role="listbox"]')).toBeTruthy();
-});
-
-test('should support portal element ref for combobox listbos', () => {
- const element = document.createElement('div');
- const wrapper = mount(
-
- Apple
- Banana
- Cantaloupe
-
- );
- expect(wrapper.find('Portal').exists()).toBeTruthy();
- expect(element.querySelector('ul[role="listbox"]')).toBeTruthy();
-});
diff --git a/packages/react/package.json b/packages/react/package.json
index 2803aa814..03cf2fcbf 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -43,16 +43,17 @@
"@rollup/plugin-dynamic-import-vars": "^1.4.2",
"@rollup/plugin-typescript": "^11.1.2",
"@svgr/rollup": "^6.1.2",
- "@testing-library/react": "11.1.2",
"@testing-library/jest-dom": "^6.1.3",
+ "@testing-library/react": "11.1.2",
"@types/classnames": "^2.2.10",
+ "@types/enzyme-adapter-react-16": "^1.0.9",
+ "@types/jest": "^24.7.1",
+ "@types/jest-axe": "^3.5.4",
"@types/node": "^17.0.42",
"@types/react": "^18.0.12",
"@types/react-dom": "^18.0.5",
- "@types/jest-axe": "^3.5.4",
- "@types/jest": "^24.7.1",
"@types/react-syntax-highlighter": "^15.5.2",
- "@types/enzyme-adapter-react-16": "^1.0.9",
+ "@types/sinon": "^10",
"autoprefixer": "^9.7.6",
"babel-plugin-module-resolver": "^4.0.0",
"babel-plugin-transform-export-extensions": "^6.22.0",
diff --git a/packages/react/src/components/Combobox/Combobox.test.tsx b/packages/react/src/components/Combobox/Combobox.test.tsx
new file mode 100644
index 000000000..9938dbeb7
--- /dev/null
+++ b/packages/react/src/components/Combobox/Combobox.test.tsx
@@ -0,0 +1,914 @@
+import React from 'react';
+import {
+ render,
+ screen,
+ waitFor,
+ fireEvent,
+ createEvent
+} from '@testing-library/react';
+import { within } from '@testing-library/dom';
+import { spy } from 'sinon';
+import Combobox from './Combobox';
+import ComboboxGroup from './ComboboxGroup';
+import ComboboxOption from './ComboboxOption';
+
+// Utility function for checking if the ComboboxListbox is currently open
+const assertListboxIsOpen = (isOpen: boolean) => {
+ const Listbox = screen.getByRole('listbox');
+ const input = screen.getByRole('combobox');
+ expect(JSON.parse(input.getAttribute('aria-expanded') + '')).toEqual(isOpen);
+ if (isOpen) {
+ expect(Array.from(Listbox.classList)).toContain('Combobox__listbox--open');
+ } else {
+ expect(Array.from(Listbox.classList)).not.toContain(
+ 'Combobox__listbox--open'
+ );
+ }
+};
+
+// Utility function for checking for active element for the given Combobox Option of a Combobox component
+const assertOptionIsActive = (index: number) => {
+ const combobox = screen.getByRole('combobox');
+ const options = screen.queryAllByRole('option');
+ const activeOption = options.at(index);
+ expect(combobox.getAttribute('aria-activedescendant')).toBeTruthy();
+ expect(combobox.getAttribute('aria-activedescendant')).toEqual(
+ activeOption?.getAttribute('id')
+ );
+ expect(
+ activeOption?.classList.contains('ComboboxOption--active')
+ ).toBeTruthy();
+ options.forEach(
+ (option, index) =>
+ index !== index &&
+ expect(option.classList.contains('ComboboxOption--active')).toBeFalsy()
+ );
+};
+
+// Utility function for checking for active element for the given Combobox Option of a Combobox component
+const assertOptionIsSelected = (index: number) => {
+ const options = screen.queryAllByRole('option');
+ const selectedOption = options.at(index);
+ expect(
+ JSON.parse(selectedOption?.getAttribute('aria-selected') + '')
+ ).toEqual(true);
+ options.forEach(
+ (option, index) =>
+ index !== index &&
+ expect(option.getAttribute('aria-selected')).toEqual(false)
+ );
+};
+
+test('should render combobox with options', () => {
+ const options = [
+ { value: 'Apple', label: 'Apple' },
+ { value: 'Banana', label: 'Banana' },
+ { value: 'Cantaloupe', label: 'Cantaloupe' }
+ ];
+ render();
+ expect(screen.queryByRole('combobox')).toBeTruthy();
+ expect(screen.queryAllByRole('option').at(0)?.innerText).toEqual('Apple');
+ expect(screen.queryAllByRole('option').at(1)?.innerText).toEqual('Banana');
+ expect(screen.queryAllByRole('option').at(2)?.innerText).toEqual(
+ 'Cantaloupe'
+ );
+});
+
+test('should render combobox with children', () => {
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+ expect(screen.queryByRole('combobox')).toBeTruthy();
+ expect(screen.queryAllByRole('option').at(0)?.innerText).toEqual('Apple');
+ expect(screen.queryAllByRole('option').at(1)?.innerText).toEqual('Banana');
+ expect(screen.queryAllByRole('option').at(2)?.innerText).toEqual(
+ 'Cantaloupe'
+ );
+});
+
+test('should render combobox options with description', () => {
+ const options = [
+ {
+ value: 'Apple',
+ label: 'Apple',
+ description:
+ 'A crispy orb of deliciousness that comes in colors from "stoplight red" to "alien green".'
+ },
+ {
+ value: 'Banana',
+ label: 'Banana',
+ description:
+ 'This curvy wonder brings the tropics to your taste buds with its sunny disposition.'
+ },
+ {
+ value: 'Cantaloupe',
+ label: 'Cantaloupe',
+ description: 'A bumpy, juicy, orb with moist deliciousness inside.'
+ }
+ ];
+ render();
+ expect(screen.queryByRole('combobox')).toBeTruthy();
+ expect(
+ screen
+ .queryAllByRole('option')
+ .at(0)
+ ?.querySelector('.ComboboxOption__description')?.textContent
+ ).toEqual(options[0].description);
+ expect(
+ screen
+ .queryAllByRole('option')
+ .at(1)
+ ?.querySelector('.ComboboxOption__description')?.textContent
+ ).toEqual(options[1].description);
+ expect(
+ screen
+ .queryAllByRole('option')
+ .at(2)
+ ?.querySelector('.ComboboxOption__description')?.textContent
+ ).toEqual(options[2].description);
+});
+
+test('should render combobox children with description', () => {
+ const options = [
+ {
+ value: 'Apple',
+ label: 'Apple',
+ description:
+ 'A crispy orb of deliciousness that comes in colors from "stoplight red" to "alien green".'
+ },
+ {
+ value: 'Banana',
+ label: 'Banana',
+ description:
+ 'This curvy wonder brings the tropics to your taste buds with its sunny disposition.'
+ },
+ {
+ value: 'Cantaloupe',
+ label: 'Cantaloupe',
+ description: 'A bumpy, juicy, orb with moist deliciousness inside.'
+ }
+ ];
+ const children = options.map(({ value, label, description }, index) => (
+
+ {label}
+
+ ));
+ render({children});
+ expect(screen.queryByRole('combobox')).toBeTruthy();
+ expect(
+ screen
+ .queryAllByRole('option')
+ .at(0)
+ ?.querySelector('.ComboboxOption__description')?.textContent
+ ).toEqual(options[0].description);
+ expect(
+ screen
+ .queryAllByRole('option')
+ .at(1)
+ ?.querySelector('.ComboboxOption__description')?.textContent
+ ).toEqual(options[1].description);
+ expect(
+ screen
+ .queryAllByRole('option')
+ .at(2)
+ ?.querySelector('.ComboboxOption__description')?.textContent
+ ).toEqual(options[2].description);
+});
+
+test('should render combobox with groups', () => {
+ render(
+
+
+ Apple
+ Banana
+ Cantaloupe
+
+
+ Artichoke
+ Broccoli
+ Carrots
+
+
+ );
+
+ const group1 = screen.queryAllByRole('group').at(0) as HTMLElement;
+ const group2 = screen.queryAllByRole('group').at(1) as HTMLElement;
+
+ expect(screen.queryByRole('combobox')).toBeTruthy();
+ expect(group1).toBeTruthy();
+ expect(group2).toBeTruthy();
+ expect(group1.tagName).toEqual('UL');
+ expect(group2.tagName).toEqual('UL');
+ expect(group1.getAttribute('aria-labelledby')).toBeTruthy();
+ expect(group2.getAttribute('aria-labelledby')).toBeTruthy();
+ expect(within(group1).queryAllByRole('option').at(0)?.innerText).toEqual(
+ 'Apple'
+ );
+ expect(within(group1).queryAllByRole('option').at(1)?.innerText).toEqual(
+ 'Banana'
+ );
+ expect(within(group1).queryAllByRole('option').at(2)?.innerText).toEqual(
+ 'Cantaloupe'
+ );
+ expect(within(group2).queryAllByRole('option').at(0)?.innerText).toEqual(
+ 'Artichoke'
+ );
+ expect(within(group2).queryAllByRole('option').at(1)?.innerText).toEqual(
+ 'Broccoli'
+ );
+ expect(within(group2).queryAllByRole('option').at(2)?.innerText).toEqual(
+ 'Carrots'
+ );
+});
+
+test('should render required combobox', () => {
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+
+ expect(screen.getByRole('combobox').hasAttribute('required')).toEqual(true);
+ expect(screen.queryByText('Required')).toBeTruthy();
+});
+
+test('should render combobox with error', () => {
+ const errorId = 'combo-error';
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+
+ expect(screen.queryByText('You forgot to choose a value.')).toBeTruthy();
+ expect(screen.getByRole('combobox').getAttribute('aria-describedby')).toBe(
+ `other-id ${errorId}`
+ );
+});
+
+test('should open combobox listbox on click', async () => {
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+
+ assertListboxIsOpen(false);
+ fireEvent.click(screen.getByRole('combobox'));
+ assertListboxIsOpen(true);
+});
+
+test('should maintain focus on combobox input on click', () => {
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+
+ assertListboxIsOpen(false);
+ expect(screen.getByRole('combobox')).not.toHaveFocus();
+ screen
+ .getByRole('combobox')
+ .addEventListener('click', (event: MouseEvent) => {
+ // simulate a focus from a click
+ (event.target as HTMLInputElement)?.focus();
+ });
+ fireEvent.click(screen.getByRole('combobox'));
+ assertListboxIsOpen(true);
+ expect(screen.getByRole('combobox')).toHaveFocus();
+});
+
+test('should allow an input ref to be passed to the combobox', () => {
+ const inputRef = React.createRef();
+ render(
+
+ Apple
+
+ );
+
+ expect(inputRef.current).toBeTruthy();
+ expect(inputRef.current).toEqual(screen.getByRole('combobox'));
+});
+
+test('should open combobox listbox on focus', () => {
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+
+ assertListboxIsOpen(false);
+ fireEvent.focus(screen.getByRole('combobox'));
+ assertListboxIsOpen(true);
+});
+
+test('should open combobox listbox on keypress', () => {
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+
+ assertListboxIsOpen(false);
+ fireEvent.keyDown(screen.getByRole('combobox'), { key: 'ArrowDown' });
+ assertListboxIsOpen(true);
+});
+
+test('should close combobox listbox on "esc" keypress', () => {
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+
+ assertListboxIsOpen(false);
+ fireEvent.keyDown(screen.getByRole('combobox'), { key: 'ArrowDown' });
+ fireEvent.keyDown(screen.getByRole('combobox'), { key: 'A' });
+ assertListboxIsOpen(true);
+ fireEvent.keyDown(screen.getByRole('combobox'), { key: 'Escape' });
+ assertListboxIsOpen(false);
+});
+
+test('should not open combobox listbox on "enter" keypress', () => {
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+
+ assertListboxIsOpen(false);
+ fireEvent.keyDown(screen.getByRole('combobox'), { key: 'Enter' });
+ assertListboxIsOpen(false);
+});
+
+test('should close combobox listbox on "blur"', () => {
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+
+ assertListboxIsOpen(false);
+ fireEvent.focus(screen.getByRole('combobox'));
+ assertListboxIsOpen(true);
+ fireEvent.blur(screen.getByRole('combobox'));
+ assertListboxIsOpen(false);
+});
+
+test('should close combobox listbox when selecting option via click', async () => {
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+
+ assertListboxIsOpen(false);
+ screen.getByRole('combobox').focus();
+ assertListboxIsOpen(true);
+ fireEvent.click(screen.getAllByRole('option')[0]);
+ assertListboxIsOpen(false);
+});
+
+test('should prevent default on combobox listbox option via mousedown', () => {
+ const preventDefault = spy();
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+
+ const option = screen.getAllByRole('option')[0];
+ const fireMouseDownEvent = () => {
+ const event = createEvent.mouseDown(option);
+ // rtl doesn't let us mock preventDefault
+ // see: https://github.com/testing-library/react-testing-library/issues/572
+ event.preventDefault = preventDefault;
+ fireEvent(option, event);
+ };
+
+ assertListboxIsOpen(false);
+ fireEvent.focus(screen.getByRole('combobox'));
+ assertListboxIsOpen(true);
+ expect(preventDefault.notCalled).toBeTruthy();
+ fireMouseDownEvent();
+ expect(preventDefault.calledOnce).toBeTruthy();
+});
+
+test('should close combobox listbox when selecting option via keypress', () => {
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+ const combobox = screen.getByRole('combobox');
+
+ assertListboxIsOpen(false);
+ fireEvent.focus(combobox);
+ assertListboxIsOpen(true);
+ fireEvent.keyDown(combobox, { key: 'ArrowDown' });
+ fireEvent.keyDown(combobox, { key: 'Enter' });
+ assertListboxIsOpen(false);
+});
+
+test('should set aria-activedescendent for active combobox options', () => {
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+ const combobox = screen.getByRole('combobox');
+ // Note: Combobox forwards events to Listbox via dispatchEvent, but this doesn't
+ // work correctly within jsdom so we fire the events directly on listbox
+ const fireArrowDownKeyPress = () =>
+ fireEvent.keyDown(screen.getByRole('listbox'), { key: 'ArrowDown' });
+
+ expect(combobox.hasAttribute('aria-activedescendant')).toBeFalsy();
+ fireEvent.focus(combobox);
+ assertListboxIsOpen(true);
+
+ fireArrowDownKeyPress();
+ assertOptionIsActive(0);
+ fireArrowDownKeyPress();
+ assertOptionIsActive(1);
+ fireArrowDownKeyPress();
+ assertOptionIsActive(2);
+});
+
+test('should prevent default event on home/end keypress', () => {
+ const preventDefault = spy();
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+
+ const combobox = screen.getByRole('combobox');
+ const fireKeydownEvent = (key: string) => {
+ const event = createEvent.keyDown(combobox, { key });
+ // rtl doesn't let us mock preventDefault
+ // see: https://github.com/testing-library/react-testing-library/issues/572
+ event.preventDefault = preventDefault;
+ fireEvent(combobox, event);
+ };
+ expect(preventDefault.notCalled).toBeTruthy();
+ fireKeydownEvent('Home');
+ expect(preventDefault.callCount).toEqual(1);
+ fireKeydownEvent('End');
+ expect(preventDefault.callCount).toEqual(2);
+ fireKeydownEvent('ArrowDown');
+ expect(preventDefault.callCount).toEqual(2);
+});
+
+test('should call onActiveChange when active option changes', () => {
+ const onActiveChange = spy();
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+ const combobox = screen.getByRole('combobox');
+ // Note: Combobox forwards events to Listbox via dispatchEvent, but this doesn't
+ // work correctly within jsdom so we fire the events directly on listbox
+ const fireArrowDownKeyPress = () =>
+ fireEvent.keyDown(screen.getByRole('listbox'), { key: 'ArrowDown' });
+
+ fireEvent.focus(combobox);
+ assertListboxIsOpen(true);
+ expect(onActiveChange.notCalled).toBeTruthy();
+ fireArrowDownKeyPress();
+ expect(onActiveChange.callCount).toEqual(1);
+ expect(onActiveChange.getCall(0).firstArg.value).toEqual('Apple');
+ fireArrowDownKeyPress();
+ expect(onActiveChange.callCount).toEqual(2);
+ expect(onActiveChange.getCall(1).firstArg.value).toEqual('Banana');
+ fireArrowDownKeyPress();
+ expect(onActiveChange.callCount).toEqual(3);
+ expect(onActiveChange.getCall(2).firstArg.value).toEqual('Cantaloupe');
+});
+
+test('should set input value to empty string on open with selected option', () => {
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+ const combobox = screen.getByRole('combobox');
+
+ fireEvent.focus(combobox);
+ assertListboxIsOpen(true);
+ fireEvent.click(screen.getAllByRole('option')[1]);
+ fireEvent.blur(combobox);
+ assertListboxIsOpen(false);
+ fireEvent.focus(combobox);
+ expect(screen.getByRole('combobox').getAttribute('value')).toEqual('');
+});
+
+test('should restore input value to selected value on close with selected option', () => {
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+ const combobox = screen.getByRole('combobox');
+
+ fireEvent.focus(combobox);
+ assertListboxIsOpen(true);
+ fireEvent.click(screen.getAllByRole('option')[1]);
+ fireEvent.blur(combobox);
+ assertListboxIsOpen(false);
+ fireEvent.focus(combobox);
+ fireEvent.blur(combobox);
+ expect(screen.getByRole('combobox').getAttribute('value')).toEqual('Banana');
+});
+
+test('should handle selection with "click" event', () => {
+ const onSelectionChange = spy();
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+ const combobox = screen.getByRole('combobox');
+
+ fireEvent.focus(combobox);
+ assertListboxIsOpen(true);
+ expect(onSelectionChange.notCalled).toBeTruthy();
+ fireEvent.click(screen.getAllByRole('option')[1]);
+ expect(onSelectionChange.callCount).toEqual(1);
+ expect(onSelectionChange.firstCall.firstArg.value).toEqual('Banana');
+ assertOptionIsSelected(0);
+ fireEvent.click(combobox);
+ fireEvent.click(screen.getAllByRole('option')[2]);
+ expect(onSelectionChange.secondCall.firstArg.value).toEqual('Cantaloupe');
+ assertOptionIsSelected(0);
+});
+
+test('should handle selection with "enter" keydown event', () => {
+ const onSelectionChange = spy();
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+ const combobox = screen.getByRole('combobox');
+
+ // Note: Combobox forwards events to Listbox via dispatchEvent, but this doesn't
+ // work correctly within jsdom/enzyme so we fire the events directly on listbox
+ const fireArrowDownKeyPress = () =>
+ fireEvent.keyDown(screen.getByRole('listbox'), { key: 'ArrowDown' });
+ const fireEnterKeyPress = () =>
+ fireEvent.keyDown(screen.getByRole('listbox'), { key: 'Enter' });
+
+ fireEvent.focus(combobox);
+ assertListboxIsOpen(true);
+ expect(onSelectionChange.notCalled).toBeTruthy();
+ fireArrowDownKeyPress();
+ fireEnterKeyPress();
+ expect(onSelectionChange.callCount).toEqual(1);
+ expect(onSelectionChange.firstCall.firstArg.value).toEqual('Apple');
+ fireEvent.click(combobox);
+ assertOptionIsSelected(0);
+ fireArrowDownKeyPress();
+ fireEnterKeyPress();
+ expect(onSelectionChange.secondCall.firstArg.value).toEqual('Banana');
+ fireEvent.click(combobox);
+ assertOptionIsSelected(1);
+ fireArrowDownKeyPress();
+ fireEnterKeyPress();
+});
+
+test('should always render all options when autocomplete="none"', () => {
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+ const combobox = screen.getByRole('combobox');
+
+ fireEvent.focus(combobox);
+ assertListboxIsOpen(true);
+ fireEvent.change(combobox, { target: { value: 'a' } });
+ expect(screen.getAllByRole('option').length).toEqual(3);
+ fireEvent.change(combobox, { target: { value: 'ap' } });
+ expect(screen.getAllByRole('option').length).toEqual(3);
+ fireEvent.change(combobox, { target: { value: 'apple' } });
+ expect(screen.getAllByRole('option').length).toEqual(3);
+});
+
+test('should render matching options when autocomplete="manual"', () => {
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+ const combobox = screen.getByRole('combobox');
+
+ fireEvent.focus(combobox);
+ assertListboxIsOpen(true);
+ fireEvent.change(combobox, { target: { value: 'a' } });
+ expect(screen.getAllByRole('option').length).toEqual(3);
+ fireEvent.change(combobox, { target: { value: 'ap' } });
+ expect(screen.getAllByRole('option').length).toEqual(1);
+ expect(screen.getByRole('option').innerText).toEqual('Apple');
+});
+
+test('should render results not found when no options match when autocomplete="manual"', () => {
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+ const combobox = screen.getByRole('combobox');
+
+ fireEvent.focus(combobox);
+ assertListboxIsOpen(true);
+ fireEvent.change(combobox, { target: { value: 'x' } });
+ expect(screen.queryAllByRole('option').length).toEqual(0);
+ expect(screen.queryByText('No results found.')).toBeTruthy();
+});
+
+test('should render results not found render function when no options match when autocomplete="manual"', () => {
+ render(
+ (
+ Yo, no results found here.
+ )}
+ >
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+ const combobox = screen.getByRole('combobox');
+
+ fireEvent.focus(combobox);
+ assertListboxIsOpen(true);
+ fireEvent.change(combobox, { target: { value: 'x' } });
+ expect(screen.queryAllByRole('option').length).toEqual(0);
+ expect(screen.queryByText('Yo, no results found here.')).toBeTruthy();
+});
+
+test('should render results not found render component when no options match when autocomplete="manual"', () => {
+ render(
+ Yo, no results found here.
+ }
+ >
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+ const combobox = screen.getByRole('combobox');
+
+ fireEvent.focus(combobox);
+ assertListboxIsOpen(true);
+ fireEvent.change(combobox, { target: { value: 'x' } });
+ expect(screen.queryAllByRole('option').length).toEqual(0);
+ expect(screen.queryByText('Yo, no results found here.')).toBeTruthy();
+});
+
+test('should render matching options when autocomplete="automatic"', () => {
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+ const combobox = screen.getByRole('combobox');
+
+ fireEvent.focus(combobox);
+ assertListboxIsOpen(true);
+ fireEvent.change(combobox, { target: { value: 'a' } });
+ expect(screen.getAllByRole('option').length).toEqual(3);
+ fireEvent.change(combobox, { target: { value: 'ap' } });
+ expect(screen.getAllByRole('option').length).toEqual(1);
+ expect(screen.getByRole('option').innerText).toEqual('Apple');
+});
+
+test('should render results not found when no options match when autocomplete="automatic"', () => {
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+ const combobox = screen.getByRole('combobox');
+
+ fireEvent.focus(combobox);
+ assertListboxIsOpen(true);
+ fireEvent.change(combobox, { target: { value: 'x' } });
+ expect(screen.queryAllByRole('option').length).toEqual(0);
+ expect(screen.queryByText('No results found.')).toBeTruthy();
+});
+
+test('should set selected value to active descendent when autocomplete="automatic" loses focus', () => {
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+ const combobox = screen.getByRole('combobox');
+ // Note: Combobox forwards events to Listbox via dispatchEvent, but this doesn't
+ // work correctly within jsdom so we fire the events directly on listbox
+ const fireArrowDownKeyPress = () =>
+ fireEvent.keyDown(screen.getByRole('listbox'), { key: 'ArrowDown' });
+
+ fireEvent.focus(combobox);
+ assertListboxIsOpen(true);
+ fireArrowDownKeyPress();
+ fireEvent.blur(combobox);
+ expect(screen.getByRole('combobox').getAttribute('value')).toEqual('Banana');
+});
+
+test('should use id from props when set', () => {
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+ expect(document.querySelector('.Combobox')?.getAttribute('id')).toEqual(
+ 'this-is-a-combobox'
+ );
+});
+
+test('should set selected value with "defaultValue" prop', () => {
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+ const combobox = screen.getByRole('combobox');
+
+ expect(screen.getByRole('combobox').getAttribute('value')).toEqual('Banana');
+
+ fireEvent.focus(combobox);
+ assertListboxIsOpen(true);
+ assertOptionIsSelected(1);
+});
+
+test('should set selected value with "value" prop', () => {
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+ const combobox = screen.getByRole('combobox');
+
+ expect(screen.getByRole('combobox').getAttribute('value')).toEqual('Banana');
+
+ fireEvent.focus(combobox);
+ assertListboxIsOpen(true);
+ assertOptionIsSelected(1);
+});
+
+test('should not render hidden input when name is not provided', () => {
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+
+ expect(document.querySelector('input[type="hidden"]')).toBeFalsy();
+});
+
+test('should render hidden input with value from text contents of ComboboxOption', () => {
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+
+ expect(
+ document.querySelector('input[type="hidden"]')?.getAttribute('value')
+ ).toEqual('Banana');
+});
+
+test('should render hidden input with value from value from ComboboxOption', () => {
+ render(
+
+ 🍎
+ 🍌
+ 🍈
+
+ );
+
+ expect(
+ document.querySelector('input[type="hidden"]')?.getAttribute('value')
+ ).toEqual('Banana');
+});
+
+test('should render hidden input with value from formValue from ComboboxOption', () => {
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+
+ expect(
+ document.querySelector('input[type="hidden"]')?.getAttribute('value')
+ ).toEqual('2');
+});
+
+test('should support portal element for combobox listbox', () => {
+ const element = document.createElement('div');
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+ expect(
+ element.contains(element.querySelector('[role="listbox"]'))
+ ).toBeTruthy();
+});
+
+test('should support portal element ref for combobox listbox', () => {
+ const element = document.createElement('div');
+ render(
+
+ Apple
+ Banana
+ Cantaloupe
+
+ );
+ expect(
+ element.contains(element.querySelector('[role="listbox"]'))
+ ).toBeTruthy();
+});
diff --git a/packages/react/src/components/Combobox/Combobox.tsx b/packages/react/src/components/Combobox/Combobox.tsx
index d4e780bfd..abba6b71d 100644
--- a/packages/react/src/components/Combobox/Combobox.tsx
+++ b/packages/react/src/components/Combobox/Combobox.tsx
@@ -219,7 +219,9 @@ const Combobox = forwardRef(
const handleComboboxOptionClick = useCallback(() => {
// maintain focus on the input
- inputRef.current?.focus();
+ if (inputRef.current !== document.activeElement) {
+ inputRef.current?.focus();
+ }
}, []);
const handleBlur = useCallback(
diff --git a/packages/react/yarn.lock b/packages/react/yarn.lock
index 5be1814f5..ebfecf6c6 100644
--- a/packages/react/yarn.lock
+++ b/packages/react/yarn.lock
@@ -1790,6 +1790,18 @@
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5"
integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==
+"@types/sinon@^10":
+ version "10.0.20"
+ resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-10.0.20.tgz#f1585debf4c0d99f9938f4111e5479fb74865146"
+ integrity sha512-2APKKruFNCAZgx3daAyACGzWuJ028VVCUDk6o2rw/Z4PXT0ogwdV4KUegW0MwVs0Zu59auPXbbuBJHF12Sx1Eg==
+ dependencies:
+ "@types/sinonjs__fake-timers" "*"
+
+"@types/sinonjs__fake-timers@*":
+ version "8.1.5"
+ resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz#5fd3592ff10c1e9695d377020c033116cc2889f2"
+ integrity sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==
+
"@types/stack-utils@^1.0.1":
version "1.0.1"
resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz"