149 lines
5.0 KiB
TypeScript
149 lines
5.0 KiB
TypeScript
/**
|
|
* TopMenuBar — 기능별 그룹 메뉴 + 하단 드롭다운
|
|
*/
|
|
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
import type { MenuPage } from './TopMenuBar';
|
|
|
|
export type MenuItemDef = { page: MenuPage; label: string; icon: React.ReactNode };
|
|
|
|
type MenuGroupDef = {
|
|
id: string;
|
|
label: string;
|
|
icon: React.ReactNode;
|
|
pages: MenuPage[];
|
|
};
|
|
|
|
const IcChevron = () => (
|
|
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
|
<polyline points="2,3.5 5,6.5 8,3.5" />
|
|
</svg>
|
|
);
|
|
|
|
interface Props {
|
|
activePage: MenuPage;
|
|
items: MenuItemDef[];
|
|
groups: MenuGroupDef[];
|
|
onPage: (page: MenuPage) => void;
|
|
tradeNotifyUnread?: number;
|
|
}
|
|
|
|
export const TopMenuBarNavGroups = memo(function TopMenuBarNavGroups({
|
|
activePage,
|
|
items,
|
|
groups,
|
|
onPage,
|
|
tradeNotifyUnread = 0,
|
|
}: Props) {
|
|
const [openGroupId, setOpenGroupId] = useState<string | null>(null);
|
|
const navRef = useRef<HTMLElement>(null);
|
|
|
|
const itemByPage = useMemo(() => {
|
|
const map = new Map<MenuPage, MenuItemDef>();
|
|
for (const item of items) map.set(item.page, item);
|
|
return map;
|
|
}, [items]);
|
|
|
|
const visibleGroups = useMemo(() => groups
|
|
.map(group => ({
|
|
...group,
|
|
items: group.pages
|
|
.map(page => itemByPage.get(page))
|
|
.filter((item): item is MenuItemDef => Boolean(item)),
|
|
}))
|
|
.filter(group => group.items.length > 0), [groups, itemByPage]);
|
|
|
|
const closeDropdown = useCallback(() => setOpenGroupId(null), []);
|
|
|
|
useEffect(() => {
|
|
if (!openGroupId) return;
|
|
const onDoc = (e: MouseEvent) => {
|
|
if (navRef.current && !navRef.current.contains(e.target as Node)) {
|
|
closeDropdown();
|
|
}
|
|
};
|
|
const onKey = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') closeDropdown();
|
|
};
|
|
document.addEventListener('mousedown', onDoc);
|
|
document.addEventListener('keydown', onKey);
|
|
return () => {
|
|
document.removeEventListener('mousedown', onDoc);
|
|
document.removeEventListener('keydown', onKey);
|
|
};
|
|
}, [openGroupId, closeDropdown]);
|
|
|
|
useEffect(() => {
|
|
closeDropdown();
|
|
}, [activePage, closeDropdown]);
|
|
|
|
const handleSelect = (page: MenuPage) => {
|
|
if (page === 'chart') {
|
|
void import('../chart/ChartWorkspaceRoot');
|
|
}
|
|
onPage(page);
|
|
closeDropdown();
|
|
};
|
|
|
|
return (
|
|
<nav className="tmb-nav tmb-nav--groups" ref={navRef} aria-label="주요 메뉴">
|
|
{visibleGroups.map((group, index) => {
|
|
const isOpen = openGroupId === group.id;
|
|
const isActive = group.items.some(item => item.page === activePage);
|
|
const groupBadge = group.items.some(item => item.page === 'notifications') && tradeNotifyUnread > 0
|
|
? tradeNotifyUnread
|
|
: 0;
|
|
|
|
return (
|
|
<React.Fragment key={group.id}>
|
|
{index > 0 && <span className="tmb-divider tmb-divider--nav" aria-hidden="true" />}
|
|
<div className={`tmb-nav-group${isOpen ? ' tmb-nav-group--open' : ''}`}>
|
|
<button
|
|
type="button"
|
|
className={`tmb-nav-btn tmb-nav-group-btn${isActive ? ' tmb-nav-btn--active' : ''}${isOpen ? ' tmb-nav-group-btn--open' : ''}`}
|
|
aria-expanded={isOpen}
|
|
aria-haspopup="menu"
|
|
onClick={() => setOpenGroupId(prev => (prev === group.id ? null : group.id))}
|
|
>
|
|
<span className="tmb-nav-icon">{group.icon}</span>
|
|
<span className="tmb-nav-label">{group.label}</span>
|
|
<span className={`tmb-nav-chevron${isOpen ? ' tmb-nav-chevron--open' : ''}`}>
|
|
<IcChevron />
|
|
</span>
|
|
{groupBadge > 0 && (
|
|
<span className="tmb-notify-badge tmb-notify-badge--nav">
|
|
{groupBadge > 99 ? '99+' : groupBadge}
|
|
</span>
|
|
)}
|
|
</button>
|
|
|
|
{isOpen && (
|
|
<div className="tmb-nav-dropdown" role="menu">
|
|
{group.items.map(item => (
|
|
<button
|
|
key={item.page}
|
|
type="button"
|
|
role="menuitem"
|
|
className={`tmb-nav-dropdown-item${activePage === item.page ? ' tmb-nav-dropdown-item--active' : ''}`}
|
|
onClick={() => handleSelect(item.page)}
|
|
>
|
|
<span className="tmb-nav-dropdown-icon">{item.icon}</span>
|
|
<span className="tmb-nav-dropdown-label">{item.label}</span>
|
|
{item.page === 'notifications' && tradeNotifyUnread > 0 && (
|
|
<span className="tmb-notify-badge tmb-notify-badge--dropdown">
|
|
{tradeNotifyUnread > 99 ? '99+' : tradeNotifyUnread}
|
|
</span>
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</React.Fragment>
|
|
);
|
|
})}
|
|
</nav>
|
|
);
|
|
});
|
|
|
|
export default TopMenuBarNavGroups;
|