Skip to content

Commit

Permalink
Consolidate collection jump to functionality in search component.
Browse files Browse the repository at this point in the history
  • Loading branch information
mathewjordan committed Sep 4, 2024
1 parent 315d2b0 commit 09d4ae0
Show file tree
Hide file tree
Showing 13 changed files with 157 additions and 161 deletions.
6 changes: 2 additions & 4 deletions components/Header/Header.styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ const Primary = styled("div", {
fontFamily: "$northwesternSansRegular",
display: "flex",
height: "$gr5",
marginLeft: "$gr4",

a: {
color: "$purple",
Expand Down Expand Up @@ -152,10 +153,7 @@ const Primary = styled("div", {
[`& ${NavStyled}`]: {
width: "0",
opacity: "0",
},

[`& ${SearchStyled}`]: {
marginRight: "0",
marginLeft: "0",
},
},
},
Expand Down
11 changes: 1 addition & 10 deletions components/Header/Primary.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { render, screen } from "@/test-utils";

import HeaderPrimary from "./Primary";
import React from "react";
import { createDynamicRouteParser } from "next-router-mock/dynamic-routes";
import mockRouter from "next-router-mock";

Expand Down Expand Up @@ -38,16 +39,6 @@ describe("HeaderPrimary", () => {
expect(search).toBeInTheDocument();
});

it("renders the search jump to component", () => {
mockRouter.setCurrentUrl(
"https://devbox.library.northwestern.edu:3000/collections/1c2e2200-c12d-4c7f-8b87-a935c349898a",
);
render(<HeaderPrimary />);

expect(screen.queryByTestId("search-ui-component")).toBeNull();
expect(screen.getByTestId("search-jump-to")).toBeInTheDocument();
});

it("renders browse collections link", () => {
render(<HeaderPrimary />);
const link = screen.getByText("Browse Collections");
Expand Down
13 changes: 1 addition & 12 deletions components/Header/Primary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,10 @@ import Container from "@/components/Shared/Container";
import Link from "next/link";
import Nav from "@/components/Nav/Nav";
import Search from "@/components/Search/Search";
import SearchJumpTo from "@/components/Search/JumpTo";
import useElementPosition from "@/hooks/useElementPosition";
import { useRouter } from "next/router";
import { useSearchState } from "@/context/search-context";

const HeaderPrimary: React.FC = () => {
const router = useRouter();
const isCollectionPage =
router.pathname.includes("collection") && router.query.id;

const [searchActive, setSearchActive] = useState<boolean>(false);

const {
Expand Down Expand Up @@ -48,12 +42,7 @@ const HeaderPrimary: React.FC = () => {
>
<Container>
<PrimaryInner>
{!isCollectionPage && (
<Search isSearchActive={handleIsSearchActive} />
)}
{isCollectionPage && (
<SearchJumpTo isSearchActive={handleIsSearchActive} />
)}
<Search isSearchActive={handleIsSearchActive} />
<Nav>
<Link href="/collections">Browse Collections</Link>
</Nav>
Expand Down
17 changes: 8 additions & 9 deletions components/Search/JumpTo.styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,42 +28,41 @@ const HelperStyled = styled("div", {
});

const JumpToListStyled = styled("ul", {
position: "absolute",
left: "0px",
display: "block",
display: "flex",
flexDirection: "column",
alignSelf: "flex-end",
margin: "0",
padding: "0",
background: "$white",
width: "100%",
fontSize: "$gr2",
listStyle: "none",
top: "50px",
border: "1px solid $black10",
boxShadow: "3px 3px 8px #0003",
borderRadius: "3px",
boxShadow: "3px 3px 11px #0001",
});

const JumpItem = styled("li", {
position: "relative",
backgroundColor: "$gray6",
transition: "$dcAll",

"& a": {
display: "block",
padding: "$gr3",
borderTopWidth: "1px",
borderTopColor: "$black20",
borderTopStyle: "solid",
cursor: "pointer",
color: "$purple120",
},

"&[aria-selected='true']": {
background: "$purple10",
fontFamily: "$northwesternSansBold",

[`${HelperStyled}`]: {
color: "$purple10",
background: "$purple",
boxShadow: "2px 2px 5px #0001",
background: "$purple120",
fontFamily: "$northwesternSansRegular",

svg: {
width: "$gr3",
Expand Down
44 changes: 10 additions & 34 deletions components/Search/JumpTo.test.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,19 @@
import { render, screen } from "@/test-utils";
import SearchJumpTo from "@/components/Search/JumpTo";
import userEvent from "@testing-library/user-event";

const mockIsSearchActive = jest.fn();
import SearchJumpTo from "@/components/Search/JumpTo";

describe("SearchJumpTo component", () => {
it("renders the component", () => {
render(<SearchJumpTo isSearchActive={() => ({})} />);
const wrapper = screen.getByTestId("search-jump-to-form");
expect(wrapper).toBeInTheDocument();
});

it("conditionally renders the SearchJumpTo component", async () => {
const user = userEvent.setup();
render(
<div data-testid="page">
<span>Outside search form</span>
<SearchJumpTo isSearchActive={mockIsSearchActive} />
</div>,
);
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(<SearchJumpTo searchFocus={true} searchValue={"foo"} top={45} />);

// 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");
}
});
});
77 changes: 20 additions & 57 deletions components/Search/JumpTo.tsx
Original file line number Diff line number Diff line change
@@ -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<SearchProps> = ({ isSearchActive }) => {
const search = useRef<HTMLTextAreaElement>(null);
const SearchJumpTo: React.FC<SearchProps> = ({
searchFocus,
searchValue,
top,
}) => {
const formRef = useRef<HTMLFormElement>(null);
const [searchValue, setSearchValue] = useState<string>("");
const [searchFocus, setSearchFocus] = useState<boolean>(false);
const [isLoaded, setIsLoaded] = useState<boolean>(false);
const [showJumpTo, setShowJumpTo] = useState<boolean>(false);

React.useEffect(() => {
useEffect(() => {
if (searchFocus) setShowJumpTo(Boolean(searchValue));
}, [searchFocus, searchValue]);

useEffect(() => {
const handleMouseDown = (e: MouseEvent) => {
if (
showJumpTo &&
Expand Down Expand Up @@ -66,49 +64,14 @@ const SearchJumpTo: React.FC<SearchProps> = ({ isSearchActive }) => {
};
}, [showJumpTo]);

const handleSearchFocus = (e: FocusEvent) => {
if (e.type === "focus") {
setSearchFocus(true);
setShowJumpTo(Boolean(searchValue));
} else {
setSearchFocus(false);
}
};

const handleSearchChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
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 (
<SearchStyled ref={formRef} data-testid="search-jump-to-form">
{/* temporarily setting to textarea for later refinement */}
<textarea
placeholder="Search by keyword or phrase, ex: Berkeley Music Festival"
onChange={handleSearchChange}
onFocus={handleSearchFocus}
onBlur={handleSearchFocus}
ref={search}
name="search"
role="search"
/>
{isLoaded && <IconSearch />}
{showJumpTo && (
<SearchJumpToList
searchValue={searchValue}
setShowJumpTo={setShowJumpTo}
/>
)}
</SearchStyled>
<SearchJumpToList
searchValue={searchValue}
setShowJumpTo={setShowJumpTo}
top={top}
/>
);
};

Expand Down
25 changes: 21 additions & 4 deletions components/Search/JumpToList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe("SearchJumpToList component", () => {
<SearchJumpToList
searchValue="Dylan"
setShowJumpTo={mockSetShowJumpTo}
top={0}
/>,
);
expect(screen.getByTestId("jump-to-wrapper"));
Expand All @@ -31,7 +32,11 @@ describe("SearchJumpToList component", () => {

it("renders Helper components in each JumpTo item", () => {
render(
<SearchJumpToList searchValue="foo" setShowJumpTo={mockSetShowJumpTo} />,
<SearchJumpToList
searchValue="foo"
setShowJumpTo={mockSetShowJumpTo}
top={0}
/>,
);
const helpers = screen.getAllByTestId("helper");
expect(helpers[0]).toHaveTextContent(/in this collection/i);
Expand All @@ -40,7 +45,11 @@ describe("SearchJumpToList component", () => {

it.only("renders route query params in JumpTo items", async () => {
render(
<SearchJumpToList searchValue="foo" setShowJumpTo={mockSetShowJumpTo} />,
<SearchJumpToList
searchValue="foo"
setShowJumpTo={mockSetShowJumpTo}
top={0}
/>,
);

await act(async () => {
Expand All @@ -63,7 +72,11 @@ describe("SearchJumpToList component", () => {
it("selects items correctly on arrow key presses", async () => {
const user = userEvent.setup();
render(
<SearchJumpToList searchValue="foo" setShowJumpTo={mockSetShowJumpTo} />,
<SearchJumpToList
searchValue="foo"
setShowJumpTo={mockSetShowJumpTo}
top={0}
/>,
);
const listItems = await screen.findAllByRole("option");

Expand All @@ -89,7 +102,11 @@ describe("SearchJumpToList component", () => {
it("handles the Escape key press", async () => {
const user = userEvent.setup();
render(
<SearchJumpToList searchValue="foo" setShowJumpTo={mockSetShowJumpTo} />,
<SearchJumpToList
searchValue="foo"
setShowJumpTo={mockSetShowJumpTo}
top={0}
/>,
);

await user.keyboard("{Escape}");
Expand Down
9 changes: 8 additions & 1 deletion components/Search/JumpToList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -13,11 +14,13 @@ import { useRouter } from "next/router";
interface SearchJumpToListProps {
searchValue: string;
setShowJumpTo: Dispatch<React.SetStateAction<boolean>>;
top: number;
}

const SearchJumpToList: React.FC<SearchJumpToListProps> = ({
searchValue,
setShowJumpTo,
top,
}) => {
const router = useRouter();
const [collectionTitle, setCollectionTitle] = useState<string>("");
Expand Down Expand Up @@ -96,7 +99,11 @@ const SearchJumpToList: React.FC<SearchJumpToListProps> = ({
}, [router.query.id]);

return (
<JumpToListStyled data-testid="jump-to-wrapper" role="listbox">
<JumpToListStyled
data-testid="jump-to-wrapper"
role="listbox"
style={{ top }}
>
{jumpToItems.map((item, index) => (
<JumpItem
key={item.dataTestId}
Expand Down
5 changes: 0 additions & 5 deletions components/Search/Search.styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ const SearchStyled = styled("form", {
display: "flex",
flexShrink: "0",
flexGrow: "1",
marginRight: "$gr4",
transition: "$dcAll",
borderRadius: "3px",
flexWrap: "wrap",
Expand Down Expand Up @@ -42,10 +41,6 @@ const SearchStyled = styled("form", {
flexDirection: "column",
},

"@lg": {
marginRight: "$gr3",
},

"> svg": {
position: "absolute",
display: "flex",
Expand Down
Loading

0 comments on commit 09d4ae0

Please sign in to comment.