Files
frontend/apps/st/src/shared/ui/tab-bar.tsx
T

194 lines
7.4 KiB
TypeScript

"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { useTabStore } from "@/store/tab-store";
import { Tabs, TabsList, TabsTrigger } from "@common/components";
type TabContextMenuState = {
x: number;
y: number;
tabId: string;
};
const TabBar = () => {
const {
tabs,
activeTabId,
setActiveTab,
removeTab,
closeAllTabs,
closeOtherTabs,
closeTabsToLeft,
closeTabsToRight,
} = useTabStore();
const [contextMenu, setContextMenu] = useState<TabContextMenuState | null>(null);
const contextMenuRef = useRef<HTMLDivElement | null>(null);
const closeContextMenu = useCallback(() => {
setContextMenu(null);
}, []);
useEffect(() => {
if (!contextMenu) return;
const onPointerDown = (event: PointerEvent) => {
if (contextMenuRef.current?.contains(event.target as Node)) return;
closeContextMenu();
};
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") closeContextMenu();
};
const onScroll = () => closeContextMenu();
window.addEventListener("pointerdown", onPointerDown);
window.addEventListener("keydown", onKeyDown);
window.addEventListener("scroll", onScroll, true);
return () => {
window.removeEventListener("pointerdown", onPointerDown);
window.removeEventListener("keydown", onKeyDown);
window.removeEventListener("scroll", onScroll, true);
};
}, [closeContextMenu, contextMenu]);
if (tabs.length === 0) {
return null;
}
const handleCloseTab = (
e: React.MouseEvent | React.KeyboardEvent,
tabId: string,
) => {
e.stopPropagation();
removeTab(tabId);
};
const handleTabContextMenu = (
e: React.MouseEvent<HTMLButtonElement>,
tabId: string,
) => {
e.preventDefault();
e.stopPropagation();
setContextMenu({ x: e.clientX, y: e.clientY, tabId });
};
const handleCloseOtherTabs = () => {
if (!contextMenu || tabs.length <= 1) return;
closeOtherTabs(contextMenu.tabId);
closeContextMenu();
};
const handleCloseAllTabs = () => {
closeAllTabs();
closeContextMenu();
};
const contextTabIndex = contextMenu
? tabs.findIndex((tab) => tab.id === contextMenu.tabId)
: -1;
const canCloseTabsToLeft = contextTabIndex > 0;
const canCloseTabsToRight =
contextTabIndex >= 0 && contextTabIndex < tabs.length - 1;
const canCloseOtherTabs = tabs.length > 1;
const handleCloseTabsToLeft = () => {
if (!contextMenu || !canCloseTabsToLeft) return;
closeTabsToLeft(contextMenu.tabId);
closeContextMenu();
};
const handleCloseTabsToRight = () => {
if (!contextMenu || !canCloseTabsToRight) return;
closeTabsToRight(contextMenu.tabId);
closeContextMenu();
};
return (
<>
<Tabs
value={activeTabId || undefined}
onValueChange={setActiveTab}
className="w-full border-b border-border"
variant="outline"
>
<TabsList className="w-full !justify-start !p-4 !pb-0">
{tabs.map((tab) => (
<TabsTrigger
key={tab.id}
value={tab.id}
className="!flex !w-[140px] !items-center !justify-start !gap-2 !px-4 [&::after]:hidden"
title={tab.label}
onContextMenu={(e) => handleTabContextMenu(e, tab.id)}
>
<span className="flex-1 truncate text-xl leading-none">
{tab.label}
</span>
<span
role="button"
onClick={(e) => handleCloseTab(e, tab.id)}
aria-label={`${tab.label} 탭 닫기`}
className="flex flex-shrink-0 items-center justify-center leading-none hover:text-[#2094fa]"
>
<i className="ri-close-line" />
</span>
</TabsTrigger>
))}
</TabsList>
</Tabs>
{contextMenu &&
typeof document !== "undefined" &&
createPortal(
<div
ref={contextMenuRef}
role="menu"
className="fixed z-[100] min-w-[20rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md"
style={{ left: contextMenu.x, top: contextMenu.y }}
onContextMenu={(e) => e.preventDefault()}
>
<button
type="button"
role="menuitem"
disabled={!canCloseTabsToLeft}
className="flex w-full cursor-default select-none items-center rounded-sm px-4 py-3 text-left text-[1.2rem] outline-none transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50"
onClick={handleCloseTabsToLeft}
>
왼쪽 탭 모두 닫기
</button>
<button
type="button"
role="menuitem"
disabled={!canCloseTabsToRight}
className="flex w-full cursor-default select-none items-center rounded-sm px-4 py-3 text-left text-[1.2rem] outline-none transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50"
onClick={handleCloseTabsToRight}
>
오른쪽 탭 모두 닫기
</button>
<button
type="button"
role="menuitem"
disabled={!canCloseOtherTabs}
className="flex w-full cursor-default select-none items-center rounded-sm px-4 py-3 text-left text-[1.2rem] outline-none transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50"
onClick={handleCloseOtherTabs}
>
해당 탭 제외 모두 닫기
</button>
<button
type="button"
role="menuitem"
className="flex w-full cursor-default select-none items-center rounded-sm px-4 py-3 text-left text-[1.2rem] outline-none transition-colors hover:bg-accent hover:text-accent-foreground"
onClick={handleCloseAllTabs}
>
모든 탭 닫기
</button>
</div>,
document.body,
)}
</>
);
};
export { TabBar };