* ui playground * add types to ui * push avatar ignore ts and eslint errors * feat: V3 UI avatar (#18997) * avatar playground * add overflow-hidden back for loading indicator * update lock * feat: v3/badge (#18999) * badge intro * fix text on small badge * add dark mode warning * feat: v3/button (#19014) * TOC * wip buttons * shadows * secondary * semantic colours * minimal buttons * update disabled states * checkbox * dropdown v3 * load icons * fix design sections * fix icon gap * fix * remove client component from rsc * use peer as avatar * gif * fix button gap - dropdown iocn * support start icon in dropdowns * add alert demo + v3 component + darken semantic text * base inputs and text field * wip input * input field addon/suffix * toast page * wip select * match designs for input and select rounded * fix * fix select isloading transform * fix multi icons transition rotation * fix group heading * range slider demo + range slider * range slider popover design * v3 dialog * fix slider thumb size * wrap in suspense for useSearchParam difference in next14+ * base of tabs + horizontal tabs * fix select sizing to match input 36px * fix dark mode hover on button * target domState class for light/dark so we work on portal elements * fix default bg on popover * lightmode toasts + eleveation token * empty screen * intro to split button concept * mass colour updates * intro to themes * remove i18n from label with name * bouncy buttons * feat: small select * nav item dumb component * full demo * settings demo * fix tabs * fix mt * fix dropdown feedback from loom * alert fixes mentioned in loom * push font-cal * move to fumadocs * fix avatar * colors table * add typeography * icon + spacing guide * add border radius and shadow tables * fix colours and border radius * remove prose undline via fumas fork * fix icon buttons sizes * button group component * fix horizontal tabs * tab fixes * switches * pagination component * show toasts components raw * sync with airtable * add use client * 10px on input again * 10px in select also * fix input height * fix files merge * fix loading state for icon buttons * fix button fab * Enable addon to now overflow to next line in DS * use cal font for dialog text * add avatar square variant * fix select padding and menu size * fix type errors * fix type errors * add switch small size and fix type errors * fix ref types on input * update web styles * remove addonFilled to fix type errors * push dialog fixes * fix checkbox display * remove prose from alert examples * remove prose from all our component examples * badge rounded-lg on lg size * update colors * force w/h on icon buttons instead of inheriting * remove border-focus * remove border-focus from tailwind config * update tokens to match new colours * add new border subtle tokens and add to toasts * fix lg button padding * fix and drastically improve rtl rests * multi select use badges * use badge design for select pills * fix avatar alignment * fix tests and remove redudant ones * fix tests and remove redudant ones * update atoms globals css * extract tokens.css * extract app.css * force flex for atoms * add @calcom/ui/css * revert ui playground styles abstraction * fix input focus states * fix focus states for buttons * feat: v3/app work (#19233) * globals.css for app + event types listing search * few app wide NITS * fix select foucs within styling * fix formbuilder switch styles * fix disabled state of nested inputs * workflow fixes * webhook form fixes * i18n on tabs info * booking filters + tab * fix filters spacing * make first item so we get correct border radius if not default * round radio group in create event types + make button small * profile page * update editor br * button nits in existing places in the app * fix timezone select and multilist in guest form * fix text area on public booking page * size small event types create * margin on routing filter * filters improvments * routing insights fixes * fix date filter range * ooo fixes * fix teams/members bulk actions * create team UI * i18n in useTabs instead of in tab component now * Add event type setup border to availability * fix bulk actions on org members table * onboarding fixes * fix input types with ref stripe comparision * i18n fixes for platform * update globals of atoms * fix form builder firstname/lastname switch + correct footer useage * fix select * Revert "add @calcom/ui/css" This reverts commit 0dd8f9012907a278cfede4a98f289cb0744c9244. * revert tokens css * Revert "extract app.css" This reverts commit 10b998604816ad3310c24b36a02a638dabc5be24. * linear nits * workflow dropdown * fix page theme heading sizes to match cards * fix stepper for team creation * adjust margin to FilterSearchField * fix ooo icon * fix booker and dark mode due to darkgray 50 * more dark mode fixes * add size prop --------- Co-authored-by: Eunjae Lee <hey@eunjae.dev> Co-authored-by: Morgan Vernay <morgan@cal.com> * remove ui-playground * fix grow missing from merge * add search icon data-test-id back to search * fix FormAction style * fix datatestid for apps and tabs * change selector for team invitation e2e * fix use data-testid from item instaed of using names * rename upper case O * fix unit tests data-testid * fix unit tests data-testid * more data-testid changes * add data-testid to apps tab * data-testid to installed appPage * fix embed tests * fix filter-popover-trigger * use text-error and use dataId for locator * more e2e improvment * pass in single testId as button is now duplicate * show field-error instead * add test to eventtype tabs * Add css vars to embed too\ * add scrollbar and change dialog overlay colour * fix type * use attribute instead of class for tab detection * await promise * use new tokens for sprite generation * add correct testing Ids to e2e instead of button:text * use testIds for creation of orgs * Webhook receiver wait for 10 seconds --------- Co-authored-by: Eunjae Lee <hey@eunjae.dev> Co-authored-by: Morgan Vernay <morgan@cal.com> Co-authored-by: Hariom <hariombalhara@gmail.com> Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com> Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
296 lines
9.4 KiB
TypeScript
296 lines
9.4 KiB
TypeScript
"use client";
|
|
|
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
import { usePathname, useRouter } from "next/navigation";
|
|
import type { ForwardRefExoticComponent, ReactElement, ReactNode } from "react";
|
|
import React, { useMemo, useState } from "react";
|
|
|
|
import { Dialog as PlatformDialogPrimitives, useIsPlatform } from "@calcom/atoms/monorepo";
|
|
import { classNames } from "@calcom/lib";
|
|
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
|
|
import type { ButtonProps } from "../button";
|
|
import { Button } from "../button";
|
|
import type { IconName } from "../icon";
|
|
import { Icon } from "../icon";
|
|
|
|
const dialogClasses = cva(
|
|
"fadeIn bg-default scroll-bar fixed left-1/2 top-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 rounded-2xl text-left shadow-xl focus-visible:outline-none sm:align-middle",
|
|
{
|
|
variants: {
|
|
size: {
|
|
xl: "px-8 pt-8 sm:max-w-[90rem]",
|
|
lg: "px-8 pt-8 sm:max-w-[70rem]",
|
|
md: "px-8 pt-8 sm:max-w-[48rem]",
|
|
default: "px-8 pt-8 sm:max-w-[35rem]",
|
|
},
|
|
},
|
|
defaultVariants: {
|
|
size: "default",
|
|
},
|
|
}
|
|
);
|
|
|
|
export type DialogProps = React.ComponentProps<(typeof DialogPrimitive)["Root"]> & {
|
|
name?: string;
|
|
clearQueryParamsOnClose?: string[];
|
|
};
|
|
|
|
const enum DIALOG_STATE {
|
|
// Dialog is there in the DOM but not visible.
|
|
CLOSED = "CLOSED",
|
|
// State from the time b/w the Dialog is dismissed and the time the "dialog" query param is removed from the URL.
|
|
CLOSING = "CLOSING",
|
|
// Dialog is visible.
|
|
OPEN = "OPEN",
|
|
}
|
|
|
|
export function Dialog(props: DialogProps) {
|
|
const isPlatform = useIsPlatform();
|
|
return !isPlatform ? <WebDialog {...props} /> : <PlatformDialogPrimitives.Dialog {...props} />;
|
|
}
|
|
|
|
function WebDialog(props: DialogProps) {
|
|
const router = useRouter();
|
|
const pathname = usePathname();
|
|
const searchParams = useCompatSearchParams();
|
|
const newSearchParams = new URLSearchParams(searchParams ?? undefined);
|
|
const { children, name, ...dialogProps } = props;
|
|
|
|
// only used if name is set
|
|
const [dialogState, setDialogState] = useState(dialogProps.open ? DIALOG_STATE.OPEN : DIALOG_STATE.CLOSED);
|
|
const shouldOpenDialog = newSearchParams.get("dialog") === name;
|
|
if (name) {
|
|
const clearQueryParamsOnClose = ["dialog", ...(props.clearQueryParamsOnClose || [])];
|
|
dialogProps.onOpenChange = (open) => {
|
|
if (props.onOpenChange) {
|
|
props.onOpenChange(open);
|
|
}
|
|
|
|
// toggles "dialog" query param
|
|
if (open) {
|
|
newSearchParams.set("dialog", name);
|
|
} else {
|
|
clearQueryParamsOnClose.forEach((queryParam) => {
|
|
newSearchParams.delete(queryParam);
|
|
});
|
|
router.push(`${pathname}?${newSearchParams.toString()}`);
|
|
}
|
|
setDialogState(open ? DIALOG_STATE.OPEN : DIALOG_STATE.CLOSING);
|
|
};
|
|
|
|
if (dialogState === DIALOG_STATE.CLOSED && shouldOpenDialog) {
|
|
setDialogState(DIALOG_STATE.OPEN);
|
|
}
|
|
|
|
if (dialogState === DIALOG_STATE.CLOSING && !shouldOpenDialog) {
|
|
setDialogState(DIALOG_STATE.CLOSED);
|
|
}
|
|
|
|
// allow overriding
|
|
if (!("open" in dialogProps)) {
|
|
dialogProps.open = dialogState === DIALOG_STATE.OPEN ? true : false;
|
|
}
|
|
}
|
|
|
|
return <DialogPrimitive.Root {...dialogProps}>{children}</DialogPrimitive.Root>;
|
|
}
|
|
|
|
type DialogContentProps = React.ComponentProps<(typeof DialogPrimitive)["Content"]> & {
|
|
type?: "creation" | "confirmation";
|
|
title?: string;
|
|
description?: string | JSX.Element | null;
|
|
closeText?: string;
|
|
actionDisabled?: boolean;
|
|
Icon?: IconName;
|
|
enableOverflow?: boolean;
|
|
forceOverlayWhenNoModal?: boolean;
|
|
/**
|
|
* Disables the ability to close the dialog by clicking outside of it. Could be useful when the dialog is doing something critical which might be in progress.
|
|
*/
|
|
preventCloseOnOutsideClick?: boolean;
|
|
} & VariantProps<typeof dialogClasses>;
|
|
|
|
// enableOverflow:- use this prop whenever content inside DialogContent could overflow and require scrollbar
|
|
export const DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps>(
|
|
(
|
|
{
|
|
children,
|
|
title,
|
|
Icon: icon,
|
|
enableOverflow,
|
|
forceOverlayWhenNoModal,
|
|
type = "creation",
|
|
preventCloseOnOutsideClick,
|
|
...props
|
|
},
|
|
forwardedRef
|
|
) => {
|
|
const isPlatform = useIsPlatform();
|
|
const [Portal, Overlay, Content] = useMemo(
|
|
() =>
|
|
isPlatform
|
|
? [
|
|
({ children }: { children: ReactElement | ReactElement[] }) => <>{children}</>,
|
|
PlatformDialogPrimitives.DialogOverlay,
|
|
PlatformDialogPrimitives.DialogContent,
|
|
]
|
|
: [DialogPrimitive.Portal, DialogPrimitive.Overlay, DialogPrimitive.Content],
|
|
[isPlatform]
|
|
);
|
|
return (
|
|
<Portal>
|
|
{forceOverlayWhenNoModal ? (
|
|
<div className="fadeIn fixed inset-0 z-50 bg-neutral-800 bg-opacity-70 transition-opacity" />
|
|
) : (
|
|
<Overlay className="fadeIn fixed inset-0 z-50 bg-neutral-800 bg-opacity-70 transition-opacity dark:bg-opacity-80" />
|
|
)}
|
|
<Content
|
|
{...props}
|
|
onPointerDownOutside={(e) => {
|
|
if (preventCloseOnOutsideClick) {
|
|
e.preventDefault();
|
|
}
|
|
}}
|
|
className={classNames(
|
|
dialogClasses({ size: props.size }),
|
|
"max-h-[95vh]",
|
|
enableOverflow ? "overflow-y-auto" : "overflow-visible",
|
|
`${props.className || ""}`
|
|
)}
|
|
ref={forwardedRef}>
|
|
{type === "creation" && (
|
|
<div>
|
|
<DialogHeader title={title} subtitle={props.description} />
|
|
<div data-testid="dialog-creation" className="flex flex-col">
|
|
{children}
|
|
</div>
|
|
</div>
|
|
)}
|
|
{type === "confirmation" && (
|
|
<div className="flex">
|
|
{icon && (
|
|
<div className="bg-emphasis flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full">
|
|
<Icon name={icon} className="text-emphasis h-4 w-4" />
|
|
</div>
|
|
)}
|
|
<div className="ml-4 flex-grow">
|
|
<DialogHeader title={title} subtitle={props.description} />
|
|
<div data-testid="dialog-confirmation">{children}</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{!type && children}
|
|
</Content>
|
|
</Portal>
|
|
);
|
|
}
|
|
);
|
|
|
|
type DialogHeaderProps = {
|
|
title: React.ReactNode;
|
|
subtitle?: React.ReactNode;
|
|
} & React.HTMLAttributes<HTMLDivElement>;
|
|
|
|
export function DialogHeader(props: DialogHeaderProps) {
|
|
if (!props.title) return null;
|
|
|
|
return (
|
|
<div className="mb-4">
|
|
<h3
|
|
data-testid="dialog-title"
|
|
className="leading-20 text-semibold text-emphasis font-cal mb-1 text-xl"
|
|
id="modal-title">
|
|
{props.title}
|
|
</h3>
|
|
{props.subtitle && (
|
|
<p className="text-subtle text-sm leading-none" data-testid="dialog-subtitle">
|
|
{props.subtitle}
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
type DialogFooterProps = {
|
|
children: React.ReactNode;
|
|
showDivider?: boolean;
|
|
noSticky?: boolean;
|
|
} & React.HTMLAttributes<HTMLDivElement>;
|
|
|
|
export function DialogFooter(props: DialogFooterProps) {
|
|
return (
|
|
<div
|
|
className={classNames(
|
|
"bg-muted border-subtle bottom-0 -mx-8 mt-10 rounded-b-2xl",
|
|
props?.noSticky ? "" : "sticky",
|
|
props.className
|
|
)}>
|
|
{props.showDivider && <div data-testid="divider" className="border-subtle border-t" />}
|
|
<div className={classNames("flex justify-end space-x-2 px-8 py-4 font-sans rtl:space-x-reverse")}>
|
|
{props.children}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
DialogContent.displayName = "DialogContent";
|
|
|
|
export const DialogTrigger: ForwardRefExoticComponent<
|
|
DialogPrimitive.DialogTriggerProps & React.RefAttributes<HTMLButtonElement>
|
|
> = React.forwardRef((props, ref) => {
|
|
const isPlatform = useIsPlatform();
|
|
return !isPlatform ? (
|
|
<DialogPrimitive.Trigger {...props} ref={ref} />
|
|
) : (
|
|
<PlatformDialogPrimitives.DialogTrigger {...props} ref={ref} />
|
|
);
|
|
});
|
|
|
|
DialogTrigger.displayName = "DialogTrigger";
|
|
|
|
type DialogCloseProps = {
|
|
"data-testid"?: string;
|
|
dialogCloseProps?: React.ComponentProps<(typeof DialogPrimitive)["Close"]>;
|
|
children?: ReactNode;
|
|
onClick?: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
|
disabled?: boolean;
|
|
color?: ButtonProps["color"];
|
|
} & React.ComponentProps<typeof Button>;
|
|
|
|
export function DialogClose(
|
|
props: {
|
|
"data-testid"?: string;
|
|
dialogCloseProps?: React.ComponentProps<(typeof DialogPrimitive)["Close"]>;
|
|
children?: ReactNode;
|
|
onClick?: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
|
disabled?: boolean;
|
|
color?: ButtonProps["color"];
|
|
} & React.ComponentProps<typeof Button>
|
|
) {
|
|
const { t } = useLocale();
|
|
const isPlatform = useIsPlatform();
|
|
const Close = useMemo(
|
|
() => (isPlatform ? PlatformDialogPrimitives.DialogClose : DialogPrimitive.Close),
|
|
[isPlatform]
|
|
);
|
|
|
|
const { className, ...buttonProps } = props;
|
|
|
|
return (
|
|
<Close asChild {...props.dialogCloseProps}>
|
|
<Button
|
|
data-testid={props["data-testid"] || "dialog-rejection"}
|
|
color={props.color || "minimal"}
|
|
className={classNames(props.color === "destructive" && "destructive", className)}
|
|
{...buttonProps}>
|
|
{props.children ? props.children : t("close")}
|
|
</Button>
|
|
</Close>
|
|
);
|
|
}
|
|
|
|
DialogClose.displayName = "WebDialogClose";
|