diff --git a/src/components/common/Dropdown/Dropdown.tsx b/src/components/common/Dropdown/Dropdown.tsx index e5536de..a50d706 100644 --- a/src/components/common/Dropdown/Dropdown.tsx +++ b/src/components/common/Dropdown/Dropdown.tsx @@ -1,11 +1,10 @@ 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; } /** @@ -13,12 +12,9 @@ interface DropdownProps extends React.ComponentPropsWithoutRef<'div'> { * @returns */ export const Dropdown = ({ children, align = 'center', ...props }: DropdownProps) => { - const [showModal, setShowModal] = useModal(); - const [toggleRect, setToggleRect] = React.useState(); - return ( - + {children} - + ); }; diff --git a/src/components/common/Dropdown/DropdownContent.tsx b/src/components/common/Dropdown/DropdownContent.tsx index ebaac9d..744cf59 100644 --- a/src/components/common/Dropdown/DropdownContent.tsx +++ b/src/components/common/Dropdown/DropdownContent.tsx @@ -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(({ width, ...props }, ref) => { - const { showModal, setShowModal, toggleRect, align } = useContext(DropdownContext); - const modalRef = React.useRef(null); - const [reorgPos, setReorgPos] = React.useState({ 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(({ width, children, ...props }, ref) => { return ( - <> - {showModal && - createPortal( - <> - - - {props.children} - - , - document.body, - )} - + + {children} + ); }); 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']}; -`; diff --git a/src/components/common/Dropdown/DropdownContext.tsx b/src/components/common/Dropdown/DropdownContext.tsx deleted file mode 100644 index 6a761cb..0000000 --- a/src/components/common/Dropdown/DropdownContext.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import * as React from 'react'; - -export type DropdownAlignType = 'start' | 'center' | 'end'; - -interface DropdownContextType { - showModal: boolean; - setShowModal: (value: boolean) => void; - toggleRect: DOMRect | undefined; - setToggleRect: (value: DOMRect) => void; - align: DropdownAlignType; -} -const DropdownContext = React.createContext(null); -export default DropdownContext; diff --git a/src/components/common/Dropdown/DropdownItem.tsx b/src/components/common/Dropdown/DropdownItem.tsx index 6c4fca3..ef1172b 100644 --- a/src/components/common/Dropdown/DropdownItem.tsx +++ b/src/components/common/Dropdown/DropdownItem.tsx @@ -2,17 +2,16 @@ import { css } from '@emotion/react'; import styled from '@emotion/styled'; import * as React from 'react'; +import { PortalContext } from '@/components/portal/PortalContext'; import useContext from '@/hooks/useContext'; import { composeEventHandlers } from '@/libs/event'; -import DropdownContext from './DropdownContext'; - interface DropdownItemProps extends React.ComponentPropsWithoutRef<'div'> { disabled?: boolean; } export const DropdownItem = React.forwardRef( ({ disabled = false, ...props }, ref) => { - const { setShowModal } = useContext(DropdownContext); + const { setShowModal } = useContext(PortalContext); const onClickHandler = () => { if (!disabled) setShowModal(false); diff --git a/src/components/common/Dropdown/DropdownToggle.tsx b/src/components/common/Dropdown/DropdownToggle.tsx index 7a90356..9d8fbb9 100644 --- a/src/components/common/Dropdown/DropdownToggle.tsx +++ b/src/components/common/Dropdown/DropdownToggle.tsx @@ -1,12 +1,12 @@ import * as React from 'react'; import Slot from '@/components/Slot'; +import { PortalContext } from '@/components/portal/PortalContext'; import useContext from '@/hooks/useContext'; import { composeEventHandlers } from '@/libs/event'; import { composeRefs } from '@/libs/ref'; import { Button } from '../../'; -import DropdownContext from './DropdownContext'; type ComponentPropsWithoutRef = React.ComponentPropsWithoutRef & { asChild?: boolean; @@ -18,7 +18,7 @@ type ComponentPropsWithoutRef = React.ComponentProp */ export const DropdownToggle = React.forwardRef>( ({ asChild, onClick, ...props }, ref) => { - const { setShowModal, setToggleRect } = useContext(DropdownContext); + const { setShowModal, setToggleRect } = useContext(PortalContext); const compRef = React.useRef(null); const Comp = asChild ? Slot : Button; diff --git a/src/components/common/Select/Select.tsx b/src/components/common/Select/Select.tsx index d94b488..1685b98 100644 --- a/src/components/common/Select/Select.tsx +++ b/src/components/common/Select/Select.tsx @@ -1,13 +1,14 @@ 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; } /** @@ -15,16 +16,13 @@ interface SelectProps extends React.ComponentPropsWithoutRef<'div'> { * @returns */ export const Select = ({ children, align = 'center', multi = false, ...props }: SelectProps) => { - const [showModal, setShowModal] = useModal(); - const [toggleRect, setToggleRect] = React.useState(); const [selectedValue, setSelectedValue] = React.useState([]); return ( - - {children} - + + + {children} + + ); }; diff --git a/src/components/common/Select/SelectContent.tsx b/src/components/common/Select/SelectContent.tsx index f87c7b1..4bd7b43 100644 --- a/src/components/common/Select/SelectContent.tsx +++ b/src/components/common/Select/SelectContent.tsx @@ -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(({ width, ...props }, ref) => { - const { showModal, setShowModal, toggleRect, align } = useContext(SelectContext); - const modalRef = React.useRef(null); - const [reorgPos, setReorgPos] = React.useState({ 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(({ width, children, ...props }, ref) => { return ( - <> - {showModal && - createPortal( - <> - - - {props.children} - - , - document.body, - )} - + + {children} + ); }); 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; `; diff --git a/src/components/common/Select/SelectContext.tsx b/src/components/common/Select/SelectContext.tsx index 5064de8..211d343 100644 --- a/src/components/common/Select/SelectContext.tsx +++ b/src/components/common/Select/SelectContext.tsx @@ -1,26 +1,8 @@ import * as React from 'react'; -import { SelectAlignType, SelectType } from './type'; +import { SelectType } from './type'; interface SelectContextType { - /* - * Modal - */ - showModal: boolean; - setShowModal: (value: boolean) => void; - - /* - * Toggle Rect - * - to calcurate element size - */ - toggleRect: DOMRect | undefined; - setToggleRect: (value: DOMRect) => void; - - /* - * Align - */ - align: SelectAlignType; - /* * Multi select */ diff --git a/src/components/common/Select/SelectItem.tsx b/src/components/common/Select/SelectItem.tsx index 14caf65..9c0a5e5 100644 --- a/src/components/common/Select/SelectItem.tsx +++ b/src/components/common/Select/SelectItem.tsx @@ -2,6 +2,7 @@ import { css } from '@emotion/react'; import styled from '@emotion/styled'; import * as React from 'react'; +import { PortalContext } from '@/components/portal/PortalContext'; import useContext from '@/hooks/useContext'; import { composeEventHandlers } from '@/libs/event'; @@ -14,7 +15,8 @@ interface SelectItemProps extends React.ComponentPropsWithoutRef<'li'> { } export const SelectItem = React.forwardRef( ({ disabled = false, children, value, ...props }, ref) => { - const { setShowModal, selectedValue, setSelectedValue, multi } = useContext(SelectContext); + const { setShowModal } = useContext(PortalContext); + const { selectedValue, setSelectedValue, multi } = useContext(SelectContext); const selected = selectedValue?.some(v => v.value === value) || false; const onClickHandler = () => { diff --git a/src/components/common/Select/SelectToggle.tsx b/src/components/common/Select/SelectToggle.tsx index 1d5e7fe..2e4fb4e 100644 --- a/src/components/common/Select/SelectToggle.tsx +++ b/src/components/common/Select/SelectToggle.tsx @@ -2,6 +2,7 @@ import styled from '@emotion/styled'; import * as React from 'react'; import Slot from '@/components/Slot'; +import { PortalContext } from '@/components/portal/PortalContext'; import useContext from '@/hooks/useContext'; import { composeEventHandlers } from '@/libs/event'; import { composeRefs } from '@/libs/ref'; @@ -19,7 +20,8 @@ type ComponentPropsWithoutRef = React.ComponentProp */ export const SelectToggle = React.forwardRef>( ({ asChild, onClick, children, ...props }, ref) => { - const { setShowModal, setToggleRect, selectedValue } = useContext(SelectContext); + const { setShowModal, setToggleRect } = useContext(PortalContext); + const { selectedValue } = useContext(SelectContext); const compRef = React.useRef(null); const Comp = asChild ? Slot : Button; diff --git a/src/components/common/Select/type.ts b/src/components/common/Select/type.ts index fd7ab4a..eb89dd6 100644 --- a/src/components/common/Select/type.ts +++ b/src/components/common/Select/type.ts @@ -1,7 +1,5 @@ import { ReactNode } from 'react'; -export type SelectAlignType = 'start' | 'center' | 'end'; - export type SelectType = { name: ReactNode; value: string | number | readonly string[]; diff --git a/src/components/portal/PortalContent.tsx b/src/components/portal/PortalContent.tsx new file mode 100644 index 0000000..50c051e --- /dev/null +++ b/src/components/portal/PortalContent.tsx @@ -0,0 +1,75 @@ +import { keyframes } from '@emotion/react'; +import styled from '@emotion/styled'; +import React from 'react'; +import { createPortal } from 'react-dom'; + +import { usePortal } from '@/components/portal/usePortal'; +import useContext from '@/hooks/useContext'; +import { composeRefs } from '@/libs/ref'; +import { PositionType } from '@/types/position'; + +import { PortalContext } from './PortalContext'; + +interface ModalContentProps { + children: React.ReactNode; + ref: React.ForwardedRef; + width?: React.CSSProperties['width']; +} + +export const PortalContent = ({ children, ref, width }: ModalContentProps) => { + const { showModal, setShowModal, toggleRect } = useContext(PortalContext)!; + const modalRef = React.useRef(null); + + const close = () => { + setShowModal(false); + }; + + const { reorgPos } = usePortal({ modalRef }); + + return ( + <> + {showModal && + createPortal( + <> + + + {children} + + , + document.body, + )} + + ); +}; + +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.25rem; + 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']}; +`; diff --git a/src/components/portal/PortalContext.tsx b/src/components/portal/PortalContext.tsx new file mode 100644 index 0000000..ac097d2 --- /dev/null +++ b/src/components/portal/PortalContext.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +import { AlignType } from '@/types/align'; + +interface PortalContextType { + /* + * Modal + */ + showModal: boolean; + setShowModal: (value: boolean) => void; + + /* + * Toggle Rect + * - to calcurate element size + */ + toggleRect: DOMRect | undefined; + setToggleRect: (value: DOMRect) => void; + + /* + * Align + */ + align: AlignType; +} +export const PortalContext = React.createContext(null); diff --git a/src/components/portal/PortalProvider.tsx b/src/components/portal/PortalProvider.tsx new file mode 100644 index 0000000..2dfe676 --- /dev/null +++ b/src/components/portal/PortalProvider.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +import useModal from '@/hooks/useModal'; +import { AlignType } from '@/types/align'; + +import { PortalContext } from './PortalContext'; + +interface PortalProviderProps extends React.PropsWithChildren { + align: AlignType; +} + +export const PortalProvider = ({ align, children }: PortalProviderProps) => { + const [showModal, setShowModal] = useModal(); + const [toggleRect, setToggleRect] = React.useState(); + + return ( + + {children} + + ); +}; diff --git a/src/components/portal/usePortal.ts b/src/components/portal/usePortal.ts new file mode 100644 index 0000000..bbd34d9 --- /dev/null +++ b/src/components/portal/usePortal.ts @@ -0,0 +1,40 @@ +import React from 'react'; + +import { PortalContext } from '@/components/portal/PortalContext'; +import { PositionType } from '@/types/position'; + +export const usePortal = ({ modalRef }: { modalRef: React.RefObject }) => { + const { showModal, align, toggleRect, setShowModal, setToggleRect } = React.useContext(PortalContext)!; + const [reorgPos, setReorgPos] = React.useState({ x: 0, y: 0 }); + + 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, modalRef, setReorgPos]); + + return { showModal, align, toggleRect, setShowModal, setToggleRect, reorgPos }; +}; diff --git a/src/types/align.ts b/src/types/align.ts new file mode 100644 index 0000000..c9acf6c --- /dev/null +++ b/src/types/align.ts @@ -0,0 +1 @@ +export type AlignType = 'start' | 'center' | 'end'; diff --git a/src/types/position.ts b/src/types/position.ts new file mode 100644 index 0000000..192a03c --- /dev/null +++ b/src/types/position.ts @@ -0,0 +1,4 @@ +export type PositionType = { + x: number; + y: number; +};