Skip to content

Commit

Permalink
Merge pull request #169 from lunit-io/DS-13-dialog-release
Browse files Browse the repository at this point in the history
[DS-13] Dialog 출시
  • Loading branch information
HyejinYang authored Apr 26, 2024
2 parents 2595f0d + 17abe7a commit 056e497
Show file tree
Hide file tree
Showing 9 changed files with 731 additions and 25 deletions.
165 changes: 165 additions & 0 deletions packages/design-system/src/components/Dialog/Dialog.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { styled } from "@mui/material/styles";

import type { DialogBase } from "./Dialog";
import type { CSSObject } from "@mui/material/styles";

export interface DialogElementStyle {
[key: string]: CSSObject;
}

export type DialogStyle = Pick<DialogBase, "size" | "type" | "nonModal">;

const DIALOG_WRAPPER_STYLE: DialogElementStyle = {
small: {
width: "320px",
maxWidth: "320px",
},
medium: {
width: "500px",
maxWidth: "840px",
},
modal: {
position: "relative",
boxShadow:
"0px 12px 24px 8px rgba(0, 0, 0, 0.12), 0px 12px 44px 3px rgba(0, 0, 0, 0.18)",
},
nonModal: {
position: "fixed",
top: "30px",
right: "30px",
boxShadow:
"0px 12px 24px 8px rgba(0, 0, 0, 0.36), 0px 12px 44px 3px rgba(0, 0, 0, 0.48)",
},
};

const DIALOG_TITLE_STYLE: DialogElementStyle = {
small: {
height: "52px",
maxHeight: "100%",
padding: "20px 20px 8px 20px",
},
medium: {
height: "64px",
maxHeight: "100%",
padding: "30px 32px 6px 32px",
},
};

const DIALOG_CONTENT_STYLE: DialogElementStyle = {
small: {
paddingInline: "20px calc(20px - 10px)",
paddingBottom: "28px",
},
smallAction: {
paddingInline: "20px calc(20px - 10px)",
paddingBottom: "8px",
},
medium: {
paddingInline: "32px calc(32px - 14px)",
paddingBottom: "32px",
},
mediumAction: {
paddingInline: "32px calc(32px - 14px)",
paddingBottom: "16px",
},
};

const DIALOG_ACTION_STYLE: DialogElementStyle = {
small: {
height: "64px",
padding: "8px 20px 20px 20px",
},
medium: {
height: "84px",
padding: "16px 32px 32px 32px",
},
};

export const StyledBackdrop = styled("div")({
position: "fixed",
top: 0,
left: 0,
width: "100%",
height: "100%",
backgroundColor: "rgba(17, 17, 19, 0.7)",
zIndex: 1000,
display: "grid",
placeItems: "center",
});

export const StyledDialog = styled("div")<DialogStyle>(
({ theme, size, nonModal, type }) => ({
zIndex: 1001,
maxHeight: "80vh",
display: "flex",
flexDirection: "column",
boxSizing: "border-box",
borderRadius: "10px",
backgroundColor: theme.palette.lunit_token.core.bg_03,
color: theme.palette.lunit_token.core.text_normal,

...DIALOG_WRAPPER_STYLE[size === "small" ? "small" : "medium"],
...DIALOG_WRAPPER_STYLE[nonModal ? "nonModal" : "modal"],

"& #dialog-title": {
...DIALOG_TITLE_STYLE[size === "small" ? "small" : "medium"],
},

"& #dialog-content": {
...DIALOG_CONTENT_STYLE[
size === "small" && type !== "passive"
? "smallAction"
: size === "small"
? "small"
: size === "medium" && type !== "passive"
? "mediumAction"
: "medium"
],

scrollbarGutter: "stable",
"::-webkit-scrollbar": {
width: size === "small" ? "10px" : "14px",
},
"::webkit-scrollbar-track": {
background: "transparent",
},
"::-webkit-scrollbar-thumb": {
backgroundClip: "padding-box",
border: `2px solid transparent`,
/**
* Figma's border-radius is 6px, but actual border radius is 10px since padding 2px is added.
*/
borderRadius: "10px",
backgroundColor: theme.palette.lunit_token.component.scrollbars_bg,
},
},

"& #dialog-action": {
...DIALOG_ACTION_STYLE[size === "small" ? "small" : "medium"],
},
})
);

export const StyledDialogTitle = styled("header")({
display: "flex",
width: "100%",
flex: "0 0 auto",
alignItems: "center",
justifyContent: "flex-start",
gap: "8px",
});

export const StyledDialogTitleIconWrapper = styled("div")({
display: "flex",
justifyContent: "center",
width: "20px",
height: "20px",
position: "relative",
marginBottom: "1px",
});

export const StyledDialogContent = styled("div")(({ theme }) => ({
...theme.typography.body2_14_regular,
flex: "1 1 auto",
overflowY: "scroll",
}));
195 changes: 195 additions & 0 deletions packages/design-system/src/components/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import React, { useEffect } from "react";
import { createPortal } from "react-dom";
import { Close } from "@lunit/design-system-icons";

import { DialogAction } from "./components/DialogAction";
import {
StyledBackdrop,
StyledDialog,
StyledDialogContent,
StyledDialogTitle,
StyledDialogTitleIconWrapper,
} from "./Dialog.styled";
import Button from "../Button";
import Typography from "../Typography";

