-
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.
refactor: select, dropdown refact to portal
- Loading branch information
Showing
17 changed files
with
211 additions
and
253 deletions.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,24 +1,20 @@ | ||
import * as React from 'react'; | ||
|
||
import useModal from '@/hooks/useModal'; | ||
|
||
import DropdownContext, { DropdownAlignType } from './DropdownContext'; | ||
import { PortalProvider } from '@/components/portal/PortalProvider'; | ||
import { AlignType } from '@/types/align'; | ||
|
||
interface DropdownProps extends React.ComponentPropsWithoutRef<'div'> { | ||
align?: DropdownAlignType; | ||
align?: AlignType; | ||
} | ||
|
||
/** | ||
* Displays a list of menus. | ||
* @returns | ||
*/ | ||
export const Dropdown = ({ children, align = 'center', ...props }: DropdownProps) => { | ||
const [showModal, setShowModal] = useModal(); | ||
const [toggleRect, setToggleRect] = React.useState<DOMRect>(); | ||
|
||
return ( | ||
<DropdownContext.Provider value={{ showModal, setShowModal, toggleRect, setToggleRect, align }} {...props}> | ||
<PortalProvider align={align} {...props}> | ||
{children} | ||
</DropdownContext.Provider> | ||
</PortalProvider> | ||
); | ||
}; |
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 |
---|---|---|
@@ -1,109 +1,20 @@ | ||
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 { composeRefs } from '@/libs/ref'; | ||
|
||
import DropdownContext from './DropdownContext'; | ||
import { PortalContent } from '@/components/portal/PortalContent'; | ||
|
||
interface ModalProps extends React.ComponentPropsWithoutRef<'div'> { | ||
width?: React.CSSProperties['width']; | ||
} | ||
|
||
type PositionType = { | ||
x: number; | ||
y: number; | ||
}; | ||
|
||
/** | ||
* DropdownContent | ||
* @returns | ||
*/ | ||
export const DropdownContent = React.forwardRef<HTMLDivElement, ModalProps>(({ width, ...props }, ref) => { | ||
const { showModal, setShowModal, toggleRect, align } = useContext(DropdownContext); | ||
const modalRef = React.useRef<HTMLDivElement | null>(null); | ||
const [reorgPos, setReorgPos] = React.useState<PositionType>({ x: 0, y: 0 }); | ||
|
||
const close = () => { | ||
setShowModal(false); | ||
}; | ||
|
||
React.useEffect(() => { | ||
if (modalRef.current && toggleRect && showModal) { | ||
const rect = modalRef.current; | ||
|
||
const isOverflowing = rect.offsetHeight + toggleRect.bottom >= window.innerHeight; | ||
const reorgPos = { x: 0, y: 0 }; | ||
|
||
switch (align) { | ||
case 'center': | ||
reorgPos.x = toggleRect.x + toggleRect.width / 2 - rect.clientWidth / 2; | ||
break; | ||
case 'start': | ||
reorgPos.x = toggleRect.x; | ||
break; | ||
case 'end': | ||
reorgPos.x = toggleRect.x + toggleRect.width - rect.clientWidth; | ||
break; | ||
} | ||
|
||
if (isOverflowing) { | ||
reorgPos.y = toggleRect.y - rect.offsetHeight; | ||
} else { | ||
reorgPos.y = toggleRect.y + toggleRect.height; | ||
} | ||
|
||
setReorgPos(reorgPos); | ||
} | ||
}, [align, showModal, toggleRect]); | ||
|
||
export const DropdownContent = React.forwardRef<HTMLDivElement, ModalProps>(({ width, children, ...props }, ref) => { | ||
return ( | ||
<> | ||
{showModal && | ||
createPortal( | ||
<> | ||
<ModalBG onClick={close} className="bmates-modal-bg" /> | ||
<Modal ref={composeRefs(modalRef, ref)} width={width} position={reorgPos} {...props}> | ||
{props.children} | ||
</Modal> | ||
</>, | ||
document.body, | ||
)} | ||
</> | ||
<PortalContent width={width} ref={ref} {...props}> | ||
{children} | ||
</PortalContent> | ||
); | ||
}); | ||
DropdownContent.displayName = 'DropdownContent'; | ||
|
||
const enter = keyframes` | ||
0% { opacity: 0; } | ||
100% { opacity: 1; } | ||
`; | ||
|
||
const ModalBG = styled.div` | ||
position: fixed; | ||
background-color: transparent; | ||
pointer-events: auto; | ||
z-index: 50; | ||
inset: 0; | ||
`; | ||
|
||
const Modal = styled.div<{ width?: React.CSSProperties['width']; position: PositionType }>` | ||
display: grid; | ||
min-width: max-content; | ||
${({ width }) => width && `width: ${typeof width === 'string' ? width : `${width}px`};`} | ||
padding: 0.25rem; | ||
border-radius: 0.5rem; | ||
position: fixed; | ||
top: 0px; | ||
left: 0px; | ||
transform: ${({ position }) => `translate(${position.x}px, ${position.y}px)`}; | ||
background-color: white; | ||
pointer-events: auto; | ||
z-index: 50; | ||
animation-name: ${enter}; | ||
animation-duration: 0.15s; | ||
border: 1px solid ${({ theme }) => theme.colors.gray['300']}; | ||
box-shadow: 0px 2px 2px 0px ${({ theme }) => theme.colors.gray['300']}; | ||
`; |
This file was deleted.
Oops, something went wrong.
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
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 |
---|---|---|
@@ -1,30 +1,28 @@ | ||
import * as React from 'react'; | ||
|
||
import useModal from '@/hooks/useModal'; | ||
import { PortalProvider } from '@/components/portal/PortalProvider'; | ||
import { AlignType } from '@/types/align'; | ||
|
||
import SelectContext from './SelectContext'; | ||
import { SelectAlignType, SelectType } from './type'; | ||
import { SelectType } from './type'; | ||
|
||
interface SelectProps extends React.ComponentPropsWithoutRef<'div'> { | ||
interface SelectProps extends React.PropsWithChildren { | ||
multi?: boolean; | ||
align?: SelectAlignType; | ||
align?: AlignType; | ||
} | ||
|
||
/** | ||
* Displays a list of options. | ||
* @returns | ||
*/ | ||
export const Select = ({ children, align = 'center', multi = false, ...props }: SelectProps) => { | ||
const [showModal, setShowModal] = useModal(); | ||
const [toggleRect, setToggleRect] = React.useState<DOMRect>(); | ||
const [selectedValue, setSelectedValue] = React.useState<SelectType[]>([]); | ||
|
||
return ( | ||
<SelectContext.Provider | ||
value={{ showModal, setShowModal, toggleRect, setToggleRect, align, multi, selectedValue, setSelectedValue }} | ||
{...props} | ||
> | ||
{children} | ||
</SelectContext.Provider> | ||
<PortalProvider align={align}> | ||
<SelectContext.Provider value={{ multi, selectedValue, setSelectedValue }} {...props}> | ||
{children} | ||
</SelectContext.Provider> | ||
</PortalProvider> | ||
); | ||
}; |
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 |
---|---|---|
@@ -1,116 +1,26 @@ | ||
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 { composeRefs } from '@/libs/ref'; | ||
|
||
import SelectContext from './SelectContext'; | ||
import { PortalContent } from '@/components/portal/PortalContent'; | ||
|
||
interface ModalProps extends React.ComponentPropsWithoutRef<'ul'> { | ||
width?: React.CSSProperties['width']; | ||
} | ||
|
||
type PositionType = { | ||
x: number; | ||
y: number; | ||
}; | ||
|
||
/** | ||
* SelectContent | ||
* @returns | ||
*/ | ||
export const SelectContent = React.forwardRef<HTMLUListElement, ModalProps>(({ width, ...props }, ref) => { | ||
const { showModal, setShowModal, toggleRect, align } = useContext(SelectContext); | ||
const modalRef = React.useRef<HTMLUListElement | null>(null); | ||
const [reorgPos, setReorgPos] = React.useState<PositionType>({ x: 0, y: 0 }); | ||
|
||
const close = () => { | ||
setShowModal(false); | ||
}; | ||
|
||
React.useEffect(() => { | ||
if (modalRef.current && toggleRect && showModal) { | ||
const rect = modalRef.current; | ||
|
||
const isOverflowing = rect.offsetHeight + toggleRect.bottom >= window.innerHeight; | ||
const reorgPos = { x: 0, y: 0 }; | ||
|
||
switch (align) { | ||
case 'center': | ||
reorgPos.x = toggleRect.x + toggleRect.width / 2 - rect.clientWidth / 2; | ||
break; | ||
case 'start': | ||
reorgPos.x = toggleRect.x; | ||
break; | ||
case 'end': | ||
reorgPos.x = toggleRect.x + toggleRect.width - rect.clientWidth; | ||
break; | ||
} | ||
|
||
if (isOverflowing) { | ||
reorgPos.y = toggleRect.y - rect.offsetHeight; | ||
} else { | ||
reorgPos.y = toggleRect.y + toggleRect.height; | ||
} | ||
|
||
setReorgPos(reorgPos); | ||
} | ||
}, [align, showModal, toggleRect]); | ||
|
||
export const SelectContent = React.forwardRef<HTMLDivElement, ModalProps>(({ width, children, ...props }, ref) => { | ||
return ( | ||
<> | ||
{showModal && | ||
createPortal( | ||
<> | ||
<ModalBG onClick={close} className="bmates-modal-bg" /> | ||
<Modal | ||
ref={composeRefs(modalRef, ref)} | ||
width={width || toggleRect?.width} | ||
position={reorgPos} | ||
role="listbox" | ||
{...props} | ||
> | ||
{props.children} | ||
</Modal> | ||
</>, | ||
document.body, | ||
)} | ||
</> | ||
<PortalContent width={width} ref={ref} {...props}> | ||
<SelectListBox role="listbox">{children}</SelectListBox> | ||
</PortalContent> | ||
); | ||
}); | ||
SelectContent.displayName = 'SelectContent'; | ||
|
||
const enter = keyframes` | ||
0% { opacity: 0; } | ||
100% { opacity: 1; } | ||
`; | ||
|
||
const ModalBG = styled.div` | ||
position: fixed; | ||
background-color: transparent; | ||
pointer-events: auto; | ||
z-index: 50; | ||
inset: 0; | ||
`; | ||
|
||
const Modal = styled.ul<{ width?: React.CSSProperties['width']; position: PositionType }>` | ||
display: grid; | ||
const SelectListBox = styled.ul` | ||
margin: 0px; | ||
min-width: max-content; | ||
${({ width }) => width && `width: ${typeof width === 'string' ? width : `${width}px`};`} | ||
padding: 0.25rem; | ||
border-radius: 0.375rem; | ||
position: fixed; | ||
top: 0px; | ||
left: 0px; | ||
transform: ${({ position }) => `translate(${position.x}px, ${position.y}px)`}; | ||
background-color: white; | ||
pointer-events: auto; | ||
z-index: 50; | ||
animation-name: ${enter}; | ||
animation-duration: 0.15s; | ||
border: 1px solid ${({ theme }) => theme.colors.gray['300']}; | ||
box-shadow: 0px 2px 2px 0px ${({ theme }) => theme.colors.gray['300']}; | ||
padding: 0px; | ||
`; |
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
Oops, something went wrong.