Skip to content

Commit d8f3b16

Browse files
feat: close dropdown on item click (#111)
* add closeDropdownOnItemClick util function * use of closeDropdownOnItemClick in every dropdown * created custom Dropdown component * created custom ExportDropdownButton component * add ThemeSelector component in components index * use of ExportDropdownButton * use of custom Dropdown * fix imports to be consistent * removed unused component * removed unused global util function * fix header * Revert "fix imports to be consistent" This reverts commit dd2c0b1. * fix test error * style fix * minor fixes * fix type in Dropdown component --------- Co-authored-by: sa.cux <[email protected]>
1 parent f6d3fc4 commit d8f3b16

File tree

10 files changed

+202
-173
lines changed

10 files changed

+202
-173
lines changed

cypress/e2e/darkmode.spec.cy.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@ describe("DarkMode test", () => {
22
beforeEach(() => {
33
cy.visit("http://localhost:3000");
44
cy.wait(3000);
5-
cy.get('[data-testid="themeSelectorButton"]').click();
5+
cy.get('data-testid="themeSelectorButton"').click();
66
});
77

88
it("should select light Mode", () => {
9-
cy.get('[data-testid="light-mode-option"]').click();
9+
cy.get('data-testid="light-mode-option"').click();
1010
cy.get("html").should("have.data", "theme", "light");
1111
});
1212

1313
it("should select dark mode", () => {
14-
cy.get('[data-testid="dark-mode-option"]').click();
14+
cy.get('data-testid="dark-mode-option"').click();
1515
cy.get("html").should("have.data", "theme", "custom-dark");
1616
});
1717

@@ -27,8 +27,8 @@ describe("DarkMode test", () => {
2727
},
2828
});
2929
cy.wait(3000);
30-
cy.get('[data-testid="themeSelectorButton"]').click();
31-
cy.get('[data-testid="system-mode-option"]').click();
30+
cy.get('data-testid="themeSelectorButton"').click();
31+
cy.get('data-testid="system-mode-option"').click();
3232
cy.get("html").should("have.data", "theme", "custom-dark");
3333
});
3434
});

src/components/Dropdown.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { FC, HTMLProps, ReactElement, ReactNode } from "react";
2+
3+
export type DropdownProps = {
4+
position?:
5+
| "dropdown-top"
6+
| "dropdown-bottom"
7+
| "dropdown-left"
8+
| "dropdown-right";
9+
align?: "dropdown-end";
10+
renderButton: ReactElement;
11+
items: (HTMLProps<HTMLLIElement> & {
12+
"data-testid"?: string;
13+
onClick?: () => void;
14+
renderItem: ReactNode;
15+
})[];
16+
};
17+
18+
export const closeDropdownOnItemClick = (): void => {
19+
const activeElement = document.activeElement as HTMLElement | null;
20+
if (activeElement && activeElement instanceof HTMLElement) {
21+
activeElement.blur();
22+
}
23+
};
24+
25+
export const Dropdown: FC<DropdownProps> = ({
26+
renderButton,
27+
items,
28+
position = "dropdown-bottom",
29+
align,
30+
}) => {
31+
return (
32+
<div className={`dropdown ${position} ${align ?? ""}`}>
33+
<div
34+
tabIndex={0}
35+
role="button"
36+
className="btn border-0 p-0 bg-transparent hover:bg-transparent"
37+
>
38+
{renderButton}
39+
</div>
40+
<ul
41+
tabIndex={0}
42+
className="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52"
43+
>
44+
{items.map(({ onClick, renderItem, ...liProps }, index) => {
45+
return (
46+
<li
47+
{...liProps}
48+
key={index}
49+
onClick={() => {
50+
onClick?.();
51+
closeDropdownOnItemClick();
52+
}}
53+
>
54+
<p>{renderItem}</p>
55+
</li>
56+
);
57+
})}
58+
</ul>
59+
</div>
60+
);
61+
};

src/components/ExportDropdown.tsx

Lines changed: 0 additions & 36 deletions
This file was deleted.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { FC } from "react";
2+
import { Dropdown } from "@/components";
3+
import { exportAsImage } from "@/utils";
4+
5+
type ExportDropdownButtonProps = {
6+
selector: string;
7+
filename?: string;
8+
};
9+
10+
export const ExportDropdownButton: FC<ExportDropdownButtonProps> = ({
11+
selector,
12+
filename,
13+
}) => {
14+
return (
15+
<Dropdown
16+
renderButton={
17+
<button className="btn btn-primary rounded cursor-pointer">
18+
Export as image
19+
</button>
20+
}
21+
items={[
22+
{
23+
renderItem: "Download as PNG",
24+
onClick: () => {
25+
exportAsImage(selector, "download", filename);
26+
},
27+
},
28+
{
29+
renderItem: "Copy to Clipboard",
30+
onClick: () => {
31+
exportAsImage(selector, "clipboard", filename);
32+
},
33+
},
34+
]}
35+
/>
36+
);
37+
};

src/components/FormatStatsRender.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { FC } from "react";
22
import { useMemo } from "react";
3-
import { RepositoryContributionsCard } from "./RepositoryContributionsCard";
4-
import { ExportDropdown } from "./ExportDropdown";
3+
import {
4+
ExportDropdownButton,
5+
RepositoryContributionsCard,
6+
} from "@/components";
57
import {
68
PullRequestContributionsByRepository,
79
RepositoryRenderFormat,
@@ -37,7 +39,7 @@ export const FormatStatsRender: FC<FormatStatsRenderProps> = ({
3739
case "cards":
3840
return (
3941
<>
40-
<ExportDropdown />
42+
<ExportDropdownButton selector=".grid" filename="stats" />
4143
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 pb-4">
4244
{repositories?.map(({ repository, contributions }, i) => (
4345
<RepositoryContributionsCard

src/components/Header.tsx

Lines changed: 45 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,23 @@
11
import { MAIN_LOGIN_PROVIDER } from "@/pages/api/auth/[...nextauth]";
22
import { signIn, signOut, useSession } from "next-auth/react";
3+
import { ThemeSelector, Dropdown } from "@/components";
34
import Image from "next/image";
45
import Link from "next/link";
5-
import { ThemeSelector } from "./ThemeSelector";
6+
import { useRouter } from "next/router";
67

78
export const Header = () => {
89
const { data: session, status } = useSession();
10+
const router = useRouter();
11+
12+
const handleLogout = async () => {
13+
await signOut();
14+
};
915

1016
return (
1117
<>
1218
<header>
1319
<div className="navbar bg-base-100">
1420
<div className="navbar-start">
15-
<div className="dropdown">
16-
<label
17-
htmlFor="menu"
18-
tabIndex={0}
19-
className="btn btn-ghost lg:hidden"
20-
>
21-
<svg
22-
xmlns="http://www.w3.org/2000/svg"
23-
className="h-5 w-5"
24-
fill="none"
25-
viewBox="0 0 24 24"
26-
stroke="currentColor"
27-
>
28-
<path
29-
strokeLinecap="round"
30-
strokeLinejoin="round"
31-
strokeWidth="2"
32-
d="M4 6h16M4 12h8m-8 6h16"
33-
/>
34-
</svg>
35-
</label>
36-
<ul
37-
tabIndex={0}
38-
className="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52"
39-
>
40-
<li>
41-
<Link href="/">Home</Link>
42-
</li>
43-
{status === "authenticated" && (
44-
<li>
45-
<Link href={`/stats/${session.user.login}`}>Stats</Link>
46-
</li>
47-
)}
48-
</ul>
49-
</div>
5021
<Link href="/" className="btn btn-ghost normal-case text-xl">
5122
GitHub Stats
5223
</Link>
@@ -66,35 +37,44 @@ export const Header = () => {
6637
<div className="navbar-end">
6738
<ThemeSelector />
6839
{status === "authenticated" ? (
69-
<div className="dropdown dropdown-end">
70-
<label tabIndex={0} className="btn btn-ghost btn-circle avatar">
71-
<div className="w-10 rounded-full">
72-
<Image
73-
src={session.user.image ?? ""}
74-
alt={session.user.name ?? ""}
75-
width={40}
76-
height={40}
77-
/>
78-
</div>
79-
</label>
80-
<ul
81-
tabIndex={0}
82-
className="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52"
83-
>
84-
<li>
85-
<a>
86-
Settings
87-
<span className="badge">Soon</span>
88-
</a>
89-
</li>
90-
<li>
91-
<Link href={`/profile`}>Profile</Link>
92-
</li>
93-
<li>
94-
<a onClick={() => signOut()}>Logout</a>
95-
</li>
96-
</ul>
97-
</div>
40+
<Dropdown
41+
align="dropdown-end"
42+
renderButton={
43+
<label className="btn btn-ghost btn-circle avatar">
44+
<div className="w-10 rounded-full">
45+
<Image
46+
src={session.user.image ?? ""}
47+
alt={session.user.name ?? ""}
48+
width={40}
49+
height={40}
50+
priority
51+
/>
52+
</div>
53+
</label>
54+
}
55+
items={[
56+
{
57+
renderItem: (
58+
<span>
59+
Settings
60+
<span className="badge">Soon</span>
61+
</span>
62+
),
63+
},
64+
{
65+
onClick: () => {
66+
router.push("/profile");
67+
},
68+
renderItem: "Profile",
69+
},
70+
{
71+
onClick: () => {
72+
handleLogout();
73+
},
74+
renderItem: "Logout",
75+
},
76+
]}
77+
/>
9878
) : (
9979
<button
10080
onClick={() => signIn(MAIN_LOGIN_PROVIDER)}

src/components/ThemeSelector.test.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { fireEvent, render, screen } from "@testing-library/react";
2-
import { describe, test, expect } from "vitest";
2+
import { describe, test, expect, vi } from "vitest";
33
import { ThemeSelector } from "./ThemeSelector";
44

5+
vi.mock("next/font/google", () => ({
6+
Inter: () => <div>GoogleFont</div>,
7+
}));
8+
59
describe("ThemeSelector", () => {
610
test("should change light Icon if click Dark mode", () => {
711
render(<ThemeSelector />);

0 commit comments

Comments
 (0)