Skip to content

Commit

Permalink
feat: ResponsiveDialog component
Browse files Browse the repository at this point in the history
  • Loading branch information
BrickheadJohnny committed Nov 19, 2024
1 parent 5e57c2c commit 975c8fc
Show file tree
Hide file tree
Showing 5 changed files with 314 additions and 7 deletions.
25 changes: 25 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"autoprefixer": "^10.4.20",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"foxact": "^0.2.41",
"next": "15.0.3",
"next-themes": "^0.4.3",
"react": "19.0.0-rc-66855b96-20241106",
Expand Down
10 changes: 3 additions & 7 deletions src/app/components/ui/Drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,10 @@ import {
import { Drawer as DrawerPrimitive } from "vaul";

const Drawer = ({
shouldScaleBackground = true,
...props
}: ComponentProps<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root
shouldScaleBackground={shouldScaleBackground}
{...props}
/>
);
}: ComponentProps<typeof DrawerPrimitive.Root> & {
shouldScaleBackground?: never;
}) => <DrawerPrimitive.Root {...props} shouldScaleBackground={false} />;
Drawer.displayName = "Drawer";

const DrawerTrigger = DrawerPrimitive.Trigger;
Expand Down
157 changes: 157 additions & 0 deletions src/app/components/ui/ResponsiveDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { useMediaQuery } from "foxact/use-media-query";
import { type ComponentProps, useCallback, useState } from "react";
import {
Dialog,
DialogBody,
DialogCloseButton,
DialogContent,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
} from "./Dialog";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerFooter,
DrawerHeader,
DrawerOverlay,
DrawerPortal,
DrawerTitle,
DrawerTrigger,
} from "./Drawer";

const useIsMobile = () => useMediaQuery("(max-width: 640px)", false);

export const ResponsiveDialog = ({
open: openProp,
onOpenChange: onOpenChangeProp,
...props
}: ComponentProps<typeof Dialog> & ComponentProps<typeof Drawer>) => {
const [open, setOpen] = useState(openProp);

const onOpenChange = useCallback(
(newOpen: boolean) => {
setOpen(newOpen);
if (typeof onOpenChangeProp === "function") onOpenChangeProp(newOpen);
},
[onOpenChangeProp],
);

const isMobile = useIsMobile();

if (isMobile)
return <Drawer {...props} open={open} onOpenChange={onOpenChange} />;

return <Dialog {...props} open={open} onOpenChange={onOpenChange} />;
};
ResponsiveDialog.displayName = "ResponsiveDialog";

export const ResponsiveDialogTrigger = (
props: ComponentProps<typeof DialogTrigger> &
ComponentProps<typeof DrawerTrigger>,
) => {
const isMobile = useIsMobile();

if (isMobile) return <DrawerTrigger {...props} />;

return <DialogTrigger {...props} />;
};
ResponsiveDialogTrigger.displayName = "ResponsiveDialogTrigger";

export const ResponsiveDialogPortal = (
props: ComponentProps<typeof DialogPortal> &
ComponentProps<typeof DrawerPortal>,
) => {
const isMobile = useIsMobile();

if (isMobile) return <DrawerPortal {...props} />;

return <DialogPortal {...props} />;
};
ResponsiveDialogPortal.displayName = "ResponsiveDialogPortal";

export const ResponsiveDialogOverlay = (
props: ComponentProps<typeof DialogOverlay> &
ComponentProps<typeof DrawerOverlay>,
) => {
const isMobile = useIsMobile();

if (isMobile) return <DrawerOverlay {...props} />;

return <DialogOverlay {...props} />;
};
ResponsiveDialogOverlay.displayName = "ResponsiveDialogOverlay";

export const ResponsiveDialogContent = (
props: ComponentProps<typeof DialogContent> &
ComponentProps<typeof DrawerContent>,
) => {
const isMobile = useIsMobile();

if (isMobile) return <DrawerContent {...props} />;

return <DialogContent {...props} />;
};
ResponsiveDialogContent.displayName = "ResponsiveDialogContent";

export const ResponsiveDialogCloseButton = ({
children,
asChild,
...props
}: ComponentProps<typeof DialogCloseButton> &
ComponentProps<typeof DrawerClose>) => {
const isMobile = useIsMobile();

if (isMobile)
return (
<DrawerClose {...props} asChild={asChild}>
{children}
</DrawerClose>
);

return <DialogCloseButton {...props} />;
};
ResponsiveDialogCloseButton.displayName = "ResponsiveDialogCloseButton";

export const ResponsiveDialogHeader = (
props: ComponentProps<typeof DialogHeader> &
ComponentProps<typeof DrawerHeader>,
) => {
const isMobile = useIsMobile();

if (isMobile) return <DrawerHeader {...props} />;

return <DialogHeader {...props} />;
};
ResponsiveDialogHeader.displayName = "ResponsiveDialogHeader";

export const ResponsiveDialogTitle = (
props: ComponentProps<typeof DialogTitle> &
ComponentProps<typeof DrawerTitle>,
) => {
const isMobile = useIsMobile();

if (isMobile) return <DrawerTitle {...props} />;

return <DialogTitle {...props} />;
};
ResponsiveDialogTitle.displayName = "ResponsiveDialogTitle";

export const ResponsiveDialogBody = DialogBody;
ResponsiveDialogBody.displayName = "ResponsiveDialogBody";

export const ResponsiveDialogFooter = (
props: ComponentProps<typeof DialogFooter> &
ComponentProps<typeof DrawerFooter>,
) => {
const isMobile = useIsMobile();

if (isMobile) return <DrawerFooter {...props} />;

return <DialogFooter {...props} />;
};
ResponsiveDialogFooter.displayName = "ResponsiveDialogFooter";
128 changes: 128 additions & 0 deletions src/stories/ResponsiveDialog.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "app/components/ui/Button";
import type { DialogContentProps } from "app/components/ui/Dialog";
import {
ResponsiveDialog,
ResponsiveDialogBody,
ResponsiveDialogCloseButton,
ResponsiveDialogContent,
ResponsiveDialogFooter,
ResponsiveDialogHeader,
ResponsiveDialogTitle,
ResponsiveDialogTrigger,
} from "app/components/ui/ResponsiveDialog";

import type { ComponentProps } from "react";

const ResponsiveDialogExample = ({
size,
longContent,
scrollBody,
showHeader = true,
showFooter,
}: {
size?: DialogContentProps["size"];
longContent?: ComponentProps<typeof DynamicDialogContent>["longContent"];
scrollBody?: boolean;
showHeader?: boolean;
showFooter?: boolean;
}) => (
<ResponsiveDialog>
<ResponsiveDialogTrigger>Open responsive dialog</ResponsiveDialogTrigger>
<ResponsiveDialogContent size={size} scrollBody={scrollBody}>
{showHeader && (
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>Responsive dialog</ResponsiveDialogTitle>
</ResponsiveDialogHeader>
)}

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

{showFooter && (
<ResponsiveDialogFooter>Sneaky footer</ResponsiveDialogFooter>
)}

<ResponsiveDialogCloseButton asChild>
<Button variant="subtle" className="mx-4 mb-4">
Close
</Button>
</ResponsiveDialogCloseButton>
</ResponsiveDialogContent>
</ResponsiveDialog>
);

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

export default meta;

type Story = StoryObj<typeof ResponsiveDialogExample>;

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 975c8fc

Please sign in to comment.