// Copyright (c) Meta Platforms, Inc. and affiliates. 'use client'; /** * @file CommandPaletteItem.tsx * @input Uses React, StyleX, CommandPaletteContext, DialogContext * @output Exports CommandPaletteItem component * @position Sub-component; individual selectable item * * SYNC: When modified, update: * - /packages/cli/assets/templates/blocks/components/CommandPalette/ (showcase blocks) */ import { useCallback, useEffect, useMemo, useRef, type ReactNode } from 'react'; import * as stylex from "../style-runtime.js"; import type { BaseProps } from "../BaseProps.js"; import { mergeProps } from "../utils/index.js"; import { composeEventHandlers } from "../utils/composeEventHandlers.js"; import "../theme/tokens.stylex.js"; import { colorVars, spacingVars, radiusVars, typographyVars, typeScaleVars } from "../theme/tokens.stylex.js"; import { useCommandPaletteContext } from "./CommandPaletteContext.js"; import { useDialogContext } from "../Dialog/DialogContext.js"; import { themeProps } from "../utils/themeProps.js"; import { interactionOverlayStyles } from "../utils/interactionOverlay.stylex.js"; import { useMergedRefs } from "../hooks/useMergedRefs.js"; const styles = { item: { k1xSpc: "x78zum5", kGNEyG: "x6s0dn4", kOIVth: "x1txdalj", kzqmXN: "xh8yej3", kg3NbH: "xrrkdod", k8WAf4: "xce4md1", kaIpWk: "xx3sua9", kMv6JI: "x9ynric", kGuDYH: "xcr08ib", kMwMTN: "x1tgivj0", kWkggS: "xjbqb8w", kkrTdU: "x1ypdohk x16khyan", k9WMMc: "x1yc453h", kI3sdo: "x1a2a7pz", kfSwDN: "x87ps6o", $$css: true as const }, itemHighlighted: { kWkggS: "x1lmrjuc", $$css: true as const }, itemDisabled: { kSiTet: "xbyyjgo", kkrTdU: "xt0e3qv", $$css: true as const }, itemSelected: { kWkggS: "xgcxg3y", $$css: true as const } }; export interface CommandPaletteItemProps extends Omit, 'onChange' | 'onSelect'> { /** Ref forwarded to the root element. */ ref?: React.Ref; /** Unique value for identification and selection. */ value: string; /** Called when this item is selected (via click or Enter). */ onSelect?: (value: string) => void; /** * Whether this item is visually highlighted (keyboard focus). * When omitted inside CommandPalette, derived from context. * @default false */ isHighlighted?: boolean; /** * Whether this item is currently selected (picker mode). * @default false */ isSelected?: boolean; /** * Whether the item is disabled. * @default false */ isDisabled?: boolean; /** Item content. Fully custom — render icons, descriptions, shortcuts, etc. */ children: ReactNode; } /** * A selectable item in the command palette. * Accepts arbitrary children for full rendering control. * * When used inside CommandPalette, registers with context for * keyboard navigation and selection. Can also be used * standalone with explicit isHighlighted/isSelected props. * * @compositionHint Place inside CommandPaletteList or CommandPaletteGroup. * * @example * ``` * navigate('/settings')}> * Settings * * ``` */ export function CommandPaletteItem({ value, onSelect, isHighlighted: controlledHighlighted, isSelected: controlledSelected, isDisabled = false, children, ref, xstyle, className, style, onClick: onClickProp, onMouseEnter: onMouseEnterProp, ...props }: CommandPaletteItemProps) { const ctx = useCommandPaletteContext(); const dialogContext = useDialogContext(); const isInlineDialog = dialogContext?.isInline === true; const itemRef = useRef(null); const didMountRef = useRef(false); // Find this item's index in the flat selectable items list (DOM order). // This aligns with useCombobox's index-based navigation. const itemIndex = useMemo(() => ctx?.selectableItems.findIndex(item => item.value === value) ?? -1, [ctx?.selectableItems, value]); // Highlight from useCombobox: index-based, matches DOM order const isHighlighted = controlledHighlighted ?? (ctx ? ctx.highlightedIndex === itemIndex && itemIndex >= 0 : false); const isSelected = controlledSelected ?? (ctx ? ctx.value === value : false); useEffect(() => { // Inside CommandPalette the shared useHighlightedOptionScroll (via // useCombobox) is the single scrollIntoView owner (#6077); a second owner // here doubled every keyboard scroll and scrolled on hover. Standalone // items (no context) keep their own scroll. if (ctx) { return; } // Inline dialogs are documentation/showcase previews. Avoid scrolling the // surrounding page when picker mode auto-highlights its selected item on // mount, while preserving scroll-into-view after user navigation. const shouldSkipInitialInlineScroll = isInlineDialog && !didMountRef.current; didMountRef.current = true; if (shouldSkipInitialInlineScroll) { return; } if (isHighlighted && itemRef.current) { itemRef.current.scrollIntoView?.({ block: 'nearest' }); } }, [ctx, isHighlighted, isInlineDialog]); const handleClick = useCallback(() => { if (isDisabled) { return; } onSelect?.(value); if (ctx) { ctx.selectItem(value); ctx.onClose(); } }, [isDisabled, value, onSelect, ctx]); return
= 0 ? ctx.getItemId(itemIndex) : undefined} role="option" aria-selected={isSelected} aria-disabled={isDisabled || undefined} data-value={value} onClick={composeEventHandlers(onClickProp, handleClick)} onMouseEnter={onMouseEnterProp} {...mergeProps(themeProps('command-palette-item'), stylex.props(styles.item, !isDisabled && interactionOverlayStyles.backgroundColor, isHighlighted && styles.itemHighlighted, isSelected && styles.itemSelected, isDisabled && styles.itemDisabled, xstyle), className, style)}> {children}
; } CommandPaletteItem.displayName = 'CommandPaletteItem';