Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FE][Feat] #108 : Dropdown 버튼 구현 #153

Merged
merged 6 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ export const App = () => (
<Route path="/add-channel/:user/draw" element={<DrawRoute />} />
<Route path="/channel/:channelId/*" element={<ChannelRoutes />} />
</Routes>
);
);
35 changes: 35 additions & 0 deletions frontend/src/component/common/dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React, { createContext, ReactNode, useMemo, useState } from 'react';
import { DropdownTrigger } from '@/component/common/dropdown/DropdownTrigger.tsx';
import { DropdownItem } from '@/component/common/dropdown/DropdownItem.tsx';
import { DropdownMenu } from '@/component/common/dropdown/DropdownMenu.tsx';

interface IDropdownProps {
children: ReactNode;
}

export interface IToggleContext {
isOpen: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
}

export const ToggleContext = createContext<IToggleContext>({
isOpen: false,
setIsOpen: () => {},
});

// Todo : 드랍다운 외부에서 클릭시 창 닫히게 설정
export const Dropdown = (props: IDropdownProps) => {
const [isOpen, setIsOpen] = useState(false);

const toggleContextValue = useMemo(() => ({ isOpen, setIsOpen }), [isOpen, setIsOpen]);

return (
<aside className="relative flex w-fit flex-col">
<ToggleContext.Provider value={toggleContextValue}>{props.children}</ToggleContext.Provider>
</aside>
);
};

Dropdown.Trigger = DropdownTrigger;
Dropdown.Item = DropdownItem;
Dropdown.Menu = DropdownMenu;
27 changes: 0 additions & 27 deletions frontend/src/component/common/dropdown/DropdownButton.tsx

This file was deleted.

21 changes: 21 additions & 0 deletions frontend/src/component/common/dropdown/DropdownItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ReactNode } from 'react';

interface IDropdownItemProps {
children: ReactNode;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
}

export const DropdownItem = (props: IDropdownItemProps) => {
// undefined는 react에서 랜더링 하지 않음
return (
<li className="px-3 py-1.5 text-base">
<button
type="button"
className="flex w-full items-center justify-between whitespace-nowrap bg-transparent"
onClick={props.onClick}
>
{props.children}
</button>
</li>
);
};
63 changes: 63 additions & 0 deletions frontend/src/component/common/dropdown/DropdownMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { ReactNode, useContext, useRef, useEffect } from 'react';
import classNames from 'classnames';
import { ToggleContext } from '@/component/common/dropdown/Dropdown.tsx';

interface IDropdownMenuProps {
children: ReactNode | ReactNode[];
}

export const DropdownMenu = (props: IDropdownMenuProps) => {
const { isOpen, setIsOpen } = useContext(ToggleContext);
const ref = useRef<HTMLUListElement | null>(null);

const handleOutSideClick = (event: MouseEvent) => {
const { target } = event;

if (!(target instanceof HTMLElement)) {
return;
}

if (
ref.current &&
target &&
!ref.current.contains(target) &&
target.dataset.component !== 'DropdownTrigger'
) {
setIsOpen(false);
}
};

useEffect(() => {
document.addEventListener('click', handleOutSideClick);
return () => {
document.removeEventListener('click', handleOutSideClick);
};
}, []);

return (
isOpen && (
<ul
ref={ref}
className={classNames(
// 추후 애니메이션 조건부 적용을 위해서 classNames 사용
'align-center',
'animate-smoothAppear',
'absolute',
'right-0',
'top-8',
'z-10',
'flex',
'flex-col',
'justify-center',
'gap-2.5',
'rounded-xl',
'p-2.5',
'shadow-2xl',
'w-fit',
)}
>
{props.children}
</ul>
)
);
};
33 changes: 33 additions & 0 deletions frontend/src/component/common/dropdown/DropdownTrigger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ReactNode, useContext } from 'react';
import classNames from 'classnames';
import { ToggleContext } from '@/component/common/dropdown/Dropdown.tsx';

interface IDropdownTriggerProps {
children: ReactNode;
}

export const DropdownTrigger = (props: IDropdownTriggerProps) => {
const { setIsOpen } = useContext(ToggleContext);

const handleOnClick = () => {
setIsOpen(prevIsOpen => !prevIsOpen);
};

return (
<button
type="button"
className={classNames(
'flex',
'justify-center',
'items-center',
'bg-transparent',
'w-fit',
'h-fit',
)}
data-component="DropdownTrigger"
onClick={handleOnClick}
>
{props.children}
</button>
);
};
38 changes: 38 additions & 0 deletions frontend/src/component/header/HeaderDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Dropdown } from '@/component/common/dropdown/Dropdown.tsx';
import { MdMenu, MdLocationOn } from 'react-icons/md';
import { DropdownItem } from '@/component/common/dropdown/DropdownItem.tsx';
import classNames from 'classnames';

// interface IDropdownContainerProps {}

// TDDO : props로 전달되면 해석되도록 로직 변경 (props:props: IDropdownContainerProps)
export const HeaderDropdown = () => {
return (
<div>
<Dropdown>
<Dropdown.Trigger>
<MdMenu className="h-6 w-6" />
</Dropdown.Trigger>
<Dropdown.Menu>
{[
'사용자 1 보기',
'사용자 2 보기',
'사용자 3 보기',
'사용자 4 보기',
'사용자 5 보기',
].map((e, i) => {
return (
<DropdownItem key={e}>
{e}
<MdLocationOn
className={classNames('w-5', 'h-5', `text-marker-user${i + 1}`, 'fill-current')}
/>
{/* 아이콘 색 변경 로직 찾기, 현재는 아이콘색이 반영이 안됨 수정할 사 */}
</DropdownItem>
);
})}
</Dropdown.Menu>
</Dropdown>
</div>
);
};
2 changes: 1 addition & 1 deletion frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import './index.css';
import { App } from './App';
import { App } from './App2';
// 우선은 폰트 다 포함시켰는데, 나중에 사용할 것들만 따로 뺴자.
import '@fontsource/pretendard/100.css';
import '@fontsource/pretendard/200.css';
Expand Down
46 changes: 0 additions & 46 deletions frontend/src/stories/DropdownButton.stories.tsx

This file was deleted.

10 changes: 10 additions & 0 deletions frontend/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ module.exports = {
float: '0 4px 20px rgba(0, 0, 0, 0.13)',
basic: 'inset 0 0 3px rgba(0, 0, 0, 0.11)',
},
keyframes: {
smoothAppear: {
'0%': { opacity: '0', transform: 'translateY(-5%)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
},
animation: {
smoothAppear: 'smoothAppear 0.5s ease-out', // 0.5s는 원하는 지속 시간
smoothDisappear: 'smoothAppear 0.5s reverse ease-out', // 0.5s는 원하는 지속 시간
},
}, // 필요한 커스터마이징을 여기서 설정 가능
},
plugins: [],
Expand Down
Loading