From 10075aa247358f0c386420a69c9018421e9270bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Thu, 2 Apr 2026 15:11:04 -0300 Subject: [PATCH 1/5] fix: add debounced prop and ux to mui search input component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- src/components/mui/search-input.js | 41 ++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/src/components/mui/search-input.js b/src/components/mui/search-input.js index e83e249..d6d3c7f 100644 --- a/src/components/mui/search-input.js +++ b/src/components/mui/search-input.js @@ -11,29 +11,43 @@ * limitations under the License. * */ -import React, { useEffect, useState } from "react"; -import { TextField, IconButton } from "@mui/material"; +import React, { useEffect, useState, useCallback } 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 = () => { setSearchTerm(""); onSearch(""); }; + const onSearchDebounced = useCallback( + 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: { + startAdornment: ( + + + + ), endAdornment: term ? ( { ) } }} - onChange={(event) => setSearchTerm(event.target.value)} - onKeyDown={handleSearch} + onChange={(ev) => handleChange(ev.target.value)} + onKeyDown={handleKeyDown} fullWidth sx={{ "& .MuiOutlinedInput-root": { From baef89669553dca270249c35a03047562e98c2db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Thu, 2 Apr 2026 15:29:21 -0300 Subject: [PATCH 2/5] fix: replace useCallback with useMemo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- src/components/mui/search-input.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/mui/search-input.js b/src/components/mui/search-input.js index d6d3c7f..d537699 100644 --- a/src/components/mui/search-input.js +++ b/src/components/mui/search-input.js @@ -11,7 +11,7 @@ * limitations under the License. * */ -import React, { useEffect, useState, useCallback } from "react"; +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"; @@ -30,7 +30,7 @@ const SearchInput = ({ term, onSearch, placeholder = "Search...", debounced }) = onSearch(""); }; - const onSearchDebounced = useCallback( + const onSearchDebounced = useMemo( debounced ? debounce((value) => onSearch(value), DEBOUNCE_WAIT) : null, [onSearch, debounced] ); From 39bac298838dca3d7b574624c7323591d11a8182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Thu, 2 Apr 2026 16:03:13 -0300 Subject: [PATCH 3/5] fix: add unit test cases, adjust useMemo function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- .../mui/__tests__/search-input.test.js | 38 ++++++++++++++++++- src/components/mui/search-input.js | 2 +- 2 files changed, 38 insertions(+), 2 deletions(-) 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 d537699..1d65f83 100644 --- a/src/components/mui/search-input.js +++ b/src/components/mui/search-input.js @@ -31,7 +31,7 @@ const SearchInput = ({ term, onSearch, placeholder = "Search...", debounced }) = }; const onSearchDebounced = useMemo( - debounced ? debounce((value) => onSearch(value), DEBOUNCE_WAIT) : null, + () => debounced ? debounce((value) => onSearch(value), DEBOUNCE_WAIT) : null, [onSearch, debounced] ); From 0dd6a003fae8136f339122d59e69ef0133eea2e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Thu, 2 Apr 2026 16:10:19 -0300 Subject: [PATCH 4/5] fix: add cancel onSearchDebounced at handleClear MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- src/components/mui/search-input.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/mui/search-input.js b/src/components/mui/search-input.js index 1d65f83..86c77c2 100644 --- a/src/components/mui/search-input.js +++ b/src/components/mui/search-input.js @@ -26,6 +26,7 @@ const SearchInput = ({ term, onSearch, placeholder = "Search...", debounced }) = }, [term]); const handleClear = () => { + onSearchDebounced?.cancel(); setSearchTerm(""); onSearch(""); }; From 9f8f4b073b2983685dd390a66a59c73833a09867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Thu, 2 Apr 2026 16:32:20 -0300 Subject: [PATCH 5/5] fix: adjust duplicate search icon, adjust icon positioning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- src/components/mui/search-input.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/components/mui/search-input.js b/src/components/mui/search-input.js index 86c77c2..defb8bd 100644 --- a/src/components/mui/search-input.js +++ b/src/components/mui/search-input.js @@ -61,18 +61,12 @@ const SearchInput = ({ term, onSearch, placeholder = "Search...", debounced }) = ), - endAdornment: term ? ( - - - - ) : ( - + endAdornment: term && ( + + + + + ) } }}