diff --git a/src/Combobox/Combobox.tsx b/src/Combobox/Combobox.tsx index 8b7dbdb8..e1833afd 100644 --- a/src/Combobox/Combobox.tsx +++ b/src/Combobox/Combobox.tsx @@ -84,6 +84,16 @@ const filterIncludes = (inputValue: string, menuList: string[]) => { return filtered; }; +const filteredMenuList = (initialValue: string, filterMethod: 'startsWith' | 'includes' | CustomFilter, menuList: Array) => { + if(filterMethod === "startsWith"){ + return filterStartsWith(initialValue, menuList); + } + if (filterMethod === "includes") { + return filterIncludes(initialValue, menuList); + } + return filterMethod(initialValue, menuList); +} + export const Combobox: BsPrefixRefForwardingComponent<'input', ComboboxProps> = React.forwardRef( ( @@ -95,7 +105,7 @@ export const Combobox: BsPrefixRefForwardingComponent<'input', ComboboxProps> = label = '', icon, scrollable, - filterMethod, + filterMethod = "startsWith", ...props }, ref @@ -110,9 +120,7 @@ export const Combobox: BsPrefixRefForwardingComponent<'input', ComboboxProps> = value: initialValue, invalid: false, menuList: initialValue - ? menuList.filter((n) => - n.toLowerCase().startsWith(initialValue.toLowerCase()) - ) + ? filteredMenuList(initialValue, filterMethod, menuList) : menuList, }; const [state, setState] = useState(initialState); @@ -170,7 +178,11 @@ export const Combobox: BsPrefixRefForwardingComponent<'input', ComboboxProps> = React.useEffect(() => { setComboboxMenuId(generateId('combobox', 'ul')); }, []); - + React.useEffect(() => { + setState({...state, menuList: initialValue + ? filteredMenuList(initialValue, filterMethod, menuList) + : menuList}) + }, [menuList]); return ( <> {label && {label}} @@ -198,6 +210,7 @@ export const Combobox: BsPrefixRefForwardingComponent<'input', ComboboxProps> = {state.menuList.map((menuItem) => ( { }; ``` + ## onChangeInput `onChangeInput` is fired whenever the value of input changes whether due to typing directly in the input or from selection from menu. @@ -390,3 +391,22 @@ export const ComboboxCustomFilterExample = () => { ); }; ``` + +export const ComboboxFetchData = () => { + const [emails, setEmails] = useState([]); + useEffect(() => { + fetch(`https://jsonplaceholder.typicode.com/comments?postId=1`) + .then((response) => response.json()) + .then((json) => { + const emails = json.map(j => j.email) + setEmails(emails) + }); + }, []) + return ; +} + +## Demo with data fetching + + +{ComboboxFetchData.bind({})} + \ No newline at end of file diff --git a/tests/Combobox/Combobox.test.tsx b/tests/Combobox/Combobox.test.tsx index 4b4da790..739a004a 100644 --- a/tests/Combobox/Combobox.test.tsx +++ b/tests/Combobox/Combobox.test.tsx @@ -1,6 +1,7 @@ -import { fireEvent, render, waitFor } from '@testing-library/react'; +import { render, fireEvent, waitFor } from '@testing-library/react'; import * as React from 'react'; import { Combobox } from '../../src'; +import { CustomFilter } from '../../src/Combobox/Combobox'; const menuList = [ 'Afghanistan', 'Albania', @@ -226,23 +227,22 @@ describe(' a11y', () => { }); it('dropdown menu has role="listbox" on ul element', async () => { const { container } = render(); - fireEvent.click(container - .querySelector('input.form-control')!) - expect(container.querySelector('ul.dropdown-menu')).toHaveAttribute( - 'role', - 'listbox' - ); + fireEvent.click(container.querySelector('input.form-control')!); + await waitFor(() => { + expect(container.querySelector('ul.dropdown-menu')).toHaveAttribute( + 'role', + 'listbox' + ); + }) }); it('form control input aria-controls id points to ul id', async () => { const { container } = render(); - fireEvent.click(container - .querySelector('input.form-control')!) + fireEvent.click(container.querySelector('input.form-control')!); const menuUlId = container.querySelector('ul')?.getAttribute('id'); const inputAriaControlValue = container .querySelector('input.form-control') ?.getAttribute('aria-controls'); - - expect(inputAriaControlValue).toEqual(menuUlId); + await waitFor(() => expect(inputAriaControlValue).toEqual(menuUlId)); }); }); describe('', () => { @@ -398,22 +398,100 @@ describe('', () => { await waitFor(() => { expect(container.querySelector('input')?.value).toEqual('Afghanistan'); }); - fireEvent.keyDown(container.querySelectorAll('li>button.dropdown-item')[0], { - key: 'ArrowDown', - code: 'ArrowDown', - }); + fireEvent.keyDown( + container.querySelectorAll('li>button.dropdown-item')[0], + { + key: 'ArrowDown', + code: 'ArrowDown', + } + ); await waitFor(() => { expect(container.querySelector('input')?.value).toEqual('Albania'); }); - fireEvent.keyDown(container.querySelectorAll('li>button.dropdown-item')[1], { - key: 'ArrowDown', - code: 'ArrowDown', - }); + fireEvent.keyDown( + container.querySelectorAll('li>button.dropdown-item')[1], + { + key: 'ArrowDown', + code: 'ArrowDown', + } + ); await waitFor(() => { expect(container.querySelector('input')?.value).toEqual('Algeria'); }); }); + it('menuList is filtered with "ang" initialValue and includes filter method gives 3 items', async () => { + const { container } = render( + + ); + fireEvent.click( + container.querySelector('input.form-control.dropdown-toggle')! + ); + + await waitFor(() => { + expect(container.querySelector('ul.dropdown-menu')).toBeInTheDocument(); + }); + expect(container.querySelectorAll('li>button.dropdown-item').length).toEqual(3); + }); + it('menuList is filtered with "ang" initialValue and includes filter method gives 2 items', async () => { + const { container } = render( + + ); + fireEvent.click( + container.querySelector('input.form-control.dropdown-toggle')! + ); + + await waitFor(() => { + expect(container.querySelector('ul.dropdown-menu')).toBeInTheDocument(); + }); + expect(container.querySelectorAll('li>button.dropdown-item').length).toEqual(2); + }); + it('menuList is filtered with "ang" initialValue and endsWith custom filter method gives 0 items', async () => { + const customFilter: CustomFilter = (inputValue: string, menuItems: string[]) => { + const filtered = menuItems.filter((n) => { + const nLowerCase = n.toLowerCase(); + const valueLower = inputValue.toLowerCase(); + return nLowerCase.endsWith(valueLower); + }); + return filtered; + }; + const { container } = render( + + ); + + fireEvent.click( + container.querySelector('input.form-control.dropdown-toggle')! + ); + await waitFor(() => { + expect(container.querySelector('ul.dropdown-menu')).not.toBeInTheDocument(); + }); + expect(container.querySelectorAll('li>button.dropdown-item').length).toEqual(0); + }); + it('items in menu are updated when menuList prop changes', async () => { + const { container, rerender } = render(); + fireEvent.click( + container.querySelector('input.form-control.dropdown-toggle')! + ); + expect(container.querySelector('ul.dropdown-menu')).not.toBeInTheDocument(); + rerender(); + await waitFor(() => { + expect(container.querySelector('ul.dropdown-menu')).toBeInTheDocument(); + }); + const dropdownItems = container.querySelectorAll('li>button.dropdown-item'); + expect(dropdownItems.length).toEqual(menuList.length); + }); it('onChangeInput fires when input changes', async () => { const mockFn = jest.fn(); const { container } = render( @@ -458,7 +536,9 @@ describe('', () => { it('icon has a default value ', () => { const { container } = render(); expect( - container.querySelector('.dropdown.combobox> i.form-control-icon.bi-chevron-down') + container.querySelector( + '.dropdown.combobox> i.form-control-icon.bi-chevron-down' + ) ).toBeInTheDocument(); }); it('when icon defined, icon in container', () => { @@ -474,13 +554,15 @@ describe('', () => { }); it('when scrollable=true, adds scrollable class to combobox', () => { const { container } = render( - } /> + } + /> ); expect( - container.querySelector( - '.dropdown.combobox.scrollable' - ) + container.querySelector('.dropdown.combobox.scrollable') ).toBeInTheDocument(); }); it('For filterMethod=includes, when gh is typed matches one of menuList, country filtered menuList should show only strings including gh. Results in 2', async () => { @@ -499,14 +581,16 @@ describe('', () => { ); await waitFor(() => { expect(container.querySelector('ul.dropdown-menu')).toBeInTheDocument(); - const dropdownItem = container.querySelectorAll("li>button.dropdown-item") + const dropdownItem = container.querySelectorAll( + 'li>button.dropdown-item' + ); expect(dropdownItem.length).toEqual(2); expect(dropdownItem[0].textContent).toEqual('Afghanistan'); expect(dropdownItem[1].textContent).toEqual('Ghana'); }); }); it('For custom filterMethod, custom filter behaviour is applied instead', async () => { - const customFilter = (inputValue: string, menuItems: string[]) => { + const customFilter = (inputValue: string, menuItems: string[]) => { const filtered = menuItems.filter((n) => { const nLowerCase = n.toLowerCase(); const valueLower = inputValue.toLowerCase(); @@ -515,7 +599,10 @@ describe('', () => { return filtered; }; const { container } = render( - + ); fireEvent.change( @@ -525,7 +612,9 @@ describe('', () => { expect(container.querySelector('input')?.value).toEqual('e'); await waitFor(() => { expect(container.querySelector('ul.dropdown-menu')).toBeInTheDocument(); - const dropdownItem = container.querySelectorAll("li>button.dropdown-item") + const dropdownItem = container.querySelectorAll( + 'li>button.dropdown-item' + ); expect(dropdownItem.length).toEqual(2); expect(dropdownItem[0].textContent).toEqual('apple'); expect(dropdownItem[1].textContent).toEqual('orange');