Skip to content

Commit

Permalink
refactor: select, dropdown refact to portal
Browse files Browse the repository at this point in the history
  • Loading branch information
kyechan99 committed Mar 3, 2024
1 parent 72a0cbd commit bf6a457
Show file tree
Hide file tree
Showing 17 changed files with 211 additions and 253 deletions.
14 changes: 5 additions & 9 deletions src/components/common/Dropdown/Dropdown.tsx
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>
);
};
99 changes: 5 additions & 94 deletions src/components/common/Dropdown/DropdownContent.tsx
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']};
`;
13 changes: 0 additions & 13 deletions src/components/common/Dropdown/DropdownContext.tsx

This file was deleted.

5 changes: 2 additions & 3 deletions src/components/common/Dropdown/DropdownItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLDivElement, DropdownItemProps>(
({ disabled = false, ...props }, ref) => {
const { setShowModal } = useContext(DropdownContext);
const { setShowModal } = useContext(PortalContext);

const onClickHandler = () => {
if (!disabled) setShowModal(false);
Expand Down
4 changes: 2 additions & 2 deletions src/components/common/Dropdown/DropdownToggle.tsx
Original file line number Diff line number Diff line change
@@ -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<E extends React.ElementType> = React.ComponentPropsWithoutRef<E> & {
asChild?: boolean;
Expand All @@ -18,7 +18,7 @@ type ComponentPropsWithoutRef<E extends React.ElementType> = React.ComponentProp
*/
export const DropdownToggle = React.forwardRef<HTMLButtonElement, ComponentPropsWithoutRef<'button'>>(
({ asChild, onClick, ...props }, ref) => {
const { setShowModal, setToggleRect } = useContext(DropdownContext);
const { setShowModal, setToggleRect } = useContext(PortalContext);
const compRef = React.useRef<HTMLButtonElement | null>(null);

const Comp = asChild ? Slot : Button;
Expand Down
22 changes: 10 additions & 12 deletions src/components/common/Select/Select.tsx
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>
);
};
104 changes: 7 additions & 97 deletions src/components/common/Select/SelectContent.tsx
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;
`;
20 changes: 1 addition & 19 deletions src/components/common/Select/SelectContext.tsx
Original file line number Diff line number Diff line change
@@ -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
*/
Expand Down
Loading

0 comments on commit bf6a457

Please sign in to comment.