/** * SearchBar — a text field for querying a list, with the two controls a search * needs and an ordinary field does not, and a panel of results that opens out * of the field itself. * * ```tsx * * * * * } onPress={add}>Claude * * * ``` * * ## The results are above the field, and the field is above the keyboard * * A search that is being typed into has a keyboard under it, and a list drawn * below the field is a list drawn behind the keyboard. So `avoidKeyboard` * lifts the field until it sits `keyboardOffset` points clear of the keyboard's * top edge, and the panel opens *upward* out of it into the space that is * actually free. * * That puts the first result nearest the field and the last one furthest away, * which is the order a reader walking away from the caret expects. Pass * `panelPlacement="bottom"` for a search bar in a header, where the space is * the other way round. * * The panel is positioned absolutely rather than laid out in the flow, so * opening it never moves the page underneath — a list that pushes the field it * belongs to is a field that walks away from the finger typing into it. * * ## Touches inside the panel must not close the keyboard * * The panel scrolls with `keyboardShouldPersistTaps="always"`, and every press * inside it holds the field's focus open for a moment afterwards. Both are * needed, because a search closes the instant the field blurs and there are * two separate ways for a touch in the panel to blur it. * * `"handled"` only spares presses a child takes responsibility for, which * leaves the panel's own padding, the gaps between rows, a section heading and * the whole of `SearchBar.Status` as live dismiss surfaces — tapping the word * "Searching …" would end the search. `"always"` gives the panel back. * * The focus guard covers the other way: a control inside a row — an add * button, a remove ✕ — takes focus with the press on Android, and returning it * a frame later is not enough on its own, because the blur has already closed * the panel the control was drawn in. So a press in the panel marks the field * as still being used, and a blur arriving under that mark is answered by * asking for focus back rather than by ending the search. * * That guard only knows about presses that go through this component's own * parts, and a caller's `Pressable` in a row's `trailing` slot takes the touch * itself. So the panel also waits before believing any blur, and asks the * keyboard: it is still up, because nothing in the panel dismisses it, and a * search whose keyboard is still up has not ended. Focus goes back instead. * * ## The space kept for the field is not a target * * The card is one box around the results *and* the field, so it carries a * spacer where the field sits. That spacer is a plain view drawn over a * focused field, and a touch on a plain view is the platform's cue to dismiss * the keyboard — so winning one blurred the field and closed the panel drawn * out of that focus. Tapping the search box shut the results, which is exactly * backwards. The card and its spacer take no touches at all now. * * ## What has already been picked goes in the field * * `tokens` puts the choices made so far inside the field, before the caret, so * the query and what it has produced are one control rather than a control and * a list somewhere above it. `SearchBar.Token` is the chip; backspace on an * empty field fires `onRemoveLastToken`, which is what a token field does * everywhere else. * * They scroll rather than wrap: the field is one line tall, and a row of chips * that grew it would move the caret every time something was picked. * * ## The clear button, and why it is not the platform's * * A ✕ appears inside the field as soon as there is something to clear, and * takes it back to empty without dismissing the keyboard — clearing a query is * the start of the next one, not the end of the search. It is drawn here * rather than left to `clearButtonMode`, which exists on iOS only, cannot be * labelled for a screen reader and cannot be swapped for a spinner while * results are in flight. * * The glyph is 24 points and its touch box is 48, made up with slop rather * than with size. A 48-point circle inside a 40-point field either overflows * it or forces every search bar in an app to be as tall as the largest one. * * ## Cancel is a row, not a decoration * * `cancel="focus"` puts a Cancel button beside the field and slides it in * while the field is being edited, which is the platform's own answer to * "how do I get out of this search". It is a sibling of the field rather than * something inside it, because it acts on the search as a whole: it empties * the query, drops focus and calls `onCancel`, and a control that ends the * thing it sits inside reads as part of the query it is about to discard. * * Its width is measured once and animated on the UI thread. The button is * always mounted when `cancel` is not `never`, so the measurement is already * there the first time the field is touched and the first slide is as smooth * as the tenth. * * ## Debouncing belongs to the caller's search, not to the field * * `onChangeText` always fires on every keystroke — a controlled field that * lags its own input is unusable. `debounce` is about the *query*: it holds * `onDebouncedChange` until typing pauses, so a network search runs once per * pause instead of once per letter. Submitting flushes it immediately, since * a return key is somebody saying they are done waiting. */ import { type ReactNode } from 'react'; import { TextInput, type ViewProps } from 'react-native'; import { type VariantProps } from 'tailwind-variants'; import { type AnimatedPressableProps } from '../../primitives/animated-pressable.js'; import { type InputProps } from '../input/index.js'; declare const searchBarVariants: import("tailwind-variants").TVReturnType<{ size: { sm: { cancelLabel: string; clear: string; token: string; tokenLabel: string; tokenRemove: string; }; md: { cancelLabel: string; clear: string; token: string; tokenLabel: string; tokenRemove: string; }; lg: { cancelLabel: string; clear: string; token: string; tokenLabel: string; tokenRemove: string; }; }; /** * The field's corner. `pill` is the shape a search field takes when it is * chrome — sitting above a list, in a header — and `rounded` the one it * takes inside a form beside other fields. */ shape: { rounded: { field: string; }; pill: { field: string; }; }; /** * Which edge of the field the card grows out of. The field's corners on * that edge go square and its border comes off entirely — the card around * both of them is what draws the edge. */ attached: { none: {}; top: { field: string; }; bottom: { field: string; }; }; selected: { true: { item: string; }; }; }, { row: string; anchor: string; field: string; panel: string; panelList: string; /** The hairline between the results and the field. */ panelDivider: string; section: string; sectionLabel: string; item: string; itemLabel: string; status: string; cancelClip: string; cancelButton: string; cancelLabel: string; clear: string; tokenRow: string; token: string; tokenLabel: string; tokenRemove: string; }, undefined, { size: { sm: { cancelLabel: string; clear: string; token: string; tokenLabel: string; tokenRemove: string; }; md: { cancelLabel: string; clear: string; token: string; tokenLabel: string; tokenRemove: string; }; lg: { cancelLabel: string; clear: string; token: string; tokenLabel: string; tokenRemove: string; }; }; /** * The field's corner. `pill` is the shape a search field takes when it is * chrome — sitting above a list, in a header — and `rounded` the one it * takes inside a form beside other fields. */ shape: { rounded: { field: string; }; pill: { field: string; }; }; /** * Which edge of the field the card grows out of. The field's corners on * that edge go square and its border comes off entirely — the card around * both of them is what draws the edge. */ attached: { none: {}; top: { field: string; }; bottom: { field: string; }; }; selected: { true: { item: string; }; }; }, { row: string; anchor: string; field: string; panel: string; panelList: string; /** The hairline between the results and the field. */ panelDivider: string; section: string; sectionLabel: string; item: string; itemLabel: string; status: string; cancelClip: string; cancelButton: string; cancelLabel: string; clear: string; tokenRow: string; token: string; tokenLabel: string; tokenRemove: string; }, import("tailwind-variants").TVReturnType<{ size: { sm: { cancelLabel: string; clear: string; token: string; tokenLabel: string; tokenRemove: string; }; md: { cancelLabel: string; clear: string; token: string; tokenLabel: string; tokenRemove: string; }; lg: { cancelLabel: string; clear: string; token: string; tokenLabel: string; tokenRemove: string; }; }; /** * The field's corner. `pill` is the shape a search field takes when it is * chrome — sitting above a list, in a header — and `rounded` the one it * takes inside a form beside other fields. */ shape: { rounded: { field: string; }; pill: { field: string; }; }; /** * Which edge of the field the card grows out of. The field's corners on * that edge go square and its border comes off entirely — the card around * both of them is what draws the edge. */ attached: { none: {}; top: { field: string; }; bottom: { field: string; }; }; selected: { true: { item: string; }; }; }, { row: string; anchor: string; field: string; panel: string; panelList: string; /** The hairline between the results and the field. */ panelDivider: string; section: string; sectionLabel: string; item: string; itemLabel: string; status: string; cancelClip: string; cancelButton: string; cancelLabel: string; clear: string; tokenRow: string; token: string; tokenLabel: string; tokenRemove: string; }, undefined, unknown, unknown, undefined>>; type SearchBarVariantProps = VariantProps; /** Where the results open. */ export type SearchBarPanelPlacement = 'top' | 'bottom'; /** When the results are shown. */ export type SearchBarPanelMode = 'never' | 'focus' | 'always'; /** * What SearchBar takes from Input, minus everything it owns itself. The form * furniture is dropped along with it: a label and an error line stack above * and below the field, and Cancel sits beside the whole stack rather than * beside the field it belongs to. Use `Field` for a search that is one answer * in a form. * * The keyboard props go too. Input's would move the field and leave the Cancel * button and the panel where they were; SearchBar lifts all three together. */ type InheritedInputProps = Omit; export interface SearchBarProps extends InheritedInputProps, Omit { /** * The field's background, from `Input`. `outline` draws its own edge, for a * search bar sitting on the page; `filled` drops it, for one inside a card * or a header where a second border reads as a seam. Defaults to `outline`. */ variant?: InputProps['variant']; /** The query, when the caller holds it. Leave unset to let the field keep it. */ value?: string; /** Starting query for an uncontrolled field. Ignored once `value` is passed. */ defaultValue?: string; /** Fires on every keystroke. For a search that costs something, see `debounce`. */ onChangeText?: (value: string) => void; /** The return key, which is labelled Search. Flushes `onDebouncedChange` first. */ onSubmit?: (value: string) => void; /** * How long typing has to pause before `onDebouncedChange` runs, in * milliseconds. `0` runs it on every keystroke, which is only right for a * filter over a list already in memory. */ debounce?: number; /** The query, once typing has paused for `debounce` milliseconds. */ onDebouncedChange?: (value: string) => void; /** Fires after the ✕ empties the field. The field keeps focus. */ onClear?: () => void; /** Fires after Cancel empties the field and drops focus. */ onCancel?: () => void; /** Whether the ✕ appears once there is a query. */ isClearable?: boolean; /** * When the Cancel button is beside the field. `focus` slides it in while the * field is being edited and away again when it is not, which is what a * search bar above a list wants. `always` keeps it out, for a screen that is * nothing but the search. */ cancel?: 'never' | 'focus' | 'always'; /** The Cancel button's word. */ cancelLabel?: string; /** How the ✕ announces itself. */ clearLabel?: string; /** * Results are on their way. A spinner takes the ✕'s place, because the two * would otherwise sit on top of one another at exactly the moment a query is * both non-empty and running. */ loading?: boolean; /** The leading glyph, for a search over something with a symbol of its own. */ icon?: ReactNode; /** * Lift the whole search — field, Cancel button and panel — until it sits * clear of the software keyboard, and put it back on blur. Without it the * field stays where the page left it, which on most screens is behind the * keyboard it just opened. * * Install `react-native-keyboard-controller` for this to behave on Android. * * Do not toggle it at runtime: it changes which component wraps the row, so * the field would remount and lose focus. */ avoidKeyboard?: boolean; /** Gap kept between the field's bottom edge and the keyboard. */ keyboardOffset?: number; /** * When the results panel is shown. `focus` opens it while the field is being * typed into, `always` keeps it out for a screen that is nothing but the * search, `never` ignores the children entirely. */ panel?: SearchBarPanelMode; /** * Which side of the field the panel opens out of. `top` is the default, * because the space under a focused field belongs to the keyboard. */ panelPlacement?: SearchBarPanelPlacement; /** * Cap on the panel's height, in points. * * The panel takes the smaller of this and the room between the field and the * edge of the screen, so it never runs off the top of the display. Unset, it * is capped at about six rows: the space above a lifted field is most of the * screen, and a panel that takes all of it stops reading as something laid * over the app. Longer lists scroll. */ panelMaxHeight?: number; /** * What has been picked so far, drawn inside the field before the caret. * `SearchBar.Token` is the chip; anything else that fits on one line works * too. Tokens scroll rather than wrap, so the field stays one line tall. */ tokens?: ReactNode; /** * Fires when backspace is pressed in an empty field. Remove the last token * here — it is the gesture every token field answers, and without it the * only way back out of a choice is its own ✕. */ onRemoveLastToken?: () => void; /** The panel's contents — `SearchBar.Section`, `.Item` and `.Status`. */ children?: ReactNode; } export interface SearchBarSectionProps extends ViewProps { className?: string; /** * The heading over the run of rows — "Suggested", "Results". Announced as a * header, so a screen reader reaching the group is told what it is before * walking into it. */ label?: string; children?: ReactNode; } /** A labelled run of rows inside the panel. */ declare function SearchBarSection({ className, label, children, ...props }: SearchBarSectionProps): import("react").JSX.Element; declare namespace SearchBarSection { var displayName: string; } export interface SearchBarItemProps extends Omit { className?: string; /** Anything before the label — an avatar, a logo, a status dot. */ leading?: ReactNode; /** * Anything after it. A slot rather than a built-in button, because what a * result row offers differs per search: an add, a pin, a count, nothing. */ trailing?: ReactNode; /** A second line under the label, for what the label alone cannot say. */ description?: string; /** Draws the row as the one the search has settled on. */ selected?: boolean; /** The row's label. */ children?: ReactNode; } /** One result. */ declare function SearchBarItem({ className, leading, trailing, description, selected, children, onPressIn, ...props }: SearchBarItemProps): import("react").JSX.Element; declare namespace SearchBarItem { var displayName: string; } export interface SearchBarStatusProps extends ViewProps { className?: string; /** A spinner beside the line, for a search that is still running. */ loading?: boolean; children?: ReactNode; } /** * The one line a panel shows instead of rows — nothing typed yet, a search in * flight, or a query that matched nothing. It is a sentence rather than an * empty box because those three states look identical when they are blank, and * which one it is decides what the person does next. */ declare function SearchBarStatus({ className, loading, children, ...props }: SearchBarStatusProps): import("react").JSX.Element; declare namespace SearchBarStatus { var displayName: string; } export interface SearchBarActionProps extends AnimatedPressableProps { className?: string; children?: ReactNode; } /** * A button inside a row — an add, a pin, a remove — for the `trailing` slot. * * It exists rather than being left to a plain `Pressable` because a control * nested inside a row takes the touch itself, so the row above it never sees * the press and cannot hold the field's focus on its behalf. Pressed, this one * ends up blurring the field, and a blurred field closes the panel the button * was standing in — the press lands and the search disappears under it. */ declare function SearchBarAction({ className, children, onPressIn, ...props }: SearchBarActionProps): import("react").JSX.Element; declare namespace SearchBarAction { var displayName: string; } export interface SearchBarTokenProps extends ViewProps { className?: string; /** Anything before the label — an avatar, a logo, a status dot. */ leading?: ReactNode; /** Fires when the chip's ✕ is pressed. Without it no ✕ is drawn. */ onRemove?: () => void; /** How the ✕ announces itself. Defaults to `Remove