|
|
|
@@ -2,9 +2,10 @@
|
|
|
|
|
|
|
|
|
|
import Link from "next/link";
|
|
|
|
|
import { usePathname, useSearchParams } from "next/navigation";
|
|
|
|
|
import { ChevronDown, Heart, LayoutGrid, Menu, ShoppingCart, UserRound } from "lucide-react";
|
|
|
|
|
import { Suspense, useEffect, useMemo, useRef, useState } from "react";
|
|
|
|
|
import { ChevronDown, Heart, LayoutGrid, LifeBuoy, Menu, ShoppingCart, Sparkles, UserRound } from "lucide-react";
|
|
|
|
|
import { Suspense, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
|
|
|
|
import { AccountDialog } from "@/components/account/account-dialog";
|
|
|
|
|
import { LocationPicker } from "@/components/layout/location-picker";
|
|
|
|
|
import { PredictiveSearch } from "@/components/search/predictive-search";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet";
|
|
|
|
@@ -47,9 +48,25 @@ export function Navbar({ products, categories }: { products: Product[]; categori
|
|
|
|
|
[products],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Header compacto al hacer scroll (solo desktop, lg+): oculta la franja
|
|
|
|
|
// promocional y achica logo/padding. Histeresis (compacta pasado 160px,
|
|
|
|
|
// se expande recien por debajo de 40px) porque achicar un header sticky
|
|
|
|
|
// cambia la altura del documento -- con un solo umbral el header
|
|
|
|
|
// parpadea entre los dos estados justo en el borde.
|
|
|
|
|
const [compact, setCompact] = useState(false);
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const onScroll = () => {
|
|
|
|
|
const y = window.scrollY;
|
|
|
|
|
setCompact((current) => (current ? y > 40 : y > 160));
|
|
|
|
|
};
|
|
|
|
|
onScroll();
|
|
|
|
|
window.addEventListener("scroll", onScroll, { passive: true });
|
|
|
|
|
return () => window.removeEventListener("scroll", onScroll);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<header className="sticky top-0 z-40 border-b border-slate-200/80 bg-white/95 shadow-sm backdrop-blur-xl">
|
|
|
|
|
<div className="bg-ari-marino px-4 py-2 text-center text-xs font-black text-white sm:text-sm">
|
|
|
|
|
<header data-compact={compact} className="sticky top-0 z-40 border-b border-slate-200/80 bg-white/95 shadow-sm backdrop-blur-xl">
|
|
|
|
|
<div className={`bg-ari-marino px-4 py-2 text-center text-xs font-black text-white sm:text-sm ${compact ? "lg:hidden" : ""}`}>
|
|
|
|
|
Paga tus compras a cuotas · Envíos nacionales · <span className="underline decoration-ari-coral-claro decoration-2 underline-offset-2">Compra segura con ARI</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
@@ -61,16 +78,19 @@ export function Navbar({ products, categories }: { products: Product[]; categori
|
|
|
|
|
angosto/recortado). En desktop (lg+) se mantiene la fila unica
|
|
|
|
|
original -- ahi sobra espacio y partirla en dos solo alargaria
|
|
|
|
|
el header sin necesidad. */}
|
|
|
|
|
<div className="mx-auto max-w-7xl px-4 py-3 lg:py-4">
|
|
|
|
|
<div className="flex items-center justify-between gap-4 lg:grid lg:grid-cols-[auto_1fr_auto] lg:gap-8">
|
|
|
|
|
<div className={`mx-auto max-w-7xl px-4 py-3 transition-[padding] ${compact ? "lg:py-2" : "lg:py-4"}`}>
|
|
|
|
|
<div className="flex items-center justify-between gap-4 lg:grid lg:grid-cols-[auto_auto_1fr_auto] lg:gap-5">
|
|
|
|
|
{/* Isotipo circular con icono + wordmark integrados (como en la
|
|
|
|
|
version previa a ADR 0008), pedido explicito del usuario en
|
|
|
|
|
vez del lockup rectangular. logo-ari.svg (rectangular) se
|
|
|
|
|
mantiene para el footer. */}
|
|
|
|
|
<Link href="/" aria-label="ARI shopping" className="shrink-0">
|
|
|
|
|
{/* eslint-disable-next-line @next/next/no-img-element -- vector propio, next/image no optimiza SVG sin habilitar dangerouslyAllowSVG */}
|
|
|
|
|
<img src="/images/brand/logo-ari-circular.svg" alt="ARI shopping" className="size-16 lg:size-20" width={200} height={200} />
|
|
|
|
|
<img src="/images/brand/logo-ari-circular.svg" alt="ARI shopping" className={`size-16 transition-[width,height] ${compact ? "lg:size-12" : "lg:size-20"}`} width={200} height={200} />
|
|
|
|
|
</Link>
|
|
|
|
|
<div className="hidden lg:block">
|
|
|
|
|
<LocationPicker firstName={isLoggedIn ? customer?.firstName ?? null : null} />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="hidden lg:block">
|
|
|
|
|
<PredictiveSearch />
|
|
|
|
|
</div>
|
|
|
|
@@ -84,6 +104,17 @@ export function Navbar({ products, categories }: { products: Product[]; categori
|
|
|
|
|
<UserRound />
|
|
|
|
|
{isLoggedIn && <span className="absolute -right-1 -top-1 size-3 rounded-full border-2 border-white bg-emerald-500" />}
|
|
|
|
|
</Button>
|
|
|
|
|
{/* Texto al lado de los iconos solo en desktop (estilo "Hola
|
|
|
|
|
Cristian / Cuenta y listas" de Amazon): en mobile no hay
|
|
|
|
|
lugar y los iconos solos ya se entienden. */}
|
|
|
|
|
<button type="button" onClick={() => setAccountOpen(true)} className="hidden rounded-xl px-2 py-1 text-left leading-tight transition hover:bg-slate-100 xl:block">
|
|
|
|
|
<span className="block text-xs text-slate-500">{isLoggedIn ? `Hola, ${customer?.firstName || "cliente"}` : "Hola, inicia sesión"}</span>
|
|
|
|
|
<span className="block text-sm font-black text-ari-marino">Mi cuenta</span>
|
|
|
|
|
</button>
|
|
|
|
|
<Link href="/cambios" prefetch={false} className="mr-1 hidden rounded-xl px-2 py-1 leading-tight transition hover:bg-slate-100 xl:block">
|
|
|
|
|
<span className="block text-xs text-slate-500">Cambios y</span>
|
|
|
|
|
<span className="block text-sm font-black text-ari-marino">devoluciones</span>
|
|
|
|
|
</Link>
|
|
|
|
|
<Button variant="outline" size="icon" aria-label={`Favoritos: ${wishlistCount}`} className="relative hidden border-slate-300 bg-white text-slate-900 hover:border-ari-coral hover:bg-ari-coral/10 sm:inline-flex" onClick={openWishlist}>
|
|
|
|
|
<Heart />
|
|
|
|
|
{wishlistCount > 0 && <span className="absolute -right-1.5 -top-1.5 grid size-5 place-items-center rounded-full bg-ari-marino text-[10px] font-black text-white">{wishlistCount}</span>}
|
|
|
|
@@ -100,8 +131,8 @@ export function Navbar({ products, categories }: { products: Product[]; categori
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Suspense fallback={<DesktopCategoryNavigationFallback tree={tree} onOpenMenu={() => setMenuOpen(true)} />}>
|
|
|
|
|
<DesktopCategoryNavigation tree={tree} onOpenMenu={() => setMenuOpen(true)} />
|
|
|
|
|
<Suspense fallback={<DesktopCategoryNavigationFallback tree={tree} collections={collections} onOpenMenu={() => setMenuOpen(true)} onOpenAccount={() => setAccountOpen(true)} />}>
|
|
|
|
|
<DesktopCategoryNavigation tree={tree} collections={collections} onOpenMenu={() => setMenuOpen(true)} onOpenAccount={() => setAccountOpen(true)} />
|
|
|
|
|
</Suspense>
|
|
|
|
|
|
|
|
|
|
<Sheet open={menuOpen} onOpenChange={setMenuOpen}>
|
|
|
|
@@ -142,16 +173,26 @@ function useCategoryNavigationState() {
|
|
|
|
|
const pathname = usePathname();
|
|
|
|
|
const searchParams = useSearchParams();
|
|
|
|
|
const currentCategory = searchParams.get("category") ?? "";
|
|
|
|
|
const currentCollection = searchParams.get("collection") ?? "";
|
|
|
|
|
const isCatalog = pathname === "/catalog";
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
isAllActive: isCatalog && !currentCategory,
|
|
|
|
|
isAllActive: isCatalog && !currentCategory && !currentCollection,
|
|
|
|
|
isActive: (name: string) => isCatalog && currentCategory === name,
|
|
|
|
|
isCollectionActive: (name: string) => isCatalog && currentCollection === name,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function pillClass(active: boolean): string {
|
|
|
|
|
return `shrink-0 rounded-full px-4 py-2 text-sm font-extrabold transition ${
|
|
|
|
|
type NavigationState = ReturnType<typeof useCategoryNavigationState>;
|
|
|
|
|
|
|
|
|
|
const INACTIVE_NAVIGATION: NavigationState = {
|
|
|
|
|
isAllActive: false,
|
|
|
|
|
isActive: () => false,
|
|
|
|
|
isCollectionActive: () => false,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function pillClass(active: boolean, dense = false): string {
|
|
|
|
|
return `shrink-0 rounded-full ${dense ? "px-2.5" : "px-4"} py-2 text-sm font-extrabold transition ${
|
|
|
|
|
active
|
|
|
|
|
? "bg-white text-ari-marino shadow-lg shadow-black/10"
|
|
|
|
|
: "text-white/90 hover:bg-white/10 hover:text-white"
|
|
|
|
@@ -166,26 +207,26 @@ function mobileLinkClass(active: boolean): string {
|
|
|
|
|
}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function DesktopCategoryNavigation({ tree, onOpenMenu }: { tree: CategoryNode[]; onOpenMenu: () => void }) {
|
|
|
|
|
const { isActive, isAllActive } = useCategoryNavigationState();
|
|
|
|
|
return <CategoryNavBar tree={tree} onOpenMenu={onOpenMenu} isActive={isActive} isAllActive={isAllActive} />;
|
|
|
|
|
type NavBarProps = { tree: CategoryNode[]; collections: string[]; onOpenMenu: () => void; onOpenAccount: () => void };
|
|
|
|
|
|
|
|
|
|
function DesktopCategoryNavigation(props: NavBarProps) {
|
|
|
|
|
return <CategoryNavBar {...props} state={useCategoryNavigationState()} />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function DesktopCategoryNavigationFallback({ tree, onOpenMenu }: { tree: CategoryNode[]; onOpenMenu: () => void }) {
|
|
|
|
|
return <CategoryNavBar tree={tree} onOpenMenu={onOpenMenu} isActive={() => false} isAllActive={false} />;
|
|
|
|
|
function DesktopCategoryNavigationFallback(props: NavBarProps) {
|
|
|
|
|
return <CategoryNavBar {...props} state={INACTIVE_NAVIGATION} />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function CategoryNavBar({
|
|
|
|
|
tree,
|
|
|
|
|
onOpenMenu,
|
|
|
|
|
isActive,
|
|
|
|
|
isAllActive,
|
|
|
|
|
}: {
|
|
|
|
|
tree: CategoryNode[];
|
|
|
|
|
onOpenMenu: () => void;
|
|
|
|
|
isActive: (name: string) => boolean;
|
|
|
|
|
isAllActive: boolean;
|
|
|
|
|
}) {
|
|
|
|
|
// Desktop: con 1280px+ las 6 categorias raiz entran directo en la barra
|
|
|
|
|
// (cada una con su desplegable de subcategorias) -- antes solo se veian
|
|
|
|
|
// "Departamentos" y "Todo" y el resto de la barra quedaba vacia. Entre
|
|
|
|
|
// 1024 y 1280px no entran todas, asi que ahi se mantiene el mega menu de
|
|
|
|
|
// "Departamentos". "Personajes" (colecciones) va a la derecha desde lg.
|
|
|
|
|
// La hamburguesa solo en mobile: en desktop repetia lo mismo que la barra.
|
|
|
|
|
function CategoryNavBar({ tree, collections, onOpenMenu, onOpenAccount, state }: NavBarProps & { state: NavigationState }) {
|
|
|
|
|
const { isActive, isAllActive, isCollectionActive } = state;
|
|
|
|
|
const anyCollectionActive = collections.some(isCollectionActive);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<nav className="bg-ari-marino text-white">
|
|
|
|
|
<div className="relative mx-auto flex max-w-7xl items-center gap-1 px-4 py-2">
|
|
|
|
@@ -194,24 +235,201 @@ function CategoryNavBar({
|
|
|
|
|
size="icon"
|
|
|
|
|
onClick={onOpenMenu}
|
|
|
|
|
aria-label="Abrir menú principal"
|
|
|
|
|
className="mr-2 shrink-0 text-white hover:bg-white/10"
|
|
|
|
|
className="mr-2 shrink-0 text-white hover:bg-white/10 lg:hidden"
|
|
|
|
|
>
|
|
|
|
|
<Menu />
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
<DepartmentsMegaMenu tree={tree} isActive={isActive} />
|
|
|
|
|
<div className="shrink-0 xl:hidden">
|
|
|
|
|
<DepartmentsMegaMenu tree={tree} isActive={isActive} />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Link href="/catalog" prefetch={false} aria-current={isAllActive ? "page" : undefined} className={pillClass(isAllActive)}>
|
|
|
|
|
Todo
|
|
|
|
|
</Link>
|
|
|
|
|
|
|
|
|
|
<div className="hidden items-center gap-0.5 xl:flex">
|
|
|
|
|
{tree.map((root) => {
|
|
|
|
|
const active = isActive(root.name) || root.children.some((child) => isActive(child.name));
|
|
|
|
|
const href = `/catalog?category=${encodeURIComponent(root.name)}`;
|
|
|
|
|
if (root.children.length === 0) {
|
|
|
|
|
return (
|
|
|
|
|
<Link key={root.id} href={href} prefetch={false} aria-current={active ? "page" : undefined} className={pillClass(active, true)}>
|
|
|
|
|
{root.name}
|
|
|
|
|
</Link>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return (
|
|
|
|
|
<NavDropdown key={root.id} label={root.name} active={active}>
|
|
|
|
|
{(close) => (
|
|
|
|
|
<ul className="grid min-w-56 gap-1">
|
|
|
|
|
{root.children.map((child) => (
|
|
|
|
|
<li key={child.id}>
|
|
|
|
|
<Link
|
|
|
|
|
href={`/catalog?category=${encodeURIComponent(child.name)}`}
|
|
|
|
|
prefetch={false}
|
|
|
|
|
aria-current={isActive(child.name) ? "page" : undefined}
|
|
|
|
|
onClick={close}
|
|
|
|
|
className={`block rounded-lg px-3 py-2 text-sm transition ${isActive(child.name) ? "bg-white/10 font-bold text-ari-coral-claro" : "text-white/80 hover:bg-white/10 hover:text-white"}`}
|
|
|
|
|
>
|
|
|
|
|
{child.name}
|
|
|
|
|
</Link>
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
<li className="mt-1 border-t border-white/10 pt-2">
|
|
|
|
|
<Link href={href} prefetch={false} onClick={close} className="block rounded-lg px-3 py-2 text-sm font-black text-ari-coral-claro hover:bg-white/10">
|
|
|
|
|
Ver todo en {root.name}
|
|
|
|
|
</Link>
|
|
|
|
|
</li>
|
|
|
|
|
</ul>
|
|
|
|
|
)}
|
|
|
|
|
</NavDropdown>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="ml-auto hidden shrink-0 items-center gap-0.5 border-l border-white/20 pl-2 lg:flex xl:ml-2">
|
|
|
|
|
{collections.length > 0 && (
|
|
|
|
|
<NavDropdown label="Personajes" active={anyCollectionActive} align="right" icon={<Sparkles className="size-4" />}>
|
|
|
|
|
{(close) => (
|
|
|
|
|
<div className="grid w-[28rem] grid-cols-2 gap-1.5">
|
|
|
|
|
{collections.map((collection) => (
|
|
|
|
|
<Link
|
|
|
|
|
key={collection}
|
|
|
|
|
href={`/catalog?collection=${encodeURIComponent(collection)}`}
|
|
|
|
|
prefetch={false}
|
|
|
|
|
aria-current={isCollectionActive(collection) ? "page" : undefined}
|
|
|
|
|
onClick={close}
|
|
|
|
|
className={`rounded-xl px-3 py-2.5 text-sm font-bold transition ${isCollectionActive(collection) ? "bg-white text-ari-marino" : "bg-white/5 text-white hover:bg-white/15"}`}
|
|
|
|
|
>
|
|
|
|
|
{collection}
|
|
|
|
|
</Link>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</NavDropdown>
|
|
|
|
|
)}
|
|
|
|
|
{/* Equivalente al "Podemos ayudarte" de Amazon, con las paginas
|
|
|
|
|
de ayuda que la tienda ya tiene (antes solo en el footer). */}
|
|
|
|
|
<NavDropdown label="Ayuda" labelClassName="sr-only" ariaLabel="Ayuda" active={false} align="right" icon={<LifeBuoy className="size-4" />}>
|
|
|
|
|
{(close) => (
|
|
|
|
|
<ul className="grid min-w-60 gap-1">
|
|
|
|
|
<li><button type="button" onClick={() => { close(); onOpenAccount(); }} className={helpLinkClass}>Tu cuenta</button></li>
|
|
|
|
|
<li><Link href="/envios" prefetch={false} onClick={close} className={helpLinkClass}>Envíos y tarifas</Link></li>
|
|
|
|
|
<li><Link href="/cambios" prefetch={false} onClick={close} className={helpLinkClass}>Cambios y devoluciones</Link></li>
|
|
|
|
|
<li><Link href="/preguntas-frecuentes" prefetch={false} onClick={close} className={helpLinkClass}>Preguntas frecuentes</Link></li>
|
|
|
|
|
<li><Link href="/contacto" prefetch={false} onClick={close} className={helpLinkClass}>Contacto</Link></li>
|
|
|
|
|
<li className="mt-1 border-t border-white/10 pt-2">
|
|
|
|
|
<a href="https://wa.me/573016138155" target="_blank" rel="noopener noreferrer" onClick={close} className={`${helpLinkClass} font-black text-ari-coral-claro`}>Escríbenos por WhatsApp</a>
|
|
|
|
|
</li>
|
|
|
|
|
</ul>
|
|
|
|
|
)}
|
|
|
|
|
</NavDropdown>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</nav>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Con mouse, el mismo gesto dispara mouseenter (abre) y click: si el click
|
|
|
|
|
// alternara, cerraria el menu que se acaba de abrir por hover y para el
|
|
|
|
|
// usuario "no abre" (reportado por frontend-critic, reproducido 4/4 con
|
|
|
|
|
// Playwright; el bug ya existia en DepartmentsMegaMenu en prod). Un click
|
|
|
|
|
// dentro de los 600ms posteriores a abrir por hover lo deja abierto; en
|
|
|
|
|
// touch (sin hover) el click sigue alternando normal.
|
|
|
|
|
// El hover usa onPointerEnter/Leave filtrando pointerType === "mouse": en
|
|
|
|
|
// touch, Chromium emite un mouseenter+mouseleave sinteticos por cada tap, y
|
|
|
|
|
// ese mouseleave fantasma agendaba el cierre de 200ms -- el menu se cerraba
|
|
|
|
|
// solo en tablets (iPad horizontal cae en el rango del mega menu).
|
|
|
|
|
// Reportado por frontend-critic con log de eventos del navegador.
|
|
|
|
|
function keepOpenAfterHover(open: boolean, hoverOpenedAt: number): boolean {
|
|
|
|
|
if (open && Date.now() - hoverOpenedAt < 600) return true;
|
|
|
|
|
return !open;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const helpLinkClass = "block w-full rounded-lg px-3 py-2 text-left text-sm text-white/80 transition hover:bg-white/10 hover:text-white";
|
|
|
|
|
|
|
|
|
|
// Desplegable con la misma "intencion de hover" que DepartmentsMegaMenu
|
|
|
|
|
// (cierre con 200ms de gracia para movimientos en diagonal, click afuera y
|
|
|
|
|
// Escape cierran), pero anclado al propio boton en vez de a todo el ancho.
|
|
|
|
|
function NavDropdown({
|
|
|
|
|
label,
|
|
|
|
|
labelClassName,
|
|
|
|
|
ariaLabel,
|
|
|
|
|
active,
|
|
|
|
|
align = "left",
|
|
|
|
|
icon,
|
|
|
|
|
children,
|
|
|
|
|
}: {
|
|
|
|
|
label: string;
|
|
|
|
|
labelClassName?: string;
|
|
|
|
|
ariaLabel?: string;
|
|
|
|
|
active: boolean;
|
|
|
|
|
align?: "left" | "right";
|
|
|
|
|
icon?: ReactNode;
|
|
|
|
|
children: (close: () => void) => ReactNode;
|
|
|
|
|
}) {
|
|
|
|
|
const [open, setOpen] = useState(false);
|
|
|
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
const hoverOpenedAt = useRef(0);
|
|
|
|
|
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
|
|
|
|
|
|
const cancelClose = () => {
|
|
|
|
|
if (closeTimer.current) {
|
|
|
|
|
clearTimeout(closeTimer.current);
|
|
|
|
|
closeTimer.current = null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
const scheduleClose = () => {
|
|
|
|
|
cancelClose();
|
|
|
|
|
closeTimer.current = setTimeout(() => setOpen(false), 200);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!open) return;
|
|
|
|
|
function handlePointerDown(event: PointerEvent) {
|
|
|
|
|
if (containerRef.current && !containerRef.current.contains(event.target as Node)) setOpen(false);
|
|
|
|
|
}
|
|
|
|
|
function handleKeyDown(event: KeyboardEvent) {
|
|
|
|
|
if (event.key === "Escape") setOpen(false);
|
|
|
|
|
}
|
|
|
|
|
document.addEventListener("pointerdown", handlePointerDown);
|
|
|
|
|
document.addEventListener("keydown", handleKeyDown);
|
|
|
|
|
return () => {
|
|
|
|
|
document.removeEventListener("pointerdown", handlePointerDown);
|
|
|
|
|
document.removeEventListener("keydown", handleKeyDown);
|
|
|
|
|
};
|
|
|
|
|
}, [open]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => () => cancelClose(), []);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div ref={containerRef} className="relative shrink-0" onPointerEnter={(event) => { if (event.pointerType !== "mouse") return; cancelClose(); if (!open) hoverOpenedAt.current = Date.now(); setOpen(true); }} onPointerLeave={(event) => { if (event.pointerType === "mouse") scheduleClose(); }}>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
aria-label={ariaLabel}
|
|
|
|
|
onClick={() => setOpen((value) => keepOpenAfterHover(value, hoverOpenedAt.current))}
|
|
|
|
|
aria-expanded={open}
|
|
|
|
|
aria-haspopup="true"
|
|
|
|
|
className={`flex items-center gap-1.5 ${pillClass(active || open, true)}`}
|
|
|
|
|
>
|
|
|
|
|
{icon}
|
|
|
|
|
<span className={labelClassName}>{label}</span>
|
|
|
|
|
<ChevronDown className={`size-3.5 opacity-70 transition-transform ${open ? "rotate-180" : ""}`} />
|
|
|
|
|
</button>
|
|
|
|
|
{open && (
|
|
|
|
|
<div className={`absolute top-full z-50 pt-1 ${align === "right" ? "right-0" : "left-0"}`}>
|
|
|
|
|
<div className="rounded-2xl border border-white/10 bg-ari-marino p-3 shadow-2xl">{children(() => setOpen(false))}</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function DepartmentsMegaMenu({ tree, isActive }: { tree: CategoryNode[]; isActive: (name: string) => boolean }) {
|
|
|
|
|
const [open, setOpen] = useState(false);
|
|
|
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
const hoverOpenedAt = useRef(0);
|
|
|
|
|
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
|
|
|
|
|
|
const cancelClose = () => {
|
|
|
|
@@ -251,14 +469,15 @@ function DepartmentsMegaMenu({ tree, isActive }: { tree: CategoryNode[]; isActiv
|
|
|
|
|
|
|
|
|
|
const openNow = () => {
|
|
|
|
|
cancelClose();
|
|
|
|
|
if (!open) hoverOpenedAt.current = Date.now();
|
|
|
|
|
setOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div ref={containerRef} className="shrink-0" onMouseEnter={openNow} onMouseLeave={scheduleClose}>
|
|
|
|
|
<div ref={containerRef} className="shrink-0" onPointerEnter={(event) => { if (event.pointerType === "mouse") openNow(); }} onPointerLeave={(event) => { if (event.pointerType === "mouse") scheduleClose(); }}>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setOpen((value) => !value)}
|
|
|
|
|
onClick={() => setOpen((value) => keepOpenAfterHover(value, hoverOpenedAt.current))}
|
|
|
|
|
aria-expanded={open}
|
|
|
|
|
aria-haspopup="true"
|
|
|
|
|
aria-label="Departamentos"
|
|
|
|
|