2
0

first commit

This commit is contained in:
2024-08-09 00:39:27 +02:00
commit 79688abe2e
5698 changed files with 497838 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
/* TODO: Implement NavigationItem */
export {};

View File

@@ -0,0 +1,7 @@
export { default as HorizontalTabItem } from "./tabs/HorizontalTabItem";
export type { HorizontalTabItemProps } from "./tabs/HorizontalTabItem";
export { default as HorizontalTabs } from "./tabs/HorizontalTabs";
export type { NavTabProps } from "./tabs/HorizontalTabs";
export { default as VerticalTabItem } from "./tabs/VerticalTabItem";
export type { VerticalTabItemProps } from "./tabs/VerticalTabItem";
export { default as VerticalTabs } from "./tabs/VerticalTabs";

View File

@@ -0,0 +1,71 @@
import Link from "next/link";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useUrlMatchesCurrentUrl } from "@calcom/lib/hooks/useUrlMatchesCurrentUrl";
import { Icon, type IconName } from "../../..";
import { Avatar } from "../../avatar";
import { SkeletonText } from "../../skeleton";
export type HorizontalTabItemProps = {
name: string;
disabled?: boolean;
className?: string;
target?: string;
href: string;
linkShallow?: boolean;
linkScroll?: boolean;
icon?: IconName;
avatar?: string;
};
const HorizontalTabItem = function ({
name,
href,
linkShallow,
linkScroll,
avatar,
...props
}: HorizontalTabItemProps) {
const { t, isLocaleReady } = useLocale();
const isCurrent = useUrlMatchesCurrentUrl(href);
return (
<Link
key={name}
href={href}
shallow={linkShallow}
scroll={linkScroll}
className={classNames(
isCurrent ? "bg-emphasis text-emphasis" : "hover:bg-subtle hover:text-emphasis text-default",
"inline-flex items-center justify-center whitespace-nowrap rounded-[6px] p-2 text-sm font-medium leading-4 transition md:mb-0",
props.disabled && "pointer-events-none !opacity-30",
props.className
)}
target={props.target ? props.target : undefined}
data-testid={`horizontal-tab-${name}`}
aria-current={isCurrent ? "page" : undefined}>
{props.icon && (
<Icon
name={props.icon}
className={classNames(
isCurrent ? "text-emphasis" : "group-hover:text-subtle text-muted",
"-ml-0.5 hidden h-4 w-4 ltr:mr-2 rtl:ml-2 sm:inline-block"
)}
aria-hidden="true"
/>
)}
{isLocaleReady ? (
<div className="flex items-center gap-x-2">
{avatar && <Avatar size="sm" imageSrc={avatar} alt="avatar" />} {t(name)}
</div>
) : (
<SkeletonText className="h-4 w-24" />
)}
</Link>
);
};
export default HorizontalTabItem;

View File

@@ -0,0 +1,33 @@
import type { HorizontalTabItemProps } from "./HorizontalTabItem";
import HorizontalTabItem from "./HorizontalTabItem";
export interface NavTabProps {
tabs: HorizontalTabItemProps[];
linkShallow?: boolean;
linkScroll?: boolean;
actions?: JSX.Element;
}
const HorizontalTabs = function ({ tabs, linkShallow, linkScroll, actions, ...props }: NavTabProps) {
return (
<div className="mb-4 h-9 max-w-full lg:mb-5">
<nav
className="no-scrollbar flex max-h-9 space-x-1 overflow-x-scroll rounded-md"
aria-label="Tabs"
{...props}>
{tabs.map((tab, idx) => (
<HorizontalTabItem
className="px-4 py-2.5"
{...tab}
key={idx}
linkShallow={linkShallow}
linkScroll={linkScroll}
/>
))}
</nav>
{actions && actions}
</div>
);
};
export default HorizontalTabs;

View File

