Skip to content

Commit

Permalink
Add Modal Dialog Component (#125)
Browse files Browse the repository at this point in the history
* Init commit

* Getting all the basics to work.

* Adding tests and improving the story

* Add basic tests

* Removing unneeded const

* Changing type of as?

* restore button

* fix props

* minor editings

---------

Co-authored-by: Sergio de Cristofaro <[email protected]>
  • Loading branch information
gjones and serdec authored Oct 10, 2023
1 parent 309fdd1 commit 7e9d370
Show file tree
Hide file tree
Showing 5 changed files with 298 additions and 0 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"@radix-ui/react-avatar": "^1.0.3",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-context-menu": "^2.1.4",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-hover-card": "^1.0.6",
"@radix-ui/react-popover": "^1.0.6",
Expand Down
16 changes: 16 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ import {
WarningAlert,
CardPrimary,
} from "@/components";
import { Dialog } from "@/components/Dialog/Dialog";

const App = () => {
const [currentTheme, setCurrentTheme] = useState<ThemeName>("dark");
const [selectedButton, setSelectedButton] = useState(0);
const [checked, setChecked] = useState(false);
const [disabled] = useState(false);
const [open, setOpen] = useState(true);

return (
<ClickUIProvider
Expand Down Expand Up @@ -267,6 +269,20 @@ const App = () => {
title="Title"
/>
<Avatar text="CH" />

<Button onClick={() => setOpen(true)}>Button</Button>
<Dialog
open={open}
onOpenChange={setOpen}
>
<Dialog.Content
title="Hello"
showClose
onClose={() => setOpen(false)}
>
<p>I'm a dialog</p>
</Dialog.Content>
</Dialog>
</ClickUIProvider>
);
};
Expand Down
77 changes: 77 additions & 0 deletions src/components/Dialog/Dialog.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { GridCenter } from "../commonElement";
import { Text } from "../Typography/Text/Text";
import { Dialog } from "./Dialog";
import Separator from "../Separator/Separator";
import { Spacer } from "../Spacer/Spacer";
import { Button } from "../Button/Button";
import styled from "styled-components";
import { Link } from "../Link/Link";

const DialogComponent = ({
open,
title,
modal,
showClose,
forceMount,
}: {
open?: boolean;
title: string;
modal: boolean;
showClose: boolean;
forceMount?: boolean;
}) => (
<GridCenter>
<Dialog
open={open}
modal={modal}
>
<Dialog.Trigger>
<Link>Open dialog</Link>
</Dialog.Trigger>
<Dialog.Content
title={title}
showClose={showClose}
forceMount={forceMount ? true : undefined}
>
<Text color="muted">
Hello there, I'm a wonderful example of a modal dialog. You can close me by
using the button in my top, left corner.
</Text>
<Spacer />
<Separator size="lg" />
<ActionArea>
<Dialog.Close label="Close" />
<Button iconRight="arrow-right">Continue</Button>
</ActionArea>
</Dialog.Content>
</Dialog>
</GridCenter>
);

const ActionArea = styled.div`
display: flex;
justify-content: flex-end;
gap: ${({ theme }) => theme.click.dialog.space.gap};
`;
export default {
component: DialogComponent,
title: "Display/Dialog",
tags: ["autodocs", "dialog"],
argTypes: {
open: {
options: [true, false, undefined],
control: { type: "radio" },
},
},
};

export const Playground = {
args: {
title: "Example dialog title",
showClose: true,
open: true,
onOpenChange: () => {
console.log("ignored");
},
},
};
47 changes: 47 additions & 0 deletions src/components/Dialog/Dialog.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { screen } from "@testing-library/react";
import { DialogProps } from "@radix-ui/react-dialog";
import { Dialog } from "./Dialog";
import { fireEvent } from "@testing-library/react";
import { renderCUI } from "@/utils/test-utils";

describe("Dialog Component", () => {
const renderDialog = (props: DialogProps) =>
renderCUI(
<Dialog {...props}>
<Dialog.Trigger>
<div>Open Dialog</div>
</Dialog.Trigger>
<Dialog.Close label="Close" />
<Dialog.Content title="Test Dialog">Test Content</Dialog.Content>
</Dialog>
);

it("renders the dialog content with title", () => {
const { getByText } = renderDialog({});
const dialogTrigger = getByText("Open Dialog");
expect(dialogTrigger).not.toBeNull();
fireEvent.click(dialogTrigger);

const dialogTitle = screen.getByText("Test Dialog");
const dialogContent = screen.getByText("Test Content");

expect(dialogTitle).toBeTruthy();
expect(dialogContent).toBeTruthy();
});

it("closes the dialog when close button is clicked", () => {
const { getByText } = renderDialog({});
const dialogTrigger = getByText("Open Dialog");
expect(dialogTrigger).not.toBeNull();
fireEvent.click(dialogTrigger);

const DialogClose = screen.getByText("Close");
fireEvent.click(DialogClose);

const dialogTitle = screen.queryByText("Test Dialog");
const dialogContent = screen.queryByText("Test Content");

expect(dialogTitle).toBeFalsy();
expect(dialogContent).toBeFalsy();
});
});
157 changes: 157 additions & 0 deletions src/components/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import React, { ReactNode } from "react";
import * as RadixDialog from "@radix-ui/react-dialog";
import styled, { keyframes } from "styled-components";
import { Button, Icon, Spacer, Title } from "@/components";
import { EmptyButton } from "../commonElement";

export const Dialog = ({ children, ...props }: RadixDialog.DialogProps) => {
return <RadixDialog.Root {...props}>{children}</RadixDialog.Root>;
};

// Dialog Trigger
const Trigger = styled(RadixDialog.Trigger)`
width: fit-content;
background: transparent;
border: none;
cursor: pointer;
`;

interface DialogTriggerProps extends RadixDialog.DialogTriggerProps {
children: React.ReactNode;
}

const DialogTrigger = ({ children, ...props }: DialogTriggerProps) => {
return <Trigger {...props}>{children}</Trigger>;
};

DialogTrigger.displayName = "DialogTrigger";
Dialog.Trigger = DialogTrigger;

// Dialog Close
interface DialogCloseProps extends RadixDialog.DialogCloseProps {
label: string;
}

const DialogClose = ({ label }: DialogCloseProps) => {
label = label ?? "Close";
return (
<RadixDialog.Close asChild>
<Button type="secondary">{label}</Button>
</RadixDialog.Close>
);
};

DialogClose.displayName = "DialogClose";
Dialog.Close = DialogClose;

// Dialog Content
const overlayShow = keyframes({
"0%": { opacity: 0 },
"100%": { opacity: 1 },
});

const contentShow = keyframes({
"0%": { opacity: 0, transform: "translate(-50%, -48%) scale(.96)" },
"100%": { opacity: 1, transform: "translate(-50%, -50%) scale(1)" },
});

const DialogOverlay = styled(RadixDialog.Overlay)`
background-color: ${({ theme }) => theme.click.dialog.color.opaqueBackground.default};
position: fixed;
inset: 0;
animation: ${overlayShow} 150ms cubic-bezier(0.16, 1, 0.3, 1);
`;

const ContentArea = styled(RadixDialog.Content)`
background: ${({ theme }) => theme.click.dialog.color.background.default};
border-radius: ${({ theme }) => theme.click.dialog.radii.all};
padding: ${({ theme }) => theme.click.dialog.space.y}
${({ theme }) => theme.click.dialog.space.x};
box-shadow: ${({ theme }) => theme.click.dialog.shadow.default};
border: 1px solid ${({ theme }) => theme.click.global.color.stroke.default};
width: 75%;
max-width: 670px;
position: fixed;
top: 25%;
margin-top: 20%;
left: 50%;
max-height: 75%;
overflow: auto;
transform: translate(-50%, -50%);
animation: ${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1);
outline: none;
`;

const TitleArea = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
min-height: ${({ theme }) => theme.sizes[9]}; // 32px
`;

const CrossButton = styled(EmptyButton)`
padding: ${({ theme }) => theme.click.button.iconButton.sm.space.y}
${({ theme }) => theme.click.button.iconButton.sm.space.x};
background: ${({ theme }) =>
theme.click.button.iconButton.color.primary.background.default};
border-radius: ${({ theme }) => theme.click.button.iconButton.radii.all};
&:hover {
background: ${({ theme }) =>
theme.click.button.iconButton.color.primary.background.hover};
}
`;

const CloseButton = ({ onClose }: { onClose?: () => void }) => (
<RadixDialog.Close asChild>
<CrossButton onClick={onClose}>
<Icon
name="cross"
size="lg"
/>
</CrossButton>
</RadixDialog.Close>
);

interface DialogContentProps extends RadixDialog.DialogContentProps {
title: string;
showClose?: boolean;
forceMount?: true;
container?: HTMLElement | null;
children: ReactNode;
onClose?: () => void;
}

const DialogContent = ({
title,
children,
showClose,
onClose,
forceMount,
container,
}: DialogContentProps) => {
return (
<RadixDialog.Portal
forceMount={forceMount}
container={container}
>
<DialogOverlay />
<ContentArea>
<TitleArea>
<Title
size="xl"
type="h2"
>
{title}
</Title>
{showClose && <CloseButton onClose={onClose} />}
</TitleArea>
<Spacer />
{children}
</ContentArea>
</RadixDialog.Portal>
);
};

DialogContent.displayName = "DialogContent";
Dialog.Content = DialogContent;

1 comment on commit 7e9d370

@vercel
Copy link

@vercel vercel bot commented on 7e9d370 Oct 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

click-ui – ./

click-ui-clickhouse.vercel.app
click-ui-git-main-clickhouse.vercel.app
click-ui.vercel.app

Please sign in to comment.