93 lines
2.8 KiB
TypeScript
93 lines
2.8 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
|
|
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
export function ScrollableNav({
|
|
ariaLabel,
|
|
children,
|
|
className,
|
|
activeKey,
|
|
}: {
|
|
ariaLabel: string;
|
|
children: ReactNode;
|
|
className?: string;
|
|
activeKey?: string;
|
|
}) {
|
|
const navRef = useRef<HTMLElement>(null);
|
|
const [canScrollLeft, setCanScrollLeft] = useState(false);
|
|
const [canScrollRight, setCanScrollRight] = useState(false);
|
|
|
|
const updateScrollState = useCallback(() => {
|
|
const nav = navRef.current;
|
|
if (!nav) return;
|
|
setCanScrollLeft(nav.scrollLeft > 0);
|
|
setCanScrollRight(nav.scrollLeft + nav.clientWidth < nav.scrollWidth - 1);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const nav = navRef.current;
|
|
if (!nav) return;
|
|
const revealActive = () => {
|
|
const active = nav.querySelector<HTMLElement>('[aria-current="page"]');
|
|
if (active) {
|
|
nav.scrollLeft = Math.max(0, active.offsetLeft - nav.offsetLeft - (nav.clientWidth - active.offsetWidth) / 2);
|
|
}
|
|
updateScrollState();
|
|
};
|
|
const frame = requestAnimationFrame(revealActive);
|
|
const observer = new ResizeObserver(updateScrollState);
|
|
observer.observe(nav);
|
|
nav.addEventListener("scroll", updateScrollState, { passive: true });
|
|
window.addEventListener("resize", revealActive);
|
|
return () => {
|
|
cancelAnimationFrame(frame);
|
|
observer.disconnect();
|
|
nav.removeEventListener("scroll", updateScrollState);
|
|
window.removeEventListener("resize", revealActive);
|
|
};
|
|
}, [activeKey, updateScrollState]);
|
|
|
|
const scroll = (direction: -1 | 1) => {
|
|
navRef.current?.scrollBy({ left: direction * navRef.current.clientWidth * 0.75,
|
|
behavior: window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth" });
|
|
};
|
|
|
|
return (
|
|
<div className={cn("flex items-center gap-1", className)}>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon-sm"
|
|
className="shrink-0 sm:hidden"
|
|
disabled={!canScrollLeft}
|
|
onClick={() => scroll(-1)}
|
|
aria-label="เลื่อนเมนูไปทางซ้าย"
|
|
>
|
|
<ChevronLeftIcon />
|
|
</Button>
|
|
<nav
|
|
ref={navRef}
|
|
aria-label={ariaLabel}
|
|
className="flex min-w-0 flex-1 gap-1 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
|
>
|
|
{children}
|
|
</nav>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon-sm"
|
|
className="shrink-0 sm:hidden"
|
|
disabled={!canScrollRight}
|
|
onClick={() => scroll(1)}
|
|
aria-label="เลื่อนเมนูไปทางขวา"
|
|
>
|
|
<ChevronRightIcon />
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|