좌측메뉴 탭 이동 처리

This commit is contained in:
2026-10-02 11:39:49 +09:00
parent a323c42acf
commit 4bdf68c2b6
3 changed files with 138 additions and 45 deletions
+115 -39
View File
@@ -57,6 +57,7 @@ export function LNB({
const [currentMenuCode, setCurrentMenuCode] = useState<number | null>(null);
const prevPathRef = useRef(currentPath);
const { browser } = useDetectDevice();
const { isMobile, setOpenMobile } = useSidebar();
const appSiteId = getCookie("site-id-key") ?? "";
const siteId = propSiteId ?? appSiteId;
@@ -90,6 +91,38 @@ export function LNB({
return currentPath?.startsWith(path);
}
function handleSidebarKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
if (event.key !== "Tab") return;
const menuControls = Array.from(
event.currentTarget.querySelectorAll<HTMLElement>(
'[data-sidebar="menu-button"], [data-sidebar="menu-sub-button"]',
),
).filter(
(control) =>
control.tabIndex >= 0 &&
!control.hasAttribute("disabled") &&
control.getAttribute("aria-disabled") !== "true" &&
!control.closest('[hidden], [aria-hidden="true"]'),
);
const currentIndex = menuControls.findIndex(
(control) =>
control === event.target ||
control.contains(event.target as Node),
);
if (currentIndex < 0) return;
const nextIndex = currentIndex + (event.shiftKey ? -1 : 1);
const nextControl = menuControls[nextIndex];
if (nextControl) {
event.preventDefault();
nextControl.focus();
nextControl.scrollIntoView({ block: "nearest" });
}
}
useEffect(() => {
if (!menuList || !currentPath) return;
@@ -127,6 +160,7 @@ export function LNB({
if (prevPathRef.current !== currentPath) {
setDefaultOpenMenuIds(matchedMenuIds);
prevPathRef.current = currentPath;
if (isMobile) setOpenMobile(false);
}
if (matchedMenuCode) {
@@ -142,7 +176,7 @@ export function LNB({
siteId,
urlPath: currentPath,
});
}, [menuList, currentPath]);
}, [menuList, currentPath, isMobile, setOpenMobile]);
function MenuItem({
item,
@@ -151,65 +185,104 @@ export function LNB({
item: MenuItem;
isSubMenu?: boolean;
}) {
const { open: isSidebarOpen, setOpen } = useSidebar();
const {
open: isSidebarOpen,
setOpen,
isMobile: isMobileView,
setOpenMobile: closeMobileSidebar,
} = useSidebar();
const isMatched =
isPathMatched(item.urlPath) && currentMenuCode === item.menuCd;
const hideLabel = !isSidebarOpen && !isMobileView;
const handleClick = () => {
if (!isSidebarOpen) setOpen(true);
const handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
setCookie("menu-code-key", item.menuCd.toString());
// newWndwYn이 Y인 경우 새창으로 열기
if (item.newWndwYn === "Y") {
window.open(item.urlPath, "_blank");
if (item.newWndwYn !== "Y" && handleMenuClick) {
event.preventDefault();
handleMenuClick(item);
if (isMobileView && currentPath === item.urlPath) {
closeMobileSidebar(false);
}
} else if (isMobileView) {
closeMobileSidebar(false);
}
handleMenuClick?.(item);
};
if (item.children) {
if (item.children?.length) {
return <MenuGroup item={item} />;
}
const MenuItem = isSubMenu ? SidebarMenuSubItem : SidebarMenuItem;
const MenuButton = isSubMenu ? SidebarMenuSubButton : SidebarMenuButton;
const menuContent = (
<>
<i
className={cn(
"diveicon",
`di-${item.iconNm}`,
!hideLabel && "ml-2",
)}
/>
<span className={cn("whitespace-nowrap", hideLabel && "hidden")}>
{item.menuKornNm}
</span>
</>
);
const linkProps = {
href: item.urlPath,
target: item.newWndwYn === "Y" ? "_blank" : undefined,
rel: item.newWndwYn === "Y" ? "noopener noreferrer" : undefined,
"aria-current": isMatched ? ("page" as const) : undefined,
onClick: handleClick,
};
const menuClassName = cn(
"h-auto cursor-pointer py-4 text-[1.4rem] text-label transition-colors hover:bg-accent",
isMatched && "bg-accent !text-primary",
!isSidebarOpen && "justify-center",
);
if (isSubMenu) {
return (
<SidebarMenuSubItem>
<SidebarMenuSubButton
{...linkProps}
className={menuClassName}
data-matched={isMatched}
isActive={isMatched}
>
{menuContent}
</SidebarMenuSubButton>
</SidebarMenuSubItem>
);
}
return (
<MenuItem>
<MenuButton
className={cn(
"h-auto cursor-pointer py-4 text-[1.4rem] text-label transition-colors hover:bg-accent",
isMatched && "bg-accent !text-primary",
!isSidebarOpen && "justify-center",
)}
<SidebarMenuItem>
<SidebarMenuButton
asChild
className={menuClassName}
data-matched={isMatched}
onClick={handleClick}
tooltip={item.menuKornNm}
>
<i
className={cn(
"diveicon",
`di-${item.iconNm}`,
isSidebarOpen && "ml-2",
)}
/>
<span className={cn("whitespace-nowrap", !isSidebarOpen && "hidden")}>
{item.menuKornNm}
</span>
</MenuButton>
</MenuItem>
<a {...linkProps}>{menuContent}</a>
</SidebarMenuButton>
</SidebarMenuItem>
);
}
function MenuGroup({ item }: { item: MenuItem }) {
const { open: isSidebarOpen, setOpen: setSidebarOpen } = useSidebar();
const {
open: isSidebarOpen,
setOpen: setSidebarOpen,
isMobile,
} = useSidebar();
const hideLabel = !isSidebarOpen && !isMobile;
const [isOpen, setIsOpen] = useState(
defaultOpenMenuIds.includes(item.menuCd),
);
const handleOpenChange = (open: boolean) => {
setIsOpen(open);
if (!isSidebarOpen) setSidebarOpen(true);
if (!isMobile && !isSidebarOpen) setSidebarOpen(true);
};
return (
@@ -233,13 +306,13 @@ export function LNB({
className={cn(
"diveicon text-label",
`di-${item.iconNm}`,
isSidebarOpen && "ml-2",
!hideLabel && "ml-2",
)}
/>
<span
className={cn(
"whitespace-nowrap text-label group-hover/menu-button:text-foreground",
!isSidebarOpen && "hidden",
hideLabel && "hidden",
)}
>
{item.menuKornNm}
@@ -249,7 +322,7 @@ export function LNB({
className={cn(
"ml-auto !size-6 text-label transition-transform",
isOpen && "rotate-90",
!isSidebarOpen && "hidden",
hideLabel && "hidden",
)}
/>
</SidebarMenuButton>
@@ -270,11 +343,14 @@ export function LNB({
<Sidebar
collapsible="icon"
className={cn(
"border-border bg-background px-4 pt-[6rem]",
"border-border bg-background px-4 pt-14 md:pt-[6rem]",
classNames?.wrapper,
)}
>
<SidebarContent className="bg-background">
<SidebarContent
className="bg-background"
onKeyDown={handleSidebarKeyDown}
>
<SidebarMenu className="gap-0">
{menuList?.map((item) => (
<MenuItem key={item.menuCd} item={item} />
@@ -67,7 +67,7 @@ const SheetContent = React.forwardRef<
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
<span className="sr-only">닫기</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
@@ -211,7 +211,7 @@ const Sidebar = React.forwardRef<
<SheetContent
data-sidebar="sidebar"
data-mobile="true"
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
className="h-[100dvh] max-h-[100dvh] w-[--sidebar-width] overflow-hidden bg-sidebar p-0 text-sidebar-foreground [&>button]:z-50 [&>button]:rounded-sm [&>button]:p-2 [&>button]:text-sidebar-foreground [&>button]:opacity-100 [&>button]:hover:bg-sidebar-accent"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
@@ -223,7 +223,14 @@ const Sidebar = React.forwardRef<
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader>
<div className="flex h-full w-full flex-col">{children}</div>
<div
className={cn(
"flex h-full min-h-0 w-full flex-col",
className,
)}
>
{children}
</div>
</SheetContent>
</Sheet>
);
@@ -281,12 +288,22 @@ const SidebarTrigger = React.forwardRef<
React.ElementRef<typeof Button>,
React.ComponentProps<typeof Button>
>(({ className, onClick, ...props }, ref) => {
const { toggleSidebar } = useSidebar();
const { toggleSidebar, isMobile, openMobile, state } = useSidebar();
return (
<Button
ref={ref}
data-sidebar="trigger"
aria-label={
isMobile
? openMobile
? "좌측 메뉴 닫기"
: "좌측 메뉴 열기"
: state === "expanded"
? "좌측 메뉴 접기"
: "좌측 메뉴 펼치기"
}
aria-expanded={isMobile ? openMobile : state === "expanded"}
variant="ghost"
size="icon"
className={cn("h-7 w-7", className)}
@@ -297,7 +314,7 @@ const SidebarTrigger = React.forwardRef<
{...props}
>
<PanelLeft />
<span className="sr-only">Toggle Sidebar</span>
<span className="sr-only">좌측 메뉴 열기/닫기</span>
</Button>
);
});
@@ -422,7 +439,7 @@ const SidebarContent = React.forwardRef<
ref={ref}
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
"flex min-h-0 flex-1 flex-col gap-2 overflow-x-hidden overflow-y-auto overscroll-y-contain touch-pan-y group-data-[collapsible=icon]:overflow-hidden",
className,
)}
{...props}