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,
})