-
Notifications
You must be signed in to change notification settings - Fork 0
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 #8 from Bandmators/feature/dialog
feat(common): dialog (#7)
- Loading branch information
Showing
11 changed files
with
535 additions
and
1 deletion.
There are no files selected for viewing
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
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,50 @@ | ||
import * as React from 'react'; | ||
|
||
import { composeEventHandlers } from '@/libs/event'; | ||
|
||
export type AsChildProps<T> = ({ asChild?: false } & T) | { asChild: true; children: React.ReactNode }; | ||
|
||
interface SlotProps extends React.HTMLProps<HTMLElement> { | ||
children?: React.ReactNode; | ||
} | ||
|
||
const Slot = ({ children, ...props }: SlotProps) => { | ||
if (React.isValidElement(children)) { | ||
return React.cloneElement(children, { | ||
...mergeProps(props, children.props), | ||
}); | ||
} | ||
|
||
if (React.Children.count(children) > 1) { | ||
React.Children.only(null); | ||
} | ||
|
||
return null; | ||
}; | ||
export default Slot; | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
type AnyProps = Record<string, any>; | ||
const mergeProps = (slotProps: AnyProps, childProps: AnyProps) => { | ||
const overrideProps = { ...childProps }; | ||
|
||
for (const propName in childProps) { | ||
const slotValue = slotProps[propName]; | ||
const childValue = childProps[propName]; | ||
|
||
// on** Event compose | ||
if (/^on[A-Z]/.test(propName)) { | ||
if (slotValue && childValue) { | ||
overrideProps[propName] = (...args: unknown[]) => composeEventHandlers(childValue(...args), slotValue(...args)); | ||
} else if (slotValue) { | ||
overrideProps[propName] = slotValue; | ||
} | ||
} else if (propName === 'style') { | ||
overrideProps[propName] = { ...slotValue, ...childValue }; | ||
} else if (propName === 'className') { | ||
overrideProps[propName] = [slotValue, childValue].filter(Boolean).join(' '); | ||
} | ||
} | ||
|
||
return { ...slotProps, ...overrideProps }; | ||
}; |
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,21 @@ | ||
import * as React from 'react'; | ||
|
||
import DialogContext from './DialogContext'; | ||
|
||
interface DialogProps extends React.ComponentPropsWithoutRef<'div'> {} | ||
|
||
/** | ||
* Dialog Context Provider | ||
* @returns | ||
*/ | ||
export const Dialog = ({ children, ...props }: DialogProps) => { | ||
const [showModal, setShowModal] = React.useState<boolean>(false); | ||
|
||
return ( | ||
<DialogContext.Provider value={{ showModal, setShowModal }} {...props}> | ||
{children} | ||
</DialogContext.Provider> | ||
); | ||
}; | ||
|
||
// const DialogProvider = styled.div``; |
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,35 @@ | ||
import * as React from 'react'; | ||
|
||
import Slot from '@/components/Slot'; | ||
import useContext from '@/hooks/useContext'; | ||
import { composeEventHandlers } from '@/libs/event'; | ||
|
||
import { Button } from '..'; | ||
import DialogContext from './DialogContext'; | ||
|
||
type ComponentPropsWithoutRef<E extends React.ElementType> = React.ComponentPropsWithoutRef<E> & { | ||
asChild?: boolean; | ||
}; | ||
|
||
/** | ||
* DialogClose | ||
* @returns | ||
*/ | ||
export const DialogClose = React.forwardRef<HTMLButtonElement, ComponentPropsWithoutRef<'button'>>( | ||
({ asChild, onClick, ...props }, ref) => { | ||
const { setShowModal } = useContext(DialogContext); | ||
|
||
const Comp = asChild ? Slot : Button; | ||
|
||
return ( | ||
<Comp | ||
ref={ref} | ||
onClick={composeEventHandlers(onClick, () => { | ||
setShowModal(false); | ||
})} | ||
{...props} | ||
/> | ||
); | ||
}, | ||
); | ||
DialogClose.displayName = 'DialogClose'; |
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,146 @@ | ||
import { keyframes } from '@emotion/react'; | ||
import styled from '@emotion/styled'; | ||
import * as React from 'react'; | ||
import { createPortal } from 'react-dom'; | ||
|
||
import useContext from '@/hooks/useContext'; | ||
|
||
import DialogContext from './DialogContext'; | ||
|
||
interface ModalProps extends React.ComponentPropsWithoutRef<'div'> { | ||
/* | ||
Modal Background Event : close modal when click event | ||
*/ | ||
outEvent?: boolean; | ||
/* | ||
Hide Close Button | ||
*/ | ||
hideClose?: boolean; | ||
/* | ||
Modal max width | ||
*/ | ||
maxWidth?: number | string; | ||
} | ||
/** | ||
* DialogContent | ||
* @returns | ||
*/ | ||
export const DialogContent = React.forwardRef<HTMLDivElement, ModalProps>( | ||
({ outEvent = false, hideClose = false, maxWidth = '450px', ...props }, ref) => { | ||
const { showModal, setShowModal } = useContext(DialogContext); | ||
|
||
const close = () => { | ||
if (outEvent) setShowModal(false); | ||
}; | ||
const closeBtnHandler = () => { | ||
setShowModal(false); | ||
}; | ||
|
||
return ( | ||
<> | ||
{showModal && | ||
createPortal( | ||
<> | ||
<ModalBG onClick={close} className="bmates-modal-bg" /> | ||
<Modal ref={ref} maxWidth={maxWidth} {...props}> | ||
{props.children} | ||
{!hideClose && ( | ||
<ExitButton onClick={closeBtnHandler}> | ||
<svg | ||
xmlns="http://www.w3.org/2000/svg" | ||
width="24" | ||
height="24" | ||
viewBox="0 0 24 24" | ||
fill="none" | ||
stroke="currentColor" | ||
strokeWidth="2" | ||
strokeLinecap="round" | ||
strokeLinejoin="round" | ||
> | ||
<line x1="18" y1="6" x2="6" y2="18"></line> | ||
<line x1="6" y1="6" x2="18" y2="18"></line> | ||
</svg> | ||
</ExitButton> | ||
)} | ||
</Modal> | ||
</>, | ||
document.body, | ||
)} | ||
</> | ||
); | ||
}, | ||
); | ||
|
||
export const DialogHeader = styled.div` | ||
display: flex; | ||
flex-direction: column; | ||
`; | ||
|
||
export const DialogFooter = styled.div<{ justify?: React.CSSProperties['justifyContent'] }>` | ||
display: flex; | ||
flex-direction: row; | ||
gap: 0.5rem; | ||
align-items: flex-start; | ||
justify-content: ${({ justify }) => justify || 'flex-end'}; | ||
`; | ||
|
||
export const DialogTitle = styled.h2` | ||
font-weight: 600; | ||
line-height: 1; | ||
margin: 0px; | ||
`; | ||
export const DialogDescription = styled.p` | ||
margin: 0.375rem 0px; | ||
font-weight: 300; | ||
`; | ||
|
||
const enter = keyframes` | ||
0% { opacity: 0; } | ||
100% { opacity: 1; } | ||
`; | ||
|
||
const ModalBG = styled.div` | ||
pointer-events: auto; | ||
animation-name: ${enter}; | ||
animation-duration: 0.15s; | ||
background-color: rgba(0, 0, 0, 0.8); | ||
z-index: 50; | ||
inset: 0; | ||
position: fixed; | ||
`; | ||
|
||
const Modal = styled.div<{ maxWidth: number | string }>` | ||
display: grid; | ||
width: 100%; | ||
max-width: ${({ maxWidth }) => { | ||
if (typeof maxWidth === 'string') return maxWidth; | ||
return `${maxWidth}px`; | ||
}}; | ||
padding: 1.5rem; | ||
border-radius: 0.5rem; | ||
position: fixed; | ||
top: 50%; | ||
left: 50%; | ||
gap: 1rem; | ||
transform: translate(-50%, -50%); | ||
background-color: white; | ||
pointer-events: auto; | ||
animation-name: ${enter}; | ||
animation-duration: 0.15s; | ||
z-index: 50; | ||
`; | ||
|
||
const ExitButton = styled.button` | ||
position: absolute; | ||
right: 1rem; | ||
top: 1rem; | ||
background: transparent; | ||
border: none; | ||
padding: 0px; | ||
opacity: 0.5; | ||
cursor: pointer; | ||
svg { | ||
width: 1rem; | ||
height: 1rem; | ||
} | ||
`; |
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,8 @@ | ||
import * as React from 'react'; | ||
|
||
interface DialogContextType { | ||
showModal: boolean; | ||
setShowModal: (value: boolean) => void; | ||
} | ||
const DialogContext = React.createContext<DialogContextType | null>(null); | ||
export default DialogContext; |
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,35 @@ | ||
import * as React from 'react'; | ||
|
||
import Slot from '@/components/Slot'; | ||
import useContext from '@/hooks/useContext'; | ||
import { composeEventHandlers } from '@/libs/event'; | ||
|
||
import { Button } from '..'; | ||
import DialogContext from './DialogContext'; | ||
|
||
type ComponentPropsWithoutRef<E extends React.ElementType> = React.ComponentPropsWithoutRef<E> & { | ||
asChild?: boolean; | ||
}; | ||
|
||
/** | ||
* DialogToggle | ||
* @returns | ||
*/ | ||
export const DialogToggle = React.forwardRef<HTMLButtonElement, ComponentPropsWithoutRef<'button'>>( | ||
({ asChild, onClick, ...props }, ref) => { | ||
const { setShowModal } = useContext(DialogContext); | ||
|
||
const Comp = asChild ? Slot : Button; | ||
|
||
return ( | ||
<Comp | ||
ref={ref} | ||
onClick={composeEventHandlers(onClick, () => { | ||
setShowModal(true); | ||
})} | ||
{...props} | ||
/> | ||
); | ||
}, | ||
); | ||
DialogToggle.displayName = 'DialogToggle'; |
Oops, something went wrong.