Skip to content

Commit

Permalink
Feat Dropdown Multiple
Browse files Browse the repository at this point in the history
  • Loading branch information
cause38 committed Jun 14, 2024
1 parent 1252b76 commit 5e55195
Show file tree
Hide file tree
Showing 13 changed files with 326 additions and 21 deletions.
12 changes: 9 additions & 3 deletions src/core/components/Chip/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ const Chip = forwardRef(
) => {
const handleDelete = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
onDelete?.();
onDelete?.(e);
};

const handleLabelClick = (e: MouseEvent<T>) => {
e.stopPropagation();
onClick?.(e);
};

const renderer = () => {
Expand Down Expand Up @@ -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}
/>
);
Expand Down
4 changes: 2 additions & 2 deletions src/core/components/Chip/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ElementType } from 'react';
import { ElementType, MouseEvent } from 'react';

import { LabelProps } from '@/core/components/Label/types';

export interface ChipProps<T extends ElementType> extends LabelProps<T> {
onDelete?: () => void;
onDelete?: (e: MouseEvent<HTMLButtonElement>) => void;
}
16 changes: 11 additions & 5 deletions src/core/components/Dropdown/DropdownBase/DropdownItem.tsx
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -12,11 +18,11 @@ const DropdownItem = forwardRef(
className,
...props
}: PropsWithChildren<DropdownItemProps>,
ref: React.Ref<HTMLLIElement>,
ref: Ref<HTMLLIElement>,
) => {
const { setIsToggle } = useContext(DropdownContext) as DropdownContextValue;

const onClickHandler = (e: React.MouseEvent<HTMLLIElement>) => {
const onClickHandler = (e: MouseEvent<HTMLLIElement>) => {
setIsToggle(false);
onClick?.(e);
};
Expand Down
8 changes: 8 additions & 0 deletions src/core/components/Dropdown/DropdownBase/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,23 @@ 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<DropdownContextValue | undefined>(
undefined,
);
DropdownContext.displayName = 'DropdownContext';

const DropdownBase = ({
label,
className,
readOnly = false,
disabled = false,
trigger,
content,
feedback,
feedbackColor = 'error',
...formLabelProps
}: DropdownProps) => {
const [isToggle, setIsToggle] = useState(false);
const { contentRef } = useClickOutside<HTMLDivElement>(() =>
Expand All @@ -33,6 +36,11 @@ const DropdownBase = ({
value={{ isToggle, setIsToggle, readOnly, disabled }}
>
<div ref={contentRef} className={clsx(className, 'relative')}>
{label && (
<label className='mb-2 inline-block'>
<FormLabel label={label} {...formLabelProps} />
</label>
)}
{trigger}
{isVisibleContent && content}
</div>
Expand Down
29 changes: 19 additions & 10 deletions src/core/components/Dropdown/DropdownBase/types/index.ts
Original file line number Diff line number Diff line change
@@ -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<FormLabelProps> {
className?: string;
disabled?: boolean;
readOnly?: boolean;
trigger: React.ReactNode;
content: React.ReactNode;
feedback?: React.ReactNode;
trigger: ReactNode;
content: ReactNode;
feedback?: ReactNode;
feedbackColor?: ThemeColors;
}

Expand All @@ -23,20 +32,20 @@ export interface DropdownContextValue
export interface DropdownItemProps extends HTMLAttributes<HTMLLIElement> {}

export interface DropdownItemsProps extends HTMLAttributes<HTMLUListElement> {
items: React.ReactNode[];
items: ReactNode[];
}

export interface DropdownTriggerProps
extends Omit<HTMLAttributes<HTMLButtonElement>, 'children'> {
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
children:
| React.ReactNode
| ReactNode
| ((
props: Pick<DropdownContextValue, 'isToggle' | 'readOnly' | 'disabled'>,
) => React.ReactNode);
) => ReactNode);
}

type Dropdown = (props: DropdownProps) => React.ReactElement;
type Dropdown = (props: DropdownProps) => ReactElement;

export type ReturnType = Dropdown & {
displayName: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<typeof DropdownMultiple>;

export default meta;

export const Default = () => {
const [currentValues, setCurrentValues] = useState<ValueWithLabel<number>[]>(
[],
);
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 (
<DropdownMultiple.Item
key={idx}
checked={checked}
onClick={(e: MouseEvent<HTMLLIElement>) => {
e.stopPropagation();

if (!checked) {
setCurrentValues((prev) => [...prev, item]);
} else {
setCurrentValues((prev) =>
prev.filter((prevItem) => prevItem !== item),
);
}
}}
>
{label}
</DropdownMultiple.Item>
);
});

return (
<DropdownMultiple
label={'정산 선택'}
trigger={
<DropdownMultiple.Trigger
variant={'chip'}
currentValues={currentValues}
placeholder='선택해주세요'
onDelete={handleDelete}
/>
}
content={<DropdownMultiple.Items items={items} />}
className={'w-[500px]'}
required
/>
);
};
Original file line number Diff line number Diff line change
@@ -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<HTMLLIElement>,
) => {
return (
<li
ref={ref}
role='option'
className={clsx(
'flex cursor-pointer items-center justify-between text-body-01-regular text-gray-08 hover:font-bold',
checked && 'font-bold',
className,
)}
{...props}
>
{children}
<Check
weight='bold'
className={clsx(
'-mt-[2px] flex-shrink-0 text-gray-03',
checked && 'text-primary-03',
)}
/>
</li>
);
},
);

export default DropdownMultipleItem;
Original file line number Diff line number Diff line change
@@ -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(
<T extends ValueWithLabelType>(
{
variant = DROPDOWN_MULTIPLE_VARIANT['TEXT'],
currentValues,
onDelete,
onClick,
...props
}: DropdownMultipleTriggerProps<T>,
ref: Ref<HTMLDivElement>,
) => {
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<HTMLDivElement>) => {
if (isDisabled) return;

setIsToggle((v) => !v);
onClick?.(e);
};

return (
<div
ref={ref}
role={'button'}
onClick={onClickHandler}
className={clsx(
'flex w-full items-center justify-between gap-x-2 overflow-hidden rounded-xl border border-gray-03 p-3',
isDisabled ? '!cursor-not-allowed bg-gray-09' : 'cursor-pointer',
className,
)}
aria-haspopup='listbox'
aria-expanded={isToggle}
aria-disabled={disabled}
aria-readonly={readOnly}
{...rest}
>
{showPlaceholder || isText ? (
<Typography
theme='subhead-02-regular'
color={!showPlaceholder && !isDisabled ? 'gray-08' : 'gray-05'}
text={!showPlaceholder ? currentValues.join(', ') : placeholder}
className={clsx(
'flex-1 truncate text-start',
isDisabled && 'mr-[1.75rem]',
)}
/>
) : (
<ul
className={clsx(
'flex flex-wrap gap-2',
isDisabled && 'mr-[1.75rem]',
)}
>
{currentValues.map(({ label, value }) => (
<Chip
element={'li'}
key={value}
label={label}
size={'medium'}
rounded={'rounded-6'}
colorTheme={'secondary'}
onDelete={(e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
onDelete?.(value);
}}
/>
))}
</ul>
)}
{!isDisabled ? (
<CaretDown
size='24'
className={clsx(
'flex-shrink-0 text-gray-06',
isVisibleContent ? 'rotate-180' : 'rotate-0',
)}
weight='fill'
/>
) : null}
</div>
);
},
);

export default DropdownMultipleTrigger;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const DROPDOWN_MULTIPLE_VARIANT = {
TEXT: 'text',
CHIP: 'chip',
} as const;
Loading

0 comments on commit 5e55195

Please sign in to comment.