({})} />);
- const wrapper = screen.getByTestId("search-jump-to-form");
- expect(wrapper).toBeInTheDocument();
- });
-
it("conditionally renders the SearchJumpTo component", async () => {
- const user = userEvent.setup();
- render(
-
- Outside search form
-
-
,
- );
- const form = screen.getByTestId("search-jump-to-form");
-
- await user.type(screen.getByRole("search"), "foo");
-
- // JumpTo should be visible
- expect(form).toHaveFormValues({ search: "foo" });
- expect(screen.getByRole("listbox"));
-
- // Click outside SearchJumpTo form, it should close JumpTo
- await user.click(screen.getByText("Outside search form"));
-
- expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
+ render();
- // Type back in main search input, it should re-open JumpTo
- await user.type(screen.getByRole("search"), "baz");
+ const listbox = screen.getByRole("listbox");
+ expect(listbox).toBeInTheDocument();
+ expect(listbox).toHaveStyle({ top: "45px" });
- expect(form).toHaveFormValues({
- search: "foobaz",
- });
- expect(screen.getByRole("listbox"));
+ for (let i = 0; i < listbox.children.length; i++) {
+ const item = listbox.children[i];
+ expect(item).toBeInTheDocument();
+ expect(item).toHaveTextContent("foo");
+ }
});
});
diff --git a/components/Search/JumpTo.tsx b/components/Search/JumpTo.tsx
index 6455ad45..193b61d4 100644
--- a/components/Search/JumpTo.tsx
+++ b/components/Search/JumpTo.tsx
@@ -1,29 +1,27 @@
-import React, {
- ChangeEvent,
- FocusEvent,
- useEffect,
- useRef,
- useState,
-} from "react";
+import React, { useEffect, useRef, useState } from "react";
-import { IconSearch } from "@/components/Shared/SVG/Icons";
import SearchJumpToList from "@/components/Search/JumpToList";
-import { SearchStyled } from "./Search.styled";
import Swiper from "swiper";
interface SearchProps {
- isSearchActive: (value: boolean) => void;
+ searchFocus: boolean;
+ searchValue: string;
+ top: number;
}
-const SearchJumpTo: React.FC = ({ isSearchActive }) => {
- const search = useRef(null);
+const SearchJumpTo: React.FC = ({
+ searchFocus,
+ searchValue,
+ top,
+}) => {
const formRef = useRef(null);
- const [searchValue, setSearchValue] = useState("");
- const [searchFocus, setSearchFocus] = useState(false);
- const [isLoaded, setIsLoaded] = useState(false);
const [showJumpTo, setShowJumpTo] = useState(false);
- React.useEffect(() => {
+ useEffect(() => {
+ if (searchFocus) setShowJumpTo(Boolean(searchValue));
+ }, [searchFocus, searchValue]);
+
+ useEffect(() => {
const handleMouseDown = (e: MouseEvent) => {
if (
showJumpTo &&
@@ -66,49 +64,14 @@ const SearchJumpTo: React.FC = ({ isSearchActive }) => {
};
}, [showJumpTo]);
- const handleSearchFocus = (e: FocusEvent) => {
- if (e.type === "focus") {
- setSearchFocus(true);
- setShowJumpTo(Boolean(searchValue));
- } else {
- setSearchFocus(false);
- }
- };
-
- const handleSearchChange = (e: ChangeEvent) => {
- const value = e.target.value;
- setSearchValue(value);
- setShowJumpTo(Boolean(value));
- };
-
- useEffect(() => {
- setIsLoaded(true);
- }, []);
-
- useEffect(() => {
- !searchFocus && !searchValue ? isSearchActive(false) : isSearchActive(true);
- }, [searchFocus, searchValue, isSearchActive]);
+ if (!showJumpTo) return null;
return (
-
- {/* temporarily setting to textarea for later refinement */}
-
- {isLoaded && }
- {showJumpTo && (
-
- )}
-
+
);
};
diff --git a/components/Search/JumpToList.test.tsx b/components/Search/JumpToList.test.tsx
index b03622ee..ce768a43 100644
--- a/components/Search/JumpToList.test.tsx
+++ b/components/Search/JumpToList.test.tsx
@@ -23,6 +23,7 @@ describe("SearchJumpToList component", () => {
,
);
expect(screen.getByTestId("jump-to-wrapper"));
@@ -31,7 +32,11 @@ describe("SearchJumpToList component", () => {
it("renders Helper components in each JumpTo item", () => {
render(
- ,
+ ,
);
const helpers = screen.getAllByTestId("helper");
expect(helpers[0]).toHaveTextContent(/in this collection/i);
@@ -40,7 +45,11 @@ describe("SearchJumpToList component", () => {
it.only("renders route query params in JumpTo items", async () => {
render(
- ,
+ ,
);
await act(async () => {
@@ -63,7 +72,11 @@ describe("SearchJumpToList component", () => {
it("selects items correctly on arrow key presses", async () => {
const user = userEvent.setup();
render(
- ,
+ ,
);
const listItems = await screen.findAllByRole("option");
@@ -89,7 +102,11 @@ describe("SearchJumpToList component", () => {
it("handles the Escape key press", async () => {
const user = userEvent.setup();
render(
- ,
+ ,
);
await user.keyboard("{Escape}");
diff --git a/components/Search/JumpToList.tsx b/components/Search/JumpToList.tsx
index d5306a78..0f7c0d71 100644
--- a/components/Search/JumpToList.tsx
+++ b/components/Search/JumpToList.tsx
@@ -4,6 +4,7 @@ import {
JumpItem,
JumpToListStyled,
} from "@/components/Search/JumpTo.styled";
+
import { IconReturnDownBack } from "@/components/Shared/SVG/Icons";
import Link from "next/link";
import { getCollection } from "@/lib/collection-helpers";
@@ -13,11 +14,13 @@ import { useRouter } from "next/router";
interface SearchJumpToListProps {
searchValue: string;
setShowJumpTo: Dispatch>;
+ top: number;
}
const SearchJumpToList: React.FC = ({
searchValue,
setShowJumpTo,
+ top,
}) => {
const router = useRouter();
const [collectionTitle, setCollectionTitle] = useState("");
@@ -96,7 +99,11 @@ const SearchJumpToList: React.FC = ({
}, [router.query.id]);
return (
-
+
{jumpToItems.map((item, index) => (
svg": {
position: "absolute",
display: "flex",
diff --git a/components/Search/Search.test.tsx b/components/Search/Search.test.tsx
index d0e76c8e..da91e2b5 100644
--- a/components/Search/Search.test.tsx
+++ b/components/Search/Search.test.tsx
@@ -99,6 +99,12 @@ describe("Search component", () => {
expect(input).toBeInTheDocument();
});
+ it("renders the jump to component for collection routes", () => {
+ mockRouter.setCurrentUrl("/collections/123");
+ render();
+ expect(screen.getByTestId("search-jump-to")).toBeInTheDocument();
+ });
+
it("renders generative AI placeholder text when AI search is active", () => {
localStorage.setItem("ai", JSON.stringify("true"));
diff --git a/components/Search/Search.tsx b/components/Search/Search.tsx
index 9acfef13..77d5fe2f 100644
--- a/components/Search/Search.tsx
+++ b/components/Search/Search.tsx
@@ -9,10 +9,12 @@ import React, {
useState,
} from "react";
-import GenerativeAIToggle from "./GenerativeAIToggle";
+import GenerativeAIToggle from "@/components/Search/GenerativeAIToggle";
+import SearchJumpTo from "@/components/Search/JumpTo";
import SearchTextArea from "@/components/Search/TextArea";
import { UrlFacets } from "@/types/context/filter-context";
import { getAllFacetIds } from "@/lib/utils/facet-helpers";
+import { isCollectionPage } from "@/lib/collection-helpers";
import useGenerativeAISearchToggle from "@/hooks/useGenerativeAISearchToggle";
import useQueryParams from "@/hooks/useQueryParams";
import { useRouter } from "next/router";
@@ -25,7 +27,7 @@ const Search: React.FC = ({ isSearchActive }) => {
const router = useRouter();
const { urlFacets } = useQueryParams();
- const { isChecked, handleCheckChange } = useGenerativeAISearchToggle();
+ const { isChecked } = useGenerativeAISearchToggle();
const searchRef = useRef(null);
const formRef = useRef(null);
@@ -34,6 +36,8 @@ const Search: React.FC = ({ isSearchActive }) => {
const [searchValue, setSearchValue] = useState("");
const [searchFocus, setSearchFocus] = useState(false);
+ const appendSearchJumpTo = isCollectionPage(router?.pathname);
+
const handleSubmit = (
e?:
| SyntheticEvent
@@ -96,30 +100,41 @@ const Search: React.FC = ({ isSearchActive }) => {
}, [searchFocus, searchValue, isSearchActive]);
return (
-
-
+
-
-
-
-
- {isLoaded && }
-
+ >
+
+
+
+
+
+ {isLoaded && }
+
+ {isCollectionPage(router?.pathname) && (
+
+
+
+ )}
+
);
};
diff --git a/lib/collection-helpers.test.ts b/lib/collection-helpers.test.ts
index 53ca0e20..209011d2 100644
--- a/lib/collection-helpers.test.ts
+++ b/lib/collection-helpers.test.ts
@@ -1,8 +1,10 @@
import {
GenericAggsReturn,
GetTopMetadataAggsReturn,
+ isCollectionPage,
sortAggsByKey,
} from "@/lib/collection-helpers";
+
import { getTopMetadataAggs } from "@/lib/collection-helpers";
/* eslint sort-keys: 0 */
@@ -155,3 +157,28 @@ describe("sortAggsByKey() function", () => {
});
});
});
+
+describe("isCollectionPage", () => {
+ it("should return true for valid collection page paths", () => {
+ const pathname = "/collections/123";
+ expect(isCollectionPage(pathname)).toBe(true);
+ });
+
+ it('should return false if the first part of the path is not "collections"', () => {
+ const pathname = "/items/123";
+ expect(isCollectionPage(pathname)).toBe(false);
+ });
+
+ it("should return false if the second part of the path is undefined or empty", () => {
+ const pathname1 = "/collections/";
+ const pathname2 = "/collections";
+
+ expect(isCollectionPage(pathname1)).toBe(false);
+ expect(isCollectionPage(pathname2)).toBe(false);
+ });
+
+ it("should handle edge cases with extra slashes", () => {
+ const pathname = "//collections//123/";
+ expect(isCollectionPage(pathname)).toBe(true);
+ });
+});
diff --git a/lib/collection-helpers.ts b/lib/collection-helpers.ts
index 4933adc0..fad9671c 100644
--- a/lib/collection-helpers.ts
+++ b/lib/collection-helpers.ts
@@ -336,3 +336,16 @@ export const sortAggsByKey = (arr: GenericAggsReturn[]) => {
return collator.compare(a.key, b.key);
});
};
+
+/**
+ * Determines if current router.pathname is a dynamic collection route
+ */
+export const isCollectionPage = (pathname: string) => {
+ const pathSegments = pathname.split("/").filter(Boolean); // Filter removes any empty strings
+
+ return (
+ pathSegments[0] === "collections" &&
+ pathSegments[1] !== undefined &&
+ pathSegments[1] !== ""
+ );
+};