diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/autocomplete/methods.mdx b/packages/dnb-design-system-portal/src/docs/uilib/components/autocomplete/methods.mdx index 67d166d42e0..10ba5fe15fa 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/components/autocomplete/methods.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/autocomplete/methods.mdx @@ -7,8 +7,11 @@ You can manipulate the used data dynamically, either by changing the `data` prop - `updateData` replace all data entries. - `emptyData` remove all data entries. - `resetSelectedItem` will invalidate the selected key. -- `revalidateSelectedItem` will re-validate the selected key on the given `value`. +- `revalidateSelectedItem` will re-validate the internal selected key on the given `value`. +- `revalidateInputValue` will re-validate the current input value and update it – based on the given `value`. - `setInputValue` update the input value. +- `clearInputValue` will set the current input value to an empty string. +- `focusInput` will set focus on the input element. - `showIndicator` shows a progress indicator instead of the icon (inside the input). - `hideIndicator` hides the progress indicator inside the input. - `showIndicatorItem` shows an item with a [ProgressIndicator](/uilib/components/progress-indicator) status as an data option item. diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/autocomplete/properties.mdx b/packages/dnb-design-system-portal/src/docs/uilib/components/autocomplete/properties.mdx index a88ef96b747..298711182f8 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/components/autocomplete/properties.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/autocomplete/properties.mdx @@ -19,6 +19,7 @@ You may check out the [DrawerList Properties](#drawerlist-properties) down below | `search_numbers` | _(optional)_ if set to `true` and `search_in_word_index` is not set, the user will be able to more easily search and filter e.g. bank account numbers. Defaults to `false`. | | `search_in_word_index` | _(optional)_ this gives you the possibility to change the threshold number, which defines from what word on we search "inside words". Defaults to `3`. | | `keep_value` | _(optional)_ use `true` to not remove the typed value on input blur, if it is invalid. By default, the typed value will disappear / replaced by a selected value from the data list during the input field blur. Defaults to `false`. | +| `keep_selection` | _(optional)_ use `true` to not remove selected item on input blur, when the input value is empty. Defaults to `false`. | | `keep_value_and_selection` | _(optional)_ like `keep_value` – but would not reset to the selected value during input field blur. Also, the selected value would still be kept. Defaults to `false`. | | `prevent_selection` | _(optional)_ if set to `true`, no permanent selection will be made. Also, the typed value will not disappear on input blur (like `keep_value`). Defaults to `false`. | | `show_clear_button` | _(optional)_ if set to `true`, a clear button is shown inside the input field. Defaults to `false`. | diff --git a/packages/dnb-design-system-portal/src/shared/menu/SearchBar.tsx b/packages/dnb-design-system-portal/src/shared/menu/SearchBar.tsx index 8e9e37fb31f..970667abdce 100644 --- a/packages/dnb-design-system-portal/src/shared/menu/SearchBar.tsx +++ b/packages/dnb-design-system-portal/src/shared/menu/SearchBar.tsx @@ -16,6 +16,7 @@ import { } from './SearchBar.module.scss' import { scrollToAnimation } from '../parts/Layout' import { getIndexName } from '../../uilib/search/searchHelpers' +import { applyPageFocus } from '@dnb/eufemia/src/shared/helpers' const indexName = getIndexName() const algoliaApplicationID = 'SLD6KEYMQ9' @@ -51,9 +52,11 @@ export const SearchBarInput = () => { showIndicator() } - const onChangeHandler = ({ data }) => { + const onChangeHandler = ({ data, emptyData }) => { try { navigate(`/${data.hit.slug}`.replace('//', '/')) + emptyData() + applyPageFocus('content') } catch (e) { setStatus(e.message) } diff --git a/packages/dnb-eufemia/src/components/autocomplete/Autocomplete.d.ts b/packages/dnb-eufemia/src/components/autocomplete/Autocomplete.d.ts index bbb87dbd142..9070f94668f 100644 --- a/packages/dnb-eufemia/src/components/autocomplete/Autocomplete.d.ts +++ b/packages/dnb-eufemia/src/components/autocomplete/Autocomplete.d.ts @@ -124,6 +124,10 @@ export interface AutocompleteProps * Use `true` to not remove the typed value on input blur, if it is invalid. By default, the typed value will disappear / replaced by a selected value from the data list during the input field blur. Defaults to `false`. */ keep_value?: boolean; + /** + * Use `true` to not remove the selected value/key on input blur, if it is invalid. By default, the typed value will disappear / replaced by a selected value from the data list during the input field blur. Defaults to `false`. + */ + keep_selection?: boolean; /** * Like `keep_value` – but would not reset to the selected value during input field blur. Also, the selected value would still be kept. Defaults to `false`. */ diff --git a/packages/dnb-eufemia/src/components/autocomplete/Autocomplete.js b/packages/dnb-eufemia/src/components/autocomplete/Autocomplete.js index 8cb0c19e5ca..31f130810c5 100644 --- a/packages/dnb-eufemia/src/components/autocomplete/Autocomplete.js +++ b/packages/dnb-eufemia/src/components/autocomplete/Autocomplete.js @@ -98,6 +98,10 @@ export default class Autocomplete extends React.PureComponent { label_direction: PropTypes.oneOf(['horizontal', 'vertical']), label_sr_only: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), keep_value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), + keep_selection: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.bool, + ]), keep_value_and_selection: PropTypes.oneOfType([ PropTypes.string, PropTypes.bool, @@ -280,6 +284,7 @@ export default class Autocomplete extends React.PureComponent { label_direction: null, label_sr_only: null, keep_value: null, + keep_selection: null, keep_value_and_selection: null, show_clear_button: null, status: null, @@ -404,8 +409,7 @@ class AutocompleteInstance extends React.PureComponent { if ( props.input_value !== 'initval' && typeof state.inputValue === 'undefined' && - props.input_value && - props.input_value.length > 0 + props.input_value?.length > 0 ) { state.inputValue = props.input_value } @@ -456,17 +460,8 @@ class AutocompleteInstance extends React.PureComponent { componentDidUpdate(prevProps) { if (prevProps.value !== this.props.value) { - // Ensure we run getCurrentDataTitle after also data has been update, - // in case data has changed - this.setState({}, () => { - const inputValue = AutocompleteInstance.getCurrentDataTitle( - this.context.drawerList.selected_item, - this.context.drawerList.original_data - ) - this.setState({ - inputValue, - }) - }) + this.revalidateSelectedItem() + this.revalidateInputValue() } } @@ -510,13 +505,7 @@ class AutocompleteInstance extends React.PureComponent { toggleVisibleAndFocusOptions = () => { this.context.drawerList.toggleVisible(null, (isVisible) => { if (isVisible) { - try { - this.context.drawerList._refUl.current.focus({ - preventScroll: true, - }) - } catch (e) { - // do nothing - } + this.focusDrawerList() } }) } @@ -563,7 +552,8 @@ class AutocompleteInstance extends React.PureComponent { const data = this.runFilter(value, options) const count = this.countData(data) - const { keep_value, keep_value_and_selection } = this.props + const { keep_value, keep_selection, keep_value_and_selection } = + this.props if (value && value.length > 0) { // show the "no_options" message @@ -584,18 +574,20 @@ class AutocompleteInstance extends React.PureComponent { } } } else { - if (!isTrue(keep_value) && !isTrue(keep_value_and_selection)) { - // this will not remove selected_item + if ( + !isTrue(keep_value) && + !isTrue(keep_selection) && + !isTrue(keep_value_and_selection) + ) { this.totalReset() - } - - if (isTrue(keep_value)) { + } else if (isTrue(keep_value)) { this.resetSelectedItem() } this.showAllItems() } + // Opens the drawer, also when pressing on the clear button this.setVisible() this.setAriaLiveUpdate() @@ -680,11 +672,7 @@ class AutocompleteInstance extends React.PureComponent { emptyData = () => { this._cacheMemory = {} - this.setState({ - inputValue: '', - typedInputValue: null, - _listenForPropChanges: false, - }) + this.clearInputValue() this.context.drawerList.setData( () => [], @@ -699,6 +687,40 @@ class AutocompleteInstance extends React.PureComponent { ) } + clearInputValue = () => { + this.setState({ + inputValue: '', + typedInputValue: null, + _listenForPropChanges: false, + }) + } + + resetInputValue = () => { + const { input_value, keep_value, keep_value_and_selection } = + this.props + + if ( + isTrue(keep_value) || + isTrue(keep_value_and_selection) || + (input_value !== 'initval' && input_value.length > 0) + ) { + return // stop here + } + + clearTimeout(this._selectTimeout) + this._selectTimeout = setTimeout(() => { + if (this.hasSelectedItem()) { + const inputValue = AutocompleteInstance.getCurrentDataTitle( + this.context.drawerList.selected_item, + this.context.drawerList.original_data + ) + this.setInputValue(inputValue) + } else { + this.clearInputValue() + } + }, 1) // to make sure we actually are after the Input state handling -> "input placeholder reset" + } + showNoOptionsItem = () => { this.resetActiveItem() this.ignoreEvents() @@ -756,6 +778,22 @@ class AutocompleteInstance extends React.PureComponent { }) } + revalidateInputValue = () => { + const { input_value, value } = this.props + if (input_value && input_value !== 'initval') { + return // stop here + } + const selected_item = getCurrentIndex( + value, + this.context.drawerList.original_data + ) + const inputValue = AutocompleteInstance.getCurrentDataTitle( + selected_item, + this.context.drawerList.original_data + ) + this.setInputValue(inputValue) + } + revalidateSelectedItem = () => { const selected_item = getCurrentIndex( this.props.value, @@ -801,6 +839,7 @@ class AutocompleteInstance extends React.PureComponent { const { value } = this.props if (value && value !== 'initval') { this.revalidateSelectedItem() + this.revalidateInputValue() } else { this.resetSelectedItem() } @@ -859,7 +898,6 @@ class AutocompleteInstance extends React.PureComponent { case 'up': case 'down': if (!this.context.drawerList.opened) { - // e.preventDefault() this.setVisible() } @@ -880,6 +918,7 @@ class AutocompleteInstance extends React.PureComponent { this.ignoreEvents() this.showAll() } + if ( (!this.hasValidData() || !this.hasSelectedItem()) && !this.hasActiveItem() @@ -969,11 +1008,6 @@ class AutocompleteInstance extends React.PureComponent { no_animation, } = this.props - dispatchCustomElementEvent(this, 'on_blur', { - event, - ...this.getEventObjects('on_blur'), - }) - this.setState({ hasBlur: true, hasFocus: false, @@ -989,9 +1023,7 @@ class AutocompleteInstance extends React.PureComponent { if (!isTrue(prevent_selection)) { const existingValue = this.state.inputValue - if (!isTrue(keep_value) && !isTrue(keep_value_and_selection)) { - this.clearInputValue() - } + this.resetInputValue() const resetAfterClose = () => { if ( @@ -1018,6 +1050,11 @@ class AutocompleteInstance extends React.PureComponent { if (isTrue(open_on_focus)) { this.setHidden() } + + dispatchCustomElementEvent(this, 'on_blur', { + event, + ...this.getEventObjects('on_blur'), + }) } onTriggerKeyDownHandler = (e) => { @@ -1025,6 +1062,7 @@ class AutocompleteInstance extends React.PureComponent { switch (key) { case 'space': + case 'enter': { this.setVisible() } @@ -1040,16 +1078,32 @@ class AutocompleteInstance extends React.PureComponent { case 'up': { e.preventDefault() - try { - this._refInput.current._ref.current.focus() - } catch (e) { - warn(e) - } + this.focusInput() } break } } + focusDrawerList = () => { + try { + this.context.drawerList._refUl.current.focus({ + preventScroll: true, + }) + } catch (e) { + // do nothing + } + } + + focusInput = () => { + try { + this._refInput.current._ref.current.focus({ + preventScroll: true, + }) + } catch (e) { + warn(e) + } + } + getEventObjects = (key) => { const attributes = this.attributes @@ -1058,11 +1112,14 @@ class AutocompleteInstance extends React.PureComponent { dataList: this.context.drawerList.data, updateData: this.updateData, revalidateSelectedItem: this.revalidateSelectedItem, + revalidateInputValue: this.revalidateInputValue, resetSelectedItem: this.resetSelectedItem, + clearInputValue: this.clearInputValue, showAllItems: this.showAllItems, setVisible: this.setVisible, setHidden: this.setHidden, emptyData: this.emptyData, + focusInput: this.focusInput, setInputValue: this.setInputValue, showNoOptionsItem: this.showNoOptionsItem, showIndicatorItem: this.showIndicatorItem, @@ -1219,33 +1276,6 @@ class AutocompleteInstance extends React.PureComponent { ) } - clearInputValue = () => { - const { input_value, keep_value } = this.props - - const inputValue = AutocompleteInstance.getCurrentDataTitle( - this.context.drawerList.selected_item, - this.context.drawerList.original_data - ) - - clearTimeout(this._selectTimeout) - this._selectTimeout = setTimeout(() => { - if (this.hasSelectedItem()) { - this.setState({ - inputValue, - _listenForPropChanges: false, - }) - } else if ( - !(input_value !== 'initval' && input_value.length > 0) && - !isTrue(keep_value) - ) { - this.setState({ - inputValue: '', - _listenForPropChanges: false, - }) - } - }, 1) // to make sure we actually are after the Input state handling -> "input placeholder reset" - } - resetFilter = () => { this.context.drawerList.setData(this.context.drawerList.original_data) } @@ -1545,13 +1575,7 @@ class AutocompleteInstance extends React.PureComponent { hasFocus: true, }, () => { - try { - this._refInput.current._ref.current.focus({ - preventScroll: true, - }) - } catch (e) { - // do nothing - } + this.focusInput() this.setState({ hasFocus: false, }) @@ -1602,34 +1626,22 @@ class AutocompleteInstance extends React.PureComponent { // Do this, so screen readers get a NEW focus later on // So we first need a blur of the input basically - try { - this.context.drawerList._refUl.current.focus({ - preventScroll: true, - }) - } catch (e) { - // do nothing - } + this.focusDrawerList() this.setState( { - inputValue: AutocompleteInstance.getCurrentDataTitle( - selected_item, - this.context.drawerList.data - ), skipFocusDuringChange: false, _listenForPropChanges: false, }, () => this.setFocusOnInput() ) - } else { - this.setState({ - inputValue: AutocompleteInstance.getCurrentDataTitle( - selected_item, - this.context.drawerList.data - ), - _listenForPropChanges: false, - }) } + + const inputValue = AutocompleteInstance.getCurrentDataTitle( + selected_item, + this.context.drawerList.data + ) + this.setInputValue(inputValue) } if (typeof args.data.render === 'function') { diff --git a/packages/dnb-eufemia/src/components/autocomplete/__tests__/Autocomplete.test.tsx b/packages/dnb-eufemia/src/components/autocomplete/__tests__/Autocomplete.test.tsx index c979663048e..0b037045e91 100644 --- a/packages/dnb-eufemia/src/components/autocomplete/__tests__/Autocomplete.test.tsx +++ b/packages/dnb-eufemia/src/components/autocomplete/__tests__/Autocomplete.test.tsx @@ -875,10 +875,24 @@ describe('Autocomplete component', () => { it('has correct "opened" state on submit button click', () => { render() - toggle() - + const submitButton = document.querySelector( + 'button.dnb-input__submit-button__button:not(.dnb-input__clear-button)' + ) const elem = document.querySelector('.dnb-autocomplete') + fireEvent.click(submitButton) + + expect(elem.classList).toContain('dnb-autocomplete--opened') + + fireEvent.click(submitButton) + + expect(elem.classList).not.toContain('dnb-autocomplete--opened') + + fireEvent.keyDown(submitButton, { + key: 'Enter', + keyCode: 13, + }) + expect(elem.classList).toContain('dnb-autocomplete--opened') }) @@ -924,21 +938,23 @@ describe('Autocomplete component', () => { /> ) - document.querySelector('input').focus() + const inputElement = document.querySelector('input') + + inputElement.focus() expect(on_focus).toHaveBeenCalledTimes(1) expect(on_focus.mock.calls[0][0].attributes).toMatchObject(params) expect(document.activeElement.tagName).toBe('INPUT') // ensure we focus only once - document.querySelector('input').focus() + inputElement.focus() expect(on_focus).toHaveBeenCalledTimes(1) - fireEvent.blur(document.querySelector('input')) + fireEvent.blur(inputElement) expect(on_blur).toHaveBeenCalledTimes(1) expect(on_blur.mock.calls[0][0].attributes).toMatchObject(params) // ensure we blur only once - fireEvent.blur(document.querySelector('input')) + fireEvent.blur(inputElement) expect(on_blur).toHaveBeenCalledTimes(1) toggle() @@ -965,7 +981,7 @@ describe('Autocomplete component', () => { ).not.toContain('dnb-autocomplete--opened') // ensure we blur only once - fireEvent.blur(document.querySelector('input')) + fireEvent.blur(inputElement) expect(on_blur).toHaveBeenCalledTimes(1) toggle() @@ -1009,7 +1025,7 @@ describe('Autocomplete component', () => { }) it('can be reset to null', () => { - let value + let value: number const { rerender } = render( { /> ) - expect( - (document.querySelector('.dnb-input__input') as HTMLInputElement) - .value - ).toBe('') + const inputElement = document.querySelector( + '.dnb-input__input' + ) as HTMLInputElement + + expect(inputElement.value).toBe('') expect( document.querySelector('.dnb-input__placeholder').textContent ).toBe('placeholder') @@ -1037,10 +1054,7 @@ describe('Autocomplete component', () => { /> ) - expect( - (document.querySelector('.dnb-input__input') as HTMLInputElement) - .value - ).toBe(mockData[value]) + expect(inputElement.value).toBe(mockData[value]) rerender( { /> ) - expect( - (document.querySelector('.dnb-input__input') as HTMLInputElement) - .value - ).toBe('') + expect(inputElement.value).toBe('') value = 0 rerender( @@ -1066,10 +1077,7 @@ describe('Autocomplete component', () => { /> ) - expect( - (document.querySelector('.dnb-input__input') as HTMLInputElement) - .value - ).toBe(mockData[value]) + expect(inputElement.value).toBe(mockData[value]) rerender( { /> ) - expect( - (document.querySelector('.dnb-input__input') as HTMLInputElement) - .value - ).toBe('') + expect(inputElement.value).toBe('') }) it('will invalidate selected_item when selected_key changes', () => { @@ -1448,300 +1453,414 @@ describe('Autocomplete component', () => { ).toBe('') }) - it('should reset selected_item on input blur when no selection is made if "keep_value" and "keep_value_and_selection" is false', () => { - const on_show = jest.fn() - const on_hide = jest.fn() - const on_focus = jest.fn() - const on_blur = jest.fn() - const on_change = jest.fn() - const on_type = jest.fn() + describe('should have correct values on input blur ', () => { + it('when no selection is made and "keep_value" and "keep_value_and_selection" is false', async () => { + const on_change = jest.fn() - render( - - ) + render( + + ) - const inputElement = document.querySelector('.dnb-input__input') - const optionElements = () => - document.querySelectorAll('li.dnb-drawer-list__option') - const focusElement = () => - document.querySelector('li.dnb-drawer-list__option--focus') - const selectedElement = () => - document.querySelector('li.dnb-drawer-list__option--selected') + const inputElement: HTMLInputElement = document.querySelector( + '.dnb-input__input' + ) + const optionElements = () => + document.querySelectorAll('li.dnb-drawer-list__option') + const focusElement = () => + document.querySelector('li.dnb-drawer-list__option--focus') + const selectedElement = () => + document.querySelector('li.dnb-drawer-list__option--selected') - // open - fireEvent.mouseDown(inputElement) + // open + fireEvent.mouseDown(inputElement) - expect(optionElements().length).toBe(3) + expect(optionElements().length).toBe(3) - fireEvent.focus(inputElement) - fireEvent.change(inputElement, { - target: { value: 'cc' }, - }) + await userEvent.type(inputElement, 'cc') - // Make first item active - keyDownOnInput(40) // down + // Make first item active + keyDownOnInput(40) // down - expect(focusElement()).toBeInTheDocument() + expect(inputElement.value).toBe('cc') + expect(focusElement()).toBeInTheDocument() - closeAndReopen() + closeAndReopen() - expect(focusElement()).not.toBeInTheDocument() + expect(inputElement.value).toBe('cc') + expect(focusElement()).not.toBeInTheDocument() + expect(selectedElement()).not.toBeInTheDocument() - fireEvent.change(inputElement, { - target: { value: '' }, - }) + await userEvent.type(inputElement, 'cc') - expect(focusElement()).not.toBeInTheDocument() + // Make first item active + keyDownOnInput(40) // down - keyDownOnInput(40) // down + expect(focusElement()).toBeInTheDocument() + expect(selectedElement()).not.toBeInTheDocument() - expect(focusElement()).toBeInTheDocument() + fireEvent.blur(inputElement) - closeAndReopen() + expect(inputElement.value).toBe('cc') - // This here is what we expect - expect(focusElement()).not.toBeInTheDocument() + await wait(1) // because the implementation has a delay here of 1ms - // This also opens the drawer-list - fireEvent.change(inputElement, { - target: { value: 'cc' }, + expect(inputElement.value).toBe('') + + expect(inputElement.value).toBe('') + expect(focusElement()).not.toBeInTheDocument() + expect(selectedElement()).not.toBeInTheDocument() + expect(on_change).toHaveBeenCalledTimes(0) }) - keyDownOnInput(40) // activate - dispatchKeyDown(13) // enter + it('when a selection is made and "keep_value" and "keep_value_and_selection" is false', async () => { + const on_change = jest.fn() - closeAndReopen() + render( + + ) - // Now we have a selected item - expect(selectedElement()).toBeInTheDocument() - expect(focusElement()).toBeInTheDocument() - expect((inputElement as HTMLInputElement).value).toBe('CC cc') + const inputElement: HTMLInputElement = document.querySelector( + '.dnb-input__input' + ) - fireEvent.change(inputElement, { - target: { value: '' }, - }) + // open + fireEvent.mouseDown(inputElement) - closeAndReopen() + await userEvent.type(inputElement, 'cc') - // This here is what we expect - expect(focusElement()).not.toBeInTheDocument() - expect(selectedElement()).not.toBeInTheDocument() + keyDownOnInput(40) // down + dispatchKeyDown(13) // enter - expect(on_show).toBeCalledTimes(2) - expect(on_hide).toBeCalledTimes(2) - expect(on_focus).toBeCalledTimes(4) - expect(on_blur).toBeCalledTimes(3) - expect(on_change).toBeCalledTimes(2) - expect(on_type).toBeCalledTimes(4) - }) + fireEvent.blur(inputElement) - it('should not reset input value on input blur if "keep_value" is true and value is empty', () => { - const on_show = jest.fn() - const on_hide = jest.fn() - const on_focus = jest.fn() - const on_blur = jest.fn() - const on_change = jest.fn() - const on_type = jest.fn() + expect(inputElement.value).toBe('cc') - render( - - ) + await wait(1) // because the implementation has a delay here of 1ms - const inputElement = document.querySelector('.dnb-input__input') - const optionElements = () => - document.querySelectorAll('li.dnb-drawer-list__option') - const focusElement = () => - document.querySelector('li.dnb-drawer-list__option--focus') - const selectedElement = () => - document.querySelector('li.dnb-drawer-list__option--selected') + expect(inputElement.value).toBe('CC cc') - // open - fireEvent.mouseDown(inputElement) + expect(on_change).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + value: 2, + data: { content: ['CC', 'cc'] }, + }) + ) - expect(optionElements().length).toBe(3) + fireEvent.focus(inputElement) - fireEvent.focus(inputElement) - fireEvent.change(inputElement, { - target: { value: 'cc' }, - }) + await userEvent.type(inputElement, ' invalid') - // Make first item active - keyDownOnInput(40) // down + expect(inputElement.value).toBe('CC cc invalid') - expect(focusElement()).toBeInTheDocument() + fireEvent.blur(inputElement) - closeAndReopen() + expect(inputElement.value).toBe('CC cc invalid') - expect(focusElement()).toBeInTheDocument() + await wait(1) // because the implementation has a delay here of 1ms - fireEvent.change(inputElement, { - target: { value: '' }, + expect(inputElement.value).toBe('CC cc') + + expect(on_change).toHaveBeenCalledTimes(1) + expect(on_change).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + value: 2, + data: { content: ['CC', 'cc'] }, + }) + ) }) - expect(focusElement()).not.toBeInTheDocument() + it('if "keep_value" is true and value is empty', async () => { + const on_change = jest.fn() - keyDownOnInput(40) // down + render( + + ) - expect(focusElement()).toBeInTheDocument() + const inputElement: HTMLInputElement = document.querySelector( + '.dnb-input__input' + ) + const optionElements = () => + document.querySelectorAll('li.dnb-drawer-list__option') + const focusElement = () => + document.querySelector('li.dnb-drawer-list__option--focus') + const selectedElement = () => + document.querySelector('li.dnb-drawer-list__option--selected') - closeAndReopen() + // open + fireEvent.mouseDown(inputElement) - // This here is what we expect - expect(focusElement()).not.toBeInTheDocument() + expect(optionElements().length).toBe(3) - // This also opens the drawer-list - fireEvent.change(inputElement, { - target: { value: 'cc' }, - }) + fireEvent.focus(inputElement) + fireEvent.change(inputElement, { + target: { value: 'cc' }, + }) - keyDownOnInput(40) // activate - dispatchKeyDown(13) // enter + // Make first item active + keyDownOnInput(40) // down - closeAndReopen() + expect(focusElement()).toBeInTheDocument() - // Now we have a selected item - expect(selectedElement()).toBeInTheDocument() - expect(focusElement()).toBeInTheDocument() - expect((inputElement as HTMLInputElement).value).toBe('CC cc') + closeAndReopen() - fireEvent.change(inputElement, { - target: { value: '' }, - }) + expect(focusElement()).toBeInTheDocument() - closeAndReopen() + fireEvent.change(inputElement, { + target: { value: '' }, + }) - // This here is what we expect - expect(focusElement()).not.toBeInTheDocument() - expect(selectedElement()).not.toBeInTheDocument() + expect(focusElement()).not.toBeInTheDocument() - expect(on_show).toBeCalledTimes(2) - expect(on_hide).toBeCalledTimes(2) - expect(on_focus).toBeCalledTimes(4) - expect(on_blur).toBeCalledTimes(3) - expect(on_change).toBeCalledTimes(2) - expect(on_type).toBeCalledTimes(4) - }) + keyDownOnInput(40) // down - it('should not reset selected_item on input blur if "keep_value_and_selection" true', () => { - const on_show = jest.fn() - const on_hide = jest.fn() - const on_focus = jest.fn() - const on_blur = jest.fn() - const on_change = jest.fn() - const on_type = jest.fn() + expect(focusElement()).toBeInTheDocument() - render( - - ) + closeAndReopen() - const inputElement = document.querySelector( - '.dnb-input__input' - ) as HTMLInputElement - const optionElements = () => - document.querySelectorAll('li.dnb-drawer-list__option') - const focusElement = () => - document.querySelector('li.dnb-drawer-list__option--focus') - const selectedElement = () => - document.querySelector('li.dnb-drawer-list__option--selected') + // This here is what we expect + expect(focusElement()).not.toBeInTheDocument() - // open - fireEvent.mouseDown(inputElement) + // This also opens the drawer-list + fireEvent.change(inputElement, { + target: { value: 'cc' }, + }) - expect(optionElements().length).toBe(3) + keyDownOnInput(40) // activate + dispatchKeyDown(13) // enter - fireEvent.focus(inputElement) - fireEvent.change(inputElement, { - target: { value: 'cc' }, - }) + fireEvent.blur(inputElement) - // Make first item active - keyDownOnInput(40) // down + await wait(1) // because the implementation has a delay here of 1ms - expect(focusElement()).toBeInTheDocument() - expect(inputElement.value).toBe('cc') + expect(inputElement.value).toBe('CC cc') + expect(on_change).toHaveBeenCalledTimes(1) + expect(on_change).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + value: 2, + data: { content: ['CC', 'cc'] }, + }) + ) - closeAndReopen() + fireEvent.change(inputElement, { + target: { value: '' }, + }) - expect(focusElement()).not.toBeInTheDocument() - expect(inputElement.value).toBe('cc') + closeAndReopen() - fireEvent.change(inputElement, { - target: { value: '' }, + expect(focusElement()).not.toBeInTheDocument() + expect(selectedElement()).not.toBeInTheDocument() + + expect(on_change).toHaveBeenCalledTimes(2) + expect(on_change).not.toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + value: expect.anything(), + data: { content: expect.anything() }, + }) + ) }) - expect(focusElement()).not.toBeInTheDocument() + it('if "keep_value_and_selection" is true', async () => { + const on_change = jest.fn() - keyDownOnInput(40) // down + render( + + ) - expect(focusElement()).toBeInTheDocument() + const inputElement = document.querySelector( + '.dnb-input__input' + ) as HTMLInputElement + const optionElements = () => + document.querySelectorAll('li.dnb-drawer-list__option') + const focusElement = () => + document.querySelector('li.dnb-drawer-list__option--focus') + const selectedElement = () => + document.querySelector('li.dnb-drawer-list__option--selected') - closeAndReopen() + // open + fireEvent.mouseDown(inputElement) - // This here is what we expect - expect(focusElement()).not.toBeInTheDocument() - expect(inputElement.value).toBe('') + expect(optionElements().length).toBe(3) - // This also opens the drawer-list - fireEvent.change(inputElement, { - target: { value: 'cc' }, - }) + fireEvent.focus(inputElement) + fireEvent.change(inputElement, { + target: { value: 'cc' }, + }) - keyDownOnInput(40) // activate - dispatchKeyDown(13) // enter + // Make first item active + keyDownOnInput(40) // down - closeAndReopen() + expect(focusElement()).toBeInTheDocument() + expect(inputElement.value).toBe('cc') - // Now we have a selected item - expect(selectedElement()).toBeInTheDocument() - expect(focusElement()).toBeInTheDocument() - expect(inputElement.value).toBe('CC cc') + closeAndReopen() - fireEvent.change(inputElement, { - target: { value: '' }, + expect(focusElement()).not.toBeInTheDocument() + expect(inputElement.value).toBe('cc') + + fireEvent.change(inputElement, { + target: { value: '' }, + }) + + expect(focusElement()).not.toBeInTheDocument() + + keyDownOnInput(40) // down + + expect(focusElement()).toBeInTheDocument() + + closeAndReopen() + + // This here is what we expect + expect(focusElement()).not.toBeInTheDocument() + expect(inputElement.value).toBe('') + + // This also opens the drawer-list + fireEvent.change(inputElement, { + target: { value: 'cc' }, + }) + + keyDownOnInput(40) // activate + dispatchKeyDown(13) // enter + + fireEvent.blur(inputElement) + + await wait(1) // because the implementation has a delay here of 1ms + + expect(inputElement.value).toBe('CC cc') + + fireEvent.change(inputElement, { + target: { value: '' }, + }) + + closeAndReopen() + + expect(focusElement()).toBeInTheDocument() + expect(selectedElement()).toBeInTheDocument() + expect(inputElement.value).toBe('') + + expect(on_change).toHaveBeenCalledTimes(1) + expect(on_change).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + value: 2, + data: { content: ['CC', 'cc'] }, + }) + ) }) - closeAndReopen() + it('if "keep_election" is true', async () => { + const on_change = jest.fn() - // This here is what we expect - expect(focusElement()).toBeInTheDocument() - expect(selectedElement()).toBeInTheDocument() - expect(inputElement.value).toBe('') + render( + + ) + + const inputElement = document.querySelector( + '.dnb-input__input' + ) as HTMLInputElement + const optionElements = () => + document.querySelectorAll('li.dnb-drawer-list__option') + const focusElement = () => + document.querySelector('li.dnb-drawer-list__option--focus') + const selectedElement = () => + document.querySelector('li.dnb-drawer-list__option--selected') + + // open + fireEvent.mouseDown(inputElement) + + expect(optionElements().length).toBe(3) + + fireEvent.focus(inputElement) + fireEvent.change(inputElement, { + target: { value: 'cc' }, + }) + + // Make first item active + keyDownOnInput(40) // down + + expect(focusElement()).toBeInTheDocument() + expect(inputElement.value).toBe('cc') + + closeAndReopen() - expect(on_show).toBeCalledTimes(2) - expect(on_hide).toBeCalledTimes(2) - expect(on_focus).toBeCalledTimes(4) - expect(on_blur).toBeCalledTimes(3) - expect(on_change).toBeCalledTimes(1) - expect(on_type).toBeCalledTimes(4) + expect(focusElement()).not.toBeInTheDocument() + expect(inputElement.value).toBe('cc') + + fireEvent.change(inputElement, { + target: { value: '' }, + }) + + expect(focusElement()).not.toBeInTheDocument() + + keyDownOnInput(40) // down + + expect(focusElement()).toBeInTheDocument() + + closeAndReopen() + + // This here is what we expect + expect(focusElement()).not.toBeInTheDocument() + expect(inputElement.value).toBe('') + + // This also opens the drawer-list + fireEvent.change(inputElement, { + target: { value: 'cc' }, + }) + + keyDownOnInput(40) // activate + dispatchKeyDown(13) // enter + + fireEvent.blur(inputElement) + + await wait(1) // because the implementation has a delay here of 1ms + + expect(inputElement.value).toBe('CC cc') + + fireEvent.change(inputElement, { + target: { value: '' }, + }) + + closeAndReopen() + + expect(focusElement()).toBeInTheDocument() + expect(selectedElement()).toBeInTheDocument() + expect(inputElement.value).toBe('') + + expect(on_change).toHaveBeenCalledTimes(1) + expect(on_change).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + value: 2, + data: { content: ['CC', 'cc'] }, + }) + ) + }) }) it('should have a button for screen readers to open options – regardless', () => { @@ -1779,8 +1898,15 @@ describe('Autocomplete component', () => { it('should keep input focus when using show-all or select item', () => { render() - document.querySelector('input').focus() - fireEvent.change(document.querySelector('input'), { + const inputElement = document.querySelector('input') + + fireEvent.keyDown(inputElement, { + key: 'Enter', + keyCode: 13, + }) + + inputElement.focus() + fireEvent.change(inputElement, { target: { value: 'cc' }, }) @@ -1793,7 +1919,7 @@ describe('Autocomplete component', () => { ).length ).toBe(mockData.length - 1) - document.querySelector('input').focus() + inputElement.focus() expect(Array.from(document.activeElement.classList)).toContain( 'dnb-input__input' @@ -1817,7 +1943,7 @@ describe('Autocomplete component', () => { ).length ).toBe(mockData.length) - fireEvent.blur(document.querySelector('input')) + fireEvent.blur(inputElement) fireEvent.click( document.querySelectorAll('li.dnb-drawer-list__option')[0] ) @@ -2234,16 +2360,16 @@ describe('Autocomplete component', () => { /> ) - expect(document.querySelector('input')).toBeInTheDocument() - expect( - Array.from(document.querySelector('input').classList) - ).toContain('dnb-autocomplete__input') - expect( - document.querySelector('input').getAttribute('aria-label') - ).toBe('label') + const inputElement = document.querySelector('input') + + expect(inputElement).toBeInTheDocument() + expect(Array.from(inputElement.classList)).toContain( + 'dnb-autocomplete__input' + ) + expect(inputElement.getAttribute('aria-label')).toBe('label') const value = 'new value' - fireEvent.change(document.querySelector('input'), { + fireEvent.change(inputElement, { target: { value }, }) expect(onChange).toHaveBeenCalledTimes(1) @@ -2492,14 +2618,19 @@ describe('Autocomplete component', () => { const MockComponent = () => { const [value, setValue] = React.useState('+47') + const allData = React.useMemo(() => [data[1]], []) return ( setValue(data?.selectedKey)} - on_focus={({ updateData }) => updateData(data)} + on_change={({ data }) => { + setValue(data?.selectedKey) + }} + on_focus={({ updateData }) => { + updateData(data) + }} search_numbers no_animation /> @@ -2509,6 +2640,10 @@ describe('Autocomplete component', () => { render() const inputElement: HTMLInputElement = document.querySelector('input') + const items = () => + document.querySelectorAll('li.dnb-drawer-list__option') + const firstItemElement = () => items()[0] + const mainElement = () => document.querySelector('.dnb-autocomplete') expect(inputElement.value).toEqual('NO (+47)') @@ -2523,34 +2658,35 @@ describe('Autocomplete component', () => { document.querySelector('li.dnb-drawer-list__option--selected') .textContent ).toBe('+47 Norge') + expect(items()).toHaveLength(4) await userEvent.type(inputElement, '{Backspace}') expect(inputElement.value).toEqual('NO (+47') + expect(firstItemElement().textContent).toBe('+47 Norge') - expect( - document.querySelectorAll('li.dnb-drawer-list__option')[0] - .textContent - ).toBe('+47 Norge') + await userEvent.type(inputElement, '{Backspace>7}+41') - fireEvent.focus(inputElement) - fireEvent.change(inputElement, { target: { value: '+41' } }) - fireEvent.click( - document.querySelectorAll('li.dnb-drawer-list__option')[0] - ) + expect(inputElement.value).toEqual('+41') + expect(firstItemElement().textContent).toBe('+41 Sveits') + expect(items()).toHaveLength(2) + + expect(mainElement().classList).toContain('dnb-autocomplete--opened') + + fireEvent.keyDown(inputElement, { + key: 'Enter', + keyCode: 13, + }) expect(inputElement.value).toEqual('CH (+41)') + expect(mainElement().classList).not.toContain( + 'dnb-autocomplete--opened' + ) }) it('should reset value and open drawer on clear button click', async () => { - const on_focus = jest.fn() render( - + ) const inputElement = document.querySelector( @@ -2640,6 +2776,29 @@ describe('Autocomplete component', () => { ) }) + it('should clear input value', () => { + render() + + fireEvent.focus(inputElement()) + keyDownOnInput(13) // enter + keyDownOnInput(40) // down + keyDownOnInput(13) // enter + + fireEvent.blur(inputElement()) + + expect(inputElement()).toHaveValue('AA c') + + fireEvent.blur(inputElement()) + + fireEvent.focus(inputElement()) + fireEvent.change(inputElement(), { + target: { value: '' }, + }) + fireEvent.blur(inputElement()) + + expect(inputElement()).toHaveValue('') + }) + it('should not emit on submit button press', () => { const on_blur = jest.fn() const onBlur = jest.fn() @@ -2891,30 +3050,26 @@ describe('Autocomplete component', () => { }) it('should dismiss focus only on blur', () => { - const on_focus = jest.fn() - const on_blur = jest.fn() - const onBlur = jest.fn() const on_change = jest.fn() render( ) + const inputElement = document.querySelector('input') + expect(document.querySelector('.dnb-input')).toHaveAttribute( 'data-input-state', 'virgin' ) - fireEvent.focus(document.querySelector('input')) + fireEvent.focus(inputElement) - fireEvent.keyDown(document.querySelector('input'), { + fireEvent.keyDown(inputElement, { key: 'Enter', keyCode: 13, }) @@ -2924,7 +3079,7 @@ describe('Autocomplete component', () => { 'focus' ) - fireEvent.keyDown(document.querySelector('input'), { + fireEvent.keyDown(inputElement, { key: 'Enter', keyCode: 13, })