@@ -0,0 +1,111 @@
import Link from "next/link";
import { Fragment } from "react";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useUrlMatchesCurrentUrl } from "@calcom/lib/hooks/useUrlMatchesCurrentUrl";
import { Icon, type IconName } from "../../..";
import { Skeleton } from "../../skeleton";
export type VerticalTabItemProps = {
name: string;
info?: string;
icon?: IconName;
disabled?: boolean;
children?: VerticalTabItemProps[];
textClassNames?: string;
className?: string;
isChild?: boolean;
hidden?: boolean;
disableChevron?: boolean;
href: string;
isExternalLink?: boolean;
linkShallow?: boolean;
linkScroll?: boolean;
avatar?: string;
iconClassName?: string;
};
const VerticalTabItem = ({
name,
href,
info,
isChild,
disableChevron,
linkShallow,
linkScroll,
...props
}: VerticalTabItemProps) => {
const { t } = useLocale();
const isCurrent = useUrlMatchesCurrentUrl(href);
return (
<Fragment key={name}>
{!props.hidden && (
<>
<Link
key={name}
href={href}
shallow={linkShallow}
scroll={linkScroll}
target={props.isExternalLink ? "_blank" : "_self"}
className={classNames(
props.textClassNames || "text-default text-sm font-medium leading-none",
"min-h-7 hover:bg-subtle [&[aria-current='page']]:bg-emphasis [&[aria-current='page']]:text-emphasis group-hover:text-default group flex w-64 flex-row items-center rounded-md px-3 py-2 transition",
props.disabled && "pointer-events-none !opacity-30",
(isChild || !props.icon) && "ml-7 w-auto ltr:mr-5 rtl:ml-5",
!info ? "h-6" : "h-auto",
props.className
)}
data-testid={`vertical-tab-${name}`}
aria-current={isCurrent ? "page" : undefined}>
{props.icon && (
<Icon
name={props.icon}
className={classNames(
"mr-2 h-[16px] w-[16px] stroke-[2px] ltr:mr-2 rtl:ml-2 md:mt-0",
props.iconClassName
)}
data-testid="icon-component"
/>
)}
<div className="h-fit">
<span className="flex items-center space-x-2 rtl:space-x-reverse">
<Skeleton title={t(name)} as="p" className="max-w-36 min-h-4 truncate">
{t(name)}
</Skeleton>
{props.isExternalLink ? <Icon name="external-link" data-testid="external-link" /> : null}
</span>
{info && (
<Skeleton
data-testid="apps-info"
as="p"
title={t(info)}
className="max-w-44 mt-1 truncate text-xs font-normal">
{t(info)}
</Skeleton>
)}
</div>
{!disableChevron && isCurrent && (
<div className="ml-auto self-center">
<Icon
name="chevron-right"
width={20}
height={20}
className="text-default h-auto w-[20px] stroke-[1.5px]"
data-testid="chevron-right"
/>
</div>
)}
</Link>
{props.children?.map((child) => (
<VerticalTabItem key={child.name} {...child} isChild />
))}
</>
)}
</Fragment>
);
};
export default VerticalTabItem;

View File

@@ -0,0 +1,54 @@
import { classNames } from "@calcom/lib";
import type { VerticalTabItemProps } from "./VerticalTabItem";
import VerticalTabItem from "./VerticalTabItem";
export { VerticalTabItem };
export interface NavTabProps {
tabs: VerticalTabItemProps[];
children?: React.ReactNode;
className?: string;
sticky?: boolean;
linkShallow?: boolean;
linkScroll?: boolean;
itemClassname?: string;
iconClassName?: string;
}
const NavTabs = function ({
tabs,
className = "",
sticky,
linkShallow,
linkScroll,
itemClassname,
iconClassName,
...props
}: NavTabProps) {
return (
<nav
className={classNames(
`no-scrollbar flex flex-col space-y-0.5 overflow-scroll ${className}`,
sticky && "sticky top-0 -mt-7"
)}
aria-label="Tabs"
{...props}>
{/* padding top for sticky */}
{sticky && <div className="pt-6" />}
{props.children}
{tabs.map((tab, idx) => (
<VerticalTabItem
{...tab}
key={idx}
linkShallow={linkShallow}
linkScroll={linkScroll}
className={itemClassname}
iconClassName={iconClassName}
/>
))}
</nav>
);
};
export default NavTabs;

View File

@@ -0,0 +1,134 @@
import { Canvas, Meta, Story } from "@storybook/addon-docs/blocks";
import {
Title,
VariantRow,
VariantsTable,
CustomArgsTable,
Examples,
Example,
} from "@calcom/storybook/components";
import { Icon } from "@calcom/ui";
import HorizontalTabs from "../HorizontalTabs";
<Meta title="UI/Navigation/HorizontalTabs" component={HorizontalTabs} />
<Title title="Horizontal Tabs" suffix="Brief" subtitle="Version 1.0 — Last Update: 17 Aug 2023" />
## Definition
The HorizontalTabs component is a user interface element used for displaying a horizontal set of tabs, often employed for navigation or organization purposes within a web application.
## Structure
The HorizontalTabs component is designed to work alongside the HorizontalTabItem component, which represents individual tabs within the tab bar.
export const tabs = [
{
name: "Tab 1",
href: "?path=/story/ui-navigation-horizontaltabs--horizontal-tabs/tab1",
disabled: false,
linkShallow: true,
linkScroll: true,
icon: Plus,
},
{
name: "Tab 2",
href: "?path=/story/ui-navigation-horizontaltabs--horizontal-tabs/tab2",
disabled: false,
linkShallow: true,
linkScroll: true,
avatar: "Avatar",
},
{
name: "Tab 3",
href: "?path=/story/ui-navigation-horizontaltabs--horizontal-tabs/tab3",
disabled: true,
linkShallow: true,
linkScroll: true,
},
];
<CustomArgsTable of={HorizontalTabs} />
<Examples title="Default">
<Example title="Default">
<HorizontalTabs
tabs={[
{
name: "tab 1",
href: "?path=/story/ui-navigation-horizontaltabs--horizontal-tabs/tab1",
},
]}
/>
</Example>
<Example title="With avatar">
<HorizontalTabs
tabs={[
{
name: "Tab 1",
href: "?path=/story/ui-navigation-horizontaltabs--horizontal-tabs/tab1",
avatar: "Avatar",
},
]}
/>
</Example>
<Example title="With icon">
<HorizontalTabs
tabs={[
{
name: "Tab 1",
href: "?path=/story/ui-navigation-horizontaltabs--horizontal-tabs/tab1",
icon: Plus,
},
]}
/>
</Example>
<Example title="Disabled">
<HorizontalTabs
tabs={[
{
name: "Tab 1",
href: "?path=/story/ui-navigation-horizontaltabs--horizontal-tabs/tab1",
disabled: true,
},
]}
/>
</Example>
</Examples>
## HorizontalTabs Story
<Canvas>
<Story
name="Horizontal Tabs"
args={{
name: "Tab 1",
href: "/tab1",
disabled: false,
className: "",
linkShallow: true,
linkScroll: true,
icon: "",
avatar: "",
}}
argTypes={{
name: { control: "text", description: "Tab name" },
href: { control: "text", description: "Tab link" },
disabled: { control: "boolean", description: "Whether the tab is disabled" },
className: { control: "text", description: "Additional CSS class" },
linkShallow: { control: "boolean", description: "Whether to use shallow links" },
linkScroll: { control: "boolean", description: "Whether to scroll to links" },
icon: { control: "text", description: "SVGComponent icon" },
avatar: { control: "text", description: "Avatar image URL" },
}}>
{(...props) => (
<VariantsTable titles={["Default"]} columnMinWidth={150}>
<VariantRow>
<HorizontalTabs tabs={tabs} className="overflow-hidden" actions={<button>Click me</button>} />
</VariantRow>
</VariantsTable>
)}
</Story>
</Canvas>

View File

@@ -0,0 +1,158 @@
import { Meta, Story, Canvas } from "@storybook/addon-docs/blocks";
import {
Title,
CustomArgsTable,
Examples,
Example,
VariantsTable,
VariantRow,
} from "@calcom/storybook/components";
import { Icon } from "@calcom/ui";
import VerticalTabs from "../VerticalTabs";
<Meta title="UI/Navigation/VerticalTabs" component={VerticalTabs} />
<Title title="Vertical Tabs Brief" subtitle="Version 1.0 — Last Update: 17 Aug 2023" />
## Definition
The VerticalTabs component is a user interface element utilized to present a vertical set of tabs, commonly employed for navigation or organizing content within a web application.
## Structure
The VerticalTabs component is designed to complement the HorizontalTabItem component, which represents individual tabs within the tab bar. This combination allows for creating intuitive navigation experiences and organized content presentation.
export const tabs = [
{
name: "Tab 1",
href: "/tab1",
disabled: false,
linkShallow: true,
linkScroll: true,
disableChevron: true,
icon: Plus,
},
{
name: "Tab 2",
href: "/tab2",
disabled: false,
linkShallow: true,
linkScroll: true,
avatar: "Avatar",
},
{
name: "Tab 3",
href: "/tab3",
disabled: true,
linkShallow: true,
linkScroll: true,
},
];
<CustomArgsTable of={VerticalTabs} />
<Examples title="Default">
<Example title="Default">
<VerticalTabs
tabs={[
{
name: "tab 1",
href: "/tab1",
},
]}
/>
</Example>
<Example title="Disabled chevron">
<VerticalTabs
tabs={[
{
name: "Tab 1",
href: "/tab1",
disableChevron: true,
},
]}
/>
</Example>
<Example title="With icon">
<VerticalTabs
tabs={[
{
name: "Tab 1",
href: "/tab1",
icon: Plus,
},
]}
/>
</Example>
<Example title="Disabled">
<VerticalTabs
tabs={[
{
name: "Tab 1",
href: "/tab1",
disabled: true,
},
]}
/>
</Example>
</Examples>
## VerticalTabs Story
<Canvas>
<Story
name="Vertical Tabs"
args={{
name: "Tab 1",
info: "Tab information",
icon: Plus,
disabled: false,
children: [
{
name: "Sub Tab 1",
href: "/sub-tab1",
disabled: false,
className: "sub-tab",
},
],
textClassNames: "",
className: "",
isChild: false,
hidden: false,
disableChevron: true,
href: "/tab1",
isExternalLink: true,
linkShallow: true,
linkScroll: true,
avatar: "",
iconClassName: "",
}}
argTypes={{
name: { control: "text", description: "Tab name" },
info: { control: "text", description: "Tab information" },
icon: { control: "object", description: "SVGComponent icon" },
disabled: { control: "boolean", description: "Whether the tab is disabled" },
children: { control: "object", description: "Array of child tabs" },
textClassNames: { control: "text", description: "Additional text class names" },
className: { control: "text", description: "Additional CSS class" },
isChild: { control: "boolean", description: "Whether the tab is a child tab" },
hidden: { control: "boolean", description: "Whether the tab is hidden" },
disableChevron: { control: "boolean", description: "Whether to disable the chevron" },
href: { control: "text", description: "Tab link" },
isExternalLink: { control: "boolean", description: "Whether the link is external" },
linkShallow: { control: "boolean", description: "Whether to use shallow links" },
linkScroll: { control: "boolean", description: "Whether to scroll to links" },
avatar: { control: "text", description: "Avatar image URL" },
iconClassName: { control: "text", description: "Additional icon class name" },
}}>
{(...props) => (
<VariantsTable titles={["Default"]} columnMinWidth={150}>
<VariantRow>
<VerticalTabs tabs={tabs} className="overflow-hidden" />
</VariantRow>
</VariantsTable>
)}
</Story>
</Canvas>

View File

@@ -0,0 +1,145 @@
/* eslint-disable playwright/missing-playwright-await */
import { fireEvent, render, screen } from "@testing-library/react";
import { vi } from "vitest";
import HorizontalTabs from "./HorizontalTabs";
import VerticalTabs from "./VerticalTabs";
vi.mock("@calcom/lib/hooks/useUrlMatchesCurrentUrl", () => ({
useUrlMatchesCurrentUrl() {
return {
route: "/",
pathname: "",
query: "",
asPath: "",
push: vi.fn(),
events: {
on: vi.fn(),
off: vi.fn(),
},
beforePopState: vi.fn(() => null),
prefetch: vi.fn(() => null),
};
},
}));
vi.mock("@calcom/lib/hooks/useLocale", () => ({
useLocale: () => {
return {
t: (str: string) => str,
isLocaleReady: true,
i18n: {
language: "en",
defaultLocale: "en",
locales: ["en"],
exists: () => false,
},
};
},
}));
describe("Tests for navigation folder", () => {
describe("Test HorizontalTabs Component", () => {
const mockTabs = [
{ name: "Tab 1", href: "/tab1" },
{ name: "Tab 2", href: "/tab2", avatar: "Avatar" },
{ name: "Tab 3", href: "/tab3" },
];
beforeEach(() => {
vi.clearAllMocks();
});
test("Should render tabs with correct name and href", () => {
render(<HorizontalTabs tabs={mockTabs} />);
mockTabs.forEach((tab) => {
const tabLabelElement = screen.getByTestId(`horizontal-tab-${tab.name}`);
expect(tabLabelElement).toBeInTheDocument();
const name = screen.getByText(tab.name);
expect(name).toBeInTheDocument();
expect(tabLabelElement).toHaveAttribute("href", tab.href);
});
});
test("Should render actions correctly", () => {
const handleClick = vi.fn();
const mockActions = <button onClick={handleClick}>Actions</button>;
render(<HorizontalTabs tabs={mockTabs} actions={mockActions} />);
const actionsElement = screen.getByText("Actions");
expect(actionsElement).toBeInTheDocument();
fireEvent.click(actionsElement);
expect(handleClick).toHaveBeenCalled();
});
});
describe("Test VerticalTabs Component", () => {
const mockTabs = [
{
name: "Tab 1",
href: "/tab1",
disableChevron: true,
disabled: true,
icon: "plus" as const,
},
{ name: "Tab 2", href: "/tab2", isExternalLink: true },
{ name: "Tab 3", href: "/tab3", info: "info" },
];
beforeEach(() => {
vi.clearAllMocks();
});
test("Should render tabs with correct name and href", () => {
render(<VerticalTabs tabs={mockTabs} />);
mockTabs.forEach((tab) => {
const tabLabelElement = screen.getByTestId(`vertical-tab-${tab.name}`);
expect(tabLabelElement).toBeInTheDocument();
const name = screen.getByText(tab.name);
expect(name).toBeInTheDocument();
expect(tabLabelElement).toHaveAttribute("href", tab.href);
});
});
test("Should render correctly if props are passed", async () => {
render(<VerticalTabs tabs={mockTabs} />);
const iconElement = await screen.findAllByTestId("icon-component");
const externalLink = await screen.findAllByTestId("external-link");
const chevronRight = await screen.findAllByTestId("chevron-right");
mockTabs.forEach((tab) => {
const tabName = screen.getByText(tab.name);
expect(tabName).toBeInTheDocument();
const aTag = screen.getByTestId(`vertical-tab-${tab.name}`);
const tabContainer = tabName.closest("a");
const infoElement = tabContainer?.querySelector("p[title='info']");
expect(chevronRight.length).toEqual(mockTabs.length - 1);
if (tab.disabled) {
expect(aTag).tabToBeDisabled();
} else {
expect(aTag).not.tabToBeDisabled();
}
if (tab.info) {
expect(infoElement).toBeInTheDocument();
} else {
expect(infoElement).toBeNull();
}
if (tab.isExternalLink) {
expect(aTag).toHaveAttribute("target", "_blank");
} else {
expect(aTag).toHaveAttribute("target", "_self");
}
});
expect(externalLink.length).toEqual(1);
expect(iconElement.length).toEqual(1);
});
});
});