diff --git a/src/components/page-filters/page-filters-search/__tests__/page-filters-search.test.tsx b/src/components/page-filters/page-filters-search/__tests__/page-filters-search.test.tsx index 8c36f681f..75e265f84 100644 --- a/src/components/page-filters/page-filters-search/__tests__/page-filters-search.test.tsx +++ b/src/components/page-filters/page-filters-search/__tests__/page-filters-search.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { render, screen, act, fireEvent } from '@/test-utils/rtl'; +import { render, screen, userEvent } from '@/test-utils/rtl'; import { mockPageQueryParamConfig, @@ -13,55 +13,81 @@ jest.mock('@/hooks/use-page-query-params/use-page-query-params', () => jest.fn(() => [mockQueryParamsValues, mockSetQueryParams]) ); +beforeEach(() => { + jest.useFakeTimers(); +}); + afterEach(() => { jest.clearAllMocks(); + jest.useRealTimers(); }); describe(PageFiltersSearch.name, () => { it('should render search bar correctly and call setSearch on input change', async () => { - setup({}); + const { user } = setup({}); const searchInput = await screen.findByRole('textbox'); - act(() => { - fireEvent.change(searchInput, { target: { value: 'test-search' } }); - }); + await user.type(searchInput, 'test-search'); expect(mockSetQueryParams).toHaveBeenCalledWith({ search: 'test-search' }); }); it('should prune quotes and spaces from input text if no regexp is passed', async () => { - setup({}); + const { user } = setup({}); const searchInput = await screen.findByRole('textbox'); - act(() => { - fireEvent.change(searchInput, { target: { value: ` "test-search'` } }); - }); + await user.type(searchInput, ` "test-search'`); expect(mockSetQueryParams).toHaveBeenCalledWith({ search: 'test-search' }); }); it('should prune symbols from input text if regexp is passed', async () => { - setup({ searchTrimRegExp: /[-]/g }); + const { user } = setup({ searchTrimRegExp: /[-]/g }); const searchInput = await screen.findByRole('textbox'); - act(() => { - fireEvent.change(searchInput, { target: { value: 'test-search' } }); - }); + await user.type(searchInput, 'test-search'); expect(mockSetQueryParams).toHaveBeenCalledWith({ search: 'testsearch' }); }); + + it('should debounce setSearch if a debounce duration is passed', async () => { + const { user } = setup({ inputDebounceDurationMs: 400 }); + + const searchInput = await screen.findByRole('textbox'); + + await user.type(searchInput, 'test-'); + jest.advanceTimersByTime(200); + + await user.type(searchInput, 'search'); + jest.advanceTimersByTime(500); + + expect(mockSetQueryParams).toHaveBeenCalledTimes(1); + expect(mockSetQueryParams).toHaveBeenCalledWith({ + search: 'test-search', + }); + }); }); -function setup({ searchTrimRegExp }: { searchTrimRegExp?: RegExp }) { - render( +function setup({ + searchTrimRegExp, + inputDebounceDurationMs, +}: { + searchTrimRegExp?: RegExp; + inputDebounceDurationMs?: number; +}) { + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + const renderResult = render( ); + + return { user, ...renderResult }; } diff --git a/src/components/page-filters/page-filters-search/page-filters-search.tsx b/src/components/page-filters/page-filters-search/page-filters-search.tsx index ea0a4f8bf..82558c1bb 100644 --- a/src/components/page-filters/page-filters-search/page-filters-search.tsx +++ b/src/components/page-filters/page-filters-search/page-filters-search.tsx @@ -1,7 +1,8 @@ -import React from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Search } from 'baseui/icon'; import { Input } from 'baseui/input'; +import debounce from 'lodash/debounce'; import usePageQueryParams from '@/hooks/use-page-query-params/use-page-query-params'; import { @@ -21,20 +22,42 @@ export default function PageFiltersSearch< searchQueryParamKey, searchPlaceholder, searchTrimRegExp = /['"\s]/g, + inputDebounceDurationMs, }: Props) { const [queryParams, setQueryParams] = usePageQueryParams( pageQueryParamsConfig, { replace: true, pageRerender: false } ); + const queryParamsSearch = queryParams[searchQueryParamKey]; + + const [inputState, setInputState] = useState(''); + + useEffect(() => { + setInputState(queryParamsSearch); + }, [queryParamsSearch]); + + const setSearch = useCallback( + (value: string) => + setQueryParams({ [searchQueryParamKey]: value } as Partial< + PageQueryParamSetterValues

+ >), + [searchQueryParamKey, setQueryParams] + ); + + const setSearchMaybeDebounced = useMemo(() => { + if (inputDebounceDurationMs) + return debounce(setSearch, inputDebounceDurationMs); + return setSearch; + }, [setSearch, inputDebounceDurationMs]); + return ( { const searchValue = event.target.value.replaceAll(searchTrimRegExp, ''); - setQueryParams({ - [searchQueryParamKey]: searchValue || undefined, - } as Partial>); + setInputState(searchValue); + setSearchMaybeDebounced(searchValue); }} placeholder={searchPlaceholder} startEnhancer={() => } diff --git a/src/components/page-filters/page-filters-search/page-filters-search.types.ts b/src/components/page-filters/page-filters-search/page-filters-search.types.ts index ab2942901..99424ddd1 100644 --- a/src/components/page-filters/page-filters-search/page-filters-search.types.ts +++ b/src/components/page-filters/page-filters-search/page-filters-search.types.ts @@ -12,4 +12,5 @@ export type Props< searchQueryParamKey: PageQueryParamValues

[K] extends string ? K : never; searchPlaceholder: string; searchTrimRegExp?: RegExp; + inputDebounceDurationMs?: number; }; diff --git a/src/components/page-filters/page-filters.tsx b/src/components/page-filters/page-filters.tsx index c0d3826d4..5f97d53e6 100644 --- a/src/components/page-filters/page-filters.tsx +++ b/src/components/page-filters/page-filters.tsx @@ -18,9 +18,7 @@ export default function PageFilters< >({ pageFiltersConfig, pageQueryParamsConfig, - searchQueryParamKey, - searchPlaceholder, - searchTrimRegExp, + ...restSearchProps }: Props) { const [areFiltersShown, setAreFiltersShown] = useState(false); @@ -32,9 +30,7 @@ export default function PageFilters<