import type { SxProps } from "@mui/material/styles";
import type { TypographyProps } from "@mui/material";

export interface DialogBase {
isOpen: boolean;
onClose(): void;
type?: "passive" | "action"; // default passive
nonModal?: boolean; // default false
title: string;
titleIcon?: React.ReactNode;
titleVariant?: TypographyProps["variant"];
children: React.ReactNode;
actions?: React.ReactNode;
enableBackButtonClose?: boolean;
enableBackdropClose?: boolean;
size?: "small" | "medium"; // default "small"
sx?: SxProps;
style?: React.CSSProperties;
className?: string;
}

export interface PassiveDialogType extends DialogBase {
type: "passive";
actions?: undefined;
enableBackButtonClose?: true;
enableBackdropClose?: true;
}
export interface ActionDialogType extends DialogBase {
type: "action";
actions: React.ReactNode;
enableBackButtonClose?: boolean;
enableBackdropClose?: boolean;
}

export interface PassiveModalProps extends PassiveDialogType {
nonModal?: false;
}
export interface ActionModalProps extends ActionDialogType {
nonModal?: false;
}
export type ModalProps = PassiveModalProps | ActionModalProps;

export interface PassiveNonModalProps extends PassiveDialogType {
nonModal?: true;
}
export interface ActionNonModalProps extends ActionDialogType {
nonModal?: true;
enableBackdropClose?: false;
}
export type NonModalProps = PassiveNonModalProps | ActionNonModalProps;

export type DialogProps = ModalProps | NonModalProps;

function Dialog(props: DialogProps) {
const { isOpen, type, nonModal = false, onClose } = props;

const isActionModal = type === "action" && !nonModal;
const isPassiveModal = type === "passive" && !nonModal;
const isActionNonModal = type === "action" && nonModal;

function handleBackdropClose(e: React.MouseEvent<HTMLDivElement>) {
const isClosable =
isPassiveModal || (isActionModal && props.enableBackdropClose);

if (!isClosable) return;
if (e.target !== e.currentTarget) return;

onClose();
}

useEffect(() => {
const isClosable =
isOpen &&
(isPassiveModal ||
(isActionModal && props.enableBackdropClose) ||
(isActionNonModal && props.enableBackButtonClose));

if (!isClosable) return;

function handleEscClose(event: KeyboardEvent) {
if (event.key === "Escape") onClose();
}
function handleBackButtonClose(event: KeyboardEvent) {
if (event.key === "Backspace") onClose();
}

document.addEventListener("keydown", handleEscClose);
document.addEventListener("keydown", handleBackButtonClose);

return () => {
document.removeEventListener("keydown", handleEscClose);
document.removeEventListener("keydown", handleBackButtonClose);
};
}, [isOpen, isPassiveModal, onClose]);

if (!isOpen) return null;
return createPortal(
nonModal ? (
<DialogBase dialogProps={{ ...props }} />
) : (
<StyledBackdrop
onClick={handleBackdropClose}
data-testid="dialog-backdrop"
>
<DialogBase dialogProps={{ ...props }} />
</StyledBackdrop>
),

document.body
);
}

function DialogBase({ dialogProps }: { dialogProps: DialogBase }) {
const {
nonModal = false,
onClose,
title,
titleIcon,
titleVariant = "headline5",
children,
actions,
type,
size = "small",
sx,
style,
className,
} = dialogProps;

return (
<StyledDialog
role="dialog"
aria-labelledby="dialog-title"
size={size}
nonModal={nonModal}
type={type}
sx={{
...sx,
}}
style={style}
className={`dialog ${className ?? ""}`}
>
<StyledDialogTitle id="dialog-title" className="dialog-title-wrapper">
{titleIcon && (
<StyledDialogTitleIconWrapper className="dialog-title-icon">
{titleIcon}
</StyledDialogTitleIconWrapper>
)}
<Typography
component="h2"
id="dialog-title-text"
variant={titleVariant}
>
{title}
</Typography>
{type === "passive" && (
<Button
id="dialog-title-close-button"
data-testid="dialog-title-close-button"
kind="ghost"
color="secondary"
icon={<Close />}
onClick={onClose}
sx={{
marginRight: 0,
marginLeft: "auto",
}}
/>
)}
</StyledDialogTitle>
<StyledDialogContent id="dialog-content">{children}</StyledDialogContent>
{type === "action" && actions !== null ? (
// `actions !== null` is used to not render DialogAction when actions is undefined
// There was a case when actions is undefined, but DialogAction is rendered with null children
<DialogAction>{actions}</DialogAction>
) : null}
</StyledDialog>
);
}

export default Dialog;
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from "react";
import { styled } from "@mui/material/styles";

interface DialogActionProps {
children: React.ReactNode;
justifyContent?: React.CSSProperties["justifyContent"];
sx?: React.CSSProperties;
}

const StyledDialogActions = styled("div")({
display: "flex",
flex: "0 0 auto",
alignItems: "center",
justifyContent: "flex-end",
gap: 8,
});

export function DialogAction(props: DialogActionProps) {
const { children, justifyContent, sx } = props;

return (
<StyledDialogActions
id="dialog-action"
data-testid="dialog-action"
className="dialog-action"
sx={{
justifyContent,
...sx,
}}
>
{children}
</StyledDialogActions>
);
}

export default DialogAction;
Loading

0 comments on commit 056e497

Please sign in to comment.