Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: close dropdown on item click #111

Merged
Merged
Show file tree
Hide file tree
Changes from 15 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
62 changes: 62 additions & 0 deletions src/components/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { FC, ReactElement } from "react";

export type DropdownProps = React.PropsWithChildren<{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you really need React.PropsWithChildren?

position?:
| "dropdown-top"
| "dropdown-bottom"
| "dropdown-left"
| "dropdown-right";
align?: "dropdown-end";
renderButton: ReactElement;
items: (React.HTMLProps<HTMLLIElement> & {
["data-testid"]?: string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you need the square brackets here (and the other places where data-testid is used in ThemeSelector.jsx)

onClick?: () => void;
renderItem: string | ReactElement;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With ReactElement you can remove string

Suggested change
renderItem: string | ReactElement;
renderItem: ReactElement;

})[];
}>;

export const closeDropdownOnItemClick = (): void => {
const activeElement = document.activeElement as HTMLElement | null;
if (activeElement && activeElement instanceof HTMLElement) {
activeElement.blur();
}
};

export const Dropdown: FC<DropdownProps> = ({
renderButton,
items,
position = "dropdown-bottom",
align,
}) => {
return (
<div className={`dropdown ${position} ${align ?? ""}`}>
<div
tabIndex={0}
role="button"
className="btn border-0 p-0 bg-transparent hover:bg-transparent"
>
{renderButton}
</div>
<ul
tabIndex={0}
className="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52"
>
{items.map((item, index) => {
const { onClick, renderItem, ...liProps } = item;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you don't need item, you can already destructure it in the loop

Suggested change
{items.map((item, index) => {
const { onClick, renderItem, ...liProps } = item;
{items.map(({ onClick, renderItem, ...liProps }, index) => {

return (
<li
{...liProps}
key={index}
onClick={() => {
onClick?.();
closeDropdownOnItemClick();
}}
>
<p>{renderItem}</p>
</li>
);
})}
</ul>
</div>
);
};
36 changes: 0 additions & 36 deletions src/components/ExportDropdown.tsx

This file was deleted.

37 changes: 37 additions & 0 deletions src/components/ExportDropdownButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { FC } from "react";
import { Dropdown } from "@/components";
import { exportAsImage } from "@/utils";

type ExportDropdownButtonProps = {
selector: string;
filename?: string;
};

export const ExportDropdownButton: FC<ExportDropdownButtonProps> = ({
selector,
filename,
}) => {
return (
<Dropdown
renderButton={
<button className="btn btn-primary rounded cursor-pointer">
Export as image
</button>
}
items={[
{
renderItem: "Download as PNG",
onClick: () => {
exportAsImage(selector, "download", filename);
},
},
{
renderItem: "Copy to Clipboard",
onClick: () => {
exportAsImage(selector, "clipboard", filename);
},
},
]}
/>
);
};
8 changes: 5 additions & 3 deletions src/components/FormatStatsRender.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { FC } from "react";
import { useMemo } from "react";
import { RepositoryContributionsCard } from "./RepositoryContributionsCard";
import { ExportDropdown } from "./ExportDropdown";
import {
ExportDropdownButton,
RepositoryContributionsCard,
} from "@/components";
import {
PullRequestContributionsByRepository,
RepositoryRenderFormat,
Expand Down Expand Up @@ -37,7 +39,7 @@ export const FormatStatsRender: FC<FormatStatsRenderProps> = ({
case "cards":
return (
<>
<ExportDropdown />
<ExportDropdownButton selector=".grid" filename="stats" />
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 pb-4">
{repositories?.map(({ repository, contributions }, i) => (
<RepositoryContributionsCard
Expand Down
110 changes: 45 additions & 65 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,23 @@
import { MAIN_LOGIN_PROVIDER } from "@/pages/api/auth/[...nextauth]";
import { signIn, signOut, useSession } from "next-auth/react";
import { ThemeSelector, Dropdown } from "@/components";
import Image from "next/image";
import Link from "next/link";
import { ThemeSelector } from "./ThemeSelector";
import { useRouter } from "next/router";

export const Header = () => {
const { data: session, status } = useSession();
const router = useRouter();

const handleLogout = async () => {
await signOut();
};

return (
<>
<header>
<div className="navbar bg-base-100">
<div className="navbar-start">
<div className="dropdown">
<label
htmlFor="menu"
tabIndex={0}
className="btn btn-ghost lg:hidden"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4 6h16M4 12h8m-8 6h16"
/>
</svg>
</label>
<ul
tabIndex={0}
className="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52"
>
<li>
<Link href="/">Home</Link>
</li>
{status === "authenticated" && (
<li>
<Link href={`/stats/${session.user.login}`}>Stats</Link>
</li>
)}
</ul>
</div>
<Link href="/" className="btn btn-ghost normal-case text-xl">
GitHub Stats
</Link>
Expand All @@ -66,35 +37,44 @@ export const Header = () => {
<div className="navbar-end">
<ThemeSelector />
{status === "authenticated" ? (
<div className="dropdown dropdown-end">
<label tabIndex={0} className="btn btn-ghost btn-circle avatar">
<div className="w-10 rounded-full">
<Image
src={session.user.image ?? ""}
alt={session.user.name ?? ""}
width={40}
height={40}
/>
</div>
</label>
<ul
tabIndex={0}
className="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52"
>
<li>
<a>
Settings
<span className="badge">Soon</span>
</a>
</li>
<li>
<Link href={`/profile`}>Profile</Link>
</li>
<li>
<a onClick={() => signOut()}>Logout</a>
</li>
</ul>
</div>
<Dropdown
align="dropdown-end"
renderButton={
<label className="btn btn-ghost btn-circle avatar">
<div className="w-10 rounded-full">
<Image
src={session.user.image ?? ""}
alt={session.user.name ?? ""}
width={40}
height={40}
priority
/>
</div>
</label>
}
items={[
{
renderItem: (
<span>
Settings
<span className="badge">Soon</span>
</span>
),
},
{
onClick: () => {
router.push("/profile");
},
renderItem: "Profile",
},
{
onClick: () => {
handleLogout();
},
renderItem: "Logout",
},
]}
/>
) : (
<button
onClick={() => signIn(MAIN_LOGIN_PROVIDER)}
Expand Down
6 changes: 5 additions & 1 deletion src/components/ThemeSelector.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, test, expect } from "vitest";
import { describe, test, expect, vi } from "vitest";
import { ThemeSelector } from "./ThemeSelector";

vi.mock("next/font/google", () => ({
Inter: () => <div>GoogleFont</div>,
}));

describe("ThemeSelector", () => {
test("should change light Icon if click Dark mode", () => {
render(<ThemeSelector />);
Expand Down
64 changes: 35 additions & 29 deletions src/components/ThemeSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useState } from "react";
import { Dropdown } from "@/components";

type ThemeOptions = "custom-dark" | "light" | "system";

Expand All @@ -14,10 +15,8 @@ export function ThemeSelector() {
}
}, []);

function onClick($event: React.MouseEvent<HTMLLIElement>) {
$event.stopPropagation();
const li = $event.currentTarget as HTMLLIElement;
setDocumentElement(li.id as ThemeOptions);
function onClick(theme: ThemeOptions) {
setDocumentElement(theme);
}

const setDocumentElement = (theme: ThemeOptions) => {
Expand All @@ -28,38 +27,45 @@ export function ThemeSelector() {
const buttonIcon = getButtonIconByOption(selectedTheme);

return (
<>
<div className="dropdown dropdown-bottom dropdown-end">
<Dropdown
align="dropdown-end"
position="dropdown-bottom"
renderButton={
<label
htmlFor="themeToggle"
tabIndex={0}
className="btn btn-circle btn-ghost m-1"
data-testid="themeSelectorButton"
>
{buttonIcon}
</label>
<ul
tabIndex={0}
className="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52 mt-3"
>
<li id="light" onClick={onClick}>
<a data-testid="light-mode-option">
<LightMode /> Light Mode
</a>
</li>
<li id="custom-dark" onClick={onClick}>
<a data-testid="dark-mode-option">
<DarkMode /> Dark Mode
</a>
</li>
<li id="system" onClick={onClick}>
<a data-testid="system-mode-option">
<SystemPreference /> System Preference
</a>
</li>
</ul>
</div>
</>
}
items={[
{
id: "light",
["data-testid"]: "light-mode-option",
renderItem: "Light Mode",
onClick: () => {
onClick("light");
},
},
{
id: "custom-dark",
["data-testid"]: "dark-mode-option",
renderItem: "Dark Mode",
onClick: () => {
onClick("custom-dark");
},
},
{
id: "system",
["data-testid"]: "system-mode-option",
renderItem: "System Preference",
onClick: () => {
onClick("system");
},
},
]}
/>
);
}

Expand Down
Loading
Loading