feat: refactor drawer component (#15892)

* Add sheet component

* refactor user edit sheet

* refactor availbility sheet edit

* Remove tremor raw sheet text

* remove accidently file commit

* fix overflow issue
This commit is contained in:
sean-brydon
2024-07-25 12:41:23 +01:00
committed by GitHub
parent 64ef2703a5
commit 53c37a9ddf
6 changed files with 287 additions and 287 deletions
+10
View File
@@ -105,11 +105,21 @@ module.exports = {
spinning: {
"100%": { transform: "rotate(360deg)" },
},
drawerSlideLeftAndFade: {
from: { opacity: "0", transform: "translateX(100%)" },
to: { opacity: "1", transform: "translateX(0)" },
},
drawerSlideRightAndFade: {
from: { opacity: "1", transform: "translateX(0)" },
to: { opacity: "0", transform: "translateX(100%)" },
},
},
animation: {
"fade-in-up": "fade-in-up 600ms var(--animation-delay, 0ms) cubic-bezier(.21,1.02,.73,1) forwards",
"fade-in-bottom": "fade-in-bottom cubic-bezier(.21,1.02,.73,1) forwards",
spinning: "spinning 0.75s linear infinite",
drawerSlideLeftAndFade: "drawerSlideLeftAndFade 150ms cubic-bezier(0.16, 1, 0.3, 1)",
drawerSlideRightAndFade: "drawerSlideRightAndFade 150ms ease-in",
},
boxShadow: {
dropdown: "0px 2px 6px -1px rgba(0, 0, 0, 0.08)",
@@ -15,7 +15,10 @@ import {
Form,
Label,
Sheet,
SheetBody,
SheetClose,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
showToast,
@@ -151,22 +154,7 @@ export function AvailabilityEditSheetForm(props: Props & { data: Data; isPending
...values,
});
}}>
<SheetContent
bottomActions={
<>
<Button color="secondary" className="w-full justify-center">
{t("cancel")}
</Button>
<Button
disabled={!hasEditPermission || !data.hasDefaultSchedule}
className="w-full justify-center"
type="submit"
loading={updateMutation.isPending}
form="availability-form">
{t("save")}
</Button>
</>
}>
<SheetContent>
<SheetHeader>
<SheetTitle>
{t("edit_users_availability", {
@@ -185,7 +173,7 @@ export function AvailabilityEditSheetForm(props: Props & { data: Data; isPending
</div>
)}
<div className="mt-4 flex flex-col space-y-4">
<SheetBody className="mt-4 flex flex-col space-y-4">
<div>
<Label className="text-emphasis">
<>{t("timezone")}</>
@@ -220,7 +208,22 @@ export function AvailabilityEditSheetForm(props: Props & { data: Data; isPending
/>
)}
</div>
</div>
</SheetBody>
<SheetFooter>
<SheetClose asChild>
<Button color="secondary" className="w-full justify-center">
{t("cancel")}
</Button>
</SheetClose>
<Button
disabled={!hasEditPermission || !data.hasDefaultSchedule}
className="w-full justify-center"
type="submit"
loading={updateMutation.isPending}
form="availability-form">
{t("save")}
</Button>
</SheetFooter>
</SheetContent>
</Form>
</Sheet>
@@ -19,9 +19,13 @@ import {
showToast,
Avatar,
ImageUploader,
SheetHeader,
SheetBody,
SheetFooter,
} from "@calcom/ui";
import type { Action } from "../UserListTable";
import { SheetFooterControls } from "./SheetFooterControls";
import { useEditMode } from "./store";
type MembershipOption = {
@@ -117,73 +121,83 @@ export function EditForm({
const watchTimezone = form.watch("timeZone");
return (
<Form
form={form}
id="edit-user-form"
handleSubmit={(values) => {
setMutationLoading(true);
mutation.mutate({
userId: selectedUser?.id ?? "",
role: values.role,
username: values.username,
name: values.name,
email: values.email,
avatar: values.avatar,
bio: values.bio,
timeZone: values.timeZone,
});
}}>
<div className="mt-4 flex flex-col gap-2">
<Controller
control={form.control}
name="avatar"
render={({ field: { value } }) => (
<div className="flex items-center">
<Avatar alt={`${selectedUser?.name} avatar`} imageSrc={value} size="lg" />
<div className="ml-4">
<ImageUploader
target="avatar"
id="avatar-upload"
buttonMsg={t("change_avatar")}
handleAvatarChange={(newAvatar) => {
form.setValue("avatar", newAvatar, { shouldDirty: true });
}}
imageSrc={value || undefined}
/>
</div>
<>
<Form
form={form}
id="edit-user-form"
className="flex h-full flex-col"
handleSubmit={(values) => {
setMutationLoading(true);
mutation.mutate({
userId: selectedUser?.id ?? "",
role: values.role,
username: values.username,
name: values.name,
email: values.email,
avatar: values.avatar,
bio: values.bio,
timeZone: values.timeZone,
});
}}>
<SheetHeader>
<div className="flex flex-col gap-2">
<Controller
control={form.control}
name="avatar"
render={({ field: { value } }) => (
<div className="flex items-center">
<Avatar alt={`${selectedUser?.name} avatar`} imageSrc={value} size="lg" />
<div className="ml-4">
<ImageUploader
target="avatar"
id="avatar-upload"
buttonMsg={t("change_avatar")}
handleAvatarChange={(newAvatar) => {
form.setValue("avatar", newAvatar, { shouldDirty: true });
}}
imageSrc={value || undefined}
/>
</div>
</div>
)}
/>
<div className="space-between flex flex-col leading-none">
<span className="text-emphasis text-lg font-semibold">
{selectedUser?.name ?? "Nameless User"}
</span>
<p className="subtle text-sm font-normal">
{domainUrl}/{selectedUser?.username}
</p>
</div>
)}
/>
<div className="space-between flex flex-col leading-none">
<span className="text-emphasis text-lg font-semibold">{selectedUser?.name ?? "Nameless User"}</span>
<p className="subtle text-sm font-normal">
{domainUrl}/{selectedUser?.username}
</p>
</div>
</div>
<div className="mt-6 flex flex-col space-y-3">
<TextField label={t("username")} {...form.register("username")} />
<TextField label={t("name")} {...form.register("name")} />
<TextField label={t("email")} {...form.register("email")} />
</div>
</SheetHeader>
<SheetBody className="mt-6 flex h-full flex-col space-y-3">
<TextField label={t("username")} {...form.register("username")} />
<TextField label={t("name")} {...form.register("name")} />
<TextField label={t("email")} {...form.register("email")} />
<TextAreaField label={t("bio")} {...form.register("bio")} className="min-h-52" />
<div>
<Label>{t("role")}</Label>
<ToggleGroup
isFullWidth
defaultValue={selectedUser?.role ?? "MEMBER"}
value={form.watch("role")}
options={membershipOptions}
onValueChange={(value: EditSchema["role"]) => {
form.setValue("role", value);
}}
/>
</div>
<div>
<Label>{t("timezone")}</Label>
<TimezoneSelect value={watchTimezone ?? "America/Los_Angeles"} />
</div>
</div>
</Form>
<TextAreaField label={t("bio")} {...form.register("bio")} className="min-h-52" />
<div>
<Label>{t("role")}</Label>
<ToggleGroup
isFullWidth
defaultValue={selectedUser?.role ?? "MEMBER"}
value={form.watch("role")}
options={membershipOptions}
onValueChange={(value: EditSchema["role"]) => {
form.setValue("role", value);
}}
/>
</div>
<div>
<Label>{t("timezone")}</Label>
<TimezoneSelect value={watchTimezone ?? "America/Los_Angeles"} />
</div>
</SheetBody>
<SheetFooter>
<SheetFooterControls />
</SheetFooter>
</Form>
</>
);
}
@@ -5,7 +5,19 @@ import { useOrgBranding } from "@calcom/ee/organizations/context/provider";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Avatar, Label, Loader, Sheet, SheetContent, SheetFooter, Skeleton } from "@calcom/ui";
import {
Avatar,
Label,
Loader,
Sheet,
SheetContent,
SheetBody,
Skeleton,
SheetHeader,
SheetDescription,
SheetTitle,
SheetFooter,
} from "@calcom/ui";
import type { Action, State } from "../UserListTable";
import { DisplayInfo } from "./DisplayInfo";
@@ -35,32 +47,34 @@ export function EditUserSheet({ state, dispatch }: { state: State; dispatch: Dis
setEditMode(false);
dispatch({ type: "CLOSE_MODAL" });
}}>
<SheetContent position="right" size="default">
<SheetContent>
{!isPending && loadedUser ? (
<div className="flex h-full flex-col">
<>
{!editMode ? (
<div className="flex-grow">
<div className="mt-4 flex items-center gap-2">
<>
<SheetHeader>
<Avatar
asChild
className="h-[36px] w-[36px]"
alt={`${loadedUser?.name} avatar`}
imageSrc={loadedUser.avatarUrl}
/>
<div className="space-between flex flex-col leading-none">
<SheetTitle>
<Skeleton loading={isPending} as="p" waitForTranslation={false}>
<span className="text-emphasis text-lg font-semibold">
{loadedUser?.name ?? "Nameless User"}
</span>
</Skeleton>
</SheetTitle>
<SheetDescription>
<Skeleton loading={isPending} as="p" waitForTranslation={false}>
<p className="subtle text-sm font-normal">
{orgBranding?.fullDomain ?? WEBAPP_URL}/{loadedUser?.username}
</p>
</Skeleton>
</div>
</div>
<div className="mt-6 flex flex-col space-y-5">
</SheetDescription>
</SheetHeader>
<SheetBody className="flex flex-col space-y-5">
<DisplayInfo label={t("email")} value={loadedUser?.email ?? ""} displayCopy />
<DisplayInfo
label={t("bio")}
@@ -94,22 +108,22 @@ export function EditUserSheet({ state, dispatch }: { state: State; dispatch: Dis
}
asBadge={teamNames && teamNames?.length > 0}
/>
</div>
</div>
</SheetBody>
<SheetFooter>
<SheetFooterControls />
</SheetFooter>
</>
) : (
<div className="mb-4 flex-grow">
<>
<EditForm
selectedUser={loadedUser}
avatarUrl={loadedUser.avatarUrl ?? avatarURL}
domainUrl={orgBranding?.fullDomain ?? WEBAPP_URL}
dispatch={dispatch}
/>
</div>
</>
)}
<SheetFooter className="mt-auto">
<SheetFooterControls />
</SheetFooter>
</div>
</>
) : (
<Loader />
)}
+144 -186
View File
@@ -1,215 +1,173 @@
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { cva, type VariantProps } from "class-variance-authority";
import * as SheetPrimitives from "@radix-ui/react-dialog";
import * as React from "react";
import classNames from "@calcom/lib/classNames";
import { Icon } from "@calcom/ui";
const Sheet = SheetPrimitive.Root;
import { Button } from "../button";
const SheetTrigger = SheetPrimitive.Trigger;
const Sheet = (props: React.ComponentPropsWithoutRef<typeof SheetPrimitives.Root>) => {
return <SheetPrimitives.Root {...props} />;
};
Sheet.displayName = "Sheet";
const SheetClose = SheetPrimitive.Close;
const portalVariants = cva("fixed inset-0 z-50 flex", {
variants: {
position: {
top: "items-start",
bottom: "items-end",
left: "justify-start",
right: "justify-end",
},
},
defaultVariants: { position: "right" },
const SheetTrigger = React.forwardRef<
React.ElementRef<typeof SheetPrimitives.Trigger>,
React.ComponentPropsWithoutRef<typeof SheetPrimitives.Trigger>
>(({ className, ...props }, ref) => {
return <SheetPrimitives.Trigger ref={ref} className={classNames(className)} {...props} />;
});
SheetTrigger.displayName = "Sheet.Trigger";
interface SheetPortalProps extends SheetPrimitive.DialogPortalProps, VariantProps<typeof portalVariants> {}
const SheetClose = React.forwardRef<
React.ElementRef<typeof SheetPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof SheetPrimitives.Close>
>(({ className, ...props }, ref) => {
return <SheetPrimitives.Close ref={ref} className={classNames(className)} {...props} />;
});
SheetClose.displayName = "Sheet.Close";
const SheetPortal = ({ position, className, children, ...props }: SheetPortalProps) => (
<SheetPrimitive.Portal className={classNames(className)} {...props}>
<div className={portalVariants({ position })}>{children}</div>
</SheetPrimitive.Portal>
);
SheetPortal.displayName = SheetPrimitive.Portal.displayName;
const SheetPortal = SheetPrimitives.Portal;
SheetPortal.displayName = "SheetPortal";
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
// eslint-disable-next-line @typescript-eslint/no-unused-vars
>(({ className, children, ...props }, ref) => (
<SheetPrimitive.Overlay
className={classNames(
"data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in fixed inset-0 z-50 bg-neutral-800 bg-opacity-70 backdrop-blur-[1px] transition-all duration-100",
className
)}
{...props}
ref={ref}
/>
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
React.ElementRef<typeof SheetPrimitives.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitives.Overlay>
>(({ className, ...props }, forwardedRef) => {
return (
<SheetPrimitives.Overlay
ref={forwardedRef}
className={classNames(
// base
"fixed inset-0 z-50 overflow-y-auto",
// background color
"bg-black/30",
// transition
"data-[state=closed]:animate-hide data-[state=open]:animate-dialogOverlayShow",
className
)}
{...props}
style={{
animationDuration: "400ms",
animationFillMode: "backwards",
}}
/>
);
});
const sheetVariants = cva(
"fixed z-50 scale-100 gap-4 bg-default m-4 rounded-xl p-6 opacity-100 shadow-lg border border-default flex flex-col ",
{
variants: {
position: {
top: "animate-in slide-in-from-top w-full duration-200",
bottom: "animate-in slide-in-from-bottom w-full duration-200",
left: "animate-in slide-in-from-left h-full duration-200",
right: "animate-in slide-in-from-right h-full duration-200",
},
size: {
content: "",
default: "",
sm: "",
lg: "",
xl: "",
full: "",
},
},
compoundVariants: [
{
position: ["top", "bottom"],
size: "content",
class: "max-h-screen",
},
{
position: ["top", "bottom"],
size: "default",
class: "h-1/3",
},
{
position: ["top", "bottom"],
size: "sm",
class: "h-1/4",
},
{
position: ["top", "bottom"],
size: "lg",
class: "h-1/2",
},
{
position: ["top", "bottom"],
size: "xl",
class: "h-5/6",
},
{
position: ["top", "bottom"],
size: "full",
class: "h-screen",
},
{
position: ["right", "left"],
size: "content",
class: "max-w-screen",
},
{
position: ["right", "left"],
size: "default",
class: "w-[calc(100%-2rem)] lg:w-1/3 max-h-[calc(100vh-2rem)]",
},
{
position: ["right", "left"],
size: "sm",
class: "w-1/4 max-h-[calc(100vh-2rem)]",
},
{
position: ["right", "left"],
size: "lg",
class: "w-1/2 h-[calc(100vh-2rem)]",
},
{
position: ["right", "left"],
size: "xl",
class: "w-5/6 h-[calc(100vh-2rem)]",
},
{
position: ["right", "left"],
size: "full",
class: "w-screen h-[calc(100vh-2rem)]",
},
],
defaultVariants: {
position: "right",
size: "default",
},
SheetOverlay.displayName = "SheetOverlay";
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitives.Content>,
React.ComponentPropsWithoutRef<typeof SheetPrimitives.Content>
>(({ className, ...props }, forwardedRef) => {
return (
<SheetPortal>
<SheetOverlay>
<SheetPrimitives.Content
ref={forwardedRef}
className={classNames(
// base
"fixed inset-y-2 mx-auto flex w-[95vw] flex-1 flex-col overflow-y-auto rounded-md border p-4 shadow-lg focus:outline-none max-sm:inset-x-2 sm:inset-y-2 sm:right-2 sm:max-w-lg sm:p-6",
// border color
"border-subtle",
// background color
"bg-default",
// transition
"data-[state=closed]:animate-SheetSlideRightAndFade data-[state=open]:animate-SheetSlideLeftAndFade",
className
)}
{...props}
/>
</SheetOverlay>
</SheetPortal>
);
});
SheetContent.displayName = "SheetContent";
const SheetHeader = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<"div">>(
({ children, className, ...props }, ref) => {
return (
<div
ref={ref}
className="border-subtle flex items-start justify-between gap-x-4 border-b pb-2"
{...props}>
<div className={classNames("mt-1 flex flex-col gap-y-1", className)}>{children}</div>
<SheetPrimitives.Close asChild>
<Button variant="icon" StartIcon="x" color="minimal" className="aspect-square p-1" />
</SheetPrimitives.Close>
</div>
);
}
);
export interface DialogContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {
bottomActions?: React.ReactNode;
}
const SheetContent = React.forwardRef<React.ElementRef<typeof SheetPrimitive.Content>, DialogContentProps>(
({ position, size, className, children, bottomActions, ...props }, ref) => (
<SheetPortal position={position}>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
{...props}
className={classNames(sheetVariants({ position, size }), className)}>
<div className="no-scrollbar h-full overflow-y-auto">{children}</div>
{bottomActions && (
<div className="mt-auto flex justify-end">
<div className="flex gap-2">{bottomActions}</div>
</div>
)}
<SheetPrimitive.Close className="focus:ring-emphasis data-[state=open]:bg-deafult absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
<Icon name="x" className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
)
);
SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={classNames("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
);
SheetHeader.displayName = "SheetHeader";
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={classNames("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...props}
/>
);
SheetFooter.displayName = "SheetFooter";
SheetHeader.displayName = "Sheet.Header";
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={classNames("text-foreground text-lg font-semibold", className)}
React.ElementRef<typeof SheetPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitives.Title>
>(({ className, ...props }, forwardedRef) => (
<SheetPrimitives.Title
ref={forwardedRef}
className={classNames(
// base
"text-base font-semibold",
// text color
"text-default",
className
)}
{...props}
/>
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
SheetTitle.displayName = "SheetTitle";
const SheetBody = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<"div">>(
({ className, ...props }, ref) => {
return <div ref={ref} className={classNames("flex-1 overflow-y-auto py-4", className)} {...props} />;
}
);
SheetBody.displayName = "Sheet.Body";
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={classNames("text-muted-foreground text-sm", className)}
{...props}
/>
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
React.ElementRef<typeof SheetPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitives.Description>
>(({ className, ...props }, forwardedRef) => {
return (
<SheetPrimitives.Description
ref={forwardedRef}
className={classNames("text-default", className)}
{...props}
/>
);
});
SheetDescription.displayName = "SheetDescription";
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => {
return (
<div
className={classNames(
"border-muted x flex flex-col-reverse border-t pt-4 sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
);
};
SheetFooter.displayName = "SheetFooter";
export {
Sheet,
SheetTrigger,
SheetBody,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
};
+1
View File
@@ -172,6 +172,7 @@ export { WizardLayoutAppDir } from "./layouts/WizardLayoutAppDir";
export { DataTable } from "./components/data-table";
export {
Sheet,
SheetBody,
SheetClose,
SheetContent,
SheetDescription,