diff --git a/src/components/quicklaunch.jsx b/src/components/quicklaunch.jsx index 220c6dbb2..96df9fb24 100644 --- a/src/components/quicklaunch.jsx +++ b/src/components/quicklaunch.jsx @@ -15,6 +15,16 @@ const MOBILE_BUTTON_POSITIONS = { "bottom-right": "bottom-4 right-4", }; +function parseUrl(searchString) { + if (!/.+[.:].+/.test(searchString)) return null; // basic test for probably a url + + try { + return new URL(searchString.toLowerCase().startsWith("http") ? searchString : `https://${searchString}`); + } catch { + return null; + } +} + function getSearchResults({ hideVisitURL, searchDescriptions, @@ -77,7 +87,7 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea const searchField = useRef(); const [currentItemIndex, setCurrentItemIndex] = useState(null); - const [url, setUrl] = useState(null); + const url = parseUrl(searchString); const [searchSuggestions, setSearchSuggestions] = useState([]); const { data: widgets } = useSWR("/api/widgets"); @@ -134,17 +144,8 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea function handleSearchChange(event) { const rawSearchString = event.target.value; setCurrentItemIndex(null); - try { - if (!/.+[.:].+/g.test(rawSearchString)) throw new Error(); // basic test for probably a url - let urlString = rawSearchString; - if (urlString.toLowerCase().indexOf("http") !== 0) urlString = `https://${rawSearchString}`; - setUrl(new URL(urlString)); // basic validation - setSearchString(rawSearchString); - return; - } catch (e) { - setUrl(null); - } - setSearchString(rawSearchString.toLowerCase()); + // urls keep their casing, everything else is lowercased for matching + setSearchString(parseUrl(rawSearchString) ? rawSearchString : rawSearchString.toLowerCase()); } function handleSearchKeyDown(event) { diff --git a/src/components/quicklaunch.test.jsx b/src/components/quicklaunch.test.jsx index 65cf7a508..031471916 100644 --- a/src/components/quicklaunch.test.jsx +++ b/src/components/quicklaunch.test.jsx @@ -46,13 +46,25 @@ function Wrapper({ servicesAndBookmarks = [], initialOpen = true } = {}) { const [isOpen, setSearching] = useState(initialOpen); return ( - + <> + + + ); } @@ -151,6 +163,35 @@ describe("components/quicklaunch", () => { openSpy.mockRestore(); }); + it("does not carry a previous url result into a search seeded by a keypress", async () => { + renderWithProviders(, { + settings: { + target: "_self", + quicklaunch: { + provider: "duckduckgo", + showSearchSuggestions: false, + }, + }, + }); + + const input = screen.getByPlaceholderText("Search"); + await waitFor(() => expect(input).toHaveFocus()); + + fireEvent.change(input, { target: { value: "example.com" } }); + expect(await screen.findByText("quicklaunch.visit URL")).toBeInTheDocument(); + + fireEvent.keyDown(input, { key: "Escape" }); + await act(async () => { + await new Promise((r) => setTimeout(r, 350)); + }); + + // reopen by "typing" a character, the way pages/index does + fireEvent.click(screen.getByTestId("seed-key")); + + expect(input).toHaveValue("g"); + expect(screen.queryByText("quicklaunch.visit URL")).not.toBeInTheDocument(); + }); + it("closes on Escape and clears the search string after the timeout", async () => { renderWithProviders(, { settings: {