// Copyright (c) Meta Platforms, Inc. and affiliates. 'use client'; /** * @file NavHeadingMenu.tsx * @input Uses React, StyleX, useListFocus, NavMenuContext * @output Exports NavHeadingMenu component and NavHeadingMenuProps type * @position Core implementation; consumed by index.ts * * SYNC: When modified, update: * - /packages/core/src/NavMenu/index.ts */ import React, { useCallback, useMemo, type ReactNode } from 'react'; import * as stylex from "../style-runtime.js"; import "../theme/tokens.stylex.js"; import { spacingVars } from "../theme/tokens.stylex.js"; import { composeEventHandlers, mergeProps } from "../utils/index.js"; import type { BaseProps } from "../BaseProps.js"; import { useListFocus } from "../hooks/useListFocus.js"; import { useTypeahead } from "../hooks/useTypeahead.js"; import { themeProps } from "../utils/themeProps.js"; import { NavHeadingMenuContext, useNavHeadingCloseContext, type NavHeadingMenuSize } from "./NavMenuContext.js"; import { useMergedRefs } from "../hooks/useMergedRefs.js"; const styles = { root: { k1xSpc: "x78zum5", kXwgrk: "xdt5ytf", kOIVth: "x1lsbc85", $$css: true as const } }; const sizeStyles = { sm: { k7Eaqz: "x5w4yej", $$css: true as const }, md: { k7Eaqz: "x1jzhcrs", $$css: true as const }, lg: { k7Eaqz: "xlm99nl", $$css: true as const } }; export interface NavHeadingMenuProps extends BaseProps { ref?: React.Ref; /** Menu items (NavHeadingMenuItem, dividers, custom content). */ children: ReactNode; /** * Size — controls min-width and flows to items for padding. * @default 'md' */ size?: NavHeadingMenuSize; /** * Minimum width override. Takes precedence over size-based defaults. */ minWidth?: number | string; } /** * Accessible menu container for nav heading popovers. * * Provides `role="menu"` with arrow-key navigation (Home/End/Escape) * and a size context that flows to child items for consistent padding. * Pass as the `menu` prop of SideNavHeading or TopNavHeading. * * The parent heading component injects the close callback via context, * so items automatically dismiss the popover on selection. * * @example * ``` * * * * * } * /> * ``` */ export function NavHeadingMenu({ ref, children, size = 'md', minWidth, xstyle, className, style: styleProp, 'data-testid': testId, onKeyDown: onKeyDownProp, ...rest }: NavHeadingMenuProps) { const closeCtx = useNavHeadingCloseContext(); const closeMenu = closeCtx?.closeMenu; const { listRef, handleKeyDown, focusItem } = useListFocus({ itemSelector: '[role="menuitem"]:not([aria-disabled="true"])', onEscape: closeMenu }); // First-character typeahead over the (enabled) menu items (menus-11). const getMenuItems = useCallback((): HTMLElement[] => listRef.current ? Array.from(listRef.current.querySelectorAll('[role="menuitem"]:not([aria-disabled="true"])')) : [], [listRef]); const typeahead = useTypeahead({ getItemLabels: () => getMenuItems().map(el => el.textContent), onMatch: focusItem, getCurrentIndex: () => getMenuItems().findIndex(el => el === document.activeElement || el.contains(document.activeElement)) }); // Extend useListFocus with Enter/Space activation. Items rendered without an // `href` are `
` elements, which have no native keyboard // activation — without this, Enter/Space on a focused onClick-only item does // nothing. Anchor items (with `href`) already activate on Enter natively. const listKeyDown = useCallback((e: React.KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { const focused = document.activeElement as HTMLElement | null; if (focused?.getAttribute('role') === 'menuitem') { e.preventDefault(); focused.click(); return; } } if (typeahead.onKeyDown(e)) { e.preventDefault(); return; } handleKeyDown(e); }, [handleKeyDown, typeahead]); const ctx = useMemo(() => ({ closeMenu: closeMenu ?? (() => {}), size }), [closeMenu, size]); const inlineStyle = minWidth != null ? { ...styleProp, minWidth } : styleProp; return
{children}
; } NavHeadingMenu.displayName = 'NavHeadingMenu';