// Copyright (c) Meta Platforms, Inc. and affiliates. 'use client'; /** * @file TopNavMegaMenu.tsx * @input Uses React, StyleX, usePopover (Popover API + CSS anchor positioning) * @output Exports TopNavMegaMenu component and related types * @position Navigation item with hover-triggered full-width mega menu for TopNav * * Uses usePopover to promote the panel to the top layer via the Popover API, * eliminating z-index stacking. CSS anchor positioning places the panel below * the nav wrapper. * * The default (desktop) trigger opens on hover and click. Hover opens are * transient; click/keyboard opens are pinned. A click shortly after hover-open * confirms and pins the panel instead of closing it. The panel remains an auto * popover for native dismissal and sibling exclusivity; `popoverTarget` * registers the trigger as its native invoker so the guard runs before any * dismiss. * * Supports three render modes via TopNavRenderContext: * - 'default': desktop popover mega menu (hover/click triggered) * - 'mobile-bar': returns null (hidden in compact mobile bar) * - 'drawer': drill-down navigation with back button * * SYNC: When modified, update these files to stay in sync: * - /packages/core/src/TopNav/TopNav.doc.mjs * - /packages/core/src/TopNav/index.ts * - /packages/cli/assets/templates/blocks/components/TopNav/ (showcase blocks) */ import React, { useCallback, useEffect, useRef, useState, type ReactNode } from 'react'; import * as stylex from "../style-runtime.js"; import "../theme/tokens.stylex.js"; import { colorVars, spacingVars, radiusVars, durationVars, easeVars, fontWeightVars, shadowVars, typeScaleVars, borderVars } from "../theme/tokens.stylex.js"; import { usePopover } from "../Popover/usePopover.js"; import { useMenuHover } from "../hooks/useMenuHover.js"; import { Grid } from "../Grid/Grid.js"; import { Icon } from "../Icon/index.js"; import { mergeProps, composeEventHandlers } from "../utils/index.js"; import type { BaseProps } from "../BaseProps.js"; import { navItemStyles } from "../NavItem/navItemStyles.stylex.js"; import { useTopNavSlot } from "./TopNavContext.js"; import { useTopNavRenderMode } from "./TopNavRenderContext.js"; import { themeProps } from "../utils/themeProps.js"; import { focusOutlineProps } from "../utils/focusOutline.stylex.js"; import { interactionOverlayStyles } from "../utils/interactionOverlay.stylex.js"; import { useMergedRefs } from "../hooks/useMergedRefs.js"; // ============================================================================= // Styles // ============================================================================= const styles = { trigger: { k1xSpc: "x3nfvp2", kGNEyG: "x6s0dn4", kOIVth: "x1txdalj", k8WAf4: "x1vofgu7", kg3NbH: "xrrkdod", kaIpWk: "xh6dtrn", kGuDYH: "xcr08ib", kLWn49: "x1kq96og", k63SB2: "x1e4wzip", kMwMTN: "xv1l7n4", kybGjl: "x1hl2dhg", kkrTdU: "x1ypdohk x16khyan", k1ekBW: "xs2xxs2", kIyJzY: "xuedmi6", kAMwcw: "xlr8y92", kWkggS: "xjbqb8w xoevpu5", kMv6JI: "xjb2p0i", $$css: true as const }, triggerOpen: { kMwMTN: "x1tgivj0", kWkggS: "x1lmrjuc", $$css: true as const }, chevron: { k1xSpc: "x3nfvp2", kGNEyG: "x6s0dn4", kzqmXN: "x15vhz18", kZKoxP: "xlzyvqe", kGuDYH: "x1qlqyl8", k1ekBW: "x11xpdln", kIyJzY: "xuedmi6", kAMwcw: "xlr8y92", $$css: true as const }, chevronOpen: { k3aq6I: "x19jd1h0", $$css: true as const }, panelAnimation: { kSiTet: "xg01cxk xofkqq2", k3aq6I: "x1bvilyr x9cjr8z", k1ekBW: "x4bbghf", kIyJzY: "xgneliz", kAMwcw: "xlr8y92", kzIqYQ: "xd00j3c", kamtoy: "x4itv7f", k1tdAh: "x12p7p72", $$css: true as const }, panelViewportFit: { k1xSpc: "x1s85apg x1y40yee", kXwgrk: "xdt5ytf", kskxy: "xbhvqqn", $$css: true as const }, menuWrapper: { kzQI83: "xgyuaek", kmuXW: "xs83m0k", kCS8Yb: "xchdapg", k7Eaqz: "xeuugli", $$css: true as const }, drawerHeader: { kjj79g: "x1qughib", $$css: true as const }, drawerChevron: { k1xSpc: "x3nfvp2", kzqmXN: "x15vhz18", kZKoxP: "xlzyvqe", kGuDYH: "x1qlqyl8", k1ekBW: "x11xpdln", kIyJzY: "xuedmi6", kAMwcw: "xlr8y92", $$css: true as const }, drawerChevronExpanded: { k3aq6I: "x19jd1h0", $$css: true as const } }; // ============================================================================= // Types // ============================================================================= export interface TopNavMegaMenuProps extends BaseProps { ref?: React.Ref; /** The visible label for the nav item trigger. */ label: string; /** * Menu items slot — typically one or more TopNavMegaMenuItem components, * but accepts any ReactNode for custom layouts. */ items?: ReactNode; /** * Featured content slot — rendered in the right panel on desktop, * and below the items in the mobile drawer. */ featured?: ReactNode; /** Delay before showing the menu on hover (ms). @default 150 */ delay?: number; /** Delay before hiding the menu after mouse leaves (ms). @default 250 */ hideDelay?: number; /** * Callback fired when the mega menu opens or closes. * Useful for coordinating wrapper styles (e.g. hiding other shadows). */ onOpenChange?: (isOpen: boolean) => void; } // ============================================================================= // TopNavMegaMenu // ============================================================================= /** * A navigation item that displays a full-width mega menu on hover. * * Uses a composed children API with sub-components: * - `items` — ReactNode slot, typically TopNavMegaMenuItem components * - `featured` — ReactNode slot for the right-panel / drawer featured card * * Supports three render modes via TopNavRenderContext: * - `'default'`: desktop popover with hover/click trigger * - `'mobile-bar'`: hidden (returns null) * - `'drawer'`: inline collapsible matching TopNavMenu pattern * * @example * ``` * * } * href="/analytics" * /> * } * href="/messaging" * /> * * } * featured={ * <> * New: AI Features *

Explore our latest AI-powered tools.

* * } * /> * } * /> * ``` */ export function TopNavMegaMenu({ ref, label, items, featured, delay = 150, hideDelay = 250, onOpenChange, ...rest }: TopNavMegaMenuProps) { const renderMode = useTopNavRenderMode(); // ========================================================================= // Mobile-bar mode — hidden // ========================================================================= if (renderMode === 'mobile-bar') { return null; } // ========================================================================= // Drawer mode — inline collapsible // ========================================================================= if (renderMode === 'drawer') { return ; } // ========================================================================= // Default mode — desktop popover // ========================================================================= return ; } TopNavMegaMenu.displayName = 'TopNavMegaMenu'; // ============================================================================= // DefaultMegaMenu — desktop popover mode // ============================================================================= /** The panel is a grid of links, not `role="menuitem"` rows. */ const PANEL_ITEM_SELECTOR = 'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])'; function DefaultMegaMenu({ ref, label, items, featured, delay = 150, hideDelay = 250, onOpenChange, xstyle, className, style, onClick: onClickProp, onMouseEnter: onMouseEnterProp, onMouseLeave: onMouseLeaveProp, ...rest }: TopNavMegaMenuProps) { const slot = useTopNavSlot(); const triggerButtonRef = useRef(null); const handlePopoverShow = useCallback(() => { onOpenChange?.(true); }, [onOpenChange]); const handlePopoverHide = useCallback(() => { onOpenChange?.(false); }, [onOpenChange]); const popover = usePopover({ // role: 'none' — the panel exposes its own role="group" labeled by // `label`. Pointer/hover opens keep focus on the trigger; keyboard and // assistive-tech opens move focus into the panel (a labeled group you exit // with Escape or by tabbing out). Either way role="dialog" // aria-modal="true" would be wrong: it announces an unnamed modal dialog // around a grid of links (and, when focus stays on the trigger, marks the // focused control inert). role: 'none', // hasSurface: false — mega menu provides its own surface (panelContainer) // with border-top and custom overflow. Animation is applied via the // render() call's xstyle prop (panelAnimation), not the hook options. hasSurface: false, // Keep native outside-click/Escape dismissal and sibling exclusivity. // The trigger's popoverTarget association prevents its activation from // being treated as an ordinary outside interaction. hasLightDismiss: true, onShow: handlePopoverShow, onHide: handlePopoverHide }); // Set the CSS anchor to the parent