diff --git a/packages/combobox/.size-snapshot.json b/packages/combobox/.size-snapshot.json index c74341e29..9ef801005 100644 --- a/packages/combobox/.size-snapshot.json +++ b/packages/combobox/.size-snapshot.json @@ -1,20 +1,20 @@ { "index.cjs.js": { - "bundled": 26329, - "minified": 14211, + "bundled": 26260, + "minified": 14174, "gzipped": 4002 }, "index.esm.js": { - "bundled": 25261, - "minified": 13144, - "gzipped": 3979, + "bundled": 25192, + "minified": 13107, + "gzipped": 3980, "treeshaked": { "rollup": { "code": 1296, "import_statements": 177 }, "webpack": { - "code": 13081 + "code": 13044 } } } diff --git a/packages/combobox/package.json b/packages/combobox/package.json index a6ff36bfe..58387754d 100644 --- a/packages/combobox/package.json +++ b/packages/combobox/package.json @@ -23,7 +23,7 @@ "@babel/runtime": "^7.8.4", "@zendeskgarden/container-field": "^3.0.7", "@zendeskgarden/container-utilities": "^1.0.8", - "downshift": "^7.6.0" + "downshift": "^8.0.0" }, "peerDependencies": { "prop-types": "^15.6.1", diff --git a/packages/combobox/src/ComboboxContainer.spec.tsx b/packages/combobox/src/ComboboxContainer.spec.tsx index 8892898eb..82a29d49d 100644 --- a/packages/combobox/src/ComboboxContainer.spec.tsx +++ b/packages/combobox/src/ComboboxContainer.spec.tsx @@ -6,6 +6,7 @@ */ import React, { createRef, PropsWithChildren } from 'react'; +import { act } from 'react-dom/test-utils'; import { render, RenderResult } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { ComboboxContainer, useCombobox } from './'; @@ -279,11 +280,26 @@ describe('ComboboxContainer', () => { } }); + it('expands and collapses the listbox on input click', async () => { + const { getByTestId } = render(); + const input = getByTestId('input'); + + expect(input).toHaveAttribute('aria-expanded', 'false'); + + await user.click(input); + + expect(input).toHaveAttribute('aria-expanded', 'true'); + + await user.click(input); + + expect(input).toHaveAttribute('aria-expanded', 'false'); + }); + describe('when focused', () => { let input: HTMLElement; let listboxOptions: HTMLElement[]; - beforeEach(async () => { + beforeEach(() => { const { getByTestId, getAllByRole } = render( ); @@ -291,7 +307,7 @@ describe('ComboboxContainer', () => { input = getByTestId('input'); listboxOptions = getAllByRole('option'); - await user.click(input); + input.focus(); }); it('expands and activates the first option on down arrow', async () => { @@ -395,7 +411,7 @@ describe('ComboboxContainer', () => { let listboxOptions: HTMLElement[]; let rerender: RenderResult['rerender']; - beforeEach(async () => { + beforeEach(() => { const { getByTestId, getAllByRole, @@ -415,7 +431,7 @@ describe('ComboboxContainer', () => { listboxOptions = getAllByRole('option'); rerender = _rerender; - await user.click(input); + input.focus(); }); it('applies the correct accessibility attributes', () => { @@ -548,7 +564,9 @@ describe('ComboboxContainer', () => { expect(listboxOptions[1]).toHaveAttribute('aria-selected', 'false'); expect(listboxOptions[3]).toHaveAttribute('aria-selected', 'true'); - await user.click(tags[1]); + await act(async () => { + await user.click(tags[1]); + }); expect(tags[1]).toHaveFocus(); @@ -667,6 +685,19 @@ describe('ComboboxContainer', () => { expect(trigger).not.toHaveAttribute('aria-expanded'); } }); + + it('remains collapsed on input click', async () => { + const { getByTestId } = render( + + ); + const input = getByTestId('input'); + + expect(input).toHaveAttribute('aria-expanded', 'false'); + + await user.click(input); + + expect(input).toHaveAttribute('aria-expanded', 'false'); + }); }); describe('with disabled options', () => { @@ -694,7 +725,7 @@ describe('ComboboxContainer', () => { listboxOptions = getAllByRole('option'); rerender = _rerender; - await user.click(input); + input.focus(); await user.keyboard('{ArrowDown}'); }); @@ -769,11 +800,11 @@ describe('ComboboxContainer', () => { await user.click(input); await user.keyboard('{ArrowDown}'); - expect(handleChange).toHaveBeenCalledTimes(1); + expect(handleChange).toHaveBeenCalledTimes(2); const changeTypes = handleChange.mock.calls.map(([change]) => change.type); - expect(changeTypes).toMatchObject(['input:keyDown:ArrowDown']); + expect(changeTypes).toMatchObject(['input:click', 'input:keyDown:ArrowDown']); }); it('handles controlled selection as expected', () => { @@ -864,7 +895,12 @@ describe('ComboboxContainer', () => { const triggerRef = createRef(); const inputRef = createRef(); const listboxRef = createRef(); - const { selection: _selection, removeSelection: _removeSelection } = useCombobox({ + const { + selection: _selection, + removeSelection: _removeSelection, + getInputProps, + getListboxProps + } = useCombobox({ triggerRef, inputRef, listboxRef, @@ -875,7 +911,13 @@ describe('ComboboxContainer', () => { selection = _selection; removeSelection = _removeSelection; - return <>{children}; + return ( + <> + + {children} +
    + + ); }; it('clears multiselectable values', async () => { @@ -1014,7 +1056,10 @@ describe('ComboboxContainer', () => { const trigger = getByTestId('trigger'); await user.click(trigger); - await user.click(document.body); + + await act(async () => { + await user.click(document.body); + }); expect(input).toHaveAttribute('aria-expanded', 'false'); }); @@ -1092,7 +1137,7 @@ describe('ComboboxContainer', () => { ); const tag = getByTestId('tag'); @@ -1123,7 +1168,7 @@ describe('ComboboxContainer', () => { layout="Garden" isEditable={false} isMultiselectable - options={[{ value: 'test-1' }, { value: 'test-2', selected: true }, { value: 'test-2' }]} + options={[{ value: 'test-1' }, { value: 'test-2', selected: true }, { value: 'test-3' }]} /> ); const tag = getByTestId('tag'); diff --git a/packages/combobox/src/useCombobox.ts b/packages/combobox/src/useCombobox.ts index 689587acb..bf921a957 100644 --- a/packages/combobox/src/useCombobox.ts +++ b/packages/combobox/src/useCombobox.ts @@ -181,9 +181,13 @@ export const useCombobox = < false }; - case useDownshift.stateChangeTypes.InputFocus: - // Prevent expansion on focus. - return { ...state, isOpen: false }; + case useDownshift.stateChangeTypes.InputClick: + if (!isAutocomplete) { + // Prevent input click listbox expansion on non-autocomplete comboboxes. + changes.isOpen = state.isOpen; + } + + break; case useDownshift.stateChangeTypes.InputKeyDownArrowDown: case useDownshift.stateChangeTypes.FunctionOpenMenu: @@ -300,7 +304,7 @@ export const useCombobox = < initialHighlightedIndex: initialActiveIndex, onStateChange: handleDownshiftStateChange, stateReducer, - environment: win + environment: win as any /* HACK around Downshift's addition of Node to environment */ }); const closeListbox = useCallback(() => { @@ -413,7 +417,7 @@ export const useCombobox = < previousStateRef.current = { ...previousStateRef.current, - type: useDownshift.stateChangeTypes.InputFocus + type: useDownshift.stateChangeTypes.InputClick }; } }); @@ -449,11 +453,11 @@ export const useCombobox = < }; if (isEditable && triggerContainsInput) { - const handleClick = (event: MouseEvent) => { + const handleClick = (event: React.MouseEvent) => { if (disabled) { event.preventDefault(); } else if (isAutocomplete) { - triggerProps.onClick(event); + triggerProps.onClick && triggerProps.onClick(event); } else { inputRef.current?.focus(); } @@ -591,13 +595,7 @@ export const useCombobox = < ); const getInputProps = useCallback( - ({ - role = isEditable ? 'combobox' : null, - 'aria-labelledby': ariaLabeledBy = null, - onClick, - onFocus, - ...other - } = {}) => { + ({ role = isEditable ? 'combobox' : null, onClick, onFocus, ...other } = {}) => { const inputProps = { 'data-garden-container-id': 'containers.combobox.input', 'data-garden-container-version': PACKAGE_VERSION, @@ -613,11 +611,10 @@ export const useCombobox = < triggerRef.current?.contains(event.target) && event.stopPropagation(); - return getDownshiftInputProps({ + return getDownshiftInputProps({ ...inputProps, disabled, role, - 'aria-labelledby': ariaLabeledBy, 'aria-autocomplete': isAutocomplete ? 'list' : undefined, onClick: composeEventHandlers(onClick, handleClick), ...getFieldInputProps(), @@ -672,7 +669,7 @@ export const useCombobox = < triggerRef.current?.contains(event.target) && event.stopPropagation(); - const handleKeyDown = (event: KeyboardEvent) => { + const handleKeyDown = (event: React.KeyboardEvent) => { if (event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE) { setDownshiftSelection(option.value); } else { @@ -699,7 +696,7 @@ export const useCombobox = < triggerRef.current?.focus(); } - inputProps.onKeyDown(event); + inputProps.onKeyDown && inputProps.onKeyDown(event); } } }; @@ -716,13 +713,12 @@ export const useCombobox = < ); const getListboxProps = useCallback( - ({ role = 'listbox', 'aria-labelledby': ariaLabeledBy = null, ...other }) => - getDownshiftListboxProps({ + ({ role = 'listbox', ...other }) => + getDownshiftListboxProps({ 'data-garden-container-id': 'containers.combobox.listbox', 'data-garden-container-version': PACKAGE_VERSION, ref: listboxRef, role, - 'aria-labelledby': ariaLabeledBy, 'aria-multiselectable': isMultiselectable ? true : undefined, ...other } as IDownshiftListboxProps), @@ -772,9 +768,10 @@ export const useCombobox = < }; } - return getDownshiftOptionProps({ + return getDownshiftOptionProps({ item: option.value, index: values.indexOf(option.value), + 'aria-disabled': undefined, 'aria-selected': ariaSelected, ...optionProps } as IDownshiftOptionProps); diff --git a/packages/combobox/src/utils.ts b/packages/combobox/src/utils.ts index 085472bf2..426ba4fc2 100644 --- a/packages/combobox/src/utils.ts +++ b/packages/combobox/src/utils.ts @@ -20,7 +20,7 @@ const typeMap: Record = { [useDownshift.stateChangeTypes.FunctionSetInputValue]: 'fn:setInputValue', [useDownshift.stateChangeTypes.InputBlur]: 'input:blur', [useDownshift.stateChangeTypes.InputChange]: 'input:change', - [useDownshift.stateChangeTypes.InputFocus]: 'input:focus', + [useDownshift.stateChangeTypes.InputClick]: 'input:click', [useDownshift.stateChangeTypes.InputKeyDownArrowDown]: `input:keyDown:${KEYS.DOWN}`, [useDownshift.stateChangeTypes.InputKeyDownArrowUp]: `input:keyDown:${KEYS.UP}`, [useDownshift.stateChangeTypes.InputKeyDownEnd]: `input:keyDown:${KEYS.END}`, diff --git a/packages/combobox/yarn.lock b/packages/combobox/yarn.lock index 13c3402e7..ee90783bf 100644 --- a/packages/combobox/yarn.lock +++ b/packages/combobox/yarn.lock @@ -14,10 +14,10 @@ compute-scroll-into-view@^2.0.4: resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-2.0.4.tgz#2b444b2b9e4724819d2531efacb7ac094155fdf6" integrity sha512-y/ZA3BGnxoM/QHHQ2Uy49CLtnWPbt4tTPpEEZiEmmiWBFKjej7nEyH8Ryz54jH0MLXflUYA3Er2zUxPSJu5R+g== -downshift@^7.6.0: - version "7.6.2" - resolved "https://registry.yarnpkg.com/downshift/-/downshift-7.6.2.tgz#16fc951b7ff8f9c1c47d0f71b5ff10d78fb06e4c" - integrity sha512-iOv+E1Hyt3JDdL9yYcOgW7nZ7GQ2Uz6YbggwXvKUSleetYhU2nXD482Rz6CzvM4lvI1At34BYruKAL4swRGxaA== +downshift@^8.0.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/downshift/-/downshift-8.1.0.tgz#72023513256134723fe807a54168ebc64f9ddf6c" + integrity sha512-e9EBBLZvB2G73qT272x3hExttGCH1q1usbjirm+1aMcFXuzSWhgBdbnAHPlFI2rEq61cU/kDrEIMrY+ozMhvmg== dependencies: "@babel/runtime" "^7.14.8" compute-scroll-into-view "^2.0.4"