Skip to content

Commit

Permalink
feat: simple dialog component
Browse files Browse the repository at this point in the history
  • Loading branch information
BrickheadJohnny committed Nov 19, 2024
1 parent ce229b9 commit d022757
Show file tree
Hide file tree
Showing 2 changed files with 297 additions and 0 deletions.
176 changes: 176 additions & 0 deletions src/app/components/ui/Dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
"use client";

import { X } from "@phosphor-icons/react/dist/ssr";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { FocusScope, type FocusScopeProps } from "@radix-ui/react-focus-scope";
import { type VariantProps, cva } from "class-variance-authority";
import { cn } from "lib/cn";
import {
type ComponentPropsWithoutRef,
type ElementRef,
type HTMLAttributes,
forwardRef,
} from "react";

const Dialog = DialogPrimitive.Root;

const DialogTrigger = DialogPrimitive.Trigger;

const DialogPortal = DialogPrimitive.Portal;

const DialogOverlay = forwardRef<
ElementRef<typeof DialogPrimitive.Overlay>,
ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-modal grid items-end justify-center overflow-y-auto bg-black/50 backdrop-blur-sm duration-200 data-[state=closed]:animate-out data-[state=open]:animate-in sm:items-center",
className,
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;

export const dialogContentVariants = cva(
"flex flex-col mt-4 md:my-16 relative rounded-xl max-sm:rounded-b-none bg-card shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 focus-visible:outline-none ring-ring focus-visible:ring-4 ring-offset-0",
{
variants: {
size: {
sm: "w-[min(theme(maxWidth.sm),_100vw)]",
md: "w-[min(theme(maxWidth.md),_100vw)]",
lg: "w-[min(theme(maxWidth.lg),_100vw)]",
xl: "w-[min(theme(maxWidth.xl),_100vw)]",
"2xl": "w-[min(theme(maxWidth.2xl),_100vw)]",
"3xl": "w-[min(theme(maxWidth.3xl),_100vw)]",
"4xl": "w-[min(theme(maxWidth.4xl),_100vw)]",
},
},
defaultVariants: {
size: "md",
},
},
);

export interface DialogContentProps
extends ComponentPropsWithoutRef<typeof DialogPrimitive.Content>,
VariantProps<typeof dialogContentVariants> {
scrollBody?: boolean;
trapFocus?: FocusScopeProps["trapped"];
}

const DialogContent = forwardRef<
ElementRef<typeof DialogPrimitive.Content>,
DialogContentProps
>(
(
{ size, trapFocus = true, className, scrollBody, children, ...props },
ref,
) => (
<DialogPortal>
<DialogOverlay>
<FocusScope trapped={trapFocus} loop>
<DialogPrimitive.Content
ref={ref}
className={cn(dialogContentVariants({ size, className }), {
"max-h-[calc(100vh-2*theme(space.16))]": scrollBody,
})}
{...props}
>
{children}
</DialogPrimitive.Content>
</FocusScope>
</DialogOverlay>
</DialogPortal>
),
);
DialogContent.displayName = DialogPrimitive.Content.displayName;

const DialogCloseButton = forwardRef<
ElementRef<typeof DialogPrimitive.Close>,
DialogPrimitive.DialogCloseProps
>(({ className, ...props }, ref) => (
<DialogPrimitive.Close
ref={ref}
className={cn(
"absolute top-8 right-8 rounded-full opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-ring focus-visible:ring-4 disabled:pointer-events-none data-[state=open]:text-foreground/50 md:right-10",
className,
)}
{...props}
>
<X weight="bold" className="h-5 w-5" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
));
DialogCloseButton.displayName = DialogPrimitive.Close.displayName;

