"use client"; import { usePathname } from "next/navigation"; import { useEffect, useRef, useState } from "react"; import { COOKIE_BAR_ID } from "@/lib/analytics"; import { NAVIGATION_ITEMS } from "@/lib/site-navigation"; import { headerOpensWhatsApp, type WhatsAppContexts, whatsAppSubjectForPath, } from "@/lib/whatsapp"; import HoverPrefetchLink from "./HoverPrefetchLink"; import { Logo } from "./Logo"; import WhatsAppLink from "./WhatsAppLink"; const MENU_ID = "site-navigation-menu"; const CTA_LABEL = "Falar sobre meu projeto"; /** Fora da home o botão abre o WhatsApp: "Falar sobre meu projeto" é sempre o formulário. */ const WHATSAPP_LABEL = "Chamar no WhatsApp"; /** Ids que ficam inertes com o menu aberto: o conteúdo da página e o rodapé (o id do rodapé vem de app/layout.tsx). */ const INERT_IDS = ["main-content", "site-footer"]; /** A partir do breakpoint xl a barra mostra os links e o menu do celular deixa de existir. */ const DESKTOP_QUERY = "(min-width: 1280px)"; /** * Controles que o Tab percorre com o menu aberto: todos os da barra de cookies, quando ela está aberta (o link "Política * de privacidade", Aceitar e Recusar; ela vem antes do cabeçalho no layout e não fica inerte), e depois logo, botão do * menu, links e botão de contato do painel. * A navegação e o botão da barra de desktop ficam de fora (data-menu-trap-skip): estão escondidos no celular. */ function menuFocusables(header: HTMLElement) { const cookieBar = document.getElementById(COOKIE_BAR_ID); const roots = cookieBar ? [cookieBar, header] : [header]; return roots .flatMap((root) => Array.from( root.querySelectorAll("a[href], button:not([disabled])"), ), ) .filter((element) => !element.closest("[data-menu-trap-skip]")); } /** Tab (e Shift+Tab) com o menu aberto: o foco dá a volta entre o cabeçalho e a barra de cookies, sem sair para a página inerte. */ function trapTab(event: KeyboardEvent, header: HTMLElement) { const focusables = menuFocusables(header); const first = focusables[0]; const last = focusables[focusables.length - 1]; const [edge, opposite] = event.shiftKey ? [first, last] : [last, first]; const active = document.activeElement; const leaving = active === edge || !focusables.some((element) => element === active); if (!leaving || !opposite) return; event.preventDefault(); opposite.focus(); } /** * Botão de contato do cabeçalho. Fora da home (serviço, case, /ia, /cases, /servicos e /curriculo) ele abre o WhatsApp * já com o assunto da página, com o nome "Chamar no WhatsApp"; na home e nas demais páginas leva ao formulário da home * ("Falar sobre meu projeto"). */ function ContactCta({ className, placement, viaWhatsApp, subject, onNavigate, }: { className: string; placement: string; viaWhatsApp: boolean; subject?: string; onNavigate?: () => void; }) { if (viaWhatsApp) { return ( {WHATSAPP_LABEL} ); } return ( {CTA_LABEL} ); } export function Header({ whatsAppContexts, }: { /** Nomes dos serviços e dos cases, montados no servidor (app/layout.tsx), para o WhatsApp falar da página aberta. */ whatsAppContexts?: WhatsAppContexts; }) { const pathname = usePathname(); const viaWhatsApp = headerOpensWhatsApp(pathname); const subject = whatsAppSubjectForPath(pathname, whatsAppContexts); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [scrolled, setScrolled] = useState(false); const headerRef = useRef(null); const menuButtonRef = useRef(null); // Trocou a rota: o menu fecha (o cabeçalho vive no layout e não remonta entre as páginas). // biome-ignore lint/correctness/useExhaustiveDependencies: pathname só dispara o fechamento a cada página useEffect(() => { setMobileMenuOpen(false); }, [pathname]); useEffect(() => { let wasScrolled = false; const updateScrolled = () => { const nextScrolled = window.scrollY > 0; if (nextScrolled === wasScrolled) return; wasScrolled = nextScrolled; setScrolled(nextScrolled); }; updateScrolled(); window.addEventListener("scroll", updateScrolled, { passive: true }); return () => window.removeEventListener("scroll", updateScrolled); }, []); const closeMobileMenu = () => setMobileMenuOpen(false); // Menu aberto: o conteúdo e o rodapé ficam inertes, a página não rola, o Tab não sai do cabeçalho e o Esc fecha. useEffect(() => { const header = headerRef.current; const menuButton = menuButtonRef.current; if (!mobileMenuOpen || !header || !menuButton) return; const root = document.documentElement; const inertTargets = INERT_IDS.flatMap((id) => { const element = document.getElementById(id); return element ? [element] : []; }); for (const element of inertTargets) element.setAttribute("inert", ""); const previousOverflow = root.style.overflow; root.style.overflow = "hidden"; // Devolve a página ao normal na hora, sem esperar o React: um link do menu para #id precisa achar o destino // focável quando o gerenciador de âncoras da home (useHomeFragmentNavigation) tenta focá-lo no clique. let released = false; const release = () => { if (released) return; released = true; for (const element of inertTargets) element.removeAttribute("inert"); root.style.overflow = previousOverflow; }; const onKeyDown = (event: KeyboardEvent) => { if (event.key === "Tab") return trapTab(event, header); if (event.key !== "Escape") return; event.preventDefault(); release(); setMobileMenuOpen(false); menuButton.focus(); }; // Na fase de captura da janela, antes do document: roda antes do gerenciador de âncoras da home. const onLinkClick = (event: MouseEvent) => { const link = event.target instanceof Element ? event.target.closest("a[href]") : null; if (link && header.contains(link)) release(); }; const desktop = window.matchMedia?.(DESKTOP_QUERY); const onViewportChange = (event: MediaQueryListEvent) => { if (!event.matches) return; release(); setMobileMenuOpen(false); }; document.addEventListener("keydown", onKeyDown); window.addEventListener("click", onLinkClick, true); desktop?.addEventListener?.("change", onViewportChange); return () => { release(); document.removeEventListener("keydown", onKeyDown); window.removeEventListener("click", onLinkClick, true); desktop?.removeEventListener?.("change", onViewportChange); }; }, [mobileMenuOpen]); return (
{mobileMenuOpen ? ( // Tela cheia abaixo da barra, com fundo opaco: o conteúdo da página não aparece por trás dos links. // A barra de cookies (fixa, acima do cabeçalho) cobre a base da tela: o espaço dela sobe o botão de contato.
) : null}
); }