Skip to content

Commit

Permalink
feat-fe: DropdownSub 컴포넌트 구현 (#762)
Browse files Browse the repository at this point in the history
Co-authored-by: Jeongwoo Park <[email protected]>
  • Loading branch information
github-actions[bot] and lurgi committed Oct 7, 2024
1 parent f462f50 commit c3b226b
Show file tree
Hide file tree
Showing 24 changed files with 843 additions and 248 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,3 @@ export const Default: Story = {
applicantId: 1,
},
};

Default.parameters = {
docs: {},
};
46 changes: 13 additions & 33 deletions frontend/src/components/ApplicantModal/ApplicantBaseInfo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import specificApplicant from '@hooks/useSpecificApplicant';
import formatDate from '@utils/formatDate';
import { useModal } from '@contexts/ModalContext';

import { DropdownListItem } from '@customTypes/common';
import type { DropdownItemType } from '@components/_common/molecules/DropdownItemRenderer';
import DropdownItemRenderer from '@components/_common/molecules/DropdownItemRenderer';

import S from './style';

interface ApplicantBaseInfoProps {
Expand All @@ -35,38 +37,15 @@ export default function ApplicantBaseInfo({ applicantId }: ApplicantBaseInfoProp

const { applicant, process } = applicantBaseInfo;

const items = processList
.filter(({ processName }) => processName !== process.name)
.map(
({ processId, processName }) =>
({
id: processId,
name: processName,
onClick: ({ targetProcessId }: { targetProcessId: number | string }) => {
moveApplicantProcess({ processId: Number(targetProcessId), applicants: [applicantId] });
},
}) as DropdownListItem,
);
items.push({
id: 'emailButton',
name: '이메일 보내기',
hasSeparate: true,
onClick: () => {
// TODO: 이메일 보내는 로직 추가
alert('오픈해야함');
const menuItems: DropdownItemType[] = processList.map(({ processName, processId }) => ({
type: 'clickable',
id: processId,
name: processName,
onClick: ({ targetProcessId }) => {
moveApplicantProcess({ processId: targetProcessId, applicants: [applicantId] });
},
});
}));

items.push({
id: 'rejectButton',
name: '불합격 처리',
isHighlight: true,
hasSeparate: true,
onClick: () => {
// TODO: 불합격 로직 추가
alert('오픈해야함');
},
});
const rejectAppHandler = () => {
const confirmAction = (message: string, action: () => void) => {
const isConfirmed = window.confirm(message);
Expand All @@ -90,11 +69,12 @@ export default function ApplicantBaseInfo({ applicantId }: ApplicantBaseInfoProp
<Dropdown
initValue={process.name}
size="sm"
items={items}
width={112}
isShadow={false}
disabled={applicant.isRejected}
/>
>
<DropdownItemRenderer items={menuItems} />
</Dropdown>
<Button
size="sm"
color="error"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
import type { Meta, StoryObj } from '@storybook/react';

import { DropdownProvider } from '@contexts/DropdownContext';
import DropdownItem, { DropdownItemProps } from '.';

export default {
component: DropdownItem,
title: 'Common/Atoms/DropdownItem',
decorators: [
(Story) => (
<DropdownProvider>
<Story />
</DropdownProvider>
),
],
} as Meta;

const Template: StoryObj<DropdownItemProps> = {
render: (args) => <DropdownItem {...args} />,
};

export const Default: StoryObj<DropdownItemProps> = {
...Template,
args: {
item: 'Menu Label',
},
};

export const Highlight: StoryObj<DropdownItemProps> = {
...Template,
args: {
item: 'Menu Label',
isHighlight: true,
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/components/_common/atoms/DropdownItem/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useDropdown } from '@contexts/DropdownContext';
import S from './style';

export interface DropdownItemProps {
Expand All @@ -15,9 +16,13 @@ export default function DropdownItem({
isHighlight = false,
hasSeparate = false,
}: DropdownItemProps) {
const { close, setSelected } = useDropdown();

const clickHandler = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
onClick();
close();
setSelected(item);
};

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const Item = styled.button<{ isHighlight: boolean; size: 'sm' | 'md'; hasSeparat
text-align: left;
width: 100%;
padding: ${({ size }) => (size === 'md' ? '6px 8px' : '6px 4px')};
padding: ${({ size }) => (size === 'md' ? '6px 24px' : '6px 12px')};
${({ theme, size }) => (size === 'md' ? theme.typography.common.default : theme.typography.common.small)};
color: ${({ isHighlight, theme }) => (isHighlight ? theme.baseColors.redscale[500] : 'none')};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import type { Meta, StoryObj } from '@storybook/react';
import { DropdownProvider } from '@contexts/DropdownContext';

import DropdownItemRenderer from '@components/_common/molecules/DropdownItemRenderer';
import type { DropdownItemType } from '@components/_common/molecules/DropdownItemRenderer';
import DropdownSubTrigger from '.';

const meta = {
title: 'Common/Atoms/DropdownSubTrigger',
component: DropdownSubTrigger,
parameters: {
layout: 'centered',
docs: {
description: {
component:
'DropdownSubTrigger 컴포넌트는 하위 메뉴를 가진 드롭다운 아이템을 표시합니다. 내부적으로 DropdownItemRenderer을 사용하여 중첩된 메뉴 구조를 지원합니다.',
},
},
},
tags: ['autodocs'],
argTypes: {
item: {
description: '드롭다운 아이템의 텍스트',
control: 'text',
},
size: {
description: '드롭다운 아이템의 크기',
control: { type: 'radio' },
options: ['sm', 'md'],
},
isHighlight: {
description: '아이템 강조 여부',
control: 'boolean',
},
hasSeparate: {
description: '구분선 표시 여부',
control: 'boolean',
},
placement: {
description: '하위 메뉴의 위치',
control: { type: 'radio' },
options: ['left', 'right'],
},
children: {
description: '하위 메뉴 아이템들 (DropdownItemRenderer으로 구성)',
control: 'object',
},
},
decorators: [
(Story) => (
<div style={{ width: '150px' }}>
<DropdownProvider>
<Story />
</DropdownProvider>
</div>
),
],
} satisfies Meta<typeof DropdownSubTrigger>;

export default meta;
type Story = StoryObj<typeof meta>;

const sampleSubItems: DropdownItemType[] = [
{
id: 1,
name: 'Subitem 1',
type: 'clickable',
onClick: ({ targetProcessId }) => alert(`Clicked Subitem 1, processId: ${targetProcessId}`),
},
{
id: 2,
name: 'Subitem 2',
type: 'clickable',
onClick: ({ targetProcessId }) => alert(`Clicked Subitem 2, processId: ${targetProcessId}`),
},
{
id: 3,
name: 'Nested Submenu',
type: 'subTrigger',
items: [
{
id: 4,
name: 'Nested Subitem',
type: 'clickable',
onClick: ({ targetProcessId }) => alert(`Clicked Nested Subitem, processId: ${targetProcessId}`),
},
],
},
];

export const Default: Story = {
args: {
item: 'Main Item',
size: 'md',
isHighlight: false,
hasSeparate: false,
placement: 'right',
children: <DropdownItemRenderer items={sampleSubItems} />,
},
};

export const SmallSize: Story = {
args: {
...Default.args,
size: 'sm',
},
};

export const Highlighted: Story = {
args: {
...Default.args,
isHighlight: true,
},
};

export const WithSeparator: Story = {
args: {
...Default.args,
hasSeparate: true,
},
};

export const LeftPlacement: Story = {
args: {
...Default.args,
placement: 'left',
},
};

export const ComplexNestedStructure: Story = {
args: {
...Default.args,
children: (
<DropdownItemRenderer
items={[
...sampleSubItems,
{
id: 5,
name: 'Another Submenu',
type: 'subTrigger',
items: [
{
id: 6,
name: 'Deep Nested Item 1',
type: 'clickable',
onClick: ({ targetProcessId }) => alert(`Clicked Deep Nested Item 1, processId: ${targetProcessId}`),
},
{
id: 7,
name: 'Deep Nested Item 2',
type: 'clickable',
onClick: ({ targetProcessId }) => alert(`Clicked Deep Nested Item 2, processId: ${targetProcessId}`),
isHighlight: true,
},
],
},
]}
/>
),
},
};
59 changes: 59 additions & 0 deletions frontend/src/components/_common/atoms/DropdownSubTrigger/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { PropsWithChildren, useRef, useState } from 'react';
import { HiChevronRight } from 'react-icons/hi';
import S from './style';

export interface DropdownSubTriggerProps extends PropsWithChildren {
item: string;
size: 'sm' | 'md';
isHighlight?: boolean;
placement?: 'left' | 'right';
hasSeparate?: boolean;
}

export default function DropdownSubTrigger({
item,
size,
isHighlight = false,
hasSeparate = false,
placement = 'right',
children,
}: DropdownSubTriggerProps) {
const [isSubOpen, setIsSubOpen] = useState(false);
const ref = useRef<HTMLDivElement | null>(null);

const handleMouseOver = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
setIsSubOpen(true);
};

const handleMouseLeave = () => {
setIsSubOpen(false);
};

return (
<S.Item
ref={ref}
size={size}
onMouseOver={handleMouseOver}
onMouseLeave={handleMouseLeave}
isHighlight={isHighlight}
hasSeparate={hasSeparate}
>
{item}
<HiChevronRight size={size === 'sm' ? 16 : 18} />
{isSubOpen && (
<S.SubItemBoundary
placement={placement}
width={ref.current?.offsetWidth || 0}
>
<S.SubItemContainer
size={size}
width={ref.current?.offsetWidth || 0}
>
{children}
</S.SubItemContainer>
</S.SubItemBoundary>
)}
</S.Item>
);
}
Loading

0 comments on commit c3b226b

Please sign in to comment.