import { forwardRef, useLayoutEffect, useMemo, useState } from 'react'; import { type GestureResponderEvent, View } from 'react-native'; import { useThemeColor } from '../../helpers/external/hooks'; import { CloseIcon } from '../../helpers/internal/components'; import { AnimationSettingsProvider, FormFieldProvider, } from '../../helpers/internal/contexts'; import type { TextInputComponent, ViewRef } from '../../helpers/internal/types'; import { createContext } from '../../helpers/internal/utils'; import { Button } from '../button'; import { Input } from '../input'; import { useSearchFieldRootAnimation } from './search-field.animation'; import { DISPLAY_NAME } from './search-field.constants'; import { searchFieldClassNames } from './search-field.styles'; import type { SearchFieldClearButtonProps, SearchFieldContextType, SearchFieldGroupProps, SearchFieldInputProps, SearchFieldProps, SearchFieldSearchIconProps, SearchFieldSlotsContextType, } from './search-field.types'; import { SearchIcon } from './search-icon'; const [SearchFieldProvider, useSearchField] = createContext({ name: 'SearchFieldContext', strict: false, }); const [SearchFieldSlotsProvider, useSearchFieldSlots] = createContext({ name: 'SearchFieldSlotsContext', strict: false, }); /** * Registers an optional SearchField slot for the lifetime of the caller. * The effect still runs when the caller later returns `null` (ClearButton * with an empty value), so Input can keep trailing padding and avoid a * text jump when the first character is typed. * * @param setPresent - Slot setter from SearchFieldSlotsContext, or * `undefined` when rendered outside SearchField */ function useRegisterSearchFieldSlot( setPresent: ((isPresent: boolean) => void) | undefined ): void { useLayoutEffect(() => { if (typeof setPresent !== 'function') { return; } setPresent(true); return () => { setPresent(false); }; }, [setPresent]); } // -------------------------------------------------- const SearchFieldRoot = forwardRef((props, ref) => { const { children, className, value, onChange, isDisabled = false, isInvalid = false, isRequired = false, animation, ...restProps } = props; const rootClassName = searchFieldClassNames.root({ className }); const { isAllAnimationsDisabled } = useSearchFieldRootAnimation({ animation, }); const [hasSearchIcon, setHasSearchIcon] = useState(false); const [hasClearButton, setHasClearButton] = useState(false); const searchFieldContextValue = useMemo( () => ({ value, onChange, isDisabled, isInvalid, isRequired }), [value, onChange, isDisabled, isInvalid, isRequired] ); const searchFieldSlotsContextValue = useMemo( () => ({ hasSearchIcon, hasClearButton, setHasSearchIcon, setHasClearButton, }), [hasSearchIcon, hasClearButton] ); const formFieldContextValue = useMemo( () => ({ isDisabled, isInvalid, isRequired, hasFieldPadding: false }), [isDisabled, isInvalid, isRequired] ); const animationSettingsContextValue = useMemo( () => ({ isAllAnimationsDisabled, }), [isAllAnimationsDisabled] ); return ( {children} ); }); // -------------------------------------------------- const SearchFieldGroup = forwardRef( (props, ref) => { const { children, className, ...restProps } = props; const groupClassName = searchFieldClassNames.group({ className }); return ( {children} ); } ); // -------------------------------------------------- const SearchFieldSearchIcon = forwardRef( (props, ref) => { const { children, className, iconProps, ...restProps } = props; const slots = useSearchFieldSlots(); useRegisterSearchFieldSlot(slots?.setHasSearchIcon); const searchIconClassName = searchFieldClassNames.searchIcon({ className }); return ( {children ?? ( )} ); } ); // -------------------------------------------------- const SearchFieldInput: TextInputComponent = forwardRef( (props, ref) => { const { className, containerClassName: containerClassNameProp, placeholder = 'Search...', returnKeyType = 'search', accessibilityRole = 'search', accessibilityLabel = 'Search', ...restProps } = props; const searchField = useSearchField(); const slots = useSearchFieldSlots(); const inputClassName = searchFieldClassNames.input({ hasSearchIcon: slots?.hasSearchIcon ?? false, hasClearButton: slots?.hasClearButton ?? false, className, }); const inputContainerClassName = searchFieldClassNames.inputContainer({ className: containerClassNameProp, }); return ( ); } ); // -------------------------------------------------- const SearchFieldClearButton = forwardRef( (props, ref) => { const { iconProps, className, children, onPress, ...restProps } = props; const searchField = useSearchField(); const slots = useSearchFieldSlots(); const themeColorMuted = useThemeColor('muted'); /** * Register before the empty-value early return so composed ClearButtons * keep trailing input padding while they are visually hidden. */ useRegisterSearchFieldSlot(slots?.setHasClearButton); if (searchField?.value !== undefined && searchField.value.length === 0) { return null; } const handlePress = (event: GestureResponderEvent) => { searchField?.onChange?.(''); if (typeof onPress === 'function') { onPress(event); } }; const clearButtonClassName = searchFieldClassNames.clearButton({ className, }); return ( ); } ); // -------------------------------------------------- SearchFieldRoot.displayName = DISPLAY_NAME.SEARCH_FIELD; SearchFieldGroup.displayName = DISPLAY_NAME.SEARCH_FIELD_GROUP; SearchFieldSearchIcon.displayName = DISPLAY_NAME.SEARCH_FIELD_SEARCH_ICON; SearchFieldInput.displayName = DISPLAY_NAME.SEARCH_FIELD_INPUT; SearchFieldClearButton.displayName = DISPLAY_NAME.SEARCH_FIELD_CLEAR_BUTTON; /** * Compound SearchField component with sub-components. * * @component SearchField - Root container that accepts `value`, `onChange`, * `isDisabled`, `isInvalid`, and `isRequired`, providing them to children via * SearchFieldContext. Also provides FormFieldProvider and animation settings. * * @component SearchField.Group - Flex-row container for the search icon, input, * and clear button. * * @component SearchField.SearchIcon - Magnifying glass icon positioned * absolutely on the leading edge (left in LTR, right in RTL). Registers * itself so Input reserves leading space only while this part is composed. * * @component SearchField.Input - Wraps the Input component with search-specific * defaults: "Search..." placeholder and search a11y role. Reserves leading * space when SearchIcon is composed and trailing space when ClearButton is * composed. Reads `value` / `onChangeText` from SearchFieldContext. * * @component SearchField.ClearButton - Small button that clears the search * input. Automatically hidden when value is empty. Calls `onChange("")` from * context on press. Registers itself so Input keeps trailing space while * this part is composed, including when it is visually hidden. * * @see Full documentation: https://heroui.com/docs/native/components/search-field */ const CompoundSearchField = Object.assign(SearchFieldRoot, { /** Flex-row container for search icon, input, and clear button */ Group: SearchFieldGroup, /** Magnifying glass search icon */ SearchIcon: SearchFieldSearchIcon, /** Text input with search-specific defaults */ Input: SearchFieldInput, /** Small clear button to dismiss search text */ ClearButton: SearchFieldClearButton, }); export { useSearchField }; export default CompoundSearchField;