-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #169 from lunit-io/DS-13-dialog-release
[DS-13] Dialog 출시
- Loading branch information
Showing
9 changed files
with
731 additions
and
25 deletions.
There are no files selected for viewing
165 changes: 165 additions & 0 deletions
165
packages/design-system/src/components/Dialog/Dialog.styled.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
195
packages/design-system/src/components/Dialog/Dialog.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
36 changes: 36 additions & 0 deletions
36
packages/design-system/src/components/Dialog/components/DialogAction.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.