first commit
This commit is contained in:
94
calcom/packages/ui/components/badge/Badge.tsx
Normal file
94
calcom/packages/ui/components/badge/Badge.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
import { cva } from "class-variance-authority";
|
||||
import React from "react";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
|
||||
import { Icon, type IconName } from "../..";
|
||||
|
||||
export const badgeStyles = cva("font-medium inline-flex items-center justify-center rounded gap-x-1", {
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-attention text-attention",
|
||||
warning: "bg-attention text-attention",
|
||||
orange: "bg-attention text-attention",
|
||||
success: "bg-success text-success",
|
||||
green: "bg-success text-success",
|
||||
gray: "bg-subtle text-emphasis",
|
||||
blue: "bg-info text-info",
|
||||
red: "bg-error text-error",
|
||||
error: "bg-error text-error",
|
||||
grayWithoutHover: "bg-gray-100 text-gray-800 dark:bg-darkgray-200 dark:text-darkgray-800",
|
||||
},
|
||||
size: {
|
||||
sm: "px-1 py-0.5 text-xs leading-3",
|
||||
md: "py-1 px-1.5 text-xs leading-3",
|
||||
lg: "py-1 px-2 text-sm leading-4",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "md",
|
||||
},
|
||||
});
|
||||
|
||||
type InferredBadgeStyles = VariantProps<typeof badgeStyles>;
|
||||
|
||||
type IconOrDot =
|
||||
| {
|
||||
startIcon?: IconName;
|
||||
withDot?: never;
|
||||
}
|
||||
| { startIcon?: never; withDot?: true };
|
||||
|
||||
export type BadgeBaseProps = InferredBadgeStyles & {
|
||||
children: React.ReactNode;
|
||||
rounded?: boolean;
|
||||
customStartIcon?: React.ReactNode;
|
||||
} & IconOrDot;
|
||||
|
||||
export type BadgeProps =
|
||||
/**
|
||||
* This union type helps TypeScript understand that there's two options for this component:
|
||||
* Either it's a div element on which the onClick prop is not allowed, or it's a button element
|
||||
* on which the onClick prop is required. This is because the onClick prop is used to determine
|
||||
* whether the component should be a button or a div.
|
||||
*/
|
||||
| (BadgeBaseProps & Omit<React.HTMLAttributes<HTMLDivElement>, "onClick"> & { onClick?: never })
|
||||
| (BadgeBaseProps & Omit<React.HTMLAttributes<HTMLButtonElement>, "onClick"> & { onClick: () => void });
|
||||
|
||||
export const Badge = function Badge(props: BadgeProps) {
|
||||
const {
|
||||
customStartIcon,
|
||||
variant,
|
||||
className,
|
||||
size,
|
||||
startIcon,
|
||||
withDot,
|
||||
children,
|
||||
rounded,
|
||||
...passThroughProps
|
||||
} = props;
|
||||
const isButton = "onClick" in passThroughProps && passThroughProps.onClick !== undefined;
|
||||
const StartIcon = startIcon;
|
||||
const classes = classNames(
|
||||
badgeStyles({ variant, size }),
|
||||
rounded && "h-5 w-5 rounded-full p-0",
|
||||
className
|
||||
);
|
||||
|
||||
const Children = () => (
|
||||
<>
|
||||
{withDot ? <Icon name="dot" data-testid="go-primitive-dot" className="h-3 w-3 stroke-[3px]" /> : null}
|
||||
{customStartIcon ||
|
||||
(StartIcon ? (
|
||||
<Icon name={StartIcon} data-testid="start-icon" className="h-3 w-3 stroke-[3px]" />
|
||||
) : null)}
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
|
||||
const Wrapper = isButton ? "button" : "div";
|
||||
|
||||
return React.createElement(Wrapper, { ...passThroughProps, className: classes }, <Children />);
|
||||
};
|
||||
14
calcom/packages/ui/components/badge/InfoBadge.tsx
Normal file
14
calcom/packages/ui/components/badge/InfoBadge.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Icon } from "../..";
|
||||
import { Tooltip } from "../tooltip/Tooltip";
|
||||
|
||||
export function InfoBadge({ content }: { content: string }) {
|
||||
return (
|
||||
<>
|
||||
<Tooltip side="top" content={content}>
|
||||
<span title={content}>
|
||||
<Icon name="info" className="text-subtle relative left-1 right-1 top-px mt-px h-4 w-4" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
}
|
||||
16
calcom/packages/ui/components/badge/UpgradeOrgsBadge.tsx
Normal file
16
calcom/packages/ui/components/badge/UpgradeOrgsBadge.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
import { Tooltip } from "../tooltip";
|
||||
import { Badge } from "./Badge";
|
||||
|
||||
export const UpgradeOrgsBadge = function UpgradeOrgsBadge() {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<Tooltip content={t("orgs_upgrade_to_enable_feature")}>
|
||||
<a href="https://cal.com/enterprise" target="_blank">
|
||||
<Badge variant="gray">{t("upgrade")}</Badge>
|
||||
</a>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
22
calcom/packages/ui/components/badge/UpgradeTeamsBadge.tsx
Normal file
22
calcom/packages/ui/components/badge/UpgradeTeamsBadge.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { useHasPaidPlan } from "@calcom/lib/hooks/useHasPaidPlan";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
import { Tooltip } from "../tooltip";
|
||||
import { Badge } from "./Badge";
|
||||
|
||||
export const UpgradeTeamsBadge = function UpgradeTeamsBadge() {
|
||||
const { t } = useLocale();
|
||||
const { hasPaidPlan } = useHasPaidPlan();
|
||||
|
||||
if (hasPaidPlan) return null;
|
||||
|
||||
return (
|
||||
<Tooltip content={t("upgrade_to_enable_feature")}>
|
||||
<Link href="/teams">
|
||||
<Badge variant="gray">{t("upgrade")}</Badge>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
99
calcom/packages/ui/components/badge/badge.stories.mdx
Normal file
99
calcom/packages/ui/components/badge/badge.stories.mdx
Normal file
@@ -0,0 +1,99 @@
|
||||
import { Canvas, Meta, Story, ArgsTable } from "@storybook/addon-docs";
|
||||
|
||||
import {
|
||||
Examples,
|
||||
Example,
|
||||
Note,
|
||||
Title,
|
||||
CustomArgsTable,
|
||||
VariantsTable,
|
||||
VariantRow,
|
||||
} from "@calcom/storybook/components";
|
||||
|
||||
import { Plus } from "../icon";
|
||||
import { Badge } from "./Badge";
|
||||
|
||||
<Meta title="UI/Badge" component={Badge} />
|
||||
|
||||
<Title title="Badge" suffix="Brief" subtitle="Version 2.0 — Last Update: 22 Aug 2022" />
|
||||
|
||||
## Definition
|
||||
|
||||
Badges are small status descriptors for UI elements. A badge consists of a small circle, typically containing a number or other short set of characters, that appears in proximity to another object. We provide three different types of badges such as status, alert, and brand badge.
|
||||
|
||||
Status badge communicate status information. It is generally used within a container such as accordion and tables to label status for easy scanning.
|
||||
|
||||
## Structure
|
||||
|
||||
<CustomArgsTable of={Badge} />
|
||||
|
||||
<Examples title="Badge style">
|
||||
<Example title="Gray">
|
||||
<Badge variant="gray">Badge text</Badge>
|
||||
</Example>
|
||||
<Example title="Green/Success">
|
||||
<Badge variant="success">Badge text</Badge>
|
||||
</Example>
|
||||
<Example title="Orange/Default">
|
||||
<Badge variant="default">Badge text</Badge>
|
||||
</Example>
|
||||
<Example title="Red/Error">
|
||||
<Badge variant="red">Badge text</Badge>
|
||||
</Example>
|
||||
</Examples>
|
||||
|
||||
<Examples title="Variants">
|
||||
<Example title="Default">
|
||||
<Badge>Button text</Badge>
|
||||
</Example>
|
||||
<Example title="With Dot">
|
||||
<Badge withDot>Button Text</Badge>
|
||||
</Example>
|
||||
<Example title="With Icon">
|
||||
<Badge startIcon="plus">Button Text</Badge>
|
||||
</Example>
|
||||
</Examples>
|
||||
|
||||
## Alert Badges
|
||||
|
||||
## Usage
|
||||
|
||||
Alert badge is used in conjunction with an item, profile or label to indicate numeric value and messages associated with them.
|
||||
|
||||
<Title offset title="Badge" suffix="Variants" />
|
||||
|
||||
<Canvas>
|
||||
<Story
|
||||
name="All Variants"
|
||||
args={{
|
||||
severity: "default",
|
||||
label: "Badge text",
|
||||
}}
|
||||
argTypes={{
|
||||
severity: {
|
||||
control: {
|
||||
type: "inline-radio",
|
||||
options: ["default", "success", "gray", "error"],
|
||||
},
|
||||
},
|
||||
label: {
|
||||
control: {
|
||||
type: "text",
|
||||
},
|
||||
},
|
||||
}}>
|
||||
{({ severity, label }) => (
|
||||
<VariantsTable titles={["Default", "With Dot", "With Icon"]} columnMinWidth={150}>
|
||||
<VariantRow variant={severity}>
|
||||
<Badge variant={severity}>{label}</Badge>
|
||||
<Badge variant={severity} withDot>
|
||||
{label}
|
||||
</Badge>
|
||||
<Badge variant={severity} startIcon="plus">
|
||||
{label}
|
||||
</Badge>
|
||||
</VariantRow>
|
||||
</VariantsTable>
|
||||
)}
|
||||
</Story>
|
||||
</Canvas>
|
||||
91
calcom/packages/ui/components/badge/badge.test.tsx
Normal file
91
calcom/packages/ui/components/badge/badge.test.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
/* eslint-disable playwright/missing-playwright-await */
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { vi } from "vitest";
|
||||
|
||||
import { Badge, badgeStyles } from "./Badge";
|
||||
|
||||
describe("Tests for Badge component", () => {
|
||||
const variants = [
|
||||
"default",
|
||||
"warning",
|
||||
"orange",
|
||||
"success",
|
||||
"green",
|
||||
"gray",
|
||||
"blue",
|
||||
"red",
|
||||
"error",
|
||||
"grayWithoutHover",
|
||||
];
|
||||
|
||||
const sizes = ["sm", "md", "lg"];
|
||||
const children = "Test Badge";
|
||||
|
||||
test.each(variants)("Should apply variant class", (variant) => {
|
||||
render(<Badge variant={variant as any}>{children}</Badge>);
|
||||
const badgeClass = screen.getByText(children).className;
|
||||
const badgeComponentClass = badgeStyles({ variant: variant as any });
|
||||
expect(badgeClass).toEqual(badgeComponentClass);
|
||||
});
|
||||
|
||||
test.each(sizes)("Should apply size class", (size) => {
|
||||
render(<Badge size={size as any}>{children}</Badge>);
|
||||
const badgeClass = screen.getByText(children).className;
|
||||
const badgeComponentClass = badgeStyles({ size: size as any });
|
||||
expect(badgeClass).toEqual(badgeComponentClass);
|
||||
});
|
||||
|
||||
test("Should render without errors", () => {
|
||||
render(<Badge>{children}</Badge>);
|
||||
expect(screen.getByText(children)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("Should render WithDot if the prop is true and shouldn't render if is false", async () => {
|
||||
const { rerender } = render(<Badge withDot>{children}</Badge>);
|
||||
expect(await screen.findByTestId("go-primitive-dot")).toBeInTheDocument();
|
||||
|
||||
rerender(<Badge>{children}</Badge>);
|
||||
expect(screen.queryByTestId("go-primitive-dot")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("Should render with a startIcon when startIcon prop is provided shouldn't render if is false", () => {
|
||||
const { rerender } = render(<Badge customStartIcon={<svg data-testid="start-icon" />}>{children}</Badge>);
|
||||
expect(screen.getByTestId("start-icon")).toBeInTheDocument();
|
||||
|
||||
rerender(<Badge>{children}</Badge>);
|
||||
expect(screen.queryByTestId("start-icon")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("Should render as a button when onClick prop is provided and shouldn't if is not", () => {
|
||||
const handleClick = vi.fn();
|
||||
const { rerender } = render(<Badge onClick={handleClick}>{children}</Badge>);
|
||||
const badge = screen.getByText(children);
|
||||
expect(badge.tagName).toBe("BUTTON");
|
||||
fireEvent.click(badge);
|
||||
expect(handleClick).toHaveBeenCalledTimes(1);
|
||||
|
||||
rerender(<Badge>{children}</Badge>);
|
||||
const updateBadge = screen.getByText(children);
|
||||
expect(updateBadge.tagName).not.toBe("BUTTON");
|
||||
});
|
||||
|
||||
test("Should render as a div when onClick prop is not provided", () => {
|
||||
render(<Badge>{children}</Badge>);
|
||||
const badge = screen.getByText(children);
|
||||
expect(badge.tagName).toBe("DIV");
|
||||
});
|
||||
|
||||
test("Should render children when provided", () => {
|
||||
const { getByText } = render(
|
||||
<Badge>
|
||||
<span>Child element 1</span>
|
||||
<span>Child element 2</span>
|
||||
</Badge>
|
||||
);
|
||||
|
||||
expect(getByText("Child element 1")).toBeInTheDocument();
|
||||
expect(getByText("Child element 2")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
5
calcom/packages/ui/components/badge/index.ts
Normal file
5
calcom/packages/ui/components/badge/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { Badge } from "./Badge";
|
||||
export { UpgradeTeamsBadge } from "./UpgradeTeamsBadge";
|
||||
export { UpgradeOrgsBadge } from "./UpgradeOrgsBadge";
|
||||
export { InfoBadge } from "./InfoBadge";
|
||||
export type { BadgeProps } from "./Badge";
|
||||
Reference in New Issue
Block a user