Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion __tests__/components/header/HeaderSearchModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ function setup(options: SetupOptions = {}) {
};
});
}
render(<HeaderSearchModal onClose={onClose} />);
render(<HeaderSearchModal onClose={onClose} wave={null} />);
return { onClose, push, profilesRefetch, nftsRefetch, wavesRefetch };
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,82 +1,83 @@
import React from 'react';
import { render, screen, fireEvent, act } from '@testing-library/react';
import HeaderSearchButton from '@/components/header/header-search/HeaderSearchButton';
import useDeviceInfo from '@/hooks/useDeviceInfo';
import React from "react";
import { render, screen, fireEvent, act } from "@testing-library/react";
import HeaderSearchButton from "@/components/header/header-search/HeaderSearchButton";
import useDeviceInfo from "@/hooks/useDeviceInfo";

let keyFilter: (e: KeyboardEvent) => boolean;
let keyCb: () => void;

jest.mock('react-use', () => ({
jest.mock("react-use", () => ({
useKey: (filter: (e: KeyboardEvent) => boolean, cb: () => void) => {
keyFilter = filter;
keyCb = cb;
},
}));

jest.mock('@/components/utils/animation/CommonAnimationWrapper', () => ({
jest.mock("@/components/utils/animation/CommonAnimationWrapper", () => ({
__esModule: true,
default: ({ children }: any) => <div data-testid="wrapper">{children}</div>,
}));

jest.mock('@/components/utils/animation/CommonAnimationOpacity', () => ({
jest.mock("@/components/utils/animation/CommonAnimationOpacity", () => ({
__esModule: true,
default: ({ children, ...props }: any) => <div {...props}>{children}</div>,
}));

jest.mock('@/components/header/header-search/HeaderSearchModal', () => ({
jest.mock("@/components/header/header-search/HeaderSearchModal", () => ({
__esModule: true,
default: (props: any) => (
<div data-testid="modal" onClick={() => props.onClose()}></div>
),
}));

jest.mock('@heroicons/react/24/outline', () => ({
jest.mock("@heroicons/react/24/outline", () => ({
MagnifyingGlassIcon: (props: any) => <svg data-testid="icon" {...props} />,
}));

jest.mock('@/hooks/useDeviceInfo');
jest.mock("@/hooks/useDeviceInfo");

const useDeviceInfoMock = useDeviceInfo as jest.MockedFunction<typeof useDeviceInfo>;
const useDeviceInfoMock = useDeviceInfo as jest.MockedFunction<
typeof useDeviceInfo
>;

describe('HeaderSearchButton', () => {
describe("HeaderSearchButton", () => {
beforeEach(() => {
jest.clearAllMocks();
keyFilter = () => false;
keyCb = () => {};
});

it('opens modal when button is clicked and closes via onClose', () => {
it("opens modal when button is clicked and closes via onClose", () => {
useDeviceInfoMock.mockReturnValue({ isApp: false } as any);
render(<HeaderSearchButton />);
expect(screen.queryByTestId('modal')).toBeNull();
render(<HeaderSearchButton wave={null} />);
expect(screen.queryByTestId("modal")).toBeNull();

fireEvent.click(screen.getByRole('button', { name: /search/i }));
expect(screen.getByTestId('modal')).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: /search/i }));
expect(screen.getByTestId("modal")).toBeInTheDocument();

fireEvent.click(screen.getByTestId('modal'));
expect(screen.queryByTestId('modal')).toBeNull();
fireEvent.click(screen.getByTestId("modal"));
expect(screen.queryByTestId("modal")).toBeNull();
});

it('opens modal when meta+k is pressed', () => {
it("opens modal when meta+k is pressed", () => {
useDeviceInfoMock.mockReturnValue({ isApp: false } as any);
render(<HeaderSearchButton />);
expect(screen.queryByTestId('modal')).toBeNull();
render(<HeaderSearchButton wave={null} />);
expect(screen.queryByTestId("modal")).toBeNull();

const event = new KeyboardEvent('keydown', { key: 'k', metaKey: true });
const event = new KeyboardEvent("keydown", { key: "k", metaKey: true });
if (keyFilter(event)) {
act(() => {
keyCb();
});
}

expect(screen.getByTestId('modal')).toBeInTheDocument();
expect(screen.getByTestId("modal")).toBeInTheDocument();
});

it('uses larger icon when app mode is true', () => {
it("uses larger icon when app mode is true", () => {
useDeviceInfoMock.mockReturnValue({ isApp: true } as any);
render(<HeaderSearchButton />);
const icon = screen.getByTestId('icon');
expect(icon).toHaveClass('tw-h-6 tw-w-6');
render(<HeaderSearchButton wave={null} />);
const icon = screen.getByTestId("icon");
expect(icon).toHaveClass("tw-h-6 tw-w-6");
});
});

Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ const PLACEHOLDER_TEXT = "Search 6529.io";
describe("HeaderSearchModal focus management", () => {
it("keeps focus trapped within the modal while it is open", async () => {
const user = userEvent.setup();
render(<HeaderSearchButton />);
render(<HeaderSearchButton wave={null} />);

const trigger = screen.getByRole("button", { name: /search/i });
await user.click(trigger);
Expand Down Expand Up @@ -183,7 +183,7 @@ describe("HeaderSearchModal focus management", () => {

it("returns focus to the trigger button when the modal closes", async () => {
const user = userEvent.setup();
render(<HeaderSearchButton />);
render(<HeaderSearchButton wave={null} />);

const trigger = screen.getByRole("button", { name: /search/i });
await user.click(trigger);
Expand Down
50 changes: 29 additions & 21 deletions components/header/AppHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"use client";

import { capitalizeEveryWord, formatAddress } from "@/helpers/Helpers";
import { resolveIpfsUrlSync } from "@/components/ipfs/IPFSContext";
import Image from "next/image";
import { useNavigationHistoryContext } from "@/contexts/NavigationHistoryContext";
import { useMyStreamOptional } from "@/contexts/wave/MyStreamContext";
import { capitalizeEveryWord, formatAddress } from "@/helpers/Helpers";
import { useIdentity } from "@/hooks/useIdentity";
import { useWaveById } from "@/hooks/useWaveById";
import { Bars3Icon } from "@heroicons/react/24/outline";
import Image from "next/image";
import { useParams, usePathname } from "next/navigation";
import { useState } from "react";
import { useAuth } from "../auth/Auth";
Expand All @@ -15,16 +17,12 @@ import Spinner from "../utils/Spinner";
import AppSidebar from "./AppSidebar";
import HeaderSearchButton from "./header-search/HeaderSearchButton";
import HeaderActionButtons from "./HeaderActionButtons";
import { useMyStreamOptional } from "@/contexts/wave/MyStreamContext";
import { useNavigationHistoryContext } from "@/contexts/NavigationHistoryContext";



const COLLECTION_TITLES: Record<string, string> = {
"the-memes": "The Memes",
"6529-gradient": "6529 Gradient",
"meme-lab": "Meme Lab",
"nextgen": "NextGen",
nextgen: "NextGen",
};

const sliceString = (str: string, length: number): string => {
Expand All @@ -33,7 +31,10 @@ const sliceString = (str: string, length: number): string => {
return `${str.slice(0, half)}...${str.slice(-half)}`;
};

const getCollectionTitle = (basePath: string, pageTitle: string): string | null => {
const getCollectionTitle = (
basePath: string,
pageTitle: string
): string | null => {
const prefix = COLLECTION_TITLES[basePath];
if (prefix && !Number.isNaN(Number(pageTitle))) {
return `${prefix} #${pageTitle}`;
Expand Down Expand Up @@ -69,7 +70,7 @@ export default function AppHeader() {
return profile?.pfp ?? null;
})();

const pathSegments = (pathname ?? "").split("/").filter(Boolean);
const pathSegments = pathname.split("/").filter(Boolean);
const basePath = pathSegments.length ? pathSegments[0] : "";
const pageTitle = pathSegments.length
? pathSegments[pathSegments.length - 1]
Expand All @@ -87,7 +88,7 @@ export default function AppHeader() {
pathname === "/waves/create" || pathname === "/messages/create";
const isInsideWave = !!waveId;

const isProfilePage = typeof params?.["user"] === "string";
const isProfilePage = typeof params["user"] === "string";

const showBackButton =
isInsideWave || isCreateRoute || (isProfilePage && canGoBack);
Expand All @@ -99,7 +100,7 @@ export default function AppHeader() {
if (isMessagesRoute && !waveId) return "Messages";
if (waveId) {
if (isLoading || isFetching || wave?.id !== waveId) return <Spinner />;
return wave?.name ?? "Wave";
return wave.name;
}
Comment thread
simo6529 marked this conversation as resolved.

const collectionTitle = getCollectionTitle(basePath!, pageTitle!);
Expand All @@ -112,42 +113,49 @@ export default function AppHeader() {
})();

return (
<div className="tw-w-full tw-bg-black tw-text-iron-50 tw-pt-[env(safe-area-inset-top,0px)]">
<div className="tw-flex tw-items-center tw-justify-between tw-px-4 tw-h-16">
<div className="tw-w-full tw-bg-black tw-pt-[env(safe-area-inset-top,0px)] tw-text-iron-50">
<div className="tw-flex tw-h-16 tw-items-center tw-justify-between tw-px-4">
{showBackButton && <BackButton />}
{!showBackButton && (
<button
type="button"
aria-label="Open menu"
onClick={() => setMenuOpen(true)}
className={`tw-flex tw-items-center tw-justify-center tw-overflow-hidden tw-h-10 tw-w-10 tw-rounded-full tw-border tw-border-solid ${
className={`tw-flex tw-h-10 tw-w-10 tw-items-center tw-justify-center tw-overflow-hidden tw-rounded-full tw-border tw-border-solid ${
address
? "tw-bg-iron-900 tw-border-white/20"
: "tw-bg-transparent tw-border-transparent"
}`}>
? "tw-border-white/20 tw-bg-iron-900"
: "tw-border-transparent tw-bg-transparent"
}`}
>
{address ? (
pfp ? (
<Image
src={resolveIpfsUrlSync(pfp)}
alt="pfp"
width={40}
height={40}
className="tw-h-10 tw-w-10 tw-rounded-full tw-object-contain tw-flex-shrink-0"
className="tw-h-10 tw-w-10 tw-flex-shrink-0 tw-rounded-full tw-object-contain"
/>
) : (
<div className="tw-h-10 tw-w-10 tw-rounded-full tw-bg-iron-900 tw-ring-1 tw-ring-inset tw-ring-white/10 tw-flex-shrink-0" />
<div className="tw-h-10 tw-w-10 tw-flex-shrink-0 tw-rounded-full tw-bg-iron-900 tw-ring-1 tw-ring-inset tw-ring-white/10" />
)
) : (
<Bars3Icon className="tw-size-6 tw-flex-shrink-0" />
)}
</button>
)}
<div className="tw-flex-1 tw-text-center tw-font-semibold tw-text-sm">
<div className="tw-flex-1 tw-text-center tw-text-sm tw-font-semibold">
{finalTitle}
</div>
<div className="tw-flex tw-items-center tw-gap-x-2">
<HeaderActionButtons />
<HeaderSearchButton />
<HeaderSearchButton
wave={
isInsideWave && (isWavesRoute || isMessagesRoute)
? (wave ?? null)
: null
}
/>
</div>
</div>
<AppSidebar open={menuOpen} onClose={() => setMenuOpen(false)} />
Expand Down
25 changes: 15 additions & 10 deletions components/header/header-search/HeaderSearchButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ import CommonAnimationOpacity from "@/components/utils/animation/CommonAnimation
import HeaderSearchModal from "./HeaderSearchModal";
import { useKey } from "react-use";
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import type { ApiWave } from "@/generated/models/ApiWave";

export default function HeaderSearchButton() {
interface HeaderSearchButtonProps {
readonly wave: ApiWave | null;
}

export default function HeaderSearchButton({ wave }: HeaderSearchButtonProps) {
const [isOpen, setIsOpen] = useState<boolean>(false);
const buttonRef = useRef<HTMLButtonElement>(null);
const wasOpenRef = useRef(false);
Expand All @@ -36,11 +41,9 @@ export default function HeaderSearchButton() {
const handleOpen = () => setIsOpen(true);
const handleClose = () => setIsOpen(false);

useKey(
(event) => event.metaKey && event.key === "k",
handleOpen,
{ event: "keydown" }
);
useKey((event) => event.metaKey && event.key === "k", handleOpen, {
event: "keydown",
});

const iconSizeClasses = isApp ? "tw-h-6 tw-w-6" : "tw-h-5 tw-w-5";

Expand All @@ -53,11 +56,12 @@ export default function HeaderSearchButton() {
title="Search"
onClick={handleOpen}
className={clsx(
"tw-flex tw-items-center tw-justify-center tw-rounded-lg tw-h-10 tw-w-10 tw-border-0 tw-text-iron-300 hover:tw-text-iron-50 tw-shadow-sm focus-visible:tw-outline focus-visible:tw-outline-2 focus-visible:tw-outline-primary-400 tw-transition tw-duration-300 tw-ease-out",
"tw-flex tw-h-10 tw-w-10 tw-items-center tw-justify-center tw-rounded-lg tw-border-0 tw-text-iron-300 tw-shadow-sm tw-transition tw-duration-300 tw-ease-out hover:tw-text-iron-50 focus-visible:tw-outline focus-visible:tw-outline-2 focus-visible:tw-outline-primary-400",
isApp
? "tw-bg-black"
: "tw-bg-iron-800 tw-ring-1 tw-ring-inset tw-ring-iron-700 hover:tw-bg-iron-700"
)}>
)}
>
<MagnifyingGlassIcon
className={clsx("tw-flex-shrink-0", iconSizeClasses)}
/>
Expand All @@ -68,8 +72,9 @@ export default function HeaderSearchButton() {
key="modal"
elementClasses="tw-absolute tw-z-10"
elementRole="dialog"
onClicked={(e) => e.stopPropagation()}>
<HeaderSearchModal onClose={handleClose} />
onClicked={(e) => e.stopPropagation()}
>
<HeaderSearchModal onClose={handleClose} wave={wave} />
</CommonAnimationOpacity>
)}
</CommonAnimationWrapper>
Expand Down
Loading