좌측메뉴 탭 이동 처리
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user