diff --git a/frontends/mit-learn/src/page-components/SearchField/SearchField.tsx b/frontends/mit-learn/src/page-components/SearchField/SearchField.tsx new file mode 100644 index 0000000000..5de4ede3d4 --- /dev/null +++ b/frontends/mit-learn/src/page-components/SearchField/SearchField.tsx @@ -0,0 +1,38 @@ +import React from "react" +import { SearchInput } from "ol-components" +import type { SearchInputProps, SearchSubmissionEvent } from "ol-components" +import { usePostHog } from "posthog-js/react" + +type SearchFieldProps = SearchInputProps & { + onSubmit: (event: SearchSubmissionEvent) => void + setPage: (page: number) => void +} + +const { POSTHOG } = APP_SETTINGS + +/** + * A wrapper around SearchInput that handles a little application logic like + * - resetting search page to 1 on submission + * - firing tracking events + */ +const SearchField: React.FC = ({ + onSubmit, + setPage, + ...others +}) => { + const posthog = usePostHog() + const handleSubmit: SearchInputProps["onSubmit"] = ( + event, + { isEnter } = {}, + ) => { + onSubmit(event) + setPage(1) + if (POSTHOG?.api_key) { + posthog.capture("search_update", { isEnter: isEnter }) + } + } + + return +} + +export { SearchField } diff --git a/frontends/mit-learn/src/pages/ChannelPage/ChannelSearch.test.tsx b/frontends/mit-learn/src/pages/ChannelPage/ChannelSearch.test.tsx index 8063466e3e..0e77812442 100644 --- a/frontends/mit-learn/src/pages/ChannelPage/ChannelSearch.test.tsx +++ b/frontends/mit-learn/src/pages/ChannelPage/ChannelSearch.test.tsx @@ -1,4 +1,4 @@ -import { screen, within, waitFor, renderTestApp } from "@/test-utils" +import { screen, within, waitFor, renderTestApp, user } from "@/test-utils" import { setMockResponse, urls, factories, makeRequest } from "api/test-utils" import type { LearningResourcesSearchResponse } from "api" import invariant from "tiny-invariant" @@ -264,4 +264,45 @@ describe("ChannelSearch", () => { } }, ) + + test("Submitting search text updates URL correctly", async () => { + const resources = factories.learningResources.resources({ + count: 10, + }).results + const { channel } = setMockApiResponses({ + search: { + count: 1000, + metadata: { + aggregations: { + resource_type: [ + { key: "course", doc_count: 100 }, + { key: "podcast", doc_count: 200 }, + { key: "program", doc_count: 300 }, + { key: "irrelevant", doc_count: 400 }, + ], + }, + suggestions: [], + }, + results: resources, + }, + }) + setMockResponse.get(urls.userMe.get(), {}) + + const initialSearch = "?q=meow&page=2" + const finalSearch = "?q=woof" + + const { location } = renderTestApp({ + url: `/c/${channel.channel_type}/${channel.name}${initialSearch}`, + }) + + const queryInput = await screen.findByRole("textbox", { + name: "Search for", + }) + expect(queryInput.value).toBe("meow") + await user.clear(queryInput) + await user.paste("woof") + expect(location.current.search).toBe(initialSearch) + await user.click(screen.getByRole("button", { name: "Search" })) + expect(location.current.search).toBe(finalSearch) + }) }) diff --git a/frontends/mit-learn/src/pages/ChannelPage/ChannelSearch.tsx b/frontends/mit-learn/src/pages/ChannelPage/ChannelSearch.tsx index 4b32b3c7ee..2e88ed9bac 100644 --- a/frontends/mit-learn/src/pages/ChannelPage/ChannelSearch.tsx +++ b/frontends/mit-learn/src/pages/ChannelPage/ChannelSearch.tsx @@ -14,7 +14,8 @@ import type { } from "@mitodl/course-search-utils" import { useSearchParams } from "@mitodl/course-search-utils/react-router" import SearchDisplay from "@/page-components/SearchDisplay/SearchDisplay" -import { Container, SearchInput, styled, VisuallyHidden } from "ol-components" +import { Container, styled, VisuallyHidden } from "ol-components" +import { SearchField } from "@/page-components/SearchField/SearchField" import { getFacetManifest } from "@/pages/SearchPage/SearchPage" @@ -30,7 +31,7 @@ const SearchInputContainer = styled(Container)(({ theme }) => ({ }, })) -const StyledSearchInput = styled(SearchInput)({ +const StyledSearchField = styled(SearchField)({ width: "624px", }) @@ -172,7 +173,7 @@ const ChannelSearch: React.FC = ({
Search within {channelTitle} - setCurrentText(e.target.value)} @@ -182,6 +183,7 @@ const ChannelSearch: React.FC = ({ onClear={() => { setCurrentTextAndQuery("") }} + setPage={setPage} /> diff --git a/frontends/mit-learn/src/pages/SearchPage/SearchPage.test.tsx b/frontends/mit-learn/src/pages/SearchPage/SearchPage.test.tsx index 697d3f9ae5..6370ad254a 100644 --- a/frontends/mit-learn/src/pages/SearchPage/SearchPage.test.tsx +++ b/frontends/mit-learn/src/pages/SearchPage/SearchPage.test.tsx @@ -201,25 +201,24 @@ describe("SearchPage", () => { await within(facetsContainer).findByText("Resource Type") }) - test.each([{ withPage: false }, { withPage: true }])( - "Submitting text updates URL", - async ({ withPage }) => { - setMockApiResponses({}) - const urlQueryString = withPage ? "?q=meow&page=2" : "?q=meow" - const { location } = renderWithProviders(, { - url: urlQueryString, - }) - const queryInput = await screen.findByRole("textbox", { - name: "Search for", - }) - expect(queryInput.value).toBe("meow") - await user.clear(queryInput) - await user.paste("woof") - expect(location.current.search).toBe(urlQueryString) - await user.click(screen.getByRole("button", { name: "Search" })) - expect(location.current.search).toBe("?q=woof") - }, - ) + test.each([ + { initialQuery: "?q=meow&page=2", finalQuery: "?q=woof" }, + { initialQuery: "?q=meow", finalQuery: "?q=woof" }, + ])("Submitting text updates URL", async ({ initialQuery, finalQuery }) => { + setMockApiResponses({}) + const { location } = renderWithProviders(, { + url: initialQuery, + }) + const queryInput = await screen.findByRole("textbox", { + name: "Search for", + }) + expect(queryInput.value).toBe("meow") + await user.clear(queryInput) + await user.paste("woof") + expect(location.current.search).toBe(initialQuery) + await user.click(screen.getByRole("button", { name: "Search" })) + expect(location.current.search).toBe(finalQuery) + }) test("unathenticated users do not see admin options", async () => { setMockApiResponses({ diff --git a/frontends/mit-learn/src/pages/SearchPage/SearchPage.tsx b/frontends/mit-learn/src/pages/SearchPage/SearchPage.tsx index 476945824c..1b3cfd56a7 100644 --- a/frontends/mit-learn/src/pages/SearchPage/SearchPage.tsx +++ b/frontends/mit-learn/src/pages/SearchPage/SearchPage.tsx @@ -9,13 +9,8 @@ import { getDepartmentName, } from "@mitodl/course-search-utils" import SearchDisplay from "@/page-components/SearchDisplay/SearchDisplay" -import { - SearchInput, - styled, - Container, - theme, - VisuallyHidden, -} from "ol-components" +import { styled, Container, theme, VisuallyHidden } from "ol-components" +import { SearchField } from "@/page-components/SearchField/SearchField" import type { LearningResourceOfferor } from "api" import { useOfferorsList } from "api/hooks/learningResources" import { capitalize } from "ol-utilities" @@ -56,7 +51,7 @@ const SearchFieldContainer = styled(Container)({ justifyContent: "center", }) -const SearchField = styled(SearchInput)(({ theme }) => ({ +const StyledSearchField = styled(SearchField)(({ theme }) => ({ [theme.breakpoints.down("sm")]: { width: "100%", }, @@ -216,14 +211,6 @@ const SearchPage: React.FC = () => { onFacetsChange, }) - const onSearchTermSubmit = useCallback( - (term: string) => { - setCurrentTextAndQuery(term) - setPage(1) - }, - [setPage, setCurrentTextAndQuery], - ) - const page = +(searchParams.get("page") ?? "1") return ( @@ -234,16 +221,17 @@ const SearchPage: React.FC = () => {
- setCurrentText(e.target.value)} onSubmit={(e) => { - onSearchTermSubmit(e.target.value) + setCurrentTextAndQuery(e.target.value) }} onClear={() => { - onSearchTermSubmit("") + setCurrentTextAndQuery("") }} + setPage={setPage} />
diff --git a/frontends/ol-components/package.json b/frontends/ol-components/package.json index 87b9df5a3c..8942839161 100644 --- a/frontends/ol-components/package.json +++ b/frontends/ol-components/package.json @@ -26,7 +26,6 @@ "material-ui-popup-state": "^5.1.0", "ol-test-utilities": "0.0.0", "ol-utilities": "0.0.0", - "posthog-js": "^1.165.0", "react": "18.3.1", "react-router": "^6.22.2", "react-router-dom": "^6.22.2", diff --git a/frontends/ol-components/src/components/SearchInput/SearchInput.test.tsx b/frontends/ol-components/src/components/SearchInput/SearchInput.test.tsx index bac9a47464..6e00b8a3ec 100644 --- a/frontends/ol-components/src/components/SearchInput/SearchInput.test.tsx +++ b/frontends/ol-components/src/components/SearchInput/SearchInput.test.tsx @@ -66,7 +66,17 @@ describe("SearchInput", () => { it("Calls onSubmit when search is clicked", async () => { const { user, spies } = renderSearchInput({ value: "chemistry" }) await user.click(getSearchButton()) - expect(spies.onSubmit).toHaveBeenCalledWith(searchEvent("chemistry")) + expect(spies.onSubmit).toHaveBeenCalledWith(searchEvent("chemistry"), { + isEnter: false, + }) + }) + + it("Calls onSubmit when 'Enter' is pressed", async () => { + const { user, spies } = renderSearchInput({ value: "chemistry" }) + await user.type(getSearchInput(), "{enter}") + expect(spies.onSubmit).toHaveBeenCalledWith(searchEvent("chemistry"), { + isEnter: true, + }) }) it("Calls onClear clear is clicked", async () => { diff --git a/frontends/ol-components/src/components/SearchInput/SearchInput.tsx b/frontends/ol-components/src/components/SearchInput/SearchInput.tsx index 09e711f40e..3391e299dd 100644 --- a/frontends/ol-components/src/components/SearchInput/SearchInput.tsx +++ b/frontends/ol-components/src/components/SearchInput/SearchInput.tsx @@ -1,9 +1,8 @@ -import React, { useCallback } from "react" +import React from "react" import { RiSearch2Line, RiCloseLine } from "@remixicon/react" import { Input, AdornmentButton } from "../Input/Input" import type { InputProps } from "../Input/Input" import styled from "@emotion/styled" -import { usePostHog } from "posthog-js/react" const StyledInput = styled(Input)(({ theme }) => ({ boxShadow: "0px 8px 20px 0px rgba(120, 147, 172, 0.10)", @@ -34,7 +33,10 @@ export interface SearchSubmissionEvent { preventDefault: () => void } -type SearchSubmitHandler = (event: SearchSubmissionEvent) => void +type SearchSubmitHandler = ( + event: SearchSubmissionEvent, + opts?: { isEnter?: boolean }, +) => void interface SearchInputProps { className?: string @@ -54,35 +56,15 @@ const muiInputProps = { "aria-label": "Search for" } const SearchInput: React.FC = (props) => { const { onSubmit, value } = props - const posthog = usePostHog() - const { POSTHOG } = APP_SETTINGS + const event = { + target: { value }, + preventDefault: () => null, + } - const handleSubmit = useCallback( - ( - ev: - | React.SyntheticEvent - | React.SyntheticEvent, - isEnter: boolean = false, - ) => { - const event = { - target: { value }, - preventDefault: () => null, - } - if (!(!POSTHOG?.api_key || POSTHOG.api_key.length < 1)) { - posthog.capture("search_update", { isEnter: isEnter }) - } - onSubmit(event) - }, - [onSubmit, value, posthog, POSTHOG], - ) - const onInputKeyDown: React.KeyboardEventHandler = - useCallback( - (e) => { - if (e.key !== "Enter") return - handleSubmit(e, true) - }, - [handleSubmit], - ) + const onInputKeyDown: React.KeyboardEventHandler = (e) => { + if (e.key !== "Enter") return + onSubmit(event, { isEnter: true }) + } return ( = (props) => { onSubmit(event, { isEnter: false })} > diff --git a/frontends/ol-components/src/index.ts b/frontends/ol-components/src/index.ts index 9c988a0768..eb81aa4375 100644 --- a/frontends/ol-components/src/index.ts +++ b/frontends/ol-components/src/index.ts @@ -200,7 +200,10 @@ export * from "./constants/imgConfigs" export { Input, AdornmentButton } from "./components/Input/Input" export type { InputProps, AdornmentButtonProps } from "./components/Input/Input" export { SearchInput } from "./components/SearchInput/SearchInput" -export type { SearchInputProps } from "./components/SearchInput/SearchInput" +export type { + SearchInputProps, + SearchSubmissionEvent, +} from "./components/SearchInput/SearchInput" export { TextField } from "./components/TextField/TextField" export { SimpleSelect, diff --git a/yarn.lock b/yarn.lock index 6452e2d589..7fa390a739 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15408,7 +15408,6 @@ __metadata: material-ui-popup-state: "npm:^5.1.0" ol-test-utilities: "npm:0.0.0" ol-utilities: "npm:0.0.0" - posthog-js: "npm:^1.165.0" prop-types: "npm:^15.8.1" react: "npm:18.3.1" react-router: "npm:^6.22.2" @@ -16578,17 +16577,6 @@ __metadata: languageName: node linkType: hard -"posthog-js@npm:^1.165.0": - version: 1.165.0 - resolution: "posthog-js@npm:1.165.0" - dependencies: - fflate: "npm:^0.4.8" - preact: "npm:^10.19.3" - web-vitals: "npm:^4.0.1" - checksum: 10/4a640b90af24ffb173b4d20f27aab572437c8641b1ff48ad23e98d593fa7e94e63e660a4ce967a18eaabaf5102ecaff8a258315b47d1916e79a7f1ec7ad3bc7d - languageName: node - linkType: hard - "preact@npm:^10.19.3": version: 10.23.1 resolution: "preact@npm:10.23.1"