// Copyright (c) Meta Platforms, Inc. and affiliates. 'use client'; /** * @file TopNavItem.tsx * @input Uses React, ReactNode, BaseProps, LinkComponentType * @output Exports TopNavItem component and TopNavItemProps * @position Navigation item component for TopNav startContent * * SYNC: When modified, update these files to stay in sync: * - /packages/core/src/TopNav/TopNav.doc.mjs * - /packages/core/src/TopNav/TopNav.test.tsx * - /packages/core/src/TopNav/index.ts * - /apps/storybook/stories/TopNav.stories.tsx * - /packages/cli/assets/templates/blocks/components/TopNav/ (showcase blocks) */ import type { ReactNode } from 'react'; import type { BaseProps } from "../BaseProps.js"; import * as stylex from "../style-runtime.js"; import "../theme/tokens.stylex.js"; import { colorVars, spacingVars, radiusVars, durationVars, easeVars, fontWeightVars, typeScaleVars } from "../theme/tokens.stylex.js"; import { useLinkComponent } from "../Link/useLinkComponent.js"; import type { LinkComponentType } from "../Link/types.js"; import { useTopNavRenderMode } from "./TopNavRenderContext.js"; import { navItemStyles, type NavItemSize } from "../NavItem/navItemStyles.stylex.js"; import { mergeProps } from "../utils/index.js"; import { useAppShellMobile } from "../AppShell/AppShellMobileContext.js"; import { themeProps } from "../utils/themeProps.js"; import { focusOutlineProps } from "../utils/focusOutline.stylex.js"; import { interactionOverlayStyles } from "../utils/interactionOverlay.stylex.js"; /** * NavItem styles with hover/selected states */ const styles = { base: { 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", $$css: true as const }, selected: { kMwMTN: "x1tgivj0", k63SB2: "x2mo6ok", kWkggS: "x17x4s8c xcu6dpe xjtnuge", $$css: true as const }, iconOnly: { kg3NbH: "xf314gf", $$css: true as const } }; export interface TopNavItemProps extends BaseProps { /** Ref forwarded to the root element */ ref?: React.Ref; /** Link destination URL. */ href?: string; /** Where to open the linked document. */ target?: string; /** Link relationship. */ rel?: string; /** Causes the browser to download the linked URL. */ download?: string | boolean; /** Referrer policy for the link. */ referrerPolicy?: React.HTMLAttributeReferrerPolicy; /** * Custom component to render instead of ``. * Overrides the provider-level default set by LinkProvider. * Must accept href, className, style, and children props. */ as?: LinkComponentType; /** * The accessible label for the nav item. * Rendered as visible text by default. When `isIconOnly` is true, * used as aria-label instead. */ label: string; /** * Whether this nav item is currently selected/highlighted. * @default false */ isSelected?: boolean; /** * Whether the nav item is disabled. * A disabled item renders as a plain anchor without an href (and without * target), so it cannot navigate and is removed from the tab order. * @default false */ isDisabled?: boolean; /** * Renders the item as a square icon-only element. * When true, `label` becomes the aria-label and visible text is hidden. * Requires `icon` to be set. * @default false */ isIconOnly?: boolean; /** * Optional icon to display before the label. */ icon?: ReactNode; /** * Optional content to render instead of the label. */ children?: ReactNode; /** * Size variant for the nav item. Has no effect in horizontal mode; * controls height/padding in drawer mode. * @default 'md' */ size?: NavItemSize; } /** * Click handler for disabled items. The disabled anchor renders without an * href, so there is no navigation to block in practice; preventDefault is a * defensive guard against synthetic/programmatic clicks. */ function preventDefaultClick(event: React.MouseEvent): void { event.preventDefault(); } /** * A navigation item for use within TopNav startContent. * * Renders as an anchor element with hover/selected states. * Supports icons and selected state indication with highlighted appearance. * * @example * ``` * * * * } isIconOnly /> * * } * /> * ``` */ export function TopNavItem({ as, href, target, onClick, label, isSelected = false, isDisabled = false, isIconOnly = false, icon, children, size = 'md', xstyle, className, style, ref, ...props }: TopNavItemProps) { const LinkComponent = useLinkComponent(as); const renderMode = useTopNavRenderMode(); const { closeMobileNav } = useAppShellMobile(); // A disabled item renders as a plain with no href: an href-less anchor // is not focusable and exposes no link affordance, so programmatic focus + // Enter, AT activation commands, and middle-click cannot navigate or fire // the consumer onClick. The router LinkComponent is deliberately skipped — // a disabled item performs no navigation, and custom router links may // require a live href. target is omitted with the href. (Mirrors the // disabled paths of Link and SideNavItem.) const Root = isDisabled ? 'a' : LinkComponent; // ========================================================================= // Drawer mode — render as a SideNavItem-style vertical list element // ========================================================================= if (renderMode === 'drawer') { const handleDrawerClick = (e: React.MouseEvent) => { if (isDisabled) { preventDefaultClick(e); return; } // Forward the original onClick if present onClick?.(e); closeMobileNav(); }; return {icon} {!isIconOnly && (children ?? label)} ; } // ========================================================================= // Default / mobile-bar mode — standard horizontal nav item // ========================================================================= return {icon} {!isIconOnly && (children ?? label)} ; } TopNavItem.displayName = 'TopNavItem';