const DialogHeader = ({
className,
...props
}: HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("flex flex-col space-y-1.5 px-6 py-8 sm:px-10", className)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";

interface DialogBodyProps extends HTMLAttributes<HTMLDivElement> {
scroll?: boolean;
}
const DialogBody = ({ className, scroll, ...props }: DialogBodyProps) => (
<div
className={cn(
"flex flex-col overflow-visible px-6 pb-10 has-[~div]:pb-0 sm:px-10",
{
"custom-scrollbar flex-shrink-1 flex-grow-1 overflow-y-auto": scroll,
},
className,
)}
{...props}
/>
);
DialogBody.displayName = "DialogBody";

const DialogFooter = ({
className,
...props
}: HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse px-6 pt-8 pb-10 sm:flex-row sm:justify-end sm:space-x-2 sm:px-10",
className,
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";

const DialogTitle = forwardRef<
ElementRef<typeof DialogPrimitive.Title>,
ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"font-display font-extrabold text-xl tracking-wide",
className,
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;

export {
Dialog,
DialogBody,
DialogCloseButton,
DialogContent,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
};
121 changes: 121 additions & 0 deletions src/stories/Dialog.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import type { Meta, StoryObj } from "@storybook/react";
import {
Dialog,
DialogBody,
DialogCloseButton,
DialogContent,
type DialogContentProps,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "app/components/ui/Dialog";

import type { ComponentProps } from "react";

const DialogExample = ({
size,
longContent,
scrollBody,
showHeader = true,
showFooter,
}: {
size?: DialogContentProps["size"];
longContent?: ComponentProps<typeof DynamicDialogContent>["longContent"];
scrollBody?: boolean;
showHeader?: boolean;
showFooter?: boolean;
}) => (
<Dialog defaultOpen>
<DialogTrigger>Open dialog</DialogTrigger>
<DialogContent size={size} scrollBody={scrollBody}>
{showHeader && (
<DialogHeader>
<DialogTitle>Awesome dialog</DialogTitle>
</DialogHeader>
)}

<DialogBody scroll={scrollBody}>
<DynamicDialogContent longContent={longContent} />
</DialogBody>

{showFooter && <DialogFooter>Sneaky dialog footer</DialogFooter>}

<DialogCloseButton />
</DialogContent>
</Dialog>
);

const meta: Meta<typeof DialogExample> = {
title: "Design system/Dialog",
component: DialogExample,
};

export default meta;

type Story = StoryObj<typeof DialogExample>;

export const Default: Story = {
args: {
longContent: false,
size: "md",
scrollBody: false,
showHeader: true,
showFooter: false,
},
argTypes: {
size: {
control: {
type: "select",
},
options: ["sm", "md", "lg", "xl", "2xl", "3xl", "4xl"],
},
},
};

const DynamicDialogContent = ({ longContent }: { longContent?: boolean }) => {
if (!longContent) return <p>This is a simple dialog.</p>;

return (
<>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin a luctus
lorem. Etiam aliquam vel est vel lacinia. Nulla vehicula tortor quis
erat pellentesque, et interdum nisl aliquet. Aenean ac malesuada mauris.
Aliquam scelerisque lorem ut metus interdum, ut venenatis orci egestas.
In semper mollis eros lobortis mollis. Praesent vestibulum convallis
ipsum at fermentum. Donec porta facilisis lacus eu dignissim. Proin
hendrerit orci gravida risus commodo fermentum. Donec finibus sapien eu
nibh mattis dictum. Praesent ac tempor odio, et lobortis ex. Donec nec
mauris et lorem facilisis auctor. Maecenas vitae convallis leo.
</p>
<p>
Maecenas maximus felis scelerisque turpis euismod rutrum. Pellentesque a
dolor scelerisque, elementum sapien eu, porta libero. Donec volutpat
egestas tincidunt. Vivamus blandit eros mollis viverra aliquam. Mauris
eu turpis id est gravida finibus. In viverra, elit eget eleifend
sagittis, massa quam faucibus erat, sed mattis odio nunc vitae enim.
Donec lacus diam, lobortis at facilisis in, placerat ut diam. Maecenas
sed dui sit amet massa tristique vulputate non ac erat. Nullam orci
urna, finibus eu blandit et, pharetra ac enim. Donec magna augue,
interdum at sollicitudin id, fringilla nec sapien. In nisl quam, rhoncus
blandit ipsum in, volutpat aliquam nisi. Nam accumsan lobortis ante, at
tristique ante vehicula eget. Sed ornare varius velit, ut ultrices ante
auctor non. Cras bibendum, libero sed varius fringilla, quam lorem
fermentum nunc, vitae varius mauris libero sed tortor.
</p>
<p>
Donec ut aliquam massa. Etiam congue turpis at purus tempor maximus.
Etiam semper libero non varius pellentesque. Ut egestas faucibus purus
non faucibus. Duis sed nisi consequat, laoreet nisi in, laoreet purus.
Integer aliquam mi ac metus interdum rhoncus. In ac iaculis quam. Sed eu
nibh lectus. Donec imperdiet vestibulum nisl in facilisis. Sed molestie
ipsum eu orci imperdiet cursus. Morbi sit amet quam mi. Etiam maximus
scelerisque orci, id gravida ligula sagittis in. Maecenas volutpat quam
elit, vel vehicula ex fringilla ac. Aenean risus lectus, pellentesque id
est eget, feugiat hendrerit ligula. Nulla tempor pulvinar lacus, ut
euismod tortor.
</p>
</>
);
};

0 comments on commit d022757

Please sign in to comment.