Skip to content

Commit

Permalink
Merge pull request #100 from YAPP-Github/feature/FE-061
Browse files Browse the repository at this point in the history
Feature/fe-061 Dropdown 수정사항 반영
  • Loading branch information
naro-Kim authored Jul 11, 2023
2 parents 463b3d3 + 61dca9a commit d0feccc
Show file tree
Hide file tree
Showing 19 changed files with 506 additions and 172 deletions.
8 changes: 5 additions & 3 deletions src/components/BottomSheet/BottomSheet.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { themeTokens } from '@/styles/theme.css';
import { style, globalStyle } from '@vanilla-extract/css';
import { fontVariant } from '@/styles/variant.css';
import { recipe } from '@vanilla-extract/recipes';
import { sizeProp } from '@/utils/sizeProp';
const { color, zIndices } = themeTokens;

export const titleWrapper = style({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '30px',
padding: '20px 20px 30px 20px',
});
globalStyle(`${titleWrapper} > div`, {
...fontVariant.title3,
Expand Down Expand Up @@ -46,13 +47,14 @@ export const wrap = recipe({
position: 'fixed',
display: 'flex',
bottom: '0',
left: '0',
width: '100%',
background: color.white,
zIndex: zIndices.modal,
padding: '20px',
borderRadius: '14px 14px 0px 0px',
flexDirection: 'column',
justifyContent: 'flex-end',
justifyContent: 'flex-start',
minHeight: sizeProp('632px'),
},
variants: {
open: {
Expand Down
29 changes: 6 additions & 23 deletions src/components/BottomSheet/BottomSheet.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,15 @@
'use client';
import { HTMLAttributes } from 'react';
import cx from 'classnames';
import { titleWrapper, background, wrap } from './BottomSheet.css';
import IconButton from '../IconButton/IconButton';
import { useLockScroll } from '@/hooks';

import { ComponentProps } from 'react';
import BottomSheetView from './BottomSheetView';
import { useModalControl } from './hooks';

type BottomSheetProps = {
onClose: () => void;
title?: string;
} & HTMLAttributes<HTMLDivElement>;
type BottomSheetProps = Omit<ComponentProps<typeof BottomSheetView>, 'isOpen'>;

const BottomSheet = ({ className, onClose, title, children, ...rest }: BottomSheetProps) => {
useLockScroll();
const BottomSheet = ({ title, onClose, ...rest }: BottomSheetProps) => {
const { ref, isOpen, closeModalWithTransition } = useModalControl({ onClose });

return (
<>
<div className={cx(background({ open: isOpen }))} data-testid="outside"></div>
<div className={cx(wrap({ open: isOpen }), className)} {...rest} ref={ref}>
<div className={titleWrapper}>
<div>{title}</div>
<IconButton iconType="close" width={36} height={36} onClick={closeModalWithTransition}></IconButton>
</div>
{children}
</div>
</>
);
return <BottomSheetView ref={ref} title={title} isOpen={isOpen} onClose={closeModalWithTransition} {...rest} />;
};

export default BottomSheet;
39 changes: 39 additions & 0 deletions src/components/BottomSheet/BottomSheetView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { HTMLAttributes, forwardRef } from 'react';
import cx from 'classnames';
import { IconButton } from '@/components';
import { useLockScroll } from '@/hooks';
import { titleWrapper, background, wrap } from './BottomSheet.css';

interface Props extends HTMLAttributes<HTMLDivElement> {
/** BottomSheet 제목으로 표시할 텍스트 */
title?: string;

/** bottomsheet 열림/닫힘 상태 */
isOpen: boolean;

/** bottomsheet 닫을때 호출되는 함수 */
onClose: () => void;
}

const BottomSheetView = forwardRef<HTMLDivElement, Props>(
({ title, isOpen, children, onClose, className, ...rest }, ref) => {
useLockScroll();

return (
<>
<div className={cx(background({ open: isOpen }))} data-testid="outside" onClick={onClose}></div>
<div className={cx(wrap({ open: isOpen }), className)} {...rest} ref={ref}>
<div className={titleWrapper}>
<div>{title}</div>
<IconButton iconType="close" width={36} height={36} onClick={onClose}></IconButton>
</div>
{children}
</div>
</>
);
},
);

BottomSheetView.displayName = 'BottomSheetView';

export default BottomSheetView;
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { useState, useEffect, useRef } from 'react';
import { useClickOutside } from '@/hooks';

interface Props {
onClose: () => void;
}
const useModalControl = ({ onClose }: Props) => {
const ref = useRef<HTMLDivElement>(null);
const [isOpen, setIsOpen] = useState(false);
const closeStatus = useRef(false);
const closeModalWithTransition = () => {
Expand All @@ -14,11 +14,7 @@ const useModalControl = ({ onClose }: Props) => {
const openModalWithTransition = () => {
setIsOpen(true);
};
const ref = useClickOutside<HTMLDivElement>({
onClickOutside: () => {
closeModalWithTransition();
},
});

const handleTransitionEnd = (event: TransitionEvent) => {
if (event.propertyName === 'transform' && closeStatus.current) {
onClose();
Expand Down
54 changes: 42 additions & 12 deletions src/components/Dropdown/Dropdown.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { style } from '@vanilla-extract/css';
import { themeTokens } from '@/styles/theme.css';
import { fontVariant } from '@/styles/variant.css';
import { RecipeVariants, recipe } from '@vanilla-extract/recipes';
import { sizeProp } from '@/utils/sizeProp';

const { color, space, borderRadius, zIndices } = themeTokens;

Expand All @@ -10,17 +11,15 @@ export const wrapper = style({
});

export const buttonBase = style({
height: '3.5rem',
width: '100%',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: `0 ${space.lg}`,
width: '100%',
backgroundColor: color.grey50,
borderRadius: borderRadius.md,
cursor: 'pointer',
border: `1px solid ${color.grey100}`,
color: color.sub,
padding: `0 ${space.lg}`,
});

export const buttonVariant = recipe({
Expand All @@ -30,8 +29,19 @@ export const buttonVariant = recipe({
borderColor: color.red500,
},
},
size: {
medium: {
height: '3.5rem',
borderRadius: borderRadius.md,
},
small: {
height: '2.5rem',
borderRadius: borderRadius.xs,
},
},
},
defaultVariants: {
size: 'medium',
isError: false,
},
});
Expand Down Expand Up @@ -80,13 +90,7 @@ export const menu = recipe({
overflow: 'auto',
selectors: {
'&::-webkit-scrollbar': {
width: '12px',
},
'&::-webkit-scrollbar-thumb': {
borderRadius: borderRadius.pill,
border: `4px solid rgba(0,0,0,0)`,
backgroundClip: 'padding-box',
backgroundColor: color.grey300,
display: 'none',
},
},
},
Expand Down Expand Up @@ -133,7 +137,6 @@ export const item = recipe({
base: {
...fontVariant.label2,
margin: '0',
padding: `${space.md} ${space.lg}`,
listStyleType: 'none',
cursor: 'pointer',
display: 'flex',
Expand All @@ -159,15 +162,42 @@ export const item = recipe({
},
},
},
size: {
medium: {
padding: `${space.md} ${space.lg}`,
},
small: {
padding: `${space.md} ${space.md}`,
},
},
},
defaultVariants: {
selected: false,
size: 'small',
},
});

export const checkedIconColor = style({
color: color.grey500,
});

export const modal = style({
width: 'calc(100% - 40px) !important',
padding: `${space.sm} !important`,
maxHeight: sizeProp('540px'),
overflow: 'scroll',
selectors: {
'&::-webkit-scrollbar': {
display: 'none',
},
},
});

export const modalContent = style({
margin: '0 !important',
});

export type MenuVariant = RecipeVariants<typeof menu>;
export type ButtonVariant = RecipeVariants<typeof buttonVariant>;
export type Size = NonNullable<ButtonVariant>['size'];
export type Placement = NonNullable<MenuVariant>['placement'];
56 changes: 56 additions & 0 deletions src/components/Dropdown/DropdownBottomSheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use client';

import cx from 'classnames';
import BottomSheetView from '@/components/BottomSheet/BottomSheetView';
import { HTMLAttributes, useMemo, forwardRef, useImperativeHandle, ComponentProps } from 'react';
import { useDropdownContext } from './contexts/DropdownContext';
import { type DropdownMenuContextValue, DropdownMenuContextProvider } from './contexts/DropdownMenuContext';
import useDropdownBottomSheet from './hooks/useDropdownBottomSheet/useDropdownBottomSheet';

interface Props
extends HTMLAttributes<HTMLDivElement>,
Partial<DropdownMenuContextValue>,
Omit<ComponentProps<typeof BottomSheetView>, 'isOpen' | 'onClose'> {}

export interface DropdownMenuHandle {
/** menu를 닫는다 */
close: () => void;
}

const DropdownBottomSheet = forwardRef<DropdownMenuHandle, Props>(
({ children, className, selectable = false, selectedItemKey = null, onSelectChange = () => null, ...rest }, ref) => {
const { isOpen: isDropdownOpen, closeDropdown } = useDropdownContext();

const [bottomSheetRef, isBottmSheetOpen, , closeBottomSheet] = useDropdownBottomSheet({
isDropdownOpen,
onClose: closeDropdown,
});

const menuContextValue = useMemo(
() => ({ selectable, selectedItemKey, onSelectChange, closeDropdown: closeBottomSheet }),
[selectable, selectedItemKey, onSelectChange, closeBottomSheet],
);

useImperativeHandle(ref, () => ({
close: closeBottomSheet,
}));

return (
<DropdownMenuContextProvider value={menuContextValue}>
<BottomSheetView
ref={bottomSheetRef}
isOpen={isBottmSheetOpen}
onClose={closeBottomSheet}
className={cx(className)}
{...rest}
>
{children}
</BottomSheetView>
</DropdownMenuContextProvider>
);
},
);

DropdownBottomSheet.displayName = 'DropdownBottomSheet';

export default DropdownBottomSheet;
30 changes: 23 additions & 7 deletions src/components/Dropdown/DropdownButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,38 @@

import cx from 'classnames';
import { ButtonHTMLAttributes } from 'react';
import { buttonBase, buttonVariant, buttonText } from './Dropdown.css';
import { buttonBase, buttonVariant, buttonText, type Size } from './Dropdown.css';
import CaretDownIcon from './Icons/CaretDownIcon';
import DropdownToggle from './DropdownToggle';

/**
* @property {string} placeholder? - 선택된 요소가 없을때 기본적으로 버튼에 표시할 텍스트
*/

interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
/**
* 선택된 요소가 없을때 기본적으로 버튼에 표시할 텍스트 @default 선택
*/
placeholder?: string;

/**
* 버튼의 크기를 설정한다. @default medium
*/
size?: Size;

/**
* 에러 상태의 유무 @default false
*/
isError?: boolean;
}

const DropdownButton = ({ children, placeholder = '선택', onClick, isError, ...rest }: Props) => {
const DropdownButton = ({
children,
placeholder = '선택',
onClick,
isError = false,
size,
className,
...rest
}: Props) => {
return (
<DropdownToggle className={cx(buttonBase, buttonVariant({ isError }))} onClick={onClick} {...rest}>
<DropdownToggle className={cx(buttonBase, buttonVariant({ isError, size }), className)} onClick={onClick} {...rest}>
<span className={buttonText}>{children ?? placeholder}</span>
<span aria-hidden={true}>
<CaretDownIcon size={24} />
Expand Down
21 changes: 12 additions & 9 deletions src/components/Dropdown/DropdownItem.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
'use client';

import { LiHTMLAttributes, MouseEvent } from 'react';
import { item, checkedIconColor } from './Dropdown.css';
import { useDropdownContext } from './contexts/DropdownContext';
import { item, checkedIconColor, type Size } from './Dropdown.css';

import clsx from 'classnames';
import { useDropdownMenuContext } from './contexts/DropdownMenuContext';
import CheckedIcon from './Icons/CheckedIcon';

/**
* @property {string} itemKey - Item 별로 가지는 고유값 (선택 state 값에 사용됨)
*/
interface Props extends LiHTMLAttributes<HTMLLIElement> {
/**
* Item 별로 가지는 고유값 (선택 state 값에 사용됨)
*/
itemKey: string;

/**
* Item의 크기를 설정한다. @default medium
*/
size?: Size;
}

const DropdownItem = ({ children, className, onClick, itemKey, ...rest }: Props) => {
const { closeDropdown } = useDropdownContext();
const { selectable, selectedItemKey, onSelectChange } = useDropdownMenuContext();
const DropdownItem = ({ children, className, onClick, itemKey, size, ...rest }: Props) => {
const { selectable, selectedItemKey, onSelectChange, closeDropdown } = useDropdownMenuContext();

const handleClickItem = (e: MouseEvent<HTMLLIElement>) => {
if (selectable) onSelectChange(selectedItemKey === itemKey ? null : itemKey);
Expand All @@ -29,7 +32,7 @@ const DropdownItem = ({ children, className, onClick, itemKey, ...rest }: Props)

return (
<li
className={clsx(item({ selected: isSelected }), className)}
className={clsx(item({ selected: isSelected, size }), className)}
tabIndex={0}
role="menuitemradio"
aria-checked={isSelected}
Expand Down
Loading

0 comments on commit d0feccc

Please sign in to comment.