From 6e693314c635f993564ad6f6df0cb9e2f85f0bc8 Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Sun, 18 Sep 2022 12:53:23 +0300 Subject: [PATCH] feat(useSelect): migrate to the 1.2 ARIA pattern --- cypress/e2e/combobox.cy.js | 4 +- cypress/e2e/useMultipleCombobox.cy.js | 16 + cypress/e2e/useMultipleSelect.cy.js | 16 + cypress/e2e/useSelect.cy.js | 6 +- docusaurus/pages/combobox.js | 7 +- docusaurus/pages/useCombobox.js | 7 +- docusaurus/pages/useMultipleCombobox.js | 4 +- docusaurus/pages/useMultipleSelect.js | 22 +- docusaurus/pages/useSelect.js | 20 +- docusaurus/utils.js | 1 + package.json | 2 +- src/hooks/reducer.js | 1 - src/hooks/testUtils.js | 74 +- .../__tests__/getComboboxProps.test.js | 5 +- .../__tests__/getInputProps.test.js | 463 ++++--- .../__tests__/getItemProps.test.js | 71 +- .../__tests__/getMenuProps.test.js | 17 +- .../__tests__/getToggleButtonProps.test.js | 96 +- src/hooks/useCombobox/__tests__/memo.test.js | 15 +- src/hooks/useCombobox/__tests__/props.test.js | 485 ++++---- src/hooks/useCombobox/testUtils.js | 78 +- .../__tests__/getDropdownProps.test.js | 105 +- .../__tests__/getSelectedItemProps.test.js | 213 ++-- .../__tests__/props.test.js | 207 ++-- src/hooks/useMultipleSelection/testUtils.js | 87 +- src/hooks/useSelect/MIGRATION_V7.md | 171 +++ src/hooks/useSelect/README.md | 148 ++- .../useSelect/__tests__/getItemProps.test.js | 140 ++- .../useSelect/__tests__/getMenuProps.test.js | 813 +----------- .../__tests__/getToggleButtonProps.test.js | 1103 ++++++++++++++--- src/hooks/useSelect/__tests__/memo.test.js | 19 +- src/hooks/useSelect/__tests__/props.test.js | 851 ++++--------- src/hooks/useSelect/index.js | 205 ++- src/hooks/useSelect/reducer.js | 145 +-- src/hooks/useSelect/stateChangeTypes.js | 45 +- src/hooks/useSelect/testUtils.js | 286 ++++- src/hooks/useSelect/utils.ts | 1 - src/hooks/utils.js | 19 +- typings/index.d.ts | 35 +- 39 files changed, 2982 insertions(+), 3021 deletions(-) create mode 100644 cypress/e2e/useMultipleCombobox.cy.js create mode 100644 cypress/e2e/useMultipleSelect.cy.js create mode 100644 docusaurus/utils.js create mode 100644 src/hooks/useSelect/MIGRATION_V7.md diff --git a/cypress/e2e/combobox.cy.js b/cypress/e2e/combobox.cy.js index 3e20cb657..488036b4e 100644 --- a/cypress/e2e/combobox.cy.js +++ b/cypress/e2e/combobox.cy.js @@ -22,7 +22,7 @@ describe('combobox', () => { it('can arrow up to select last item', () => { cy.findByTestId('combobox-input') .type('{uparrow}{enter}') // open menu, last option is focused - .should('have.value', 'Purple') + .should('have.value', 'Skyblue') }) it('can arrow down to select first item', () => { @@ -46,7 +46,7 @@ describe('combobox', () => { it('can use end arrow to select last item', () => { cy.findByTestId('combobox-input') .type('{downarrow}{end}{enter}') // open to first, go to last by end. - .should('have.value', 'Purple') + .should('have.value', 'Skyblue') }) it('resets the item on blur', () => { diff --git a/cypress/e2e/useMultipleCombobox.cy.js b/cypress/e2e/useMultipleCombobox.cy.js new file mode 100644 index 000000000..20fec955b --- /dev/null +++ b/cypress/e2e/useMultipleCombobox.cy.js @@ -0,0 +1,16 @@ +describe('useMultipleCombobox', () => { + before(() => { + cy.visit('/useMultipleCombobox') + }) + + it('can select multiple items', () => { + cy.findByRole('button', {name: 'toggle menu'}).click() + cy.findByRole('option', {name: 'Green'}).click() + cy.findByRole('option', {name: 'Gray'}).click() + cy.findByRole('button', {name: 'toggle menu'}).click() + cy.findByText('Black').should('be.visible') + cy.findByText('Red').should('be.visible') + cy.findByText('Green').should('be.visible') + cy.findByText('Gray').should('be.visible') + }) +}) diff --git a/cypress/e2e/useMultipleSelect.cy.js b/cypress/e2e/useMultipleSelect.cy.js new file mode 100644 index 000000000..26b6dd102 --- /dev/null +++ b/cypress/e2e/useMultipleSelect.cy.js @@ -0,0 +1,16 @@ +describe('useMultipleSelect', () => { + before(() => { + cy.visit('/useMultipleSelect') + }) + + it('can select multiple options', () => { + cy.findByRole('combobox').click() + cy.findByRole('option', {name: 'Green'}).click() + cy.findByRole('option', {name: 'Gray'}).click() + cy.findByRole('combobox').click() + cy.findByText('Black').should('be.visible') + cy.findByText('Red').should('be.visible') + cy.findByText('Green').should('be.visible') + cy.findByText('Gray').should('be.visible') + }) +}) diff --git a/cypress/e2e/useSelect.cy.js b/cypress/e2e/useSelect.cy.js index 786b70ef6..ca84646d2 100644 --- a/cypress/e2e/useSelect.cy.js +++ b/cypress/e2e/useSelect.cy.js @@ -4,15 +4,15 @@ describe('useSelect', () => { }) it('can open and close a menu', () => { - cy.findByTestId('select-toggle-button') + cy.findByRole('combobox') .click() cy.findAllByRole('option') .should('have.length.above', 0) - cy.findByTestId('select-toggle-button') + cy.findByRole('combobox') .click() cy.findAllByRole('option') .should('have.length', 0) - cy.findByTestId('select-toggle-button') + cy.findByRole('combobox') .click() cy.findAllByRole('option') .should('have.length.above', 0) diff --git a/docusaurus/pages/combobox.js b/docusaurus/pages/combobox.js index 4a096be04..cc2c84c6c 100644 --- a/docusaurus/pages/combobox.js +++ b/docusaurus/pages/combobox.js @@ -1,10 +1,9 @@ import * as React from 'react' import Downshift from '../../src' +import {colors} from '../utils' export default function ComboBox() { - const items = ['Black', 'Red', 'Green', 'Blue', 'Orange', 'Purple'] - return ( {({ @@ -73,10 +72,10 @@ export default function ComboBox() { > {isOpen && (inputValue - ? items.filter(i => + ? colors.filter(i => i.toLowerCase().includes(inputValue.toLowerCase()), ) - : items + : colors ).map((item, index) => (
  • { setInputItems( - items.filter(item => + colors.filter(item => item.toLowerCase().startsWith(inputValue.toLowerCase()), ), ) diff --git a/docusaurus/pages/useMultipleCombobox.js b/docusaurus/pages/useMultipleCombobox.js index 6651ad261..2cd551c38 100644 --- a/docusaurus/pages/useMultipleCombobox.js +++ b/docusaurus/pages/useMultipleCombobox.js @@ -1,8 +1,8 @@ import React from 'react' import {useCombobox, useMultipleSelection} from '../../src' +import {colors} from '../utils' -const colors = ['Black', 'Red', 'Green', 'Blue', 'Orange', 'Purple'] const initialSelectedItems = [colors[0], colors[1]] function getFilteredItems(selectedItems, inputValue) { @@ -166,7 +166,7 @@ export default function DropdownMultipleCombobox() { +
      Choose an element: - +
        {isOpen && - items.map((item, index) => ( + colors.map((item, index) => (
      • `item-id-${index}`, + input: 'input-id', + selectedItemPrefix: 'selected-item-id', + selectedItem: index => `selected-item-id-${index}`, +} + +export const defaultIds = { labelId: 'downshift-test-id-label', menuId: 'downshift-test-id-menu', getItemId: index => `downshift-test-id-item-${index}`, @@ -38,14 +48,13 @@ const defaultIds = { inputId: 'downshift-test-id-input', } -const waitForDebouncedA11yStatusUpdate = () => +export const waitForDebouncedA11yStatusUpdate = () => act(() => jest.advanceTimersByTime(200)) -const MemoizedItem = React.memo(function Item({ +export const MemoizedItem = React.memo(function Item({ index, item, getItemProps, - dataTestIds, stringItem, ...rest }) { @@ -60,4 +69,55 @@ const MemoizedItem = React.memo(function Item({ ) }) -export {items, defaultIds, waitForDebouncedA11yStatusUpdate, MemoizedItem} +export const user = userEvent.setup({delay: null}) + +export function getLabel() { + return screen.getByText(/choose an element/i) +} +export function getMenu() { + return screen.getByRole('listbox') +} +export function getToggleButton() { + return screen.getByTestId(dataTestIds.toggleButton) +} +export function getItemAtIndex(index) { + return getItems()[index] +} +export function getItems() { + return screen.queryAllByRole('option') +} +export function getInput() { + return screen.getByRole('textbox') +} +export async function clickOnItemAtIndex(index) { + await user.click(getItemAtIndex(index)) +} +export async function clickOnToggleButton() { + await user.click(getToggleButton()) +} +export async function mouseMoveItemAtIndex(index) { + await user.hover(getItemAtIndex(index)) +} +export async function mouseLeaveItemAtIndex(index) { + await user.unhover(getItemAtIndex(index)) +} +export async function keyDownOnToggleButton(keys) { + if (document.activeElement !== getToggleButton()) { + getToggleButton().focus() + } + + await user.keyboard(keys) +} +export async function keyDownOnInput(keys) { + if (document.activeElement !== getInput()) { + getInput().focus() + } + + await user.keyboard(keys) +} +export function getA11yStatusContainer() { + return screen.queryByRole('status') +} +export async function tab(shiftKey = false) { + await user.tab({shift: shiftKey}) +} diff --git a/src/hooks/useCombobox/__tests__/getComboboxProps.test.js b/src/hooks/useCombobox/__tests__/getComboboxProps.test.js index 3f2f5cea4..aedaae1bf 100644 --- a/src/hooks/useCombobox/__tests__/getComboboxProps.test.js +++ b/src/hooks/useCombobox/__tests__/getComboboxProps.test.js @@ -1,8 +1,7 @@ import {act, renderHook} from '@testing-library/react-hooks' import {noop} from '../../../utils' -import {renderUseCombobox} from '../testUtils' -import {defaultIds, items} from '../../testUtils' -import useCombobox from '..' // eslint-disable-next-line import/default +import {renderUseCombobox, defaultIds, items} from '../testUtils' +import useCombobox from '..' import utils from '../../utils' describe('getComboboxProps', () => { diff --git a/src/hooks/useCombobox/__tests__/getInputProps.test.js b/src/hooks/useCombobox/__tests__/getInputProps.test.js index 7f0456c86..8f6ac7c8e 100644 --- a/src/hooks/useCombobox/__tests__/getInputProps.test.js +++ b/src/hooks/useCombobox/__tests__/getInputProps.test.js @@ -1,13 +1,21 @@ -/* eslint-disable jest/no-disabled-tests */ import * as React from 'react' import {act, renderHook} from '@testing-library/react-hooks' import {fireEvent, createEvent} from '@testing-library/react' -import userEvent from '@testing-library/user-event' import * as stateChangeTypes from '../stateChangeTypes' import {noop} from '../../../utils' -import {renderUseCombobox, renderCombobox} from '../testUtils' -import {items, defaultIds} from '../../testUtils' -// eslint-disable-next-line import/default +import { + renderUseCombobox, + renderCombobox, + items, + defaultIds, + changeInputValue, + getInput, + getItems, + keyDownOnInput, + mouseLeaveItemAtIndex, + mouseMoveItemAtIndex, + tab, +} from '../testUtils' import utils from '../../utils' import useCombobox from '..' @@ -314,44 +322,45 @@ describe('getInputProps', () => { describe('initial focus', () => { test('is grabbed when isOpen is passed as true', () => { - const {input} = renderCombobox({isOpen: true}) + renderCombobox({isOpen: true}) - expect(input).toHaveFocus() + expect(getInput()).toHaveFocus() }) test('is grabbed when initialIsOpen is passed as true', () => { - const {input} = renderCombobox({initialIsOpen: true}) + renderCombobox({initialIsOpen: true}) - expect(input).toHaveFocus() + expect(getInput()).toHaveFocus() }) test('is grabbed when defaultIsOpen is passed as true', () => { - const {input} = renderCombobox({defaultIsOpen: true}) + renderCombobox({defaultIsOpen: true}) - expect(input).toHaveFocus() + expect(getInput()).toHaveFocus() }) test('is not grabbed when initial open is set to default (false)', () => { - const {input} = renderCombobox() + renderCombobox() - expect(input).not.toHaveFocus() + expect(getInput()).not.toHaveFocus() }) }) describe('event handlers', () => { - test('on change should open the menu and keep the input value', () => { - const {changeInputValue, getItems, input} = renderCombobox() + test('on change should open the menu and keep the input value', async () => { + renderCombobox() - changeInputValue('california') + await changeInputValue('california') expect(getItems()).toHaveLength(items.length) - expect(input).toHaveValue('california') + expect(getInput()).toHaveValue('california') }) describe('on key down', () => { describe('arrow up', () => { test('it prevents the default event behavior', () => { - const {input} = renderCombobox() + renderCombobox() + const input = getInput() const keyDownEvent = createEvent.keyDown(input, {key: 'ArrowUp'}) fireEvent(input, keyDownEvent) @@ -359,119 +368,188 @@ describe('getInputProps', () => { expect(keyDownEvent.defaultPrevented).toBe(true) }) - test('it does not open or highlight anything if there are no options', () => { - const {keyDownOnInput, getItems, input} = renderCombobox({items: []}) + test('it does not open or highlight anything if there are no options', async () => { + renderCombobox({items: []}) - keyDownOnInput('ArrowUp') + await keyDownOnInput('{ArrowUp}') - expect(input).not.toHaveAttribute('aria-activedescendant') + expect(getInput()).not.toHaveAttribute('aria-activedescendant') expect(getItems()).toHaveLength(0) }) - test('it does not highlight anything if there are no options', () => { - const {keyDownOnInput, getItems, input} = renderCombobox({ + test('it does not highlight anything if there are no options', async () => { + renderCombobox({ items: [], isOpen: true, }) - keyDownOnInput('ArrowUp') + await keyDownOnInput('{ArrowUp}') - expect(input).not.toHaveAttribute('aria-activedescendant') + expect(getInput()).not.toHaveAttribute('aria-activedescendant') expect(getItems()).toHaveLength(0) }) - test('it opens the menu and highlights the last option', () => { - const {keyDownOnInput, getItems, input} = renderCombobox() + test('it opens the menu and highlights the last option', async () => { + renderCombobox() - keyDownOnInput('ArrowUp') + await keyDownOnInput('{ArrowUp}') - expect(input).toHaveAttribute( + expect(getInput()).toHaveAttribute( 'aria-activedescendant', defaultIds.getItemId(items.length - 1), ) expect(getItems()).toHaveLength(items.length) }) - test('it highlights the last option number if none is highlighted', () => { - const {keyDownOnInput, input} = renderCombobox({isOpen: true}) + test('it opens the closed menu with selected option highlighted', async () => { + const selectedIndex = 4 + renderCombobox({ + initialSelectedItem: items[selectedIndex], + }) + + await keyDownOnInput('{ArrowUp}') + + expect(getInput()).toHaveAttribute( + 'aria-activedescendant', + defaultIds.getItemId(selectedIndex), + ) + }) - keyDownOnInput('ArrowUp') + test('it opens the closed menu at initialHighlightedIndex, but on first arrow up only', async () => { + const initialHighlightedIndex = 2 + renderCombobox({ + initialHighlightedIndex, + }) + + const input = getInput() + + await keyDownOnInput('{ArrowUp}') expect(input).toHaveAttribute( + 'aria-activedescendant', + defaultIds.getItemId(initialHighlightedIndex), + ) + + await keyDownOnInput('{Escape}') + await keyDownOnInput('{ArrowUp}') + + expect(input).toHaveAttribute( + 'aria-activedescendant', + defaultIds.getItemId(items.length - 1), + ) + }) + + test('it opens the closed menu at defaultHighlightedIndex, on every arrow up', async () => { + const defaultHighlightedIndex = 3 + renderCombobox({ + defaultHighlightedIndex, + }) + const input = getInput() + + await keyDownOnInput('{ArrowUp}') + + expect(input).toHaveAttribute( + 'aria-activedescendant', + defaultIds.getItemId(defaultHighlightedIndex), + ) + + await keyDownOnInput('{Escape}') + await keyDownOnInput('{ArrowUp}') + + expect(input).toHaveAttribute( + 'aria-activedescendant', + defaultIds.getItemId(defaultHighlightedIndex), + ) + }) + + test('it opens the closed menu and keeps focus on the combobox', async () => { + renderCombobox() + + await keyDownOnInput('{ArrowUp}') + + expect(getInput()).toHaveFocus() + }) + + test('it highlights the last option number if none is highlighted', async () => { + renderCombobox({isOpen: true}) + + await keyDownOnInput('{ArrowUp}') + + expect(getInput()).toHaveAttribute( 'aria-activedescendant', defaultIds.getItemId(items.length - 1), ) }) - test('it highlights the previous item', () => { + test('it highlights the previous item', async () => { const initialHighlightedIndex = 2 - const {keyDownOnInput, input} = renderCombobox({ + renderCombobox({ isOpen: true, initialHighlightedIndex, }) - keyDownOnInput('ArrowUp') + await keyDownOnInput('{ArrowUp}') - expect(input).toHaveAttribute( + expect(getInput()).toHaveAttribute( 'aria-activedescendant', defaultIds.getItemId(initialHighlightedIndex - 1), ) }) - test('with shift it highlights the 5th previous item', () => { + test('with shift it highlights the 5th previous item', async () => { const initialHighlightedIndex = 6 - const {keyDownOnInput, input} = renderCombobox({ + renderCombobox({ isOpen: true, initialHighlightedIndex, }) - keyDownOnInput('ArrowUp', {shiftKey: true}) + await keyDownOnInput('{Shift>}{ArrowUp}{/Shift}') - expect(input).toHaveAttribute( + expect(getInput()).toHaveAttribute( 'aria-activedescendant', defaultIds.getItemId(initialHighlightedIndex - 5), ) }) - test('with shift it highlights the last item if not enough items remaining', () => { + test('with shift it highlights the last item if not enough items remaining', async () => { const initialHighlightedIndex = 1 - const {keyDownOnInput, input} = renderCombobox({ + renderCombobox({ isOpen: true, initialHighlightedIndex, }) - keyDownOnInput('ArrowUp', {shiftKey: true}) + await keyDownOnInput('{Shift>}{ArrowUp}{/Shift}') - expect(input).toHaveAttribute( + expect(getInput()).toHaveAttribute( 'aria-activedescendant', defaultIds.getItemId(items.length - 1), ) }) - test('will stop at 0 if circularNavigatios is false', () => { - const {keyDownOnInput, input} = renderCombobox({ + test('will stop at 0 if circularNavigatios is false', async () => { + renderCombobox({ isOpen: true, initialHighlightedIndex: 0, circularNavigation: false, }) - keyDownOnInput('ArrowUp') + await keyDownOnInput('{ArrowUp}') - expect(input).toHaveAttribute( + expect(getInput()).toHaveAttribute( 'aria-activedescendant', defaultIds.getItemId(0), ) }) - test('will continue from 0 to last item if circularNavigatios is default', () => { - const {keyDownOnInput, input} = renderCombobox({ + test('will continue from 0 to last item if circularNavigatios is default', async () => { + renderCombobox({ isOpen: true, initialHighlightedIndex: 0, }) - keyDownOnInput('ArrowUp') + await keyDownOnInput('{ArrowUp}') - expect(input).toHaveAttribute( + expect(getInput()).toHaveAttribute( 'aria-activedescendant', defaultIds.getItemId(items.length - 1), ) @@ -480,7 +558,8 @@ describe('getInputProps', () => { describe('arrow down', () => { test('it prevents the default event behavior', () => { - const {input} = renderCombobox() + renderCombobox() + const input = getInput() const keyDownEvent = createEvent.keyDown(input, {key: 'ArrowDown'}) fireEvent(input, keyDownEvent) @@ -488,46 +567,61 @@ describe('getInputProps', () => { expect(keyDownEvent.defaultPrevented).toBe(true) }) - test('it does not opne on highlight anything if there are no options', () => { - const {keyDownOnInput, getItems, input} = renderCombobox({items: []}) + test('it does not opne on highlight anything if there are no options', async () => { + renderCombobox({items: []}) - keyDownOnInput('ArrowDown') + await keyDownOnInput('{ArrowDown}') - expect(input).not.toHaveAttribute('aria-activedescendant') + expect(getInput()).not.toHaveAttribute('aria-activedescendant') expect(getItems()).toHaveLength(0) }) - test('it does not highlight anything if there are no options', () => { - const {keyDownOnInput, getItems, input} = renderCombobox({ + test('it does not highlight anything if there are no options', async () => { + renderCombobox({ items: [], isOpen: true, }) - keyDownOnInput('ArrowDown') + await keyDownOnInput('{ArrowDown}') - expect(input).not.toHaveAttribute('aria-activedescendant') + expect(getInput()).not.toHaveAttribute('aria-activedescendant') expect(getItems()).toHaveLength(0) }) - test("it opens the menu and highlights option number '0'", () => { - const {input, keyDownOnInput, getItems} = renderCombobox() + test("it opens the menu and highlights option number '0'", async () => { + renderCombobox() - keyDownOnInput('ArrowDown') + await keyDownOnInput('{ArrowDown}') - expect(input).toHaveAttribute( + expect(getInput()).toHaveAttribute( 'aria-activedescendant', defaultIds.getItemId(0), ) expect(getItems()).toHaveLength(items.length) }) - test('it opens the menu and highlights initialHighlightedIndex only once', () => { + test('opens the closed menu with selected option highlighted', async () => { + const selectedIndex = 4 + renderCombobox({ + initialSelectedItem: items[selectedIndex], + }) + + await keyDownOnInput('{ArrowDown}') + + expect(getInput()).toHaveAttribute( + 'aria-activedescendant', + defaultIds.getItemId(selectedIndex), + ) + }) + + test('it opens the menu and highlights initialHighlightedIndex only once', async () => { const initialHighlightedIndex = 2 - const {keyDownOnInput, input, getItems} = renderCombobox({ + renderCombobox({ initialHighlightedIndex, }) + const input = getInput() - keyDownOnInput('ArrowDown') + await keyDownOnInput('{ArrowDown}') expect(input).toHaveAttribute( 'aria-activedescendant', @@ -535,8 +629,8 @@ describe('getInputProps', () => { ) expect(getItems()).toHaveLength(items.length) - keyDownOnInput('Escape') - keyDownOnInput('ArrowDown') + await keyDownOnInput('{Escape}') + await keyDownOnInput('{ArrowDown}') expect(input).toHaveAttribute( 'aria-activedescendant', @@ -545,13 +639,14 @@ describe('getInputProps', () => { expect(getItems()).toHaveLength(items.length) }) - test('it opens the menu and highlights defaultHighlightedIndex always', () => { + test('it opens the menu and highlights defaultHighlightedIndex always', async () => { const defaultHighlightedIndex = 2 - const {keyDownOnInput, input, getItems} = renderCombobox({ + renderCombobox({ defaultHighlightedIndex, }) + const input = getInput() - keyDownOnInput('ArrowDown') + await keyDownOnInput('{ArrowDown}') expect(input).toHaveAttribute( 'aria-activedescendant', @@ -559,8 +654,8 @@ describe('getInputProps', () => { ) expect(getItems()).toHaveLength(items.length) - keyDownOnInput('Escape') - keyDownOnInput('ArrowDown') + await keyDownOnInput('{Escape}') + await keyDownOnInput('{ArrowDown}') expect(input).toHaveAttribute( 'aria-activedescendant', @@ -569,108 +664,117 @@ describe('getInputProps', () => { expect(getItems()).toHaveLength(items.length) }) - test("it highlights option number '0' if none is highlighted", () => { - const {keyDownOnInput, input} = renderCombobox({isOpen: true}) + test('opens the closed menu and keeps focus on the button', async () => { + renderCombobox() - keyDownOnInput('ArrowDown') + await keyDownOnInput('{ArrowDown}') - expect(input).toHaveAttribute( + expect(getInput()).toHaveFocus() + }) + + test("it highlights option number '0' if none is highlighted", async () => { + renderCombobox({isOpen: true}) + + await keyDownOnInput('{ArrowDown}') + + expect(getInput()).toHaveAttribute( 'aria-activedescendant', defaultIds.getItemId(0), ) }) - test('it highlights the next item', () => { + test('it highlights the next item', async () => { const initialHighlightedIndex = 2 - const {keyDownOnInput, input} = renderCombobox({ + renderCombobox({ isOpen: true, initialHighlightedIndex, }) - keyDownOnInput('ArrowDown') + await keyDownOnInput('{ArrowDown}') - expect(input).toHaveAttribute( + expect(getInput()).toHaveAttribute( 'aria-activedescendant', defaultIds.getItemId(initialHighlightedIndex + 1), ) }) - test('with shift it highlights the next 5th item', () => { + test('with shift it highlights the next 5th item', async () => { const initialHighlightedIndex = 2 - const {keyDownOnInput, input} = renderCombobox({ + renderCombobox({ isOpen: true, initialHighlightedIndex, }) - keyDownOnInput('ArrowDown', {shiftKey: true}) + await keyDownOnInput('{Shift>}{ArrowDown}{/Shift}') - expect(input).toHaveAttribute( + expect(getInput()).toHaveAttribute( 'aria-activedescendant', defaultIds.getItemId(initialHighlightedIndex + 5), ) }) - test('with shift it highlights first item if not enough next items remaining', () => { + test('with shift it highlights first item if not enough next items remaining', async () => { const initialHighlightedIndex = items.length - 2 - const {keyDownOnInput, input} = renderCombobox({ + renderCombobox({ isOpen: true, initialHighlightedIndex, }) - keyDownOnInput('ArrowDown', {shiftKey: true}) + await keyDownOnInput('{Shift>}{ArrowDown}{/Shift}') - expect(input).toHaveAttribute( + expect(getInput()).toHaveAttribute( 'aria-activedescendant', defaultIds.getItemId(0), ) }) - test('will stop at last item if circularNavigatios is false', () => { - const {keyDownOnInput, input} = renderCombobox({ + test('will stop at last item if circularNavigatios is false', async () => { + renderCombobox({ isOpen: true, initialHighlightedIndex: items.length - 1, circularNavigation: false, }) - keyDownOnInput('ArrowDown', {shiftKey: true}) + await keyDownOnInput('{ArrowDown}', {shiftKey: true}) - expect(input).toHaveAttribute( + expect(getInput()).toHaveAttribute( 'aria-activedescendant', defaultIds.getItemId(items.length - 1), ) }) - test('will continue from last item to 0 if circularNavigatios is default', () => { - const {keyDownOnInput, input} = renderCombobox({ + test('will continue from last item to 0 if circularNavigatios is default', async () => { + renderCombobox({ isOpen: true, initialHighlightedIndex: items.length - 1, }) - keyDownOnInput('ArrowDown') + await keyDownOnInput('{ArrowDown}') - expect(input).toHaveAttribute( + expect(getInput()).toHaveAttribute( 'aria-activedescendant', defaultIds.getItemId(0), ) }) }) - test('end it highlights the last option number', () => { - const {keyDownOnInput, input} = renderCombobox({ + test('end it highlights the last option number', async () => { + renderCombobox({ isOpen: true, initialHighlightedIndex: 2, }) - keyDownOnInput('End') + await keyDownOnInput('{End}') - expect(input).toHaveAttribute( + expect(getInput()).toHaveAttribute( 'aria-activedescendant', defaultIds.getItemId(items.length - 1), ) }) test('end it prevents the default event and calls dispatch only when menu is open', () => { - const {input, rerender, renderSpy} = renderCombobox({isOpen: false}) + const {rerender, renderSpy} = renderCombobox({isOpen: false}) + const input = getInput() const keyDownEvent = createEvent.keyDown(input, {key: 'End'}) renderSpy.mockClear() @@ -687,22 +791,23 @@ describe('getInputProps', () => { expect(renderSpy).toHaveBeenCalledTimes(1) }) - test('home it highlights the first option number', () => { - const {keyDownOnInput, input} = renderCombobox({ + test('home it highlights the first option number', async () => { + renderCombobox({ isOpen: true, initialHighlightedIndex: 2, }) - keyDownOnInput('Home') + await keyDownOnInput('{Home}') - expect(input).toHaveAttribute( + expect(getInput()).toHaveAttribute( 'aria-activedescendant', defaultIds.getItemId(0), ) }) test('home it prevents the default event calls dispatch only when menu is open', () => { - const {input, rerender, renderSpy} = renderCombobox({isOpen: false}) + const {rerender, renderSpy} = renderCombobox({isOpen: false}) + const input = getInput() const keyDownEvent = createEvent.keyDown(input, {key: 'Home'}) renderSpy.mockClear() @@ -719,63 +824,66 @@ describe('getInputProps', () => { expect(renderSpy).toHaveBeenCalledTimes(1) // re-render on key }) - test('escape with menu open has the menu closed and focused kept on input', () => { - const {keyDownOnInput, input, getItems} = renderCombobox({ + test('escape with menu open has the menu closed and focused kept on input', async () => { + renderCombobox({ initialIsOpen: true, initialHighlightedIndex: 2, initialSelectedItem: items[0], }) + const input = getInput() - keyDownOnInput('Escape') + await keyDownOnInput('{Escape}') expect(getItems()).toHaveLength(0) expect(input).toHaveValue(items[0]) expect(input).toHaveFocus() }) - test('escape with closed menu has item removed and focused kept on input', () => { - const {keyDownOnInput, input, getItems} = renderCombobox({ + test('escape with closed menu has item removed and focused kept on input', async () => { + renderCombobox({ initialHighlightedIndex: 2, initialSelectedItem: items[0], }) + const input = getInput() input.focus() - keyDownOnInput('Escape') + await keyDownOnInput('{Escape}') expect(getItems()).toHaveLength(0) expect(input).toHaveValue('') expect(input).toHaveFocus() }) - test('escape it prevents the rerender when menu closed, no selectedItem and no inputValue', () => { - const {keyDownOnInput, rerender, renderSpy} = renderCombobox({ + test('escape it prevents the rerender when menu closed, no selectedItem and no inputValue', async () => { + const {renderSpy, rerender} = renderCombobox({ isOpen: false, inputValue: '', }) renderSpy.mockClear() - keyDownOnInput('Escape') + await keyDownOnInput('{Escape}') expect(renderSpy).toHaveBeenCalledTimes(0) // no re-render rerender({isOpen: true, inputValue: ''}) renderSpy.mockClear() // reset rerender and initial render - keyDownOnInput('Escape') + await keyDownOnInput('{Escape}') expect(renderSpy).toHaveBeenCalledTimes(1) // re-render on key rerender({isOpen: false, inputValue: 'still'}) renderSpy.mockClear() // reset rerender and initial render - keyDownOnInput('Escape') + await keyDownOnInput('{Escape}') expect(renderSpy).toHaveBeenCalledTimes(1) // re-render on key }) test('escape stops propagation when it closes the menu or clears the input', () => { - const {input} = renderCombobox({ + renderCombobox({ initialIsOpen: true, initialSelectedItem: items[0], }) + const input = getInput() const keyDownEvents = [ createEvent.keyDown(input, {key: 'Escape'}), createEvent.keyDown(input, {key: 'Escape'}), @@ -790,27 +898,28 @@ describe('getInputProps', () => { } }) - test('enter it closes the menu and selects highlighted item', () => { + test('enter it closes the menu and selects highlighted item', async () => { const initialHighlightedIndex = 2 - const {keyDownOnInput, input, getItems} = renderCombobox({ + renderCombobox({ initialIsOpen: true, initialHighlightedIndex, }) - keyDownOnInput('Enter') + await keyDownOnInput('{Enter}') expect(getItems()).toHaveLength(0) - expect(input).toHaveValue(items[initialHighlightedIndex]) + expect(getInput()).toHaveValue(items[initialHighlightedIndex]) }) - test('enter selects highlighted item and resets to user defaults', () => { + test('enter selects highlighted item and resets to user defaults', async () => { const defaultHighlightedIndex = 2 - const {keyDownOnInput, input, getItems} = renderCombobox({ + renderCombobox({ defaultHighlightedIndex, defaultIsOpen: true, }) + const input = getInput() - keyDownOnInput('Enter') + await keyDownOnInput('{Enter}') expect(input).toHaveValue(items[defaultHighlightedIndex]) expect(getItems()).toHaveLength(items.length) @@ -820,14 +929,15 @@ describe('getInputProps', () => { ) }) - test('enter while IME composing will not select highlighted item', () => { + test('enter while IME composing will not select highlighted item', async () => { const initialHighlightedIndex = 2 - const {keyDownOnInput, input, getItems} = renderCombobox({ + renderCombobox({ initialHighlightedIndex, initialIsOpen: true, }) + const input = getInput() - keyDownOnInput('Enter', {keyCode: 229}) + fireEvent.keyDown(getInput(), {key: 'Enter', keyCode: 229}) expect(input).toHaveValue('') expect(getItems()).toHaveLength(items.length) @@ -836,41 +946,42 @@ describe('getInputProps', () => { defaultIds.getItemId(initialHighlightedIndex), ) - keyDownOnInput('Enter') + await keyDownOnInput('{Enter}') expect(input).toHaveValue(items[2]) expect(getItems()).toHaveLength(0) expect(input).not.toHaveAttribute('aria-activedescendant') }) - test('enter with a closed menu does nothing', () => { - const {keyDownOnInput, getItems, input} = renderCombobox({ + test('enter with a closed menu does nothing', async () => { + renderCombobox({ initialHighlightedIndex: 2, initialIsOpen: false, }) - keyDownOnInput('Enter') + await keyDownOnInput('{Enter}') expect(getItems()).toHaveLength(0) - expect(input).not.toHaveValue() + expect(getInput()).not.toHaveValue() }) - test('enter with an open menu does nothing without a highlightedIndex', () => { - const {keyDownOnInput, getItems, input} = renderCombobox({ + test('enter with an open menu does nothing without a highlightedIndex', async () => { + renderCombobox({ initialIsOpen: true, }) - keyDownOnInput('Enter') + await keyDownOnInput('{Enter}') expect(getItems()).toHaveLength(items.length) - expect(input).not.toHaveValue() + expect(getInput()).not.toHaveValue() }) test('enter with closed menu, no item highlighted or composing event, it will not rerender or prevent event default', () => { - const {input, renderSpy, rerender} = renderCombobox({ + const {renderSpy, rerender} = renderCombobox({ isOpen: false, highlightedIndex: -1, }) + const input = getInput() let keyDownEvent = createEvent.keyDown(input, {key: 'Enter'}) renderSpy.mockClear() @@ -907,9 +1018,9 @@ describe('getInputProps', () => { expect(keyDownEvent.defaultPrevented).toBe(true) }) - test('tab it closes the menu and selects highlighted item', () => { + test('tab it closes the menu and selects highlighted item', async () => { const initialHighlightedIndex = 2 - const {input, getItems} = renderCombobox( + renderCombobox( {initialIsOpen: true, initialHighlightedIndex: 2}, ui => { return ( @@ -921,30 +1032,30 @@ describe('getInputProps', () => { }, ) - userEvent.tab() + await tab() expect(getItems()).toHaveLength(0) - expect(input).toHaveValue(items[initialHighlightedIndex]) + expect(getInput()).toHaveValue(items[initialHighlightedIndex]) }) - test('tab it prevents the rerender and does not call dispatch when menu is closed', () => { - const {rerender, renderSpy} = renderCombobox({isOpen: false}) + test('tab it prevents the rerender and does not call dispatch when menu is closed', async () => { + const {renderSpy, rerender} = renderCombobox({isOpen: false}) renderSpy.mockClear() - userEvent.tab() + await tab() expect(renderSpy).toHaveBeenCalledTimes(0) rerender({isOpen: true}) renderSpy.mockClear() - userEvent.tab() + await tab() expect(renderSpy).toHaveBeenCalledTimes(1) }) - test('shift+tab it closes the menu', () => { + test('shift+tab it closes the menu', async () => { const initialHighlightedIndex = 2 - const {input, getItems} = renderCombobox( + renderCombobox( {initialIsOpen: true, initialHighlightedIndex: 2}, ui => { return ( @@ -956,22 +1067,23 @@ describe('getInputProps', () => { }, ) - userEvent.tab() + await tab(true) expect(getItems()).toHaveLength(0) - expect(input).toHaveValue(items[initialHighlightedIndex]) + expect(getInput()).toHaveValue(items[initialHighlightedIndex]) }) - test("other than the ones supported don't affect anything", () => { + test("other than the ones supported don't affect anything", async () => { const highlightedIndex = 2 - const {keyDownOnInput, input, getItems} = renderCombobox({ + renderCombobox({ initialIsOpen: true, initialHighlightedIndex: highlightedIndex, initialSelectedItem: items[highlightedIndex], }) + const input = getInput() - keyDownOnInput('Alt') - keyDownOnInput('Control') + await keyDownOnInput('{Alt}') + await keyDownOnInput('{Control}') expect(input).toHaveFocus() expect(input).toHaveValue(items[highlightedIndex]) @@ -984,52 +1096,54 @@ describe('getInputProps', () => { }) describe('on blur', () => { - test('the open menu will be closed and highlighted item will be selected', () => { + test('the open menu will be closed and highlighted item will be selected', async () => { const initialHighlightedIndex = 2 - const {input, getItems, blurInput} = renderCombobox({ + renderCombobox({ initialIsOpen: true, initialHighlightedIndex, }) - blurInput() + await tab() expect(getItems()).toHaveLength(0) - expect(input).toHaveValue(items[initialHighlightedIndex]) + expect(getInput()).toHaveValue(items[initialHighlightedIndex]) }) - test('the open menu will be closed and highlighted item will not be selected if the highlight by mouse leaves the menu', () => { + test('the open menu will be closed and highlighted item will not be selected if the highlight by mouse leaves the menu', async () => { const initialHighlightedIndex = 2 - const {blurInput, mouseLeaveMenu, getItems, input} = renderCombobox({ + renderCombobox({ initialIsOpen: true, initialHighlightedIndex, }) - mouseLeaveMenu() - blurInput() + await mouseMoveItemAtIndex(initialHighlightedIndex) + await mouseLeaveItemAtIndex(initialHighlightedIndex) + await tab() expect(getItems()).toHaveLength(0) - expect(input).toHaveValue('') + expect(getInput()).toHaveValue('') }) - test('the value in the input will stay the same', () => { + test('the value in the input will stay the same', async () => { const inputValue = 'test me' - const {blurInput, changeInputValue, input} = renderCombobox({ + renderCombobox({ initialIsOpen: true, }) - changeInputValue(inputValue) - blurInput() + await changeInputValue(inputValue) + await tab() - expect(input).toHaveValue(inputValue) + expect(getInput()).toHaveValue(inputValue) }) test('by mouse is not triggered if target is within downshift', () => { const stateReducer = jest.fn().mockImplementation(s => s) - const {input, container} = renderCombobox({ + const {container} = renderCombobox({ isOpen: true, highlightedIndex: 0, stateReducer, }) + const input = getInput() document.body.appendChild(container) fireEvent.mouseDown(input) @@ -1062,11 +1176,12 @@ describe('getInputProps', () => { test('by touch is not triggered if target is within downshift', () => { const stateReducer = jest.fn().mockImplementation(s => s) - const {container, input} = renderCombobox({ + const {container} = renderCombobox({ isOpen: true, highlightedIndex: 0, stateReducer, }) + const input = getInput() document.body.appendChild(container) fireEvent.touchStart(input) diff --git a/src/hooks/useCombobox/__tests__/getItemProps.test.js b/src/hooks/useCombobox/__tests__/getItemProps.test.js index 690a7d6fa..8f98c7aac 100644 --- a/src/hooks/useCombobox/__tests__/getItemProps.test.js +++ b/src/hooks/useCombobox/__tests__/getItemProps.test.js @@ -1,6 +1,16 @@ import {act} from '@testing-library/react-hooks' -import {renderCombobox, renderUseCombobox} from '../testUtils' -import {items, defaultIds} from '../../testUtils' +import { + renderCombobox, + renderUseCombobox, + items, + defaultIds, + mouseMoveItemAtIndex, + getItemAtIndex, + getInput, + getItems, + clickOnItemAtIndex, + keyDownOnInput, +} from '../testUtils' describe('getItemProps', () => { test('throws error if no index or item has been passed', () => { @@ -186,37 +196,37 @@ describe('getItemProps', () => { describe('event handlers', () => { describe('on mouse over', () => { - test('it highlights the item', () => { + test('it highlights the item', async () => { const index = 1 - const {input, mouseMoveItemAtIndex, getItemAtIndex} = renderCombobox({ + renderCombobox({ isOpen: true, }) - mouseMoveItemAtIndex(index) + await mouseMoveItemAtIndex(index) expect(getItemAtIndex(index)).toHaveAttribute('aria-selected', 'true') - expect(input).toHaveAttribute( + expect(getInput()).toHaveAttribute( 'aria-activedescendant', defaultIds.getItemId(index), ) }) - test('it removes highlight from the previously highlighted item', () => { + test('it removes highlight from the previously highlighted item', async () => { const index = 1 const previousIndex = 2 - const {input, mouseMoveItemAtIndex, getItemAtIndex} = renderCombobox({ + renderCombobox({ isOpen: true, initialHighlightedIndex: previousIndex, }) - mouseMoveItemAtIndex(index) + await mouseMoveItemAtIndex(index) expect(getItemAtIndex(index)).toHaveAttribute('aria-selected', 'true') expect(getItemAtIndex(previousIndex)).toHaveAttribute( 'aria-selected', 'false', ) - expect(input).toHaveAttribute( + expect(getInput()).toHaveAttribute( 'aria-activedescendant', defaultIds.getItemId(index), ) @@ -226,70 +236,73 @@ describe('getItemProps', () => { ) }) - it('keeps highlight on multiple events', () => { + it('keeps highlight on multiple events', async () => { const index = 1 - const {input, mouseMoveItemAtIndex, getItemAtIndex} = renderCombobox({ + renderCombobox({ isOpen: true, }) - mouseMoveItemAtIndex(index) - mouseMoveItemAtIndex(index) - mouseMoveItemAtIndex(index) + await mouseMoveItemAtIndex(index) + await mouseMoveItemAtIndex(index) + await mouseMoveItemAtIndex(index) - expect(input).toHaveAttribute( + expect(getInput()).toHaveAttribute( 'aria-activedescendant', defaultIds.getItemId(index), ) expect(getItemAtIndex(index)).toHaveAttribute('aria-selected', 'true') }) - it('removes highlight from previous item even if current item is disabled', () => { + it('removes highlight from previous item even if current item is disabled', async () => { const disabledIndex = 1 const highlightedIndex = 2 const itemsWithDisabled = [...items].map((item, index) => index === disabledIndex ? {...item, disabled: true} : item, ) - const {input, mouseMoveItemAtIndex} = renderCombobox({ + renderCombobox({ items: itemsWithDisabled, isOpen: true, }) + const input = getInput() - mouseMoveItemAtIndex(highlightedIndex) + await mouseMoveItemAtIndex(highlightedIndex) expect(input).toHaveAttribute( 'aria-activedescendant', defaultIds.getItemId(highlightedIndex), ) - mouseMoveItemAtIndex(disabledIndex) + await mouseMoveItemAtIndex(disabledIndex) expect(input).not.toHaveAttribute('aria-activedescendant') }) }) describe('on click', () => { - test('it selects the item and keeps focus on the input', () => { + test('it selects the item and keeps focus on the input', async () => { const index = 1 - const {input, getItems, clickOnItemAtIndex} = renderCombobox({ + renderCombobox({ initialIsOpen: true, }) + const input = getInput() - clickOnItemAtIndex(index) + await clickOnItemAtIndex(index) expect(getItems()).toHaveLength(0) expect(input).toHaveValue(items[index]) expect(input).toHaveFocus() }) - test('it selects the item and resets to user defined defaults', () => { + test('it selects the item and resets to user defined defaults', async () => { const index = 1 const defaultHighlightedIndex = 2 - const {input, getItems, clickOnItemAtIndex} = renderCombobox({ + renderCombobox({ defaultIsOpen: true, defaultHighlightedIndex, }) + const input = getInput() - clickOnItemAtIndex(index) + await clickOnItemAtIndex(index) expect(input).toHaveValue(items[index]) expect(getItems()).toHaveLength(items.length) @@ -302,14 +315,14 @@ describe('getItemProps', () => { }) describe('scrolling', () => { - test('is performed by the menu to the item if highlighted and not 100% visible', () => { + test('is performed by the menu to the item if highlighted and not 100% visible', async () => { const scrollIntoView = jest.fn() - const {keyDownOnInput} = renderCombobox({ + renderCombobox({ initialIsOpen: true, scrollIntoView, }) - keyDownOnInput('End') + await keyDownOnInput('{End}') expect(scrollIntoView).toHaveBeenCalledTimes(1) }) diff --git a/src/hooks/useCombobox/__tests__/getMenuProps.test.js b/src/hooks/useCombobox/__tests__/getMenuProps.test.js index f82228587..ee34fac11 100644 --- a/src/hooks/useCombobox/__tests__/getMenuProps.test.js +++ b/src/hooks/useCombobox/__tests__/getMenuProps.test.js @@ -1,8 +1,7 @@ import {act, renderHook} from '@testing-library/react-hooks' import {noop} from '../../../utils' -import {renderCombobox, renderUseCombobox} from '../testUtils' -import {defaultIds, items} from '../../testUtils' -// eslint-disable-next-line import/default +import {getInput, renderCombobox, renderUseCombobox} from '../testUtils' +import {defaultIds, items, mouseLeaveItemAtIndex, mouseMoveItemAtIndex} from '../../testUtils' import utils from '../../utils' import useCombobox from '..' @@ -104,15 +103,17 @@ describe('getMenuProps', () => { describe('event handlers', () => { describe('on key down', () => { describe('on mouse leave', () => { - test('the highlightedIndex should be reset', () => { - const {mouseLeaveMenu, input} = renderCombobox({ + test('the highlightedIndex should be reset', async () => { + const initialHighlightedIndex = 2 + renderCombobox({ initialIsOpen: true, - initialHighlightedIndex: 2, + initialHighlightedIndex, }) - mouseLeaveMenu() + await mouseMoveItemAtIndex(initialHighlightedIndex) + await mouseLeaveItemAtIndex(initialHighlightedIndex) - expect(input).not.toHaveAttribute('aria-activedescendant') + expect(getInput()).not.toHaveAttribute('aria-activedescendant') }) }) }) diff --git a/src/hooks/useCombobox/__tests__/getToggleButtonProps.test.js b/src/hooks/useCombobox/__tests__/getToggleButtonProps.test.js index 3d13eb068..fa966a5e1 100644 --- a/src/hooks/useCombobox/__tests__/getToggleButtonProps.test.js +++ b/src/hooks/useCombobox/__tests__/getToggleButtonProps.test.js @@ -1,7 +1,13 @@ -/* eslint-disable jest/no-disabled-tests */ import {act} from '@testing-library/react-hooks' -import {renderCombobox, renderUseCombobox} from '../testUtils' -import {items, defaultIds} from '../../testUtils' +import { + renderCombobox, + renderUseCombobox, + items, + defaultIds, + clickOnToggleButton, + getInput, + getItems, +} from '../testUtils' describe('getToggleButtonProps', () => { describe('hook props', () => { @@ -81,100 +87,101 @@ describe('getToggleButtonProps', () => { describe('event handlers', () => { describe('on click', () => { - test('opens the closed menu', () => { - const {getItems, clickOnToggleButton} = renderCombobox() + test('opens the closed menu', async () => { + renderCombobox() - clickOnToggleButton() + await clickOnToggleButton() expect(getItems()).toHaveLength(items.length) }) - test('closes the open menu', () => { - const {getItems, clickOnToggleButton} = renderCombobox({ + test('closes the open menu', async () => { + renderCombobox({ initialIsOpen: true, }) - clickOnToggleButton() + await clickOnToggleButton() expect(getItems()).toHaveLength(0) }) - test('opens and closes menu at consecutive clicks', () => { - const {getItems, clickOnToggleButton} = renderCombobox() + test('opens and closes menu at consecutive clicks', async () => { + renderCombobox() - clickOnToggleButton() + await clickOnToggleButton() expect(getItems()).toHaveLength(items.length) - clickOnToggleButton() + await clickOnToggleButton() expect(getItems()).toHaveLength(0) - clickOnToggleButton() + await clickOnToggleButton() expect(getItems()).toHaveLength(items.length) - clickOnToggleButton() + await clickOnToggleButton() expect(getItems()).toHaveLength(0) }) - test('opens the closed menu without any option highlighted', () => { - const {input, clickOnToggleButton} = renderCombobox() + test('opens the closed menu without any option highlighted', async () => { + renderCombobox() - clickOnToggleButton() + await clickOnToggleButton() - expect(input).not.toHaveAttribute('aria-activedescendant') + expect(getInput()).not.toHaveAttribute('aria-activedescendant') }) - test('opens the closed menu with selected option highlighted', () => { + test('opens the closed menu with selected option highlighted', async () => { const selectedIndex = 3 - const {input, clickOnToggleButton} = renderCombobox({ + renderCombobox({ initialSelectedItem: items[selectedIndex], }) - clickOnToggleButton() + await clickOnToggleButton() - expect(input).toHaveAttribute( + expect(getInput()).toHaveAttribute( 'aria-activedescendant', defaultIds.getItemId(selectedIndex), ) }) - test('opens the closed menu at initialHighlightedIndex, but on first click only', () => { + test('opens the closed menu at initialHighlightedIndex, but on first click only', async () => { const initialHighlightedIndex = 3 - const {input, clickOnToggleButton} = renderCombobox({ + renderCombobox({ initialHighlightedIndex, }) - clickOnToggleButton() + await clickOnToggleButton() - expect(input).toHaveAttribute( + expect(getInput()).toHaveAttribute( 'aria-activedescendant', defaultIds.getItemId(initialHighlightedIndex), ) - clickOnToggleButton() - clickOnToggleButton() + await clickOnToggleButton() + await clickOnToggleButton() - expect(input).not.toHaveAttribute('aria-activedescendant') + expect(getInput()).not.toHaveAttribute('aria-activedescendant') }) - test('opens the closed menu at defaultHighlightedIndex, on every click', () => { + test('opens the closed menu at defaultHighlightedIndex, on every click', async () => { const defaultHighlightedIndex = 3 - const {input, clickOnToggleButton} = renderCombobox({ + renderCombobox({ defaultHighlightedIndex, }) + const input = getInput() - clickOnToggleButton() + await clickOnToggleButton() expect(input).toHaveAttribute( 'aria-activedescendant', defaultIds.getItemId(defaultHighlightedIndex), ) - clickOnToggleButton() - clickOnToggleButton() + await clickOnToggleButton() + await clickOnToggleButton() expect(input).toHaveAttribute( 'aria-activedescendant', @@ -182,21 +189,22 @@ describe('getToggleButtonProps', () => { ) }) - test('opens the closed menu at highlightedIndex from props, on every click', () => { + test('opens the closed menu at highlightedIndex from props, on every click', async () => { const highlightedIndex = 3 - const {input, clickOnToggleButton} = renderCombobox({ + renderCombobox({ highlightedIndex, }) + const input = getInput() - clickOnToggleButton() + await clickOnToggleButton() expect(input).toHaveAttribute( 'aria-activedescendant', defaultIds.getItemId(highlightedIndex), ) - clickOnToggleButton() - clickOnToggleButton() + await clickOnToggleButton() + await clickOnToggleButton() expect(input).toHaveAttribute( 'aria-activedescendant', @@ -204,12 +212,12 @@ describe('getToggleButtonProps', () => { ) }) - test('opens the closed menu and sets focus on the input', () => { - const {clickOnToggleButton, input} = renderCombobox() + test('opens the closed menu and sets focus on the input', async () => { + renderCombobox() - clickOnToggleButton() + await clickOnToggleButton() - expect(input).toHaveFocus() + expect(getInput()).toHaveFocus() }) }) }) diff --git a/src/hooks/useCombobox/__tests__/memo.test.js b/src/hooks/useCombobox/__tests__/memo.test.js index a7bbdc156..641fb5c9d 100644 --- a/src/hooks/useCombobox/__tests__/memo.test.js +++ b/src/hooks/useCombobox/__tests__/memo.test.js @@ -1,5 +1,10 @@ import React from 'react' -import {renderUseCombobox, renderCombobox} from '../testUtils' +import { + renderUseCombobox, + renderCombobox, + getInput, + keyDownOnInput, +} from '../testUtils' import {items, defaultIds, MemoizedItem} from '../../testUtils' test('functions are memoized', () => { @@ -10,7 +15,7 @@ test('functions are memoized', () => { expect(firstRenderResult).toEqual(secondRenderResult) }) -test('will skip disabled items after component rerenders and items are memoized', () => { +test('will skip disabled items after component rerenders and items are memoized', async () => { function renderItem(props) { return ( { @@ -22,10 +36,10 @@ describe('props', () => { describe('id', () => { test('if passed will override downshift default', () => { - const {toggleButton, menu, label, input} = renderCombobox({ + renderCombobox({ id: 'my-custom-little-id', }) - const elements = [toggleButton, menu, label, input] + const elements = [getToggleButton(), getMenu(), getLabel(), getInput()] elements.forEach(element => { expect(element).toHaveAttribute( @@ -38,20 +52,20 @@ describe('props', () => { describe('items', () => { test('if passed as empty then menu will not open', () => { - const {getItems} = renderCombobox({items: [], isOpen: true}) + renderCombobox({items: [], isOpen: true}) expect(getItems()).toHaveLength(0) }) - test('passed as objects should work with custom itemToString', () => { + test('passed as objects should work with custom itemToString', async () => { jest.useFakeTimers() - const {clickOnItemAtIndex, getA11yStatusContainer} = renderCombobox({ + renderCombobox({ items: [{str: 'aaa'}, {str: 'bbb'}], itemToString: item => item.str, initialIsOpen: true, }) - clickOnItemAtIndex(0) + await clickOnItemAtIndex(0) waitForDebouncedA11yStatusUpdate() expect(getA11yStatusContainer()).toHaveTextContent( @@ -61,14 +75,14 @@ describe('props', () => { }) describe('itemToString', () => { - test('should provide string version to a11y status message', () => { + test('should provide string version to a11y status message', async () => { jest.useFakeTimers() - const {clickOnItemAtIndex, getA11yStatusContainer} = renderCombobox({ + renderCombobox({ itemToString: () => 'custom-item', initialIsOpen: true, }) - clickOnItemAtIndex(0) + await clickOnItemAtIndex(0) waitForDebouncedA11yStatusUpdate() expect(getA11yStatusContainer()).toHaveTextContent( @@ -84,13 +98,13 @@ describe('props', () => { }) afterAll(jest.useRealTimers) - test('reports that an item has been selected', () => { + test('reports that an item has been selected', async () => { const itemIndex = 0 - const {clickOnItemAtIndex, getA11yStatusContainer} = renderCombobox({ + renderCombobox({ initialIsOpen: true, }) - clickOnItemAtIndex(itemIndex) + await clickOnItemAtIndex(itemIndex) waitForDebouncedA11yStatusUpdate() expect(getA11yStatusContainer()).toHaveTextContent( @@ -98,24 +112,24 @@ describe('props', () => { ) }) - test('reports nothing if item is removed', () => { - const {keyDownOnInput, getA11yStatusContainer} = renderCombobox({ + test('reports nothing if item is removed', async () => { + renderCombobox({ initialSelectedItem: items[0], }) - keyDownOnInput('Escape') + await keyDownOnInput('{Escape}') waitForDebouncedA11yStatusUpdate() jest.runAllTimers() expect(getA11yStatusContainer()).toBeEmptyDOMElement() }) - test('is called with object that contains specific props', () => { + test('is called with object that contains specific props', async () => { const getA11ySelectionMessage = jest.fn() const inputValue = 'a' const isOpen = true const highlightedIndex = 0 - const {clickOnItemAtIndex} = renderCombobox({ + renderCombobox({ inputValue, isOpen, highlightedIndex, @@ -123,7 +137,7 @@ describe('props', () => { getA11ySelectionMessage, }) - clickOnItemAtIndex(0) + await clickOnItemAtIndex(0) waitForDebouncedA11yStatusUpdate() expect(getA11ySelectionMessage).toHaveBeenCalledWith({ @@ -138,13 +152,13 @@ describe('props', () => { }) }) - test('is replaced with the user provided one', () => { - const {clickOnItemAtIndex, getA11yStatusContainer} = renderCombobox({ + test('is replaced with the user provided one', async () => { + renderCombobox({ isOpen: true, getA11ySelectionMessage: () => 'custom message', }) - clickOnItemAtIndex(3) + await clickOnItemAtIndex(3) waitForDebouncedA11yStatusUpdate() expect(getA11yStatusContainer()).toHaveTextContent('custom message') @@ -158,12 +172,12 @@ describe('props', () => { }) afterAll(jest.useRealTimers) - test('reports that no results are available if items list is empty', () => { - const {clickOnToggleButton, getA11yStatusContainer} = renderCombobox({ + test('reports that no results are available if items list is empty', async () => { + renderCombobox({ items: [], }) - clickOnToggleButton() + await clickOnToggleButton() waitForDebouncedA11yStatusUpdate() expect(getA11yStatusContainer()).toHaveTextContent( @@ -171,12 +185,12 @@ describe('props', () => { ) }) - test('reports that one result is available if one item is shown', () => { - const {clickOnToggleButton, getA11yStatusContainer} = renderCombobox({ + test('reports that one result is available if one item is shown', async () => { + renderCombobox({ items: ['bla'], }) - clickOnToggleButton() + await clickOnToggleButton() waitForDebouncedA11yStatusUpdate() expect(getA11yStatusContainer()).toHaveTextContent( @@ -184,12 +198,12 @@ describe('props', () => { ) }) - test('reports the number of results available if more than one item are shown', () => { - const {clickOnToggleButton, getA11yStatusContainer} = renderCombobox({ + test('reports the number of results available if more than one item are shown', async () => { + renderCombobox({ items: ['bla', 'blabla'], }) - clickOnToggleButton() + await clickOnToggleButton() waitForDebouncedA11yStatusUpdate() expect(getA11yStatusContainer()).toHaveTextContent( @@ -197,48 +211,48 @@ describe('props', () => { ) }) - test('is empty on menu close', () => { - const {clickOnToggleButton, getA11yStatusContainer} = renderCombobox({ + test('is empty on menu close', async () => { + renderCombobox({ items: ['bla', 'blabla'], initialIsOpen: true, }) - clickOnToggleButton() + await clickOnToggleButton() waitForDebouncedA11yStatusUpdate() expect(getA11yStatusContainer()).toBeEmptyDOMElement() }) - test('is removed after 500ms as a cleanup', () => { - const {clickOnToggleButton, getA11yStatusContainer} = renderCombobox() + test('is removed after 500ms as a cleanup', async () => { + renderCombobox() - clickOnToggleButton() + await clickOnToggleButton() waitForDebouncedA11yStatusUpdate() act(() => jest.advanceTimersByTime(500)) expect(getA11yStatusContainer()).toHaveTextContent('') }) - test('is replaced with the user provided one', () => { - const {clickOnToggleButton, getA11yStatusContainer} = renderCombobox({ + test('is replaced with the user provided one', async () => { + renderCombobox({ getA11yStatusMessage: () => 'custom message', }) - clickOnToggleButton() + await clickOnToggleButton() waitForDebouncedA11yStatusUpdate() expect(getA11yStatusContainer()).toHaveTextContent('custom message') }) - test('is called with previousResultCount that gets updated correctly', () => { + test('is called with previousResultCount that gets updated correctly', async () => { const getA11yStatusMessage = jest.fn() const inputItems = ['aaa', 'bbb'] - const {clickOnToggleButton, changeInputValue} = renderCombobox({ + renderCombobox({ getA11yStatusMessage, items: inputItems, }) - clickOnToggleButton() + await clickOnToggleButton() waitForDebouncedA11yStatusUpdate() expect(getA11yStatusMessage).toHaveBeenCalledTimes(1) @@ -250,7 +264,7 @@ describe('props', () => { ) inputItems.pop() - changeInputValue('a') + await changeInputValue('a') waitForDebouncedA11yStatusUpdate() expect(getA11yStatusMessage).toHaveBeenCalledTimes(2) @@ -262,19 +276,19 @@ describe('props', () => { ) }) - test('is called with object that contains specific props at toggle', () => { + test('is called with object that contains specific props at toggle', async () => { const getA11yStatusMessage = jest.fn() const inputValue = 'a' const highlightedIndex = 1 const initialSelectedItem = items[highlightedIndex] - const {clickOnToggleButton} = renderCombobox({ + renderCombobox({ inputValue, initialSelectedItem, items, getA11yStatusMessage, }) - clickOnToggleButton() + await clickOnToggleButton() waitForDebouncedA11yStatusUpdate() expect(getA11yStatusMessage).toHaveBeenCalledWith({ @@ -289,7 +303,7 @@ describe('props', () => { }) }) - test('is added to the document provided by the user as prop', () => { + test('is added to the document provided by the user as prop', async () => { const environment = { document: { getElementById: jest.fn(() => ({ @@ -300,23 +314,18 @@ describe('props', () => { addEventListener: jest.fn(), removeEventListener: jest.fn(), } - const {clickOnToggleButton} = renderCombobox({items: [], environment}) + renderCombobox({items: [], environment}) - clickOnToggleButton() + await clickOnToggleButton() waitForDebouncedA11yStatusUpdate() expect(environment.document.getElementById).toHaveBeenCalledTimes(1) }) - test('is called when isOpen, highlightedIndex, inputValue or items change', () => { + test('is called when isOpen, highlightedIndex, inputValue or items change', async () => { const getA11yStatusMessage = jest.fn() const inputItems = ['aaa', 'bbb'] - const { - clickOnToggleButton, - rerender, - keyDownOnInput, - changeInputValue, - } = renderCombobox({ + const {rerender} = renderCombobox({ getA11yStatusMessage, items, }) @@ -339,7 +348,7 @@ describe('props', () => { ) expect(getA11yStatusMessage).toHaveBeenCalledTimes(1) - clickOnToggleButton() + await clickOnToggleButton() waitForDebouncedA11yStatusUpdate() expect(getA11yStatusMessage).toHaveBeenCalledWith( @@ -347,7 +356,7 @@ describe('props', () => { ) expect(getA11yStatusMessage).toHaveBeenCalledTimes(2) - changeInputValue('b') + await changeInputValue('b') waitForDebouncedA11yStatusUpdate() expect(getA11yStatusMessage).toHaveBeenCalledWith( @@ -355,7 +364,7 @@ describe('props', () => { ) expect(getA11yStatusMessage).toHaveBeenCalledTimes(3) - keyDownOnInput('ArrowDown') + await keyDownOnInput('{ArrowDown}') waitForDebouncedA11yStatusUpdate() expect(getA11yStatusMessage).toHaveBeenCalledWith( @@ -365,55 +374,50 @@ describe('props', () => { }) }) - describe('highlightedIndex', () => { - test('controls the state property if passed', () => { - const highlightedIndex = 1 - const expectedItemId = defaultIds.getItemId(highlightedIndex) - const {keyDownOnInput, input} = renderCombobox({ - isOpen: true, - highlightedIndex, - }) + test('highlightedIndex controls the state property if passed', async () => { + const highlightedIndex = 1 + const expectedItemId = defaultIds.getItemId(highlightedIndex) + renderCombobox({ + isOpen: true, + highlightedIndex, + }) + const input = getInput() - expect(input).toHaveAttribute('aria-activedescendant', expectedItemId) + expect(input).toHaveAttribute('aria-activedescendant', expectedItemId) - keyDownOnInput('ArrowDown') + await keyDownOnInput('{ArrowDown}') - expect(input).toHaveAttribute('aria-activedescendant', expectedItemId) + expect(input).toHaveAttribute('aria-activedescendant', expectedItemId) - keyDownOnInput('End') + await keyDownOnInput('{End}') - expect(input).toHaveAttribute('aria-activedescendant', expectedItemId) + expect(input).toHaveAttribute('aria-activedescendant', expectedItemId) - keyDownOnInput('ArrowUp') + await keyDownOnInput('{ArrowUp}') - expect(input).toHaveAttribute('aria-activedescendant', expectedItemId) - }) + expect(input).toHaveAttribute('aria-activedescendant', expectedItemId) }) describe('inputValue', () => { - test('controls the state property if passed', () => { - const { - keyDownOnInput, - input, - blurInput, - clickOnItemAtIndex, - } = renderCombobox({isOpen: true, inputValue: 'Dohn Joe'}) + test('controls the state property if passed', async () => { + renderCombobox({isOpen: true, inputValue: 'Dohn Joe'}) + const input = getInput() - keyDownOnInput('ArrowDown') - keyDownOnInput('Enter') + await keyDownOnInput('{ArrowDown}') + await keyDownOnInput('{Enter}') expect(input).toHaveValue('Dohn Joe') - clickOnItemAtIndex(0) + await clickOnItemAtIndex(0) expect(input).toHaveValue('Dohn Joe') - keyDownOnInput('ArrowUp') - keyDownOnInput('Enter') + await keyDownOnInput('{ArrowUp}') + await keyDownOnInput('{Enter}') expect(input).toHaveValue('Dohn Joe') - blurInput() + await tab() expect(input).toHaveValue('Dohn Joe') }) @@ -421,10 +425,11 @@ describe('props', () => { test('is changed once a new selectedItem comes from props', () => { const initialSelectedItem = 'John Doe' const finalSelectedItem = 'John Wick' - const {rerender, input} = renderCombobox({ + const {rerender} = renderCombobox({ isOpen: true, selectedItem: initialSelectedItem, }) + const input = getInput() expect(input).toHaveValue(initialSelectedItem) @@ -434,68 +439,56 @@ describe('props', () => { }) }) - describe('isOpen', () => { - test('controls the state property if passed', () => { - const { - clickOnToggleButton, - getItems, - blurInput, - keyDownOnInput, - } = renderCombobox({isOpen: true}) + test('isOpen controls the state property if passed', async () => { + renderCombobox({isOpen: true}) - expect(getItems()).toHaveLength(items.length) + expect(getItems()).toHaveLength(items.length) - clickOnToggleButton() + await clickOnToggleButton() - expect(getItems()).toHaveLength(items.length) + expect(getItems()).toHaveLength(items.length) - keyDownOnInput('Escape') + await keyDownOnInput('{Escape}') - expect(getItems()).toHaveLength(items.length) + expect(getItems()).toHaveLength(items.length) - blurInput() + await tab() - expect(getItems()).toHaveLength(items.length) - }) + expect(getItems()).toHaveLength(items.length) }) - describe('selectedItem', () => { - test('controls the state property if passed', () => { - const selectedItem = items[2] - const { - keyDownOnInput, - input, - clickOnItemAtIndex, - clickOnToggleButton, - } = renderCombobox({ - selectedItem, - initialIsOpen: true, - }) + test('selectedItem controls the state property if passed', async () => { + const selectedItem = items[2] - expect(input).toHaveValue(selectedItem) + renderCombobox({ + selectedItem, + initialIsOpen: true, + }) + const input = getInput() - keyDownOnInput('ArrowDown') - keyDownOnInput('Enter') - clickOnToggleButton() + expect(input).toHaveValue(selectedItem) - expect(input).toHaveValue(selectedItem) + await keyDownOnInput('{ArrowDown}') + await keyDownOnInput('{Enter}') + await clickOnToggleButton() - keyDownOnInput('ArrowUp') - keyDownOnInput('Enter') - clickOnToggleButton() + expect(input).toHaveValue(selectedItem) - expect(input).toHaveValue(selectedItem) + await keyDownOnInput('{ArrowUp}') + await keyDownOnInput('{Enter}') + await clickOnToggleButton() - keyDownOnInput('Escape') - clickOnToggleButton() + expect(input).toHaveValue(selectedItem) - expect(input).toHaveValue(selectedItem) + await keyDownOnInput('{Escape}') + await clickOnToggleButton() - clickOnItemAtIndex(1) - clickOnToggleButton() + expect(input).toHaveValue(selectedItem) - expect(input).toHaveValue(selectedItem) - }) + await clickOnItemAtIndex(1) + await clickOnToggleButton() + + expect(input).toHaveValue(selectedItem) }) describe('stateReducer', () => { @@ -582,17 +575,9 @@ describe('props', () => { ) }) - test('is called at each state change with the appropriate change type', () => { + test('is called at each state change with the appropriate change type', async () => { const stateReducer = jest.fn((s, a) => a.changes) - const { - clickOnToggleButton, - changeInputValue, - clickOnItemAtIndex, - mouseLeaveMenu, - mouseMoveItemAtIndex, - keyDownOnInput, - blurInput, - } = renderCombobox({stateReducer}) + renderCombobox({stateReducer}) const initialState = { isOpen: false, highlightedIndex: -1, @@ -622,7 +607,8 @@ describe('props', () => { type: stateChangeTypes.ItemMouseMove, }, { - step: mouseLeaveMenu, + step: mouseLeaveItemAtIndex, + args: 2, state: { isOpen: true, highlightedIndex: -1, @@ -632,7 +618,7 @@ describe('props', () => { type: stateChangeTypes.MenuMouseLeave, }, { - step: blurInput, + step: tab, state: { isOpen: false, highlightedIndex: -1, @@ -654,7 +640,7 @@ describe('props', () => { }, { step: keyDownOnInput, - args: 'ArrowDown', + args: '{ArrowDown}', state: { isOpen: true, highlightedIndex: 0, @@ -665,7 +651,7 @@ describe('props', () => { }, { step: keyDownOnInput, - args: 'Enter', + args: '{Enter}', state: { isOpen: false, highlightedIndex: -1, @@ -676,7 +662,7 @@ describe('props', () => { }, { step: keyDownOnInput, - args: 'Escape', + args: '{Escape}', state: { isOpen: false, highlightedIndex: -1, @@ -687,7 +673,7 @@ describe('props', () => { }, { step: keyDownOnInput, - args: 'ArrowDown', + args: '{ArrowDown}', state: { isOpen: true, highlightedIndex: 0, @@ -698,7 +684,7 @@ describe('props', () => { }, { step: keyDownOnInput, - args: 'ArrowUp', + args: '{ArrowUp}', state: { isOpen: true, highlightedIndex: items.length - 1, @@ -709,7 +695,7 @@ describe('props', () => { }, { step: keyDownOnInput, - args: 'Home', + args: '{Home}', state: { isOpen: true, highlightedIndex: 0, @@ -720,7 +706,7 @@ describe('props', () => { }, { step: keyDownOnInput, - args: 'End', + args: '{End}', state: { isOpen: true, highlightedIndex: items.length - 1, @@ -729,6 +715,17 @@ describe('props', () => { }, type: stateChangeTypes.InputKeyDownEnd, }, + { + step: mouseMoveItemAtIndex, + args: 3, + state: { + isOpen: true, + highlightedIndex: 3, + inputValue: '', + selectedItem: null, + }, + type: stateChangeTypes.ItemMouseMove, + }, { step: clickOnItemAtIndex, args: 3, @@ -742,10 +739,10 @@ describe('props', () => { }, { step: keyDownOnInput, - args: 'ArrowUp', + args: '{ArrowUp}', state: { isOpen: true, - highlightedIndex: 2, + highlightedIndex: 3, inputValue: items[3], selectedItem: items[3], }, @@ -753,7 +750,7 @@ describe('props', () => { }, { step: keyDownOnInput, - args: 'Escape', + args: '{Escape}', state: { isOpen: false, highlightedIndex: -1, @@ -770,7 +767,8 @@ describe('props', () => { const {step, state, args, type} = testCases[index] const previousState = testCases[index - 1]?.state ?? initialState - step(args) + // eslint-disable-next-line no-await-in-loop + await step(args) expect(stateReducer).toHaveBeenCalledTimes(index + 1) expect(stateReducer).toHaveBeenLastCalledWith( @@ -780,21 +778,21 @@ describe('props', () => { } }) - test('replaces prop values with user defined', () => { + test('replaces prop values with user defined', async () => { const inputValue = 'Robin Hood' const stateReducer = jest.fn((s, a) => { const changes = a.changes changes.inputValue = inputValue return changes }) - const {clickOnToggleButton, input} = renderCombobox({stateReducer}) + renderCombobox({stateReducer}) - clickOnToggleButton() + await clickOnToggleButton() - expect(input).toHaveValue('Robin Hood') + expect(getInput()).toHaveValue('Robin Hood') }) - test('receives state, changes and type', () => { + test('receives state, changes and type', async () => { const stateReducer = jest.fn((s, a) => { expect(a.type).not.toBeUndefined() expect(a.type).not.toBeNull() @@ -807,12 +805,12 @@ describe('props', () => { return a.changes }) - const {clickOnToggleButton} = renderCombobox({stateReducer}) + renderCombobox({stateReducer}) - clickOnToggleButton() + await clickOnToggleButton() }) - test('changes are visible in onChange handlers', () => { + test('changes are visible in onChange handlers', async () => { const highlightedIndex = 2 const selectedItem = {foo: 'bar'} const isOpen = true @@ -828,7 +826,7 @@ describe('props', () => { const onHighlightedIndexChange = jest.fn() const onIsOpenChange = jest.fn() const onStateChange = jest.fn() - const {clickOnToggleButton} = renderCombobox({ + renderCombobox({ stateReducer, onStateChange, onSelectedItemChange, @@ -837,7 +835,7 @@ describe('props', () => { onInputValueChange, }) - clickOnToggleButton() + await clickOnToggleButton() expect(onInputValueChange).toHaveBeenCalledTimes(1) expect(onInputValueChange).toHaveBeenCalledWith( @@ -874,14 +872,14 @@ describe('props', () => { }) describe('onInputValueChange', () => { - test('is called at inputValue change', () => { + test('is called at inputValue change', async () => { const inputValue = 'test' const onInputValueChange = jest.fn() - const {changeInputValue} = renderCombobox({ + renderCombobox({ onInputValueChange, }) - changeInputValue(inputValue) + await changeInputValue(inputValue) expect(onInputValueChange).toHaveBeenCalledWith( expect.objectContaining({ @@ -890,33 +888,33 @@ describe('props', () => { ) }) - test('is not called at if inputValue is the same', () => { + test('is not called at if inputValue is the same', async () => { const inputValue = items[0] const onInputValueChange = jest.fn() - const {clickOnItemAtIndex} = renderCombobox({ + renderCombobox({ initialInputValue: inputValue, initialIsOpen: true, onInputValueChange, }) - clickOnItemAtIndex(0) + await clickOnItemAtIndex(0) expect(onInputValueChange).not.toHaveBeenCalled() }) - test('works correctly with the corresponding control prop', () => { + test('works correctly with the corresponding control prop', async () => { let inputValue = 'nep' - const {changeInputValue, input, rerender} = renderCombobox({ + const {rerender} = renderCombobox({ inputValue, onSelectedItemChange: changes => { inputValue = changes.inputValue }, }) - changeInputValue('nept') + await changeInputValue('nept') rerender({inputValue}) - expect(input).toHaveValue(inputValue) + expect(getInput()).toHaveValue(inputValue) }) test('can have downshift actions executed', () => { @@ -936,15 +934,15 @@ describe('props', () => { }) describe('onSelectedItemChange', () => { - test('is called at selectedItem change', () => { + test('is called at selectedItem change', async () => { const itemIndex = 0 const onSelectedItemChange = jest.fn() - const {clickOnItemAtIndex} = renderCombobox({ + renderCombobox({ initialIsOpen: true, onSelectedItemChange, }) - clickOnItemAtIndex(itemIndex) + await clickOnItemAtIndex(itemIndex) expect(onSelectedItemChange).toHaveBeenCalledWith( expect.objectContaining({ @@ -954,24 +952,24 @@ describe('props', () => { ) }) - test('is not called at if selectedItem is the same', () => { + test('is not called at if selectedItem is the same', async () => { const itemIndex = 0 const onSelectedItemChange = jest.fn() - const {clickOnItemAtIndex} = renderCombobox({ + renderCombobox({ initialIsOpen: true, initialSelectedItem: items[itemIndex], onSelectedItemChange, }) - clickOnItemAtIndex(itemIndex) + await clickOnItemAtIndex(itemIndex) expect(onSelectedItemChange).not.toHaveBeenCalled() }) - test('works correctly with the corresponding control prop', () => { + test('works correctly with the corresponding control prop', async () => { let selectedItem = items[2] const selectionIndex = 3 - const {clickOnItemAtIndex, input, rerender} = renderCombobox({ + const {rerender} = renderCombobox({ initialIsOpen: true, selectedItem, onSelectedItemChange: changes => { @@ -979,21 +977,21 @@ describe('props', () => { }, }) - clickOnItemAtIndex(selectionIndex) + await clickOnItemAtIndex(selectionIndex) rerender({selectedItem}) - expect(input).toHaveValue(items[selectionIndex]) + expect(getInput()).toHaveValue(items[selectionIndex]) }) - test('works correctly with the corresponding control prop in strict mode', () => { + test('works correctly with the corresponding control prop in strict mode', async () => { const onSelectedItemChange = jest.fn() const itemIndex = 0 - const {clickOnItemAtIndex} = renderCombobox( + renderCombobox( {selectedItem: null, initialIsOpen: true, onSelectedItemChange}, ui => {ui}, ) - clickOnItemAtIndex(itemIndex) + await clickOnItemAtIndex(itemIndex) expect(onSelectedItemChange).toHaveBeenCalledWith( expect.objectContaining({ @@ -1019,14 +1017,14 @@ describe('props', () => { }) describe('onHighlightedIndexChange', () => { - test('is called at each highlightedIndex change', () => { + test('is called at each highlightedIndex change', async () => { const onHighlightedIndexChange = jest.fn() - const {keyDownOnInput} = renderCombobox({ + renderCombobox({ initialIsOpen: true, onHighlightedIndexChange, }) - keyDownOnInput('ArrowDown') + await keyDownOnInput('{ArrowDown}') expect(onHighlightedIndexChange).toHaveBeenCalledWith( expect.objectContaining({ @@ -1036,37 +1034,37 @@ describe('props', () => { ) }) - test('is not called if highlightedIndex is the same', () => { + test('is not called if highlightedIndex is the same', async () => { const onHighlightedIndexChange = jest.fn() - const {keyDownOnInput, mouseMoveItemAtIndex} = renderCombobox({ + renderCombobox({ initialIsOpen: true, initialHighlightedIndex: 0, onHighlightedIndexChange, circularNavigation: false, }) - keyDownOnInput('ArrowUp') + await keyDownOnInput('{ArrowUp}') expect(onHighlightedIndexChange).not.toHaveBeenCalled() - keyDownOnInput('Home') + await keyDownOnInput('{Home}') expect(onHighlightedIndexChange).not.toHaveBeenCalled() - mouseMoveItemAtIndex(0) + await mouseMoveItemAtIndex(0) expect(onHighlightedIndexChange).not.toHaveBeenCalled() }) - test('is called on first open when initialSelectedItem is set', () => { + test('is called on first open when initialSelectedItem is set', async () => { const index = 2 const onHighlightedIndexChange = jest.fn() - const {clickOnToggleButton} = renderCombobox({ + renderCombobox({ initialSelectedItem: items[index], onHighlightedIndexChange, }) - clickOnToggleButton() + await clickOnToggleButton() expect(onHighlightedIndexChange).toHaveBeenCalledTimes(1) expect(onHighlightedIndexChange).toHaveBeenCalledWith( @@ -1076,15 +1074,15 @@ describe('props', () => { ) }) - test('is called on first open when selectedItem is set', () => { + test('is called on first open when selectedItem is set', async () => { const index = 2 const onHighlightedIndexChange = jest.fn() - const {clickOnToggleButton} = renderCombobox({ + renderCombobox({ selectedItem: items[index], onHighlightedIndexChange, }) - clickOnToggleButton() + await clickOnToggleButton() expect(onHighlightedIndexChange).toHaveBeenCalledTimes(1) expect(onHighlightedIndexChange).toHaveBeenCalledWith( @@ -1094,15 +1092,15 @@ describe('props', () => { ) }) - test('is called on first open when defaultSelectedItem is set', () => { + test('is called on first open when defaultSelectedItem is set', async () => { const index = 2 const onHighlightedIndexChange = jest.fn() - const {clickOnToggleButton} = renderCombobox({ + renderCombobox({ defaultSelectedItem: items[index], onHighlightedIndexChange, }) - clickOnToggleButton() + await clickOnToggleButton() expect(onHighlightedIndexChange).toHaveBeenCalledTimes(1) expect(onHighlightedIndexChange).toHaveBeenCalledWith( @@ -1112,9 +1110,9 @@ describe('props', () => { ) }) - test('works correctly with the corresponding control prop', () => { + test('works correctly with the corresponding control prop', async () => { let highlightedIndex = 2 - const {keyDownOnInput, input, rerender} = renderCombobox({ + const {rerender} = renderCombobox({ isOpen: true, highlightedIndex, onHighlightedIndexChange: changes => { @@ -1122,10 +1120,10 @@ describe('props', () => { }, }) - keyDownOnInput('ArrowDown') + await keyDownOnInput('{ArrowDown}') rerender({highlightedIndex, isOpen: true}) - expect(input).toHaveAttribute( + expect(getInput()).toHaveAttribute( 'aria-activedescendant', defaultIds.getItemId(3), ) @@ -1151,14 +1149,14 @@ describe('props', () => { }) describe('onIsOpenChange', () => { - test('is called at each isOpen change', () => { + test('is called at each isOpen change', async () => { const onIsOpenChange = jest.fn() - const {keyDownOnInput} = renderCombobox({ + renderCombobox({ initialIsOpen: true, onIsOpenChange, }) - keyDownOnInput('Escape') + await keyDownOnInput('{Escape}') expect(onIsOpenChange).toHaveBeenCalledWith( expect.objectContaining({ @@ -1168,28 +1166,28 @@ describe('props', () => { ) }) - test('is not called at if isOpen is the same', () => { + test('is not called at if isOpen is the same', async () => { const onIsOpenChange = jest.fn() - const {clickOnItemAtIndex} = renderCombobox({ + renderCombobox({ defaultIsOpen: true, onIsOpenChange, }) - clickOnItemAtIndex(0) + await clickOnItemAtIndex(0) expect(onIsOpenChange).not.toHaveBeenCalledWith() }) - test('works correctly with the corresponding control prop', () => { + test('works correctly with the corresponding control prop', async () => { let isOpen = true - const {keyDownOnInput, getItems, rerender} = renderCombobox({ + const {rerender} = renderCombobox({ isOpen, onIsOpenChange: changes => { isOpen = changes.isOpen }, }) - keyDownOnInput('Escape') + await keyDownOnInput('{Escape}') rerender({isOpen}) expect(getItems()).toHaveLength(0) @@ -1212,19 +1210,11 @@ describe('props', () => { }) describe('onStateChange', () => { - test('is called at each state property change', () => { + test('is called at each state property change', async () => { const onStateChange = jest.fn() - const { - clickOnToggleButton, - changeInputValue, - clickOnItemAtIndex, - mouseLeaveMenu, - mouseMoveItemAtIndex, - keyDownOnInput, - blurInput, - } = renderCombobox({onStateChange}) - - clickOnToggleButton() + renderCombobox({onStateChange}) + + await clickOnToggleButton() expect(onStateChange).toHaveBeenCalledTimes(1) expect(onStateChange).toHaveBeenLastCalledWith( @@ -1234,7 +1224,7 @@ describe('props', () => { }), ) - changeInputValue('t') + await changeInputValue('t') expect(onStateChange).toHaveBeenCalledTimes(2) expect(onStateChange).toHaveBeenLastCalledWith( @@ -1244,17 +1234,17 @@ describe('props', () => { }), ) - mouseMoveItemAtIndex(1) + await mouseMoveItemAtIndex(2) expect(onStateChange).toHaveBeenCalledTimes(3) expect(onStateChange).toHaveBeenLastCalledWith( expect.objectContaining({ - highlightedIndex: 1, + highlightedIndex: 2, type: stateChangeTypes.ItemMouseMove, }), ) - clickOnItemAtIndex(2) + await clickOnItemAtIndex(2) expect(onStateChange).toHaveBeenCalledTimes(4) expect(onStateChange).toHaveBeenLastCalledWith( @@ -1266,17 +1256,17 @@ describe('props', () => { }), ) - keyDownOnInput('ArrowDown') + await keyDownOnInput('{ArrowDown}') expect(onStateChange).toHaveBeenCalledTimes(5) expect(onStateChange).toHaveBeenLastCalledWith( expect.objectContaining({ - highlightedIndex: 3, + highlightedIndex: 2, type: stateChangeTypes.InputKeyDownArrowDown, }), ) - keyDownOnInput('End') + await keyDownOnInput('{End}') expect(onStateChange).toHaveBeenCalledTimes(6) expect(onStateChange).toHaveBeenLastCalledWith( @@ -1286,7 +1276,7 @@ describe('props', () => { }), ) - keyDownOnInput('Home') + await keyDownOnInput('{Home}') expect(onStateChange).toHaveBeenCalledTimes(7) expect(onStateChange).toHaveBeenLastCalledWith( @@ -1296,7 +1286,7 @@ describe('props', () => { }), ) - keyDownOnInput('Enter') + await keyDownOnInput('{Enter}') expect(onStateChange).toHaveBeenCalledTimes(8) expect(onStateChange).toHaveBeenLastCalledWith( @@ -1308,7 +1298,7 @@ describe('props', () => { }), ) - keyDownOnInput('Escape') + await keyDownOnInput('{Escape}') expect(onStateChange).toHaveBeenCalledTimes(9) expect(onStateChange).toHaveBeenLastCalledWith( @@ -1319,7 +1309,7 @@ describe('props', () => { }), ) - keyDownOnInput('ArrowUp') + await keyDownOnInput('{ArrowUp}') expect(onStateChange).toHaveBeenCalledTimes(10) expect(onStateChange).toHaveBeenLastCalledWith( @@ -1330,7 +1320,8 @@ describe('props', () => { }), ) - mouseLeaveMenu() + await mouseMoveItemAtIndex(25) + await mouseLeaveItemAtIndex(25) expect(onStateChange).toHaveBeenCalledTimes(11) expect(onStateChange).toHaveBeenLastCalledWith( @@ -1340,7 +1331,7 @@ describe('props', () => { }), ) - blurInput() + await tab() expect(onStateChange).toHaveBeenCalledTimes(12) expect(onStateChange).toHaveBeenLastCalledWith( diff --git a/src/hooks/useCombobox/testUtils.js b/src/hooks/useCombobox/testUtils.js index f0a67e7be..eff53baeb 100644 --- a/src/hooks/useCombobox/testUtils.js +++ b/src/hooks/useCombobox/testUtils.js @@ -1,16 +1,11 @@ import * as React from 'react' -import {render, fireEvent, screen} from '@testing-library/react' +import {render} from '@testing-library/react' import {renderHook} from '@testing-library/react-hooks' -import userEvent from '@testing-library/user-event' import {defaultProps} from '../utils' -import {items} from '../testUtils' +import {dataTestIds, items, user, getInput} from '../testUtils' import useCombobox from '.' -const dataTestIds = { - toggleButton: 'toggle-button-id', - item: index => `item-id-${index}`, - input: 'input-id', -} +export * from '../testUtils' jest.mock('../../utils', () => { const utils = jest.requireActual('../../utils') @@ -34,74 +29,25 @@ jest.mock('../utils', () => { beforeEach(jest.resetAllMocks) afterAll(jest.restoreAllMocks) -const renderCombobox = (props, uiCallback) => { +export async function changeInputValue(inputValue) { + await user.type(getInput(), inputValue) +} + +export const renderCombobox = (props, uiCallback) => { const renderSpy = jest.fn() const ui = const utils = render(uiCallback ? uiCallback(ui) : ui) const rerender = newProps => utils.rerender() - const label = screen.getByText(/choose an element/i) - const menu = screen.getByRole('listbox') - const toggleButton = screen.getByTestId(dataTestIds.toggleButton) - const input = screen.getByTestId(dataTestIds.input) - const combobox = screen.getByRole('combobox') - const getItemAtIndex = index => screen.getByTestId(dataTestIds.item(index)) - const getItems = () => screen.queryAllByRole('option') - const clickOnItemAtIndex = index => { - // keeping fireEvent so we don't trigger input blur via user event - fireEvent.click(getItemAtIndex(index)) - } - const clickOnToggleButton = () => { - userEvent.click(toggleButton) - } - const mouseMoveItemAtIndex = index => { - userEvent.hover(getItemAtIndex(index)) - } - const getA11yStatusContainer = () => screen.queryByRole('status') - const mouseLeaveMenu = () => { - userEvent.unhover(menu) - } - const changeInputValue = inputValue => { - userEvent.type(input, inputValue) - } - const focusInput = () => { - fireEvent.focus(input) - } - const keyDownOnInput = (key, options = {}) => { - if (document.activeElement !== input) { - focusInput() - } - - fireEvent.keyDown(input, {key, ...options}) - } - const blurInput = () => { - fireEvent.blur(input) - } return { ...utils, renderSpy, rerender, - label, - menu, - toggleButton, - getItemAtIndex, - clickOnItemAtIndex, - mouseMoveItemAtIndex, - getItems, - clickOnToggleButton, - getA11yStatusContainer, - mouseLeaveMenu, - input, - combobox, - changeInputValue, - keyDownOnInput, - blurInput, - focusInput, } } -const DropdownCombobox = ({renderSpy, renderItem, ...props}) => { +function DropdownCombobox({renderSpy, renderItem, ...props}) { const { isOpen, getToggleButtonProps, @@ -133,7 +79,7 @@ const DropdownCombobox = ({renderSpy, renderItem, ...props}) => { const stringItem = item instanceof Object ? itemToString(item) : item return renderItem ? ( - renderItem({index, item, getItemProps, dataTestIds, stringItem}) + renderItem({index, item, getItemProps, stringItem}) ) : (
      • { ) } -const renderUseCombobox = props => { +export const renderUseCombobox = props => { return renderHook(() => useCombobox({items, ...props})) } - -export {renderUseCombobox, dataTestIds, renderCombobox} diff --git a/src/hooks/useMultipleSelection/__tests__/getDropdownProps.test.js b/src/hooks/useMultipleSelection/__tests__/getDropdownProps.test.js index dc854d5a6..e0679fad8 100644 --- a/src/hooks/useMultipleSelection/__tests__/getDropdownProps.test.js +++ b/src/hooks/useMultipleSelection/__tests__/getDropdownProps.test.js @@ -1,7 +1,13 @@ import {act, renderHook} from '@testing-library/react-hooks' -import {renderMultipleCombobox, renderUseMultipleSelection} from '../testUtils' -import {items} from '../../testUtils' -// eslint-disable-next-line import/default +import { + clickOnInput, + focusSelectedItemAtIndex, + getSelectedItemAtIndex, + getSelectedItems, + renderMultipleCombobox, + renderUseMultipleSelection, +} from '../testUtils' +import {getInput, items, keyDownOnInput} from '../../testUtils' import utils from '../../utils' import useMultipleSelection from '..' @@ -101,138 +107,121 @@ describe('getDropdownProps', () => { describe('event handlers', () => { describe('on keydown', () => { - test('arrow left should make first selected item active', () => { - const {keyDownOnInput, getSelectedItemAtIndex} = renderMultipleCombobox( - { - multipleSelectionProps: { - initialSelectedItems: [items[0], items[1]], - }, + test('arrow left should make first selected item active', async () => { + renderMultipleCombobox({ + multipleSelectionProps: { + initialSelectedItems: [items[0], items[1]], }, - ) + }) - keyDownOnInput('ArrowLeft') + await keyDownOnInput('{ArrowLeft}') expect(getSelectedItemAtIndex(1)).toHaveFocus() }) - test('arrow left should not work if pressed with modifier keys', () => { - const {keyDownOnInput, getSelectedItems} = renderMultipleCombobox({ + test('arrow left should not work if pressed with modifier keys', async () => { + renderMultipleCombobox({ multipleSelectionProps: {initialSelectedItems: [items[0], items[1]]}, }) - keyDownOnInput('ArrowLeft', {shiftKey: true}) + await keyDownOnInput('{Shift>}{ArrowLeft}{/Shift}') expect(getSelectedItems()).toHaveLength(2) - keyDownOnInput('ArrowLeft', {altKey: true}) + await keyDownOnInput('{Alt>}{ArrowLeft}{/Alt}') expect(getSelectedItems()).toHaveLength(2) - keyDownOnInput('ArrowLeft', {metaKey: true}) + await keyDownOnInput('{Meta>}{ArrowLeft}{/Meta}') expect(getSelectedItems()).toHaveLength(2) - keyDownOnInput('ArrowLeft', {ctrlKey: true}) + await keyDownOnInput('{Control>}{ArrowLeft}{/Control}') expect(getSelectedItems()).toHaveLength(2) }) - test('backspace should remove the first selected item', () => { - const {keyDownOnInput, getSelectedItems} = renderMultipleCombobox({ + test('backspace should remove the first selected item', async () => { + renderMultipleCombobox({ multipleSelectionProps: {initialSelectedItems: [items[0], items[1]]}, }) - keyDownOnInput('Backspace') + await keyDownOnInput('{Backspace}') expect(getSelectedItems()).toHaveLength(1) }) - test('backspace should not work if pressed with modifier keys', () => { - const {keyDownOnInput, getSelectedItems} = renderMultipleCombobox({ + test('backspace should not work if pressed with modifier keys', async () => { + renderMultipleCombobox({ multipleSelectionProps: {initialSelectedItems: [items[0], items[1]]}, }) - keyDownOnInput('Backspace', {shiftKey: true}) + await keyDownOnInput('{Shift>}{Backspace}{/Shift}') expect(getSelectedItems()).toHaveLength(2) - keyDownOnInput('Backspace', {altKey: true}) + await keyDownOnInput('{Alt>}{ArrowLeft}{/Alt}') expect(getSelectedItems()).toHaveLength(2) - keyDownOnInput('Backspace', {metaKey: true}) + await keyDownOnInput('{Meta>}{ArrowLeft}{/Meta}') expect(getSelectedItems()).toHaveLength(2) - keyDownOnInput('Backspace', {ctrlKey: true}) + await keyDownOnInput('{Control>}{ArrowLeft}{/Control}') expect(getSelectedItems()).toHaveLength(2) }) - test('backspace should not work if pressed with cursor not on first position', () => { - const { - keyDownOnInput, - getSelectedItems, - input, - } = renderMultipleCombobox({ + test('backspace should not work if pressed with cursor not on first position', async () => { + renderMultipleCombobox({ multipleSelectionProps: { initialSelectedItems: [items[0], items[1]], }, comboboxProps: {initialInputValue: 'test'}, }) + const input = getInput() input.selectionStart = 1 input.selectionEnd = 1 - keyDownOnInput('Backspace') + await keyDownOnInput('{Backspace}') expect(getSelectedItems()).toHaveLength(2) }) - test('backspace should not work if pressed with cursor highlighting text', () => { - const { - keyDownOnInput, - getSelectedItems, - input, - } = renderMultipleCombobox({ + test('backspace should not work if pressed with cursor highlighting text', async () => { + renderMultipleCombobox({ multipleSelectionProps: { initialSelectedItems: [items[0], items[1]], }, comboboxProps: {initialInputValue: 'test'}, }) + const input = getInput() input.selectionStart = 0 input.selectionEnd = 3 - keyDownOnInput('Backspace') + await keyDownOnInput('{Backspace}') expect(getSelectedItems()).toHaveLength(2) }) - test("other than the ones supported don't affect anything", () => { - const { - keyDownOnInput, - getSelectedItems, - input, - } = renderMultipleCombobox({ + test("other than the ones supported don't affect anything", async () => { + renderMultipleCombobox({ multipleSelectionProps: { initialSelectedItems: [items[0], items[1]], }, }) - keyDownOnInput('Alt') - keyDownOnInput('Control') + await keyDownOnInput('{Alt}') + await keyDownOnInput('{Control}') expect(getSelectedItems()).toHaveLength(2) - expect(input).toHaveFocus() + expect(getInput()).toHaveFocus() }) }) - test('on click it should remove active status from item if any', () => { - const { - keyDownOnInput, - getSelectedItemAtIndex, - clickOnInput, - focusSelectedItemAtIndex, - } = renderMultipleCombobox({ + test('on click it should remove active status from item if any', async () => { + renderMultipleCombobox({ multipleSelectionProps: { initialSelectedItems: [items[0], items[1]], initialActiveIndex: 1, @@ -240,11 +229,11 @@ describe('getDropdownProps', () => { }) focusSelectedItemAtIndex(1) - clickOnInput() + await clickOnInput() expect(getSelectedItemAtIndex(1)).toHaveAttribute('tabindex', '-1') - keyDownOnInput('ArrowLeft') + await keyDownOnInput('{ArrowLeft}') expect(getSelectedItemAtIndex(1)).toHaveFocus() expect(getSelectedItemAtIndex(1)).toHaveAttribute('tabindex', '0') diff --git a/src/hooks/useMultipleSelection/__tests__/getSelectedItemProps.test.js b/src/hooks/useMultipleSelection/__tests__/getSelectedItemProps.test.js index 3569d83f2..aea6835f4 100644 --- a/src/hooks/useMultipleSelection/__tests__/getSelectedItemProps.test.js +++ b/src/hooks/useMultipleSelection/__tests__/getSelectedItemProps.test.js @@ -1,7 +1,15 @@ import {act} from '@testing-library/react-hooks' -import {renderUseMultipleSelection, renderMultipleCombobox} from '../testUtils' -import {items} from '../../testUtils' +import { + renderUseMultipleSelection, + renderMultipleCombobox, + clickOnSelectedItemAtIndex, + getSelectedItemAtIndex, + focusSelectedItemAtIndex, + keyDownOnSelectedItemAtIndex, + getSelectedItems, +} from '../testUtils' +import {getInput, items, tab} from '../../testUtils' describe('getSelectedItemProps', () => { test('throws error if no index or item has been passed', () => { @@ -173,35 +181,27 @@ describe('getSelectedItemProps', () => { describe('event handlers', () => { describe('on click', () => { - test('sets tabindex to "0"', () => { - const { - clickOnSelectedItemAtIndex, - getSelectedItemAtIndex, - } = renderMultipleCombobox({ + test('sets tabindex to "0"', async () => { + renderMultipleCombobox({ multipleSelectionProps: {initialSelectedItems: [items[0], items[1]]}, }) - clickOnSelectedItemAtIndex(0) + await clickOnSelectedItemAtIndex(0) expect(getSelectedItemAtIndex(0)).toHaveAttribute('tabindex', '0') expect(getSelectedItemAtIndex(0)).toHaveFocus() expect(getSelectedItemAtIndex(1)).toHaveAttribute('tabindex', '-1') }) - test('keeps tabindex "0" to an already active item', () => { - const { - clickOnSelectedItemAtIndex, - getSelectedItemAtIndex, - focusSelectedItemAtIndex, - } = renderMultipleCombobox({ + test('keeps tabindex "0" to an already active item', async () => { + renderMultipleCombobox({ multipleSelectionProps: { initialSelectedItems: [items[0], items[1]], initialActiveIndex: 0, }, }) - focusSelectedItemAtIndex(0) - clickOnSelectedItemAtIndex(0) + await clickOnSelectedItemAtIndex(0) expect(getSelectedItemAtIndex(0)).toHaveAttribute('tabindex', '0') expect(getSelectedItemAtIndex(0)).toHaveFocus() @@ -209,290 +209,226 @@ describe('getSelectedItemProps', () => { }) describe('on key down', () => { - test('arrow left should change active item descendently', () => { - const { - keyDownOnSelectedItemAtIndex, - getSelectedItemAtIndex, - } = renderMultipleCombobox({ + test('arrow left should change active item descendently', async () => { + renderMultipleCombobox({ multipleSelectionProps: { initialSelectedItems: [items[0], items[1]], initialActiveIndex: 1, }, }) - keyDownOnSelectedItemAtIndex(1, 'ArrowLeft') + await keyDownOnSelectedItemAtIndex(1, '{ArrowLeft}') expect(getSelectedItemAtIndex(0)).toHaveAttribute('tabindex', '0') expect(getSelectedItemAtIndex(0)).toHaveFocus() }) - test(`arrow left should not change active item if it's the first one added`, () => { - const { - keyDownOnSelectedItemAtIndex, - getSelectedItemAtIndex, - focusSelectedItemAtIndex, - } = renderMultipleCombobox({ + test(`arrow left should not change active item if it's the first one added`, async () => { + renderMultipleCombobox({ multipleSelectionProps: { initialSelectedItems: [items[0], items[1]], initialActiveIndex: 0, }, }) - focusSelectedItemAtIndex(0) - keyDownOnSelectedItemAtIndex(0, 'ArrowLeft') + await keyDownOnSelectedItemAtIndex(0, '{ArrowLeft}') expect(getSelectedItemAtIndex(0)).toHaveAttribute('tabindex', '0') expect(getSelectedItemAtIndex(0)).toHaveFocus() }) - test('arrow right should change active item ascendently', () => { - const { - keyDownOnSelectedItemAtIndex, - getSelectedItemAtIndex, - } = renderMultipleCombobox({ + test('arrow right should change active item ascendently', async () => { + renderMultipleCombobox({ multipleSelectionProps: { initialSelectedItems: [items[0], items[1]], initialActiveIndex: 0, }, }) - keyDownOnSelectedItemAtIndex(0, 'ArrowRight') + await keyDownOnSelectedItemAtIndex(0, '{ArrowRight}') expect(getSelectedItemAtIndex(1)).toHaveAttribute('tabindex', '0') expect(getSelectedItemAtIndex(1)).toHaveFocus() }) - test(`arrow right should make no item active if it's on last one added`, () => { - const { - keyDownOnSelectedItemAtIndex, - getSelectedItemAtIndex, - input, - } = renderMultipleCombobox({ + test(`arrow right should make no item active if it's on last one added`, async () => { + renderMultipleCombobox({ multipleSelectionProps: { initialSelectedItems: [items[0], items[1]], initialActiveIndex: 1, }, }) - keyDownOnSelectedItemAtIndex(1, 'ArrowRight') + await keyDownOnSelectedItemAtIndex(1, '{ArrowRight}') expect(getSelectedItemAtIndex(1)).toHaveAttribute('tabindex', '-1') expect(getSelectedItemAtIndex(0)).toHaveAttribute('tabindex', '-1') - expect(input).toHaveFocus() + expect(getInput()).toHaveFocus() }) - test('arrow navigation moves focus back and forth', () => { - const { - keyDownOnSelectedItemAtIndex, - getSelectedItemAtIndex, - } = renderMultipleCombobox({ + test('arrow navigation moves focus back and forth', async () => { + renderMultipleCombobox({ multipleSelectionProps: { initialSelectedItems: [items[0], items[1], items[2]], initialActiveIndex: 2, }, }) - keyDownOnSelectedItemAtIndex(2, 'ArrowLeft') + await keyDownOnSelectedItemAtIndex(2, '{ArrowLeft}') expect(getSelectedItemAtIndex(1)).toHaveAttribute('tabindex', '0') expect(getSelectedItemAtIndex(2)).toHaveAttribute('tabindex', '-1') expect(getSelectedItemAtIndex(0)).toHaveAttribute('tabindex', '-1') - keyDownOnSelectedItemAtIndex(1, 'ArrowLeft') + await keyDownOnSelectedItemAtIndex(1, '{ArrowLeft}') expect(getSelectedItemAtIndex(0)).toHaveAttribute('tabindex', '0') expect(getSelectedItemAtIndex(2)).toHaveAttribute('tabindex', '-1') expect(getSelectedItemAtIndex(1)).toHaveAttribute('tabindex', '-1') - keyDownOnSelectedItemAtIndex(0, 'ArrowRight') + await keyDownOnSelectedItemAtIndex(0, '{ArrowRight}') expect(getSelectedItemAtIndex(1)).toHaveAttribute('tabindex', '0') expect(getSelectedItemAtIndex(2)).toHaveAttribute('tabindex', '-1') expect(getSelectedItemAtIndex(0)).toHaveAttribute('tabindex', '-1') - keyDownOnSelectedItemAtIndex(2, 'ArrowRight') + await keyDownOnSelectedItemAtIndex(2, '{ArrowRight}') expect(getSelectedItemAtIndex(2)).toHaveAttribute('tabindex', '0') expect(getSelectedItemAtIndex(1)).toHaveAttribute('tabindex', '-1') expect(getSelectedItemAtIndex(0)).toHaveAttribute('tabindex', '-1') }) - test('backspace removes item and moves focus to next item if any', () => { - const { - keyDownOnSelectedItemAtIndex, - getSelectedItemAtIndex, - getSelectedItems, - focusSelectedItemAtIndex, - } = renderMultipleCombobox({ + test('backspace removes item and moves focus to next item if any', async () => { + renderMultipleCombobox({ multipleSelectionProps: { initialSelectedItems: [items[0], items[1], items[2]], initialActiveIndex: 1, }, }) - focusSelectedItemAtIndex(1) - keyDownOnSelectedItemAtIndex(1, 'Backspace') + await keyDownOnSelectedItemAtIndex(1, '{Backspace}') expect(getSelectedItems()).toHaveLength(2) expect(getSelectedItemAtIndex(1)).toHaveFocus() expect(getSelectedItemAtIndex(1)).toHaveTextContent(items[2]) }) - test('backspace removes item and moves focus to input if no items left', () => { - const { - keyDownOnSelectedItemAtIndex, - getSelectedItems, - focusSelectedItemAtIndex, - input, - } = renderMultipleCombobox({ + test('backspace removes item and moves focus to input if no items left', async () => { + renderMultipleCombobox({ multipleSelectionProps: { initialSelectedItems: [items[0]], initialActiveIndex: 0, }, }) - focusSelectedItemAtIndex(0) - keyDownOnSelectedItemAtIndex(0, 'Backspace') + await keyDownOnSelectedItemAtIndex(0, '{Backspace}') expect(getSelectedItems()).toHaveLength(0) - expect(input).toHaveFocus() + expect(getInput()).toHaveFocus() }) - test('backspace removes item and moves focus to previous item if it was the last in the array', () => { - const { - keyDownOnSelectedItemAtIndex, - getSelectedItemAtIndex, - getSelectedItems, - focusSelectedItemAtIndex, - } = renderMultipleCombobox({ + test('backspace removes item and moves focus to previous item if it was the last in the array', async () => { + renderMultipleCombobox({ multipleSelectionProps: { initialSelectedItems: [items[0], items[1], items[2]], initialActiveIndex: 2, }, }) - focusSelectedItemAtIndex(2) - keyDownOnSelectedItemAtIndex(2, 'Backspace') + await keyDownOnSelectedItemAtIndex(2, '{Backspace}') expect(getSelectedItems()).toHaveLength(2) expect(getSelectedItemAtIndex(1)).toHaveFocus() expect(getSelectedItemAtIndex(1)).toHaveTextContent(items[1]) }) - test('delete removes item and moves focus to next item if any', () => { - const { - keyDownOnSelectedItemAtIndex, - getSelectedItemAtIndex, - getSelectedItems, - focusSelectedItemAtIndex, - } = renderMultipleCombobox({ + test('delete removes item and moves focus to next item if any', async () => { + renderMultipleCombobox({ multipleSelectionProps: { initialSelectedItems: [items[0], items[1], items[2]], initialActiveIndex: 1, }, }) - focusSelectedItemAtIndex(1) - keyDownOnSelectedItemAtIndex(1, 'Delete') + await keyDownOnSelectedItemAtIndex(1, '{Delete}') expect(getSelectedItems()).toHaveLength(2) expect(getSelectedItemAtIndex(1)).toHaveFocus() expect(getSelectedItemAtIndex(1)).toHaveTextContent(items[2]) }) - test('delete removes item and moves focus to input if no items left', () => { - const { - keyDownOnSelectedItemAtIndex, - getSelectedItems, - focusSelectedItemAtIndex, - input, - } = renderMultipleCombobox({ + test('delete removes item and moves focus to input if no items left', async () => { + renderMultipleCombobox({ multipleSelectionProps: { initialSelectedItems: [items[0]], initialActiveIndex: 0, }, }) - focusSelectedItemAtIndex(0) - keyDownOnSelectedItemAtIndex(0, 'Delete') + await keyDownOnSelectedItemAtIndex(0, '{Delete}') expect(getSelectedItems()).toHaveLength(0) - expect(input).toHaveFocus() + expect(getInput()).toHaveFocus() }) - test('delete removes item and moves focus to previous item if it was the last in the array', () => { - const { - keyDownOnSelectedItemAtIndex, - getSelectedItemAtIndex, - getSelectedItems, - focusSelectedItemAtIndex, - } = renderMultipleCombobox({ + test('delete removes item and moves focus to previous item if it was the last in the array', async () => { + renderMultipleCombobox({ multipleSelectionProps: { initialSelectedItems: [items[0], items[1], items[2]], initialActiveIndex: 2, }, }) - focusSelectedItemAtIndex(2) - keyDownOnSelectedItemAtIndex(2, 'Delete') + await keyDownOnSelectedItemAtIndex(2, '{Delete}') expect(getSelectedItems()).toHaveLength(2) expect(getSelectedItemAtIndex(1)).toHaveFocus() expect(getSelectedItemAtIndex(1)).toHaveTextContent(items[1]) }) - test('navigation works correctly with both click and arrow keys', () => { - const { - keyDownOnSelectedItemAtIndex, - getSelectedItemAtIndex, - clickOnSelectedItemAtIndex, - } = renderMultipleCombobox({ + test('navigation works correctly with both click and arrow keys', async () => { + renderMultipleCombobox({ multipleSelectionProps: { initialSelectedItems: [items[0], items[1], items[2]], }, }) - clickOnSelectedItemAtIndex(1) + await clickOnSelectedItemAtIndex(1) expect(getSelectedItemAtIndex(1)).toHaveAttribute('tabindex', '0') expect(getSelectedItemAtIndex(0)).toHaveAttribute('tabindex', '-1') expect(getSelectedItemAtIndex(2)).toHaveAttribute('tabindex', '-1') - keyDownOnSelectedItemAtIndex(1, 'ArrowLeft') + await keyDownOnSelectedItemAtIndex(1, '{ArrowLeft}') expect(getSelectedItemAtIndex(0)).toHaveAttribute('tabindex', '0') expect(getSelectedItemAtIndex(2)).toHaveAttribute('tabindex', '-1') expect(getSelectedItemAtIndex(1)).toHaveAttribute('tabindex', '-1') - clickOnSelectedItemAtIndex(1) + await clickOnSelectedItemAtIndex(1) expect(getSelectedItemAtIndex(1)).toHaveAttribute('tabindex', '0') expect(getSelectedItemAtIndex(2)).toHaveAttribute('tabindex', '-1') expect(getSelectedItemAtIndex(0)).toHaveAttribute('tabindex', '-1') - keyDownOnSelectedItemAtIndex(2, 'ArrowRight') + await keyDownOnSelectedItemAtIndex(2, '{ArrowRight}') expect(getSelectedItemAtIndex(2)).toHaveAttribute('tabindex', '0') expect(getSelectedItemAtIndex(1)).toHaveAttribute('tabindex', '-1') expect(getSelectedItemAtIndex(0)).toHaveAttribute('tabindex', '-1') }) - test("other than the ones supported don't affect anything", () => { - const { - keyDownOnSelectedItemAtIndex, - getSelectedItems, - getSelectedItemAtIndex, - focusSelectedItemAtIndex, - } = renderMultipleCombobox({ + test("other than the ones supported don't affect anything", async () => { +renderMultipleCombobox({ multipleSelectionProps: {initialSelectedItems: [items[0], items[1]]}, }) - focusSelectedItemAtIndex(1) - keyDownOnSelectedItemAtIndex(1, 'Alt') - keyDownOnSelectedItemAtIndex(1, 'Control') - keyDownOnSelectedItemAtIndex(1, 'ArrowUp') - keyDownOnSelectedItemAtIndex(1, 'ArrowDown') - keyDownOnSelectedItemAtIndex(1, 'Enter') + await keyDownOnSelectedItemAtIndex(1, '{Alt}') + await keyDownOnSelectedItemAtIndex(1, '{Control}') + await keyDownOnSelectedItemAtIndex(1, '{ArrowUp}') + await keyDownOnSelectedItemAtIndex(1, '{ArrowDown}') + await keyDownOnSelectedItemAtIndex(1, '{Enter}') expect(getSelectedItems()).toHaveLength(2) expect(getSelectedItemAtIndex(1)).toHaveFocus() @@ -500,13 +436,8 @@ describe('getSelectedItemProps', () => { }) describe('on focus', () => { - test('keeps tabindex "0" when focusing input by tab/click so user can return via tab', () => { - const { - getSelectedItemAtIndex, - focusSelectedItemAtIndex, - focusInput, - input, - } = renderMultipleCombobox({ + test('keeps tabindex "0" when focusing input by tab/click so user can return via tab', async () => { + renderMultipleCombobox({ multipleSelectionProps: { initialSelectedItems: [items[0], items[1], items[2]], initialActiveIndex: 0, @@ -514,12 +445,12 @@ describe('getSelectedItemProps', () => { }) focusSelectedItemAtIndex(0) - focusInput() + await tab() expect(getSelectedItemAtIndex(0)).toHaveAttribute('tabindex', '0') expect(getSelectedItemAtIndex(1)).toHaveAttribute('tabindex', '-1') expect(getSelectedItemAtIndex(2)).toHaveAttribute('tabindex', '-1') - expect(input).toHaveFocus() + expect(getInput()).toHaveFocus() }) }) }) diff --git a/src/hooks/useMultipleSelection/__tests__/props.test.js b/src/hooks/useMultipleSelection/__tests__/props.test.js index a5774f4b1..9633d7ba2 100644 --- a/src/hooks/useMultipleSelection/__tests__/props.test.js +++ b/src/hooks/useMultipleSelection/__tests__/props.test.js @@ -1,7 +1,21 @@ import {act, renderHook} from '@testing-library/react-hooks' import * as stateChangeTypes from '../stateChangeTypes' -import {renderUseMultipleSelection, renderMultipleCombobox} from '../testUtils' -import {items} from '../../testUtils' +import { + renderUseMultipleSelection, + renderMultipleCombobox, + keyDownOnSelectedItemAtIndex, + getSelectedItems, + focusSelectedItemAtIndex, + clickOnSelectedItemAtIndex, + getSelectedItemAtIndex, + clickOnInput, +} from '../testUtils' +import { + getA11yStatusContainer, + getInput, + items, + keyDownOnInput, +} from '../../testUtils' import useMultipleSelection from '..' jest.useFakeTimers() @@ -24,11 +38,8 @@ describe('props', () => { act(() => jest.runAllTimers()) }) - test('passed as objects should work with custom itemToString', () => { - const { - keyDownOnSelectedItemAtIndex, - getA11yStatusContainer, - } = renderMultipleCombobox({ + test('passed as objects should work with custom itemToString', async () => { + renderMultipleCombobox({ multipleSelectionProps: { initialSelectedItems: [{str: 'aaa'}, {str: 'bbb'}], initialActiveIndex: 0, @@ -36,26 +47,24 @@ describe('props', () => { }, }) - keyDownOnSelectedItemAtIndex(0, 'Delete') + await keyDownOnSelectedItemAtIndex(0, '{Delete}') expect(getA11yStatusContainer()).toHaveTextContent( 'aaa has been removed.', ) }) - test('controls the state property if passed', () => { + test('controls the state property if passed', async () => { const inputItems = [items[0], items[1]] - const { - keyDownOnSelectedItemAtIndex, - getSelectedItems, - } = renderMultipleCombobox({ + + renderMultipleCombobox({ multipleSelectionProps: { selectedItems: inputItems, initialActiveIndex: 0, }, }) - keyDownOnSelectedItemAtIndex(0, 'Delete') + await keyDownOnSelectedItemAtIndex(0, '{Delete}') expect(getSelectedItems()).toHaveLength(2) }) @@ -66,11 +75,11 @@ describe('props', () => { act(() => jest.runAllTimers()) }) - test('is called with object that contains specific props', () => { + test('is called with object that contains specific props', async () => { const getA11yRemovalMessage = jest.fn() const itemToString = item => item.str const initialSelectedItems = [{str: 'aaa'}, {str: 'bbb'}] - const {keyDownOnSelectedItemAtIndex} = renderMultipleCombobox({ + renderMultipleCombobox({ multipleSelectionProps: { initialSelectedItems, initialActiveIndex: 0, @@ -80,7 +89,7 @@ describe('props', () => { }, }) - keyDownOnSelectedItemAtIndex(0, 'Delete') + await keyDownOnSelectedItemAtIndex(0, '{Delete}') expect(getA11yRemovalMessage).toHaveBeenCalledWith( expect.objectContaining({ @@ -93,12 +102,10 @@ describe('props', () => { ) }) - test('is replaced with the user provided one', () => { + test('is replaced with the user provided one', async () => { const initialSelectedItems = [items[0], items[1]] - const { - keyDownOnSelectedItemAtIndex, - getA11yStatusContainer, - } = renderMultipleCombobox({ + + renderMultipleCombobox({ multipleSelectionProps: { initialSelectedItems, initialActiveIndex: 0, @@ -106,20 +113,15 @@ describe('props', () => { }, }) - keyDownOnSelectedItemAtIndex(0, 'Delete') + await keyDownOnSelectedItemAtIndex(0, '{Delete}') expect(getA11yStatusContainer()).toHaveTextContent('custom message') }) }) describe('activeIndex', () => { - test('controls the state property if passed', () => { - const { - keyDownOnSelectedItemAtIndex, - clickOnSelectedItemAtIndex, - getSelectedItemAtIndex, - focusSelectedItemAtIndex, - } = renderMultipleCombobox({ + test('controls the state property if passed', async () => { + renderMultipleCombobox({ multipleSelectionProps: { initialSelectedItems: [items[0], items[1]], activeIndex: 1, @@ -127,23 +129,20 @@ describe('props', () => { }) focusSelectedItemAtIndex(1) - clickOnSelectedItemAtIndex(0) + await clickOnSelectedItemAtIndex(0) expect(getSelectedItemAtIndex(0)).toHaveAttribute('tabindex', '-1') expect(getSelectedItemAtIndex(1)).toHaveAttribute('tabindex', '0') - expect(getSelectedItemAtIndex(1)).toHaveFocus() - keyDownOnSelectedItemAtIndex(1, 'ArrowLeft') + await keyDownOnSelectedItemAtIndex(1, '{ArrowLeft}') expect(getSelectedItemAtIndex(0)).toHaveAttribute('tabindex', '-1') expect(getSelectedItemAtIndex(1)).toHaveAttribute('tabindex', '0') - expect(getSelectedItemAtIndex(1)).toHaveFocus() - keyDownOnSelectedItemAtIndex(1, 'ArrowRight') + await keyDownOnSelectedItemAtIndex(1, '{ArrowRight}') expect(getSelectedItemAtIndex(0)).toHaveAttribute('tabindex', '-1') expect(getSelectedItemAtIndex(1)).toHaveAttribute('tabindex', '0') - expect(getSelectedItemAtIndex(1)).toHaveFocus() }) }) @@ -240,23 +239,16 @@ describe('props', () => { ) }) - test('is called at each state change with the appropriate change type', () => { + test('is called at each state change with the appropriate change type', async () => { const stateReducer = jest.fn((s, a) => a.changes) - const { - keyDownOnSelectedItemAtIndex, - clickOnSelectedItemAtIndex, - keyDownOnInput, - input, - clickOnInput, - } = renderMultipleCombobox({ + renderMultipleCombobox({ multipleSelectionProps: { initialSelectedItems: [items[0], items[1], items[2]], stateReducer, }, }) - input.focus() - keyDownOnInput('Backspace') + await keyDownOnInput('{Backspace}') expect(stateReducer).toHaveBeenCalledTimes(1) expect(stateReducer).toHaveBeenLastCalledWith( @@ -271,7 +263,7 @@ describe('props', () => { }), ) - keyDownOnInput('ArrowLeft') + await keyDownOnInput('{ArrowLeft}') expect(stateReducer).toHaveBeenCalledTimes(2) expect(stateReducer).toHaveBeenLastCalledWith( @@ -284,7 +276,7 @@ describe('props', () => { }), ) - keyDownOnSelectedItemAtIndex(1, 'ArrowLeft') + await keyDownOnSelectedItemAtIndex(1, '{ArrowLeft}') expect(stateReducer).toHaveBeenCalledTimes(3) expect(stateReducer).toHaveBeenLastCalledWith( @@ -297,7 +289,7 @@ describe('props', () => { }), ) - keyDownOnSelectedItemAtIndex(0, 'ArrowRight') + await keyDownOnSelectedItemAtIndex(0, '{ArrowRight}') expect(stateReducer).toHaveBeenCalledTimes(4) expect(stateReducer).toHaveBeenLastCalledWith( @@ -310,7 +302,7 @@ describe('props', () => { }), ) - clickOnInput() + await clickOnInput() expect(stateReducer).toHaveBeenCalledTimes(5) expect(stateReducer).toHaveBeenLastCalledWith( @@ -323,7 +315,7 @@ describe('props', () => { }), ) - clickOnSelectedItemAtIndex(0) + await clickOnSelectedItemAtIndex(0) expect(stateReducer).toHaveBeenCalledTimes(6) expect(stateReducer).toHaveBeenLastCalledWith( @@ -336,7 +328,7 @@ describe('props', () => { }), ) - keyDownOnSelectedItemAtIndex(0, 'Delete') + await keyDownOnSelectedItemAtIndex(0, '{Delete}') expect(stateReducer).toHaveBeenCalledTimes(7) expect(stateReducer).toHaveBeenLastCalledWith( @@ -351,7 +343,7 @@ describe('props', () => { }), ) - keyDownOnSelectedItemAtIndex(0, 'Backspace') + await keyDownOnSelectedItemAtIndex(0, '{Backspace}') expect(stateReducer).toHaveBeenCalledTimes(8) expect(stateReducer).toHaveBeenLastCalledWith( @@ -367,30 +359,28 @@ describe('props', () => { ) }) - test('replaces prop values with user defined', () => { + test('replaces prop values with user defined', async () => { const stateReducer = jest.fn((s, a) => { const changes = a.changes changes.activeIndex = 0 return changes }) - const { - clickOnSelectedItemAtIndex, - getSelectedItemAtIndex, - } = renderMultipleCombobox({ + + renderMultipleCombobox({ multipleSelectionProps: { initialSelectedItems: [items[0], items[1]], stateReducer, }, }) - clickOnSelectedItemAtIndex(1) + await clickOnSelectedItemAtIndex(1) expect(getSelectedItemAtIndex(1)).toHaveAttribute('tabindex', '-1') expect(getSelectedItemAtIndex(0)).toHaveAttribute('tabindex', '0') expect(getSelectedItemAtIndex(0)).toHaveFocus() }) - test('receives state, changes and type', () => { + test('receives state, changes and type', async () => { const stateReducer = jest.fn((s, a) => { expect(a.type).not.toBeUndefined() expect(a.type).not.toBeNull() @@ -403,17 +393,17 @@ describe('props', () => { return a.changes }) - const {clickOnSelectedItemAtIndex} = renderMultipleCombobox({ + renderMultipleCombobox({ multipleSelectionProps: { initialSelectedItems: [items[0], items[1]], stateReducer, }, }) - clickOnSelectedItemAtIndex(0) + await clickOnSelectedItemAtIndex(0) }) - test('changes are visible in onChange handlers', () => { + test('changes are visible in onChange handlers', async () => { const activeIndex = 0 const inputItems = ['foo', 'bar'] const stateReducer = jest.fn(() => ({ @@ -423,7 +413,7 @@ describe('props', () => { const onSelectedItemsChange = jest.fn() const onActiveIndexChange = jest.fn() const onStateChange = jest.fn() - const {keyDownOnInput} = renderMultipleCombobox({ + renderMultipleCombobox({ multipleSelectionProps: { stateReducer, onStateChange, @@ -433,7 +423,7 @@ describe('props', () => { }, }) - keyDownOnInput('ArrowLeft') + await keyDownOnInput('{ArrowLeft}') expect(onActiveIndexChange).toHaveBeenCalledTimes(1) expect(onActiveIndexChange).toHaveBeenCalledWith( @@ -459,16 +449,16 @@ describe('props', () => { }) describe('onActiveIndexChange', () => { - test('is called at activeIndex change', () => { + test('is called at activeIndex change', async () => { const onActiveIndexChange = jest.fn() - const {clickOnSelectedItemAtIndex} = renderMultipleCombobox({ + renderMultipleCombobox({ multipleSelectionProps: { initialSelectedItems: [items[0], items[1]], onActiveIndexChange, }, }) - clickOnSelectedItemAtIndex(1) + await clickOnSelectedItemAtIndex(1) expect(onActiveIndexChange).toHaveBeenCalledTimes(1) expect(onActiveIndexChange).toHaveBeenCalledWith( @@ -478,9 +468,9 @@ describe('props', () => { ) }) - test('is not called at if selectedItem is the same', () => { + test('is not called at if selectedItem is the same', async () => { const onActiveIndexChange = jest.fn() - const {clickOnSelectedItemAtIndex} = renderMultipleCombobox({ + renderMultipleCombobox({ multipleSelectionProps: { initialSelectedItems: [items[0], items[1]], onActiveIndexChange, @@ -488,18 +478,14 @@ describe('props', () => { }, }) - clickOnSelectedItemAtIndex(1) + await clickOnSelectedItemAtIndex(1) expect(onActiveIndexChange).not.toHaveBeenCalled() }) - test('works correctly with the corresponding control prop', () => { + test('works correctly with the corresponding control prop', async () => { let activeIndex = 3 - const { - keyDownOnSelectedItemAtIndex, - getSelectedItemAtIndex, - rerender, - } = renderMultipleCombobox({ + const {rerender} = renderMultipleCombobox({ multipleSelectionProps: { initialSelectedItems: items, activeIndex, @@ -509,7 +495,7 @@ describe('props', () => { }, }) - keyDownOnSelectedItemAtIndex(3, 'ArrowLeft') + await keyDownOnSelectedItemAtIndex(3, '{ArrowLeft}') rerender({multipleSelectionProps: {activeIndex}}) expect(getSelectedItemAtIndex(2)).toHaveAttribute('tabindex', '0') @@ -531,9 +517,9 @@ describe('props', () => { }) describe('onSelectedItemsChange', () => { - test('is called at items change', () => { + test('is called at items change', async () => { const onSelectedItemsChange = jest.fn() - const {keyDownOnSelectedItemAtIndex} = renderMultipleCombobox({ + renderMultipleCombobox({ multipleSelectionProps: { initialSelectedItems: [items[0], items[1]], onSelectedItemsChange, @@ -541,7 +527,7 @@ describe('props', () => { }, }) - keyDownOnSelectedItemAtIndex(1, 'Delete') + await keyDownOnSelectedItemAtIndex(1, '{Delete}') expect(onSelectedItemsChange).toHaveBeenCalledTimes(1) expect(onSelectedItemsChange).toHaveBeenCalledWith( @@ -551,9 +537,9 @@ describe('props', () => { ) }) - test('is not called at if items is the same', () => { + test('is not called at if items is the same', async () => { const onSelectedItemsChange = jest.fn() - const {clickOnSelectedItemAtIndex} = renderMultipleCombobox({ + renderMultipleCombobox({ multipleSelectionProps: { initialSelectedItems: [items[0], items[1]], onSelectedItemsChange, @@ -561,18 +547,14 @@ describe('props', () => { }, }) - clickOnSelectedItemAtIndex(0) + await clickOnSelectedItemAtIndex(0) expect(onSelectedItemsChange).not.toHaveBeenCalled() }) - test('works correctly with the corresponding control prop', () => { + test('works correctly with the corresponding control prop', async () => { let selectedItems = [items[0], items[1]] - const { - keyDownOnSelectedItemAtIndex, - getSelectedItems, - rerender, - } = renderMultipleCombobox({ + const {rerender} = renderMultipleCombobox({ multipleSelectionProps: { selectedItems, initialActiveIndex: 0, @@ -582,7 +564,7 @@ describe('props', () => { }, }) - keyDownOnSelectedItemAtIndex(0, 'Delete') + await keyDownOnSelectedItemAtIndex(0, '{Delete}') rerender({multipleSelectionProps: {selectedItems}}) expect(getSelectedItems()).toHaveLength(1) @@ -607,22 +589,16 @@ describe('props', () => { }) describe('onStateChange', () => { - test('is called at each state property change', () => { + test('is called at each state property change', async () => { const onStateChange = jest.fn() - const { - keyDownOnSelectedItemAtIndex, - clickOnSelectedItemAtIndex, - keyDownOnInput, - input, - } = renderMultipleCombobox({ + renderMultipleCombobox({ multipleSelectionProps: { initialSelectedItems: [items[0], items[1], items[2]], onStateChange, }, }) - input.focus() - keyDownOnInput('Backspace') + await keyDownOnInput('{Backspace}') expect(onStateChange).toHaveBeenCalledTimes(1) expect(onStateChange).toHaveBeenLastCalledWith( @@ -632,7 +608,7 @@ describe('props', () => { }), ) - keyDownOnInput('ArrowLeft') + await keyDownOnInput('{ArrowLeft}') expect(onStateChange).toHaveBeenCalledTimes(2) expect(onStateChange).toHaveBeenLastCalledWith( @@ -642,7 +618,7 @@ describe('props', () => { }), ) - keyDownOnSelectedItemAtIndex(1, 'ArrowLeft') + await keyDownOnSelectedItemAtIndex(1, '{ArrowLeft}') expect(onStateChange).toHaveBeenCalledTimes(3) expect(onStateChange).toHaveBeenLastCalledWith( @@ -652,7 +628,7 @@ describe('props', () => { }), ) - keyDownOnSelectedItemAtIndex(0, 'ArrowRight') + await keyDownOnSelectedItemAtIndex(0, '{ArrowRight}') expect(onStateChange).toHaveBeenCalledTimes(4) expect(onStateChange).toHaveBeenLastCalledWith( @@ -662,7 +638,7 @@ describe('props', () => { }), ) - clickOnSelectedItemAtIndex(0) + await clickOnSelectedItemAtIndex(0) expect(onStateChange).toHaveBeenCalledTimes(5) expect(onStateChange).toHaveBeenLastCalledWith( @@ -672,7 +648,7 @@ describe('props', () => { }), ) - keyDownOnSelectedItemAtIndex(0, 'Delete') + await keyDownOnSelectedItemAtIndex(0, '{Delete}') expect(onStateChange).toHaveBeenCalledTimes(6) expect(onStateChange).toHaveBeenLastCalledWith( @@ -682,7 +658,7 @@ describe('props', () => { }), ) - keyDownOnSelectedItemAtIndex(0, 'Backspace') + await keyDownOnSelectedItemAtIndex(0, '{Backspace}') expect(onStateChange).toHaveBeenCalledTimes(7) expect(onStateChange).toHaveBeenLastCalledWith( @@ -694,13 +670,8 @@ describe('props', () => { }) }) - test('overrides navigation previos and next keys correctly', () => { - const { - keyDownOnInput, - getSelectedItemAtIndex, - keyDownOnSelectedItemAtIndex, - input, - } = renderMultipleCombobox({ + test('overrides navigation previos and next keys correctly', async () => { +renderMultipleCombobox({ multipleSelectionProps: { keyNavigationPrevious: 'ArrowUp', keyNavigationNext: 'ArrowDown', @@ -708,21 +679,21 @@ describe('props', () => { }, }) - keyDownOnInput('ArrowUp') + await keyDownOnInput('{ArrowUp}') expect(getSelectedItemAtIndex(1)).toHaveFocus() - keyDownOnSelectedItemAtIndex(1, 'ArrowUp') + await keyDownOnSelectedItemAtIndex(1, '{ArrowUp}') expect(getSelectedItemAtIndex(0)).toHaveFocus() - keyDownOnSelectedItemAtIndex(0, 'ArrowDown') + await keyDownOnSelectedItemAtIndex(0, '{ArrowDown}') expect(getSelectedItemAtIndex(1)).toHaveFocus() - keyDownOnSelectedItemAtIndex(1, 'ArrowDown') + await keyDownOnSelectedItemAtIndex(1, '{ArrowDown}') - expect(input).toHaveFocus() + expect(getInput()).toHaveFocus() }) test('can have downshift actions executed', () => { diff --git a/src/hooks/useMultipleSelection/testUtils.js b/src/hooks/useMultipleSelection/testUtils.js index 8dd5e2310..72354a960 100644 --- a/src/hooks/useMultipleSelection/testUtils.js +++ b/src/hooks/useMultipleSelection/testUtils.js @@ -1,13 +1,14 @@ import * as React from 'react' -import {render, fireEvent, screen} from '@testing-library/react' +import {render, screen} from '@testing-library/react' import {renderHook} from '@testing-library/react-hooks' -import userEvent from '@testing-library/user-event' import {defaultProps} from '../utils' -import {items} from '../testUtils' +import {items, user, getInput, dataTestIds} from '../testUtils' import useCombobox from '../useCombobox' import useMultipleSelection from '.' +export * from '../testUtils' + jest.mock('../../utils', () => { const utils = jest.requireActual('../../utils') @@ -30,21 +31,41 @@ jest.mock('../utils', () => { beforeEach(jest.resetAllMocks) afterAll(jest.restoreAllMocks) -export const dataTestIds = { - selectedItemPrefix: 'selected-item-id', - selectedItem: index => `selected-item-id-${index}`, - input: 'input-id', +export function getSelectedItemAtIndex(index) { + return screen.getByTestId(dataTestIds.selectedItem(index)) +} + +export function getSelectedItems() { + return screen.queryAllByTestId(new RegExp(dataTestIds.selectedItemPrefix)) +} + +export async function clickOnSelectedItemAtIndex(index) { + await user.click(getSelectedItemAtIndex(index)) +} +export async function keyDownOnSelectedItemAtIndex(index, key) { + const selectedItem = getSelectedItemAtIndex(index) + + if (document.activeElement !== selectedItem) { + selectedItem.focus() + } + + await user.keyboard(key) +} + +export function focusSelectedItemAtIndex(index) { + getSelectedItemAtIndex(index).focus() +} + +export async function clickOnInput() { + await user.click(getInput()) } const DropdownMultipleCombobox = ({ multipleSelectionProps = {}, comboboxProps = {}, }) => { - const { - getSelectedItemProps, - getDropdownProps, - selectedItems, - } = useMultipleSelection(multipleSelectionProps) + const {getSelectedItemProps, getDropdownProps, selectedItems} = + useMultipleSelection(multipleSelectionProps) const { getToggleButtonProps, getLabelProps, @@ -87,54 +108,12 @@ const DropdownMultipleCombobox = ({ export const renderMultipleCombobox = props => { const utils = render() - const label = screen.getByText(/choose an element/i) - const menu = screen.getByRole('listbox') - const input = screen.getByTestId(dataTestIds.input) const rerender = newProps => utils.rerender() - const getSelectedItemAtIndex = index => - screen.getByTestId(dataTestIds.selectedItem(index)) - const getSelectedItems = () => - screen.queryAllByTestId(new RegExp(dataTestIds.selectedItemPrefix)) - const clickOnSelectedItemAtIndex = index => { - fireEvent.click(getSelectedItemAtIndex(index)) - } - const keyDownOnSelectedItemAtIndex = (index, key, options = {}) => { - fireEvent.keyDown(getSelectedItemAtIndex(index), {key, ...options}) - } - const focusSelectedItemAtIndex = index => { - getSelectedItemAtIndex(index).focus() - } - const getA11yStatusContainer = () => screen.queryByRole('status') - const focusInput = () => { - input.focus() - } - const keyDownOnInput = (key, options = {}) => { - if (document.activeElement !== input) { - focusInput() - } - - fireEvent.keyDown(input, {key, ...options}) - } - const clickOnInput = () => { - userEvent.click(input) - } return { ...utils, - label, - menu, rerender, - getSelectedItemAtIndex, - clickOnSelectedItemAtIndex, - keyDownOnSelectedItemAtIndex, - focusSelectedItemAtIndex, - getSelectedItems, - getA11yStatusContainer, - input, - keyDownOnInput, - focusInput, - clickOnInput, } } diff --git a/src/hooks/useSelect/MIGRATION_V7.md b/src/hooks/useSelect/MIGRATION_V7.md new file mode 100644 index 000000000..7ae135abb --- /dev/null +++ b/src/hooks/useSelect/MIGRATION_V7.md @@ -0,0 +1,171 @@ +# Migration from v6 to v7 + +Since version _7.0.0_ Downshift follows the (ARIA 1.2 guideline for single +select combobox)[select-aria]. This brought a series of changes that are +considered breaking, both to the API and the behaviour of _useSelect_. The list +of changes, as well as the migration itself, is detailed below. + +## Table of Contents + + + + +- [Focus](#focus) +- [HTML Attributes](#html-attributes) +- [Events](#events) +- [stateChangeTypes](#statechangetypes) +- [circularNavigation](#circularnavigation) + + + +## Focus + +Since ARIA 1.2, focus stays on the trigger element at all times. +(Previously)[deprecated-select-aria], it toggled between the trigger and the +menu depending on the open state of the _select_ element. If any of your custom +implementation involved the focus on the menu element, please change it as the +focus stays on the trigger even when the menu is open. + +## HTML Attributes + +Similar to 1.1, _useSelect_ communicates to the screen reader the currently +highlighted item via the _aria-activedescendant_ attribute. However, since now +the focus is always on the trigger element, this attribute, along with others, +have shifted as shown below: + +- getToggleButtonProps additions: + - role=combobox + - aria-activedescendant=${highlightedItemId} + - aria-controls=${menuId} + - tabindex=0 +- getMenuProps removals: + - aria-activedescendant=${highlightedItemId} +- getItemProps changes: + - aria-selected=${item === selectedItem} - now the item that is selected + received _aria-selected=true_, the rest receive it as false. Previously, the + highlighted item was marked with _aria-selected=true_. + +## Events + +Event changes occured because of the focus shift, as well as new accessibility +pattern recommendantions. + +- getToggleButtonProps additions: + + - _ArrowDown+Alt_: opens the menu without any item highlighted. + - _ArrowUp+Alt_: closes the menu and selects the highlighted item. + - _End_: highlights the last item and opens the menu if closed. + - _Home_: highlights the first item and opens the menu if closed. + - _PageUp_: if menu is open, moves highlight by 10 positions to the start. + - _PageDown_: if menu is open, moves highlight by 10 positions to the end. + - _${characterKey}_: always opens the menu if closed, highlights the item + starting with that key (same behaviour as before when the menu is opened). + - _Enter_: if menu is open, closes the menu and selects the highlighted item. + - _SpaceBar_: if menu is open, closes the menu and selects the highlighted + item. If the space is part of a search query, it will be added to the search + query instead. + - _Escape_: closes the menu if open, without selecting anything. + - _Tab_ or any other _Blur_: closes the menu if open, selects highlighted + item, focus moves naturally. + - _ArrowUp_: moves highlight one position up. _Shift_ modifier is not + supported anymore. + - _ArrowDown_: moves highlight one position down. _Shift_ modifier is not + supported anymore. + +- getToggleButtonProps changes: +- _ArrowUp_: if there is an item selected, opens the menu with that item + highlighted, not with the -1 offset as it previously did. +- _ArrowDown_: if there is an item selected, opens the menu with that item + highlighted, not with the +1 offset as it previously did. + +- getMenuProps removals: + - _ArrowUp_, _ArrowDown_, _End_, _Home_, _Enter_, _Escape_, _SpaceBar_, _Tab_. + +## stateChangeTypes + +As a consequence of the [event changes](#events), the _stateChangeTypes_ +received in the _stateReducer_ and _on${statePropery}Change_ received the +following modifications: + +- MenuKeyDownArrowDown -> ToggleButtonKeyDownArrowDown +- MenuKeyDownArrowUp -> ToggleButtonKeyDownArrowUp +- MenuKeyDownEscape -> ToggleButtonKeyDownEscape +- MenuKeyDownHome -> ToggleButtonKeyDownHome +- MenuKeyDownEnd -> ToggleButtonKeyDownEnd +- MenuKeyDownEnter -> ToggleButtonKeyDownEnter +- MenuKeyDownSpaceButton -> ToggleButtonKeyDownSpaceButton +- MenuKeyDownCharacter -> ToggleButtonKeyDownCharacter +- MenuBlur -> ToggleButtonBlur +- ToggleButtonKeyDownPageUp: new state change type. +- ToggleButtonKeyDownPageDown: new state change type. + +Please change your reducer / onChange code accordingly. For instance: + +```js +function stateReducer(state, actionAndChanges) { + const {changes, type} = actionAndChanges + switch (type) { + case useSelect.stateChangeTypes.MenuKeyDownEnter: + case useSelect.stateChangeTypes.MenuKeyDownSpaceButton: + case useSelect.stateChangeTypes.ItemClick: + return { + ...changes, + isOpen: true, // keep the menu open after selection. + } + default: + return changes + } +} +``` + +Becomes: + +```js +function stateReducer(state, actionAndChanges) { + const {changes, type} = actionAndChanges + switch (type) { + case useSelect.stateChangeTypes.ToggleButtonKeyDownEnter: + case useSelect.stateChangeTypes.ToggleButtonKeyDownSpaceButton: + case useSelect.stateChangeTypes.ItemClick: + return { + ...changes, + isOpen: true, // keep the menu open after selection. + } + default: + return changes + } +} +``` + +## circularNavigation + +The prop _circularNavigation_ has been removed. Navigation inside the menu is +standard and non-circular. If you wish to make it circular, use the +_stateReducer_: + +```js +function stateReducer(state, actionAndChanges) { + const {changes, type} = actionAndChanges + switch (type) { + case useSelect.stateChangeTypes.ToggleButtonKeyDownArrowDown: + if (state.highlightedIndex === items.length - 1) { + return {...changes, highlightedIndex: 0} + } else { + return changes + } + case useSelect.stateChangeTypes.ToggleButtonKeyDownArrowUp: + if (state.highlightedIndex === 0) { + return {...changes, highlightedIndex: items.length - 1} + } else { + return changes + } + default: + return changes + } +} +``` + +[select-aria]: + https://w3c.github.io/aria-practices/examples/combobox/combobox-select-only.html +[deprecated-select-aria]: + https://www.w3.org/TR/2017/NOTE-wai-aria-practices-1.1-20171214/examples/listbox/listbox-collapsible.html diff --git a/src/hooks/useSelect/README.md b/src/hooks/useSelect/README.md index 541c0ac7b..3c7d2d26b 100644 --- a/src/hooks/useSelect/README.md +++ b/src/hooks/useSelect/README.md @@ -23,6 +23,13 @@ implement the corresponding ARIA pattern. Every functionality needed should be provided out-of-the-box: menu toggle, item selection and up/down movement between them, screen reader support, highlight by character keys etc. +## Migration to v7 + +`useSelect` received some changes related to its API and how it works in version +7, as a conequence of adapting it to the ARIA 1.2 select-only combobox pattern. +If you were using _useSelect_ previous to 7.0.0, check the [migration +guide][select-migration] and update if necessary. + ## Table of Contents @@ -55,7 +62,6 @@ between them, screen reader support, highlight by character keys etc. - [toggleButtonId](#togglebuttonid) - [getItemId](#getitemid) - [environment](#environment) - - [circularNavigation](#circularnavigation) - [stateChangeTypes](#statechangetypes) - [Control Props](#control-props) - [Returned props](#returned-props) @@ -93,9 +99,7 @@ function DropdownSelect() { return (
        - +
        {selectedItem || 'Elements'}
          {isOpen && items.map((item, index) => ( @@ -423,14 +427,6 @@ then you will need to pass in a custom object that is able to provide [access to these properties](https://gist.github.com/Rendez/1dd55882e9b850dd3990feefc9d6e177) for downshift. -### circularNavigation - -> `boolean` | defaults to `false` - -Controls the circular keyboard navigation between items. If set to `true`, when -first item is highlighted, the Arrow Up will move highlight to the last item, -and viceversa using Arrow Down. - ## stateChangeTypes There are a few props that expose changes to state @@ -442,22 +438,21 @@ object you get. This `type` corresponds to a `stateChangeTypes` property. The list of all possible values this `type` property can take is defined in [this file][state-change-file] and is as follows: -- `useSelect.stateChangeTypes.MenuKeyDownArrowDown` -- `useSelect.stateChangeTypes.MenuKeyDownArrowUp` -- `useSelect.stateChangeTypes.MenuKeyDownEscape` -- `useSelect.stateChangeTypes.MenuKeyDownHome` -- `useSelect.stateChangeTypes.MenuKeyDownEnd` -- `useSelect.stateChangeTypes.MenuKeyDownEnter` -- `useSelect.stateChangeTypes.MenuKeyDownSpaceButton` -- `useSelect.stateChangeTypes.MenuKeyDownCharacter` -- `useSelect.stateChangeTypes.MenuBlur` -- `useSelect.stateChangeTypes.MenuMouseLeave` -- `useSelect.stateChangeTypes.ItemMouseMove` -- `useSelect.stateChangeTypes.ItemClick` -- `useSelect.stateChangeTypes.ToggleButtonKeyDownCharacter` - `useSelect.stateChangeTypes.ToggleButtonKeyDownArrowDown` - `useSelect.stateChangeTypes.ToggleButtonKeyDownArrowUp` +- `useSelect.stateChangeTypes.ToggleButtonKeyDownEscape` +- `useSelect.stateChangeTypes.ToggleButtonKeyDownHome` +- `useSelect.stateChangeTypes.ToggleButtonKeyDownEnd` +- `useSelect.stateChangeTypes.ToggleButtonKeyDownPageUp` +- `useSelect.stateChangeTypes.ToggleButtonKeyDownPageDown` +- `useSelect.stateChangeTypes.ToggleButtonKeyDownEnter` +- `useSelect.stateChangeTypes.ToggleButtonKeyDownSpaceButton` +- `useSelect.stateChangeTypes.ToggleButtonKeyDownCharacter` +- `useSelect.stateChangeTypes.ToggleButtonBlur` - `useSelect.stateChangeTypes.ToggleButtonClick` +- `useSelect.stateChangeTypes.MenuMouseLeave` +- `useSelect.stateChangeTypes.ItemMouseMove` +- `useSelect.stateChangeTypes.ItemClick` - `useSelect.stateChangeTypes.FunctionToggleMenu` - `useSelect.stateChangeTypes.FunctionOpenMenu` - `useSelect.stateChangeTypes.FunctionCloseMenu` @@ -506,7 +501,7 @@ const {getToggleButtonProps, reset, ...rest} = useSelect({ return (
          - +
          Options
          {/* render the menu and items */} {/* render a button that resets the select to defaults */}