From 5e551955b2d7dbb184de56b88b1f422ece336f46 Mon Sep 17 00:00:00 2001 From: JinJu Date: Thu, 13 Jun 2024 18:37:10 +0900 Subject: [PATCH] Feat Dropdown Multiple --- src/core/components/Chip/index.tsx | 12 +- src/core/components/Chip/types/index.ts | 4 +- .../Dropdown/DropdownBase/DropdownItem.tsx | 16 ++- .../Dropdown/DropdownBase/index.tsx | 8 ++ .../Dropdown/DropdownBase/types/index.ts | 29 +++-- .../DropdownMultiple.stories.tsx | 74 ++++++++++++ .../DropdownMultiple/DropdownMultipleItem.tsx | 36 ++++++ .../DropdownMultipleTrigger.tsx | 105 ++++++++++++++++++ .../DropdownMultiple/constants/index.ts | 4 + .../Dropdown/DropdownMultiple/index.tsx | 17 +++ .../Dropdown/DropdownMultiple/types/index.ts | 34 ++++++ .../Dropdown/DropdownSelect/types/index.ts | 4 +- src/index.ts | 4 + 13 files changed, 326 insertions(+), 21 deletions(-) create mode 100644 src/core/components/Dropdown/DropdownMultiple/DropdownMultiple.stories.tsx create mode 100644 src/core/components/Dropdown/DropdownMultiple/DropdownMultipleItem.tsx create mode 100644 src/core/components/Dropdown/DropdownMultiple/DropdownMultipleTrigger.tsx create mode 100644 src/core/components/Dropdown/DropdownMultiple/constants/index.ts create mode 100644 src/core/components/Dropdown/DropdownMultiple/index.tsx create mode 100644 src/core/components/Dropdown/DropdownMultiple/types/index.ts diff --git a/src/core/components/Chip/index.tsx b/src/core/components/Chip/index.tsx index 3a5cc01..0162cc5 100644 --- a/src/core/components/Chip/index.tsx +++ b/src/core/components/Chip/index.tsx @@ -33,7 +33,12 @@ const Chip = forwardRef( ) => { const handleDelete = (e: MouseEvent) => { e.stopPropagation(); - onDelete?.(); + onDelete?.(e); + }; + + const handleLabelClick = (e: MouseEvent) => { + e.stopPropagation(); + onClick?.(e); }; const renderer = () => { @@ -66,10 +71,11 @@ const Chip = forwardRef( className={clsx( onDelete && 'flex gap-2', onClick && - `cursor-pointer brightness-100 transition-all hover:brightness-95 ${CHIP_LABEL_STYLE[colorTheme]}`, + `brightness-100 transition-all hover:brightness-95 ${CHIP_LABEL_STYLE[colorTheme]}`, + onClick ? 'cursor-pointer' : 'cursor-text', className, )} - onClick={onClick} + onClick={handleLabelClick} {...props} /> ); diff --git a/src/core/components/Chip/types/index.ts b/src/core/components/Chip/types/index.ts index ef610dc..8e7feec 100644 --- a/src/core/components/Chip/types/index.ts +++ b/src/core/components/Chip/types/index.ts @@ -1,7 +1,7 @@ -import { ElementType } from 'react'; +import { ElementType, MouseEvent } from 'react'; import { LabelProps } from '@/core/components/Label/types'; export interface ChipProps extends LabelProps { - onDelete?: () => void; + onDelete?: (e: MouseEvent) => void; } diff --git a/src/core/components/Dropdown/DropdownBase/DropdownItem.tsx b/src/core/components/Dropdown/DropdownBase/DropdownItem.tsx index 46977b8..e8c5631 100644 --- a/src/core/components/Dropdown/DropdownBase/DropdownItem.tsx +++ b/src/core/components/Dropdown/DropdownBase/DropdownItem.tsx @@ -1,7 +1,13 @@ -import { PropsWithChildren, forwardRef, useContext } from 'react'; - +import { + forwardRef, + MouseEvent, + PropsWithChildren, + Ref, + useContext, +} from 'react'; import clsx from 'clsx'; -import { DropdownContext } from '.'; + +import { DropdownContext } from './index'; import { DropdownContextValue, DropdownItemProps } from './types'; const DropdownItem = forwardRef( @@ -12,11 +18,11 @@ const DropdownItem = forwardRef( className, ...props }: PropsWithChildren, - ref: React.Ref, + ref: Ref, ) => { const { setIsToggle } = useContext(DropdownContext) as DropdownContextValue; - const onClickHandler = (e: React.MouseEvent) => { + const onClickHandler = (e: MouseEvent) => { setIsToggle(false); onClick?.(e); }; diff --git a/src/core/components/Dropdown/DropdownBase/index.tsx b/src/core/components/Dropdown/DropdownBase/index.tsx index fb9893a..925bfc7 100644 --- a/src/core/components/Dropdown/DropdownBase/index.tsx +++ b/src/core/components/Dropdown/DropdownBase/index.tsx @@ -7,6 +7,7 @@ import DropdownItem from './DropdownItem'; import DropdownItems from './DropdownItems'; import DropdownTrigger from './DropdownTrigger'; import { DropdownContextValue, DropdownProps, ReturnType } from './types'; +import FormLabel from '@/core/components/FormLabel'; export const DropdownContext = createContext( undefined, @@ -14,6 +15,7 @@ export const DropdownContext = createContext( DropdownContext.displayName = 'DropdownContext'; const DropdownBase = ({ + label, className, readOnly = false, disabled = false, @@ -21,6 +23,7 @@ const DropdownBase = ({ content, feedback, feedbackColor = 'error', + ...formLabelProps }: DropdownProps) => { const [isToggle, setIsToggle] = useState(false); const { contentRef } = useClickOutside(() => @@ -33,6 +36,11 @@ const DropdownBase = ({ value={{ isToggle, setIsToggle, readOnly, disabled }} >
+ {label && ( + + )} {trigger} {isVisibleContent && content}
diff --git a/src/core/components/Dropdown/DropdownBase/types/index.ts b/src/core/components/Dropdown/DropdownBase/types/index.ts index 0674b0c..99b49b5 100644 --- a/src/core/components/Dropdown/DropdownBase/types/index.ts +++ b/src/core/components/Dropdown/DropdownBase/types/index.ts @@ -1,16 +1,25 @@ +import { + Dispatch, + HTMLAttributes, + MouseEvent, + ReactElement, + ReactNode, + SetStateAction, +} from 'react'; + import { ThemeColors } from '@/types'; -import { Dispatch, HTMLAttributes, SetStateAction } from 'react'; import DropdownItem from '../DropdownItem'; import DropdownItems from '../DropdownItems'; import DropdownTrigger from '../DropdownTrigger'; +import { FormLabelProps } from '@/core/components/FormLabel/types'; -export interface DropdownProps { +export interface DropdownProps extends Partial { className?: string; disabled?: boolean; readOnly?: boolean; - trigger: React.ReactNode; - content: React.ReactNode; - feedback?: React.ReactNode; + trigger: ReactNode; + content: ReactNode; + feedback?: ReactNode; feedbackColor?: ThemeColors; } @@ -23,20 +32,20 @@ export interface DropdownContextValue export interface DropdownItemProps extends HTMLAttributes {} export interface DropdownItemsProps extends HTMLAttributes { - items: React.ReactNode[]; + items: ReactNode[]; } export interface DropdownTriggerProps extends Omit, 'children'> { - onClick?: (e: React.MouseEvent) => void; + onClick?: (e: MouseEvent) => void; children: - | React.ReactNode + | ReactNode | (( props: Pick, - ) => React.ReactNode); + ) => ReactNode); } -type Dropdown = (props: DropdownProps) => React.ReactElement; +type Dropdown = (props: DropdownProps) => ReactElement; export type ReturnType = Dropdown & { displayName: string; diff --git a/src/core/components/Dropdown/DropdownMultiple/DropdownMultiple.stories.tsx b/src/core/components/Dropdown/DropdownMultiple/DropdownMultiple.stories.tsx new file mode 100644 index 0000000..4398faf --- /dev/null +++ b/src/core/components/Dropdown/DropdownMultiple/DropdownMultiple.stories.tsx @@ -0,0 +1,74 @@ +import { Meta } from '@storybook/react'; +import { MouseEvent, useState } from 'react'; + +import DropdownMultiple from './index'; +import { + ValueWithLabel, + ValueWithLabelType, +} from '@/core/components/Dropdown/DropdownMultiple/types'; + +const meta = { + title: 'core/Dropdown/DropdownMultiple', + component: DropdownMultiple, + argTypes: {}, +} satisfies Meta; + +export default meta; + +export const Default = () => { + const [currentValues, setCurrentValues] = useState[]>( + [], + ); + const data = [ + { value: 0, label: '2024년 5월 선정산' }, + { value: 1, label: '5월 BIZ 본정산' }, + { value: 2, label: '5월 비즈 선정산 진짜' }, + { value: 3, label: '5월 비즈 선정산 진짜 진짜' }, + ]; + + const handleDelete = (value: ValueWithLabelType) => { + setCurrentValues((prev) => prev.filter(({ value: v }) => v !== value)); + }; + + const items = data.map((item, idx) => { + const { value, label } = item; + const checked = currentValues.some((item) => item.value === value); + + return ( + ) => { + e.stopPropagation(); + + if (!checked) { + setCurrentValues((prev) => [...prev, item]); + } else { + setCurrentValues((prev) => + prev.filter((prevItem) => prevItem !== item), + ); + } + }} + > + {label} + + ); + }); + + return ( + + } + content={} + className={'w-[500px]'} + required + /> + ); +}; diff --git a/src/core/components/Dropdown/DropdownMultiple/DropdownMultipleItem.tsx b/src/core/components/Dropdown/DropdownMultiple/DropdownMultipleItem.tsx new file mode 100644 index 0000000..58cd629 --- /dev/null +++ b/src/core/components/Dropdown/DropdownMultiple/DropdownMultipleItem.tsx @@ -0,0 +1,36 @@ +import clsx from 'clsx'; +import { forwardRef, Ref } from 'react'; +import { Check } from '@phosphor-icons/react'; + +import { DropdownSelectItemProps } from '@/core/components/Dropdown/DropdownSelect/types'; + +const DropdownMultipleItem = forwardRef( + ( + { className, children, checked, ...props }: DropdownSelectItemProps, + ref: Ref, + ) => { + return ( +
  • + {children} + +
  • + ); + }, +); + +export default DropdownMultipleItem; diff --git a/src/core/components/Dropdown/DropdownMultiple/DropdownMultipleTrigger.tsx b/src/core/components/Dropdown/DropdownMultiple/DropdownMultipleTrigger.tsx new file mode 100644 index 0000000..bbc017f --- /dev/null +++ b/src/core/components/Dropdown/DropdownMultiple/DropdownMultipleTrigger.tsx @@ -0,0 +1,105 @@ +import { CaretDown } from '@phosphor-icons/react'; +import clsx from 'clsx'; +import { forwardRef, MouseEvent, Ref, useContext } from 'react'; + +import Typography from '../../Typography'; +import { DropdownContext } from '../DropdownBase'; +import { DropdownMultipleTriggerProps, ValueWithLabelType } from './types'; +import { DropdownContextValue } from '@/core/components/Dropdown/DropdownBase/types'; +import { DROPDOWN_MULTIPLE_VARIANT } from '@/core/components/Dropdown/DropdownMultiple/constants'; +import Chip from '@/core/components/Chip'; + +const DropdownMultipleTrigger = forwardRef( + ( + { + variant = DROPDOWN_MULTIPLE_VARIANT['TEXT'], + currentValues, + onDelete, + onClick, + ...props + }: DropdownMultipleTriggerProps, + ref: Ref, + ) => { + const { isToggle, readOnly, disabled, setIsToggle } = useContext( + DropdownContext, + ) as DropdownContextValue; + const { className, placeholder, ...rest } = props; + const hasCurrentValues = currentValues.length > 0; + const showPlaceholder = placeholder && !hasCurrentValues; + const isDisabled = readOnly || disabled; + const isVisibleContent = !readOnly && !disabled && isToggle; + const isText = + !showPlaceholder && variant === DROPDOWN_MULTIPLE_VARIANT['TEXT']; + + const onClickHandler = (e: MouseEvent) => { + if (isDisabled) return; + + setIsToggle((v) => !v); + onClick?.(e); + }; + + return ( +
    + {showPlaceholder || isText ? ( + + ) : ( +
      + {currentValues.map(({ label, value }) => ( + ) => { + e.stopPropagation(); + onDelete?.(value); + }} + /> + ))} +
    + )} + {!isDisabled ? ( + + ) : null} +
    + ); + }, +); + +export default DropdownMultipleTrigger; diff --git a/src/core/components/Dropdown/DropdownMultiple/constants/index.ts b/src/core/components/Dropdown/DropdownMultiple/constants/index.ts new file mode 100644 index 0000000..e988145 --- /dev/null +++ b/src/core/components/Dropdown/DropdownMultiple/constants/index.ts @@ -0,0 +1,4 @@ +export const DROPDOWN_MULTIPLE_VARIANT = { + TEXT: 'text', + CHIP: 'chip', +} as const; diff --git a/src/core/components/Dropdown/DropdownMultiple/index.tsx b/src/core/components/Dropdown/DropdownMultiple/index.tsx new file mode 100644 index 0000000..6900ee6 --- /dev/null +++ b/src/core/components/Dropdown/DropdownMultiple/index.tsx @@ -0,0 +1,17 @@ +import DropdownBase from '../DropdownBase'; +import { ReturnType } from './types'; +import DropdownSelectItems from '@/core/components/Dropdown/DropdownSelect/DropdownSelectItems'; +import DropdownMultipleItem from '@/core/components/Dropdown/DropdownMultiple/DropdownMultipleItem'; +import DropdownMultipleTrigger from '@/core/components/Dropdown/DropdownMultiple/DropdownMultipleTrigger'; +import { DropdownProps } from '@/core/components/Dropdown/DropdownBase/types'; + +const DropdownMultiple = ({ ...props }: DropdownProps) => { + return ; +}; + +export default DropdownMultiple as unknown as ReturnType; + +DropdownMultiple.displayName = 'DropdownMultiple'; +DropdownMultiple.Trigger = DropdownMultipleTrigger; +DropdownMultiple.Items = DropdownSelectItems; +DropdownMultiple.Item = DropdownMultipleItem; diff --git a/src/core/components/Dropdown/DropdownMultiple/types/index.ts b/src/core/components/Dropdown/DropdownMultiple/types/index.ts new file mode 100644 index 0000000..abaa62f --- /dev/null +++ b/src/core/components/Dropdown/DropdownMultiple/types/index.ts @@ -0,0 +1,34 @@ +import { HTMLAttributes, MouseEvent, ReactElement } from 'react'; + +import { DropdownProps } from '@/core/components/Dropdown/DropdownBase/types'; +import DropdownSelectItems from '@/core/components/Dropdown/DropdownSelect/DropdownSelectItems'; +import DropdownMultipleTrigger from '@/core/components/Dropdown/DropdownMultiple/DropdownMultipleTrigger'; +import DropdownMultipleItem from '@/core/components/Dropdown/DropdownMultiple/DropdownMultipleItem'; +import { DROPDOWN_MULTIPLE_VARIANT } from '@/core/components/Dropdown/DropdownMultiple/constants'; + +export type DropdownMultipleVariant = + (typeof DROPDOWN_MULTIPLE_VARIANT)[keyof typeof DROPDOWN_MULTIPLE_VARIANT]; + +export type ValueWithLabelType = string | number; + +export interface ValueWithLabel { + label: string; + value: T; +} + +export interface DropdownMultipleTriggerProps + extends HTMLAttributes { + currentValues: ValueWithLabel[]; + variant?: DropdownMultipleVariant; + onClick?: (e: MouseEvent) => void; + onDelete?: (value: T) => void; +} + +type DropdownMultiple = (props: DropdownProps) => ReactElement; + +export type ReturnType = DropdownMultiple & { + displayName: string; + Trigger: typeof DropdownMultipleTrigger; + Items: typeof DropdownSelectItems; + Item: typeof DropdownMultipleItem; +}; diff --git a/src/core/components/Dropdown/DropdownSelect/types/index.ts b/src/core/components/Dropdown/DropdownSelect/types/index.ts index 131828b..6c15ba0 100644 --- a/src/core/components/Dropdown/DropdownSelect/types/index.ts +++ b/src/core/components/Dropdown/DropdownSelect/types/index.ts @@ -1,3 +1,5 @@ +import { ReactElement } from 'react'; + import { TypographyProps } from '@/core/components/Typography/types'; import { DropdownItemProps, @@ -17,7 +19,7 @@ export interface DropdownSelectItemProps extends DropdownItemProps { checked: boolean; } -type DropdownSelect = (props: DropdownProps) => React.ReactElement; +type DropdownSelect = (props: DropdownProps) => ReactElement; export type ReturnType = DropdownSelect & { displayName: string; diff --git a/src/index.ts b/src/index.ts index cc8bf0a..15563ee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,6 +19,10 @@ export { default as DropdownSelect } from '@/core/components/Dropdown/DropdownSe export { default as DropdownSelectItem } from '@/core/components/Dropdown/DropdownSelect/DropdownSelectItem'; export { default as DropdownSelectItems } from '@/core/components/Dropdown/DropdownSelect/DropdownSelectItems'; export { default as DropdownSelectTrigger } from '@/core/components/Dropdown/DropdownSelect/DropdownSelectTrigger'; +export { default as DropdownMultiple } from '@/core/components/Dropdown/DropdownMultiple'; +export { default as DropdownMultipleItem } from '@/core/components/Dropdown/DropdownMultiple/DropdownMultipleItem'; +export { default as DropdownMultipleItems } from '@/core/components/Dropdown/DropdownSelect/DropdownSelectItems'; +export { default as DropdownMultipleTrigger } from '@/core/components/Dropdown/DropdownMultiple/DropdownMultipleTrigger'; export { default as FormLabel } from '@/core/components/FormLabel'; export { default as InputBase } from '@/core/components/Input/InputBase'; export { default as InputDatePicker } from '@/core/components/Input/InputDatePicker';