diff --git a/src/components/mui/__tests__/search-input.test.js b/src/components/mui/__tests__/search-input.test.js index ad8f853..109a821 100644 --- a/src/components/mui/__tests__/search-input.test.js +++ b/src/components/mui/__tests__/search-input.test.js @@ -12,10 +12,11 @@ * */ import React from "react"; -import { render, screen } from "@testing-library/react"; +import { render, screen, act } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import "@testing-library/jest-dom"; import SearchInput from "../search-input"; +import { DEBOUNCE_WAIT } from "../../../utils/constants"; describe("SearchInput", () => { test("renders with custom placeholder", () => { @@ -58,4 +59,39 @@ describe("SearchInput", () => { rerender(); expect(screen.getByDisplayValue("new")).toBeInTheDocument(); }); + + describe("debounced prop", () => { + beforeEach(() => jest.useFakeTimers()); + afterEach(() => jest.useRealTimers()); + + test("calls onSearch after debounce delay when debounced is true", async () => { + const onSearch = jest.fn(); + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + render(); + const input = screen.getByPlaceholderText("Search..."); + await user.type(input, "something"); + expect(onSearch).not.toHaveBeenCalled(); + act(() => jest.advanceTimersByTime(DEBOUNCE_WAIT)); + expect(onSearch).toHaveBeenCalledWith("something"); + }); + + test("does not call onSearch on Enter when debounced is true", async () => { + const onSearch = jest.fn(); + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + render(); + const input = screen.getByPlaceholderText("Search..."); + await user.type(input, "something{Enter}"); + expect(onSearch).not.toHaveBeenCalled(); + }); + + test("does not call onSearch on typing without Enter when not debounced", async () => { + const onSearch = jest.fn(); + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + render(); + const input = screen.getByPlaceholderText("Search..."); + await user.type(input, "something"); + act(() => jest.runAllTimers()); + expect(onSearch).not.toHaveBeenCalled(); + }); + }); }); diff --git a/src/components/mui/search-input.js b/src/components/mui/search-input.js index e83e249..defb8bd 100644 --- a/src/components/mui/search-input.js +++ b/src/components/mui/search-input.js @@ -11,29 +11,44 @@ * limitations under the License. * */ -import React, { useEffect, useState } from "react"; -import { TextField, IconButton } from "@mui/material"; +import React, { useEffect, useState, useMemo } from "react"; +import { TextField, IconButton, InputAdornment } from "@mui/material"; import SearchIcon from "@mui/icons-material/Search"; import ClearIcon from "@mui/icons-material/Clear"; +import { debounce } from "lodash"; +import { DEBOUNCE_WAIT } from "../../utils/constants"; -const SearchInput = ({ term, onSearch, placeholder = "Search..." }) => { +const SearchInput = ({ term, onSearch, placeholder = "Search...", debounced }) => { const [searchTerm, setSearchTerm] = useState(term); useEffect(() => { setSearchTerm(term || ""); }, [term]); - const handleSearch = (ev) => { - if (ev.key === "Enter") { - onSearch(searchTerm); - } - }; - const handleClear = () => { + onSearchDebounced?.cancel(); setSearchTerm(""); onSearch(""); }; + const onSearchDebounced = useMemo( + () => debounced ? debounce((value) => onSearch(value), DEBOUNCE_WAIT) : null, + [onSearch, debounced] + ); + + useEffect(() => () => onSearchDebounced?.cancel(), [onSearchDebounced]); + + const handleChange = (value) => { + setSearchTerm(value); + if (debounced) onSearchDebounced(value); + }; + + const handleKeyDown = (ev) => { + if (!debounced && ev.key === "Enter") { + onSearch(searchTerm); + } + }; + return ( { placeholder={placeholder} slotProps={{ input: { - endAdornment: term ? ( - - - - ) : ( - + startAdornment: ( + + + + ), + endAdornment: term && ( + + + + + ) } }} - onChange={(event) => setSearchTerm(event.target.value)} - onKeyDown={handleSearch} + onChange={(ev) => handleChange(ev.target.value)} + onKeyDown={handleKeyDown} fullWidth sx={{ "& .MuiOutlinedInput-root": {