// Copyright (c) Meta Platforms, Inc. and affiliates. 'use client'; /** * @file PanelSearchInput.tsx * @input Uses React, StyleX, Icon, InputClearButton, theme tokens * @output Exports PanelSearchInput — the search row that sits at the top of a * dropdown panel * @position Shared internal primitive; used by Selector and MultiSelector * * A dropdown panel is already a bordered, elevated surface. Nesting a bordered * TextInput inside it draws a second box within that box, so the field reads as * a control dropped into the menu rather than part of it. This row is the * seamless alternative — magnifier, borderless input, clear button — separated * from the options by a divider the panel owns. * * The field is shaped like the option rows under it rather than like a form * input: same inset from the panel edge, same `--radius-element` corners, same * height. The focus ring is drawn on that rounded box, so focus reads as "this * row of the panel" — the same visual language as an item's highlight — instead * of ringing the whole panel header. It appears for keyboard focus only; see * `fieldKeyboardFocus` for why `:focus-visible` alone does not mean that. * * ┌─ panel ──────────────────┐ * │ ┌──────────────────────┐ │ ← field: rounded, inset, item-sized * │ │ ⌕ Search… ✕ │ │ * │ └──────────────────────┘ │ * ├──────────────────────────┤ ← divider (full-bleed) * │ ┌──────────────────────┐ │ * │ │ Apple │ │ ← option row: same box * * The clear button is the shared `InputClearButton`, so the affordance and its * ghost-button behavior match every other input's clear. It renders AFTER the * input in DOM order: the selector components' Tab handling depends on that * order to keep the popup open while focus moves onto it. * * Not exported from the package: it is an implementation detail of the panels * that use it, not public API. */ import { useCallback, useState, type Ref } from 'react'; import * as stylex from "../style-runtime.js"; import { Icon } from "../Icon/index.js"; import { InputClearButton } from "./InputClearButton.js"; import { mergeProps } from "../utils/index.js"; import { getInteractionModality, useInteractionModalityTracking } from "../utils/interactionModality.js"; import "../theme/tokens.stylex.js"; import { colorVars, durationVars, easeVars, radiusVars, spacingVars, typeScaleVars, typographyVars } from "../theme/tokens.stylex.js"; import type { BaseProps } from "../BaseProps.js"; const styles = { wrapper: { k8WAf4: "xu0wf1k", kg3NbH: "x7a5moj", $$css: true as const }, icon: { k1xSpc: "x78zum5", kGNEyG: "x6s0dn4", kmuXW: "x2lah0s", $$css: true as const } }; export interface PanelSearchInputProps extends Omit, 'onChange'> { /** Ref forwarded to the input element (for focus management). */ ref?: Ref; /** * Accessible name for the input. Rendered as `aria-label`: the panel has no * visible label for the field, and a placeholder is not a reliable name. */ label: string; /** Accessible name for the clear (✕) button, e.g. `Clear Search options`. */ clearLabel: string; /** Placeholder text shown while the query is empty. */ placeholder?: string; /** The current query. */ value: string; /** Called with the next query on every keystroke and on clear. */ onValueChange: (value: string) => void; /** Key handler for the input itself. */ onKeyDown?: React.KeyboardEventHandler; /** Focus handler for the input; runs after the ring's own bookkeeping. */ onFocus?: React.FocusEventHandler; /** Blur handler for the input; runs after the ring's own bookkeeping. */ onBlur?: React.FocusEventHandler; /** * Key handler for the row. Events from the input are handled by `onKeyDown`; * this one exists for keys pressed on the clear button, which has no other * handler of its own. */ onContainerKeyDown?: React.KeyboardEventHandler; } /** * Search row for the top of a dropdown panel: magnifier, borderless input, and * a clear button once a query is typed, in a rounded box shaped like the * option rows beneath it. * * `className`/`style`/`xstyle` apply to the outer row (use them to match the * option list's inline padding); every other prop (`role`, `aria-*`, `id`, …) * passes through to the ``, so the caller owns the combobox wiring. */ export function PanelSearchInput({ ref, label, clearLabel, placeholder, value, onValueChange, onKeyDown, onFocus, onBlur, onContainerKeyDown, xstyle, className, style, ...props }: PanelSearchInputProps) { // `:focus-visible` matches a text input focused by POINTER as well (CSS // Selectors 4, verified in Chromium), so it cannot express "keyboard focus" // by itself. Gate it on how the user last interacted; the CSS condition // stays `:focus-visible`, this only narrows it. const [isKeyboardFocus, setIsKeyboardFocus] = useState(false); useInteractionModalityTracking(); const handleFocus = useCallback((e: React.FocusEvent) => { setIsKeyboardFocus(getInteractionModality() === 'keyboard'); onFocus?.(e); }, [onFocus]); const handleBlur = useCallback((e: React.FocusEvent) => { setIsKeyboardFocus(false); onBlur?.(e); }, [onBlur]); const handleClear = useCallback((e?: React.MouseEvent) => { onValueChange(''); // Clearing puts the caret back where the user was typing, matching // TextInput's built-in clear. Defer focus restoration past the button's // unmount task so touch browsers don't jump page scroll on tap, while // preserving synchronous focus for keyboard users. if (!e || e.detail === 0) { if (typeof ref === 'object' && ref?.current) { ref.current.focus(); } } else { requestAnimationFrame(() => { if (typeof ref === 'object' && ref?.current) { ref.current.focus({ preventScroll: true }); } }); } }, [onValueChange, ref]); return
onValueChange(e.target.value)} onKeyDown={onKeyDown} onFocus={handleFocus} onBlur={handleBlur} className="x1iyjqo2 xs83m0k xeuugli x1717udv x1ghz6dp xc342km xng3xce xjbqb8w x1tgivj0 x9ynric xcr08ib x1w61h2b x1kq96og x1a2a7pz xeyghm5" {...props} /> {value !== '' && }
; } PanelSearchInput.displayName = 'PanelSearchInput';