Skip to content

Commit

Permalink
feat: close dropdown on item click (#111)
Browse files Browse the repository at this point in the history
* 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 <sabrina.cuccureddu@green-share.it>
  • Loading branch information
theflucs and fuoridallarete authored Feb 28, 2024
1 parent f6d3fc4 commit d8f3b16
Show file tree
Hide file tree
Showing 10 changed files with 202 additions and 173 deletions.
10 changes: 5 additions & 5 deletions cypress/e2e/darkmode.spec.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ describe("DarkMode test", () => {
beforeEach(() => {
cy.visit("http://localhost:3000");
cy.wait(3000);
cy.get('[data-testid="themeSelectorButton"]').click();
cy.get('data-testid="themeSelectorButton"').click();
});

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

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

Expand All @@ -27,8 +27,8 @@ describe("DarkMode test", () => {
},
});
cy.wait(3000);
cy.get('[data-testid="themeSelectorButton"]').click();
cy.get('[data-testid="system-mode-option"]').click();
cy.get('data-testid="themeSelectorButton"').click();
cy.get('data-testid="system-mode-option"').click();
cy.get("html").should("have.data", "theme", "custom-dark");
});
});
61 changes: 61 additions & 0 deletions src/components/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { FC, HTMLProps, ReactElement, ReactNode } from "react";

export type DropdownProps = {
position?:
| "dropdown-top"
| "dropdown-bottom"
| "dropdown-left"
| "dropdown-right";
align?: "dropdown-end";
renderButton: ReactElement;
items: (HTMLProps<HTMLLIElement> & {
"data-testid"?: string;
onClick?: () => void;
renderItem: ReactNode;
})[];
};

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(({ 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
Loading

0 comments on commit d8f3b16

Please sign in to comment.