'use client'; import * as React from 'react'; import { Icon16Clear, Icon16SearchOutline, Icon24Cancel } from '@vkontakte/icons'; import { classNames, hasReactNode, noop } from '@vkontakte/vkjs'; import { useAdaptivity } from '../../hooks/useAdaptivity'; import { useAdaptivityConditionalRender } from '../../hooks/useAdaptivityConditionalRender'; import { useBooleanState } from '../../hooks/useBooleanState'; import { useConfigDirection } from '../../hooks/useConfigDirection'; import { useExternRef } from '../../hooks/useExternRef'; import { useMergeProps } from '../../hooks/useMergeProps'; import { useNativeFormResetListener } from '../../hooks/useNativeFormResetListener'; import { usePlatform } from '../../hooks/usePlatform'; import { callMultiple } from '../../lib/callMultiple'; import { touchEnabled } from '../../lib/touch'; import { useIsomorphicLayoutEffect } from '../../lib/useIsomorphicLayoutEffect'; import { warnOnce } from '../../lib/warnOnce'; import type { HasDataAttribute, HasRootRef } from '../../types'; import { Button } from '../Button/Button'; import { IconButton, type IconButtonProps } from '../IconButton/IconButton'; import { RootComponent } from '../RootComponent/RootComponent'; import { Headline } from '../Typography/Headline/Headline'; import { VisuallyHidden } from '../VisuallyHidden/VisuallyHidden'; import styles from './Search.module.css'; const warn = warnOnce('Search'); export type RenderIconButtonFn = ( icon: React.ReactNode, props?: (Partial & HasDataAttribute) | undefined, ) => React.ReactElement; export interface SearchProps extends Pick< React.InputHTMLAttributes, | 'autoComplete' | 'autoCapitalize' | 'autoCorrect' | 'disabled' | 'list' | 'maxLength' | 'minLength' | 'name' | 'pattern' | 'enterKeyHint' | 'placeholder' | 'readOnly' | 'required' | 'value' | 'form' | 'onChange' | 'onFocus' | 'onBlur' >, Omit, 'onChange' | 'onFocus' | 'onBlur'>, HasRootRef { /** * @deprecated Since 7.9.0. Вместо этого используйте `slotProps={ input: { getRootRef: ... } }`. */ getRef?: React.Ref | undefined; /** * Свойства, которые можно прокинуть внутрь компонента: * - `root`: свойства для прокидывания в корень компонента; * - `input`: свойства для прокидывания в поле ввода; * - `clearButton`: свойства для прокидывания в кнопку очистки. */ slotProps?: | { root?: | (React.HTMLAttributes & HasRootRef & HasDataAttribute) | undefined; input?: React.InputHTMLAttributes & HasRootRef & HasDataAttribute; clearButton?: React.HTMLAttributes & HasRootRef & HasDataAttribute; } | undefined; /** * Only iOS. Текст кнопки "отмена", которая чистит текстовое поле и убирает фокус. */ after?: React.ReactNode | undefined; /** * Контент, отображаемый перед полем ввода. */ before?: React.ReactNode | undefined; /** * Иконка поиска. Может быть React-элементом или функцией, возвращающей элемент. */ icon?: React.ReactNode | ((renderFn: RenderIconButtonFn) => React.ReactNode) | undefined; /** * Обработчик нажатия на иконку поиска. */ onIconClick?: React.PointerEventHandler | undefined; /** * Значение поля ввода по умолчанию. */ defaultValue?: string | undefined; /** * Текст для скринридеров, описывающий иконку поиска. */ iconLabel?: string | undefined; /** * Текст для скринридеров, описывающий кнопку очистки. */ clearLabel?: string | undefined; /** * @deprecated Since 8.1.0. Будет удалено в **VKUI v10**. Вместо этого используйте `slotProps={ clearButton: { 'data-testid': ... } }`. * * Передает атрибут `data-testid` для кнопки очистки. */ clearButtonTestId?: string | undefined; /** * Удаляет отступы у компонента. */ noPadding?: boolean | undefined; /** * Текст для кнопки Найти. */ findButtonText?: string | undefined; /** * Обработчик, при нажатии на кнопку "Найти". */ onFindButtonClick?: React.MouseEventHandler | undefined; /** * Передает атрибут `data-testid` для кнопки поиска. */ findButtonTestId?: string | undefined; /** * Скрывает кнопку очистки. */ hideClearButton?: boolean | undefined; } /** * @see https://vkui.io/components/search */ export const Search = ({ // SearchProps after = 'Отмена', before = , icon: iconProp, onIconClick, iconLabel, clearLabel = 'Очистить', clearButtonTestId, noPadding, findButtonText = 'Найти', onFindButtonClick, findButtonTestId, hideClearButton, getRef, // input props autoComplete = 'off', autoCapitalize, autoCorrect, disabled, list, maxLength, minLength, name, pattern, placeholder: placeholderProp = 'Поиск', enterKeyHint, readOnly, required, value, form, id: idProp, inputMode, defaultValue, autoFocus, tabIndex, spellCheck, onChange: onChangeProp, onFocus: onFocusProp, onBlur: onBlurProp, slotProps, ...restProps }: SearchProps): React.ReactNode => { /* istanbul ignore if: не проверяем в тестах */ if (process.env.NODE_ENV === 'development') { if (getRef) { warn('Свойство `getRef` устаревшее, используйте `slotProps={ input: { getRootRef: ... } }`'); } if (clearButtonTestId) { warn( "Свойство `clearButtonTestId` устаревшее, используйте `slotProps={ clearButton: { 'data-testid': ... } }`", ); } } const direction = useConfigDirection(); const isRtl = direction === 'rtl'; const rootRest = useMergeProps(restProps, slotProps?.root); const { id, placeholder, getRootRef: getInputRef, onChange, onFocus: onInputFocus, onBlur: onInputBlur, ...inputRest } = useMergeProps( { getRootRef: getRef, className: styles.nativeInput, placeholder: placeholderProp, autoComplete, autoCapitalize, autoCorrect, disabled, list, maxLength, minLength, name, pattern, enterKeyHint, readOnly, required, value, form, id: idProp, inputMode, defaultValue, autoFocus, tabIndex, spellCheck, onChange: onChangeProp, onFocus: onFocusProp, onBlur: onBlurProp, }, slotProps?.input, ); const { onClick: onClearButtonClick, onPointerDown: onClearButtonPointerDown, ...clearButtonRest } = useMergeProps({ className: styles.icon }, slotProps?.clearButton); const inputRef = useExternRef(getInputRef); const [isFocused, setFocusedTrue, setFocusedFalse] = useBooleanState(false); const generatedId = React.useId(); const inputId = id ? id : `search-${generatedId}`; const [hasValue, setHasValue] = React.useState(() => Boolean(inputRest.value || inputRest.defaultValue), ); const checkHasValue: React.ChangeEventHandler = (e) => setHasValue(Boolean(e.currentTarget.value)); const { density = 'none' } = useAdaptivity(); const { density: adaptiveDensity } = useAdaptivityConditionalRender(); const platform = usePlatform(); const hasAfter = platform === 'ios' && hasReactNode(after); const onFocus = (e: React.FocusEvent) => { setFocusedTrue(); onInputFocus && onInputFocus(e); }; const onBlur = (e: React.FocusEvent) => { setFocusedFalse(); onInputBlur && onInputBlur(e); }; const onCancel = React.useCallback(() => { // eslint-disable-next-line @typescript-eslint/unbound-method const nativeInputValueSetter = Object.getOwnPropertyDescriptor( HTMLInputElement.prototype, 'value', )?.set; nativeInputValueSetter?.call(inputRef.current, ''); const ev2 = new Event('input', { bubbles: true }); inputRef.current?.dispatchEvent(ev2); }, [inputRef]); const onIconClickStart: React.PointerEventHandler = React.useCallback( (e) => onIconClick?.(e), [onIconClick], ); useIsomorphicLayoutEffect(() => { if (inputRest.value !== undefined) { setHasValue(Boolean(inputRest.value)); } }, [inputRest.value]); useNativeFormResetListener(inputRef, () => { setHasValue(Boolean(inputRest.defaultValue)); }); const renderIconButton: RenderIconButtonFn = (icon, props = {}) => ( {iconLabel} {icon} ); const showControls = Boolean( iconProp || !hideClearButton || (adaptiveDensity.compact && onFindButtonClick), ); const onClearPointerDown: React.PointerEventHandler = (e) => { // Сначала вызываем внешний обработчик, затем локальную логику, чтобы можно было предотвратить обработку фокуса на поле ввода. onClearButtonPointerDown?.(e); e.preventDefault(); inputRef.current?.focus(); if (touchEnabled()) { onCancel(); } }; const onClearClick: React.MouseEventHandler = (e) => { // eslint-disable-next-line @typescript-eslint/unbound-method const nativeInputValueSetter = Object.getOwnPropertyDescriptor( HTMLInputElement.prototype, 'value', )?.set; nativeInputValueSetter?.call(inputRef.current, ''); const ev2 = new Event('input', { bubbles: true }); inputRef.current?.dispatchEvent(ev2); onClearButtonClick?.(e); }; return (
{before}
{showControls && (
{iconProp && (typeof iconProp === 'function' ? iconProp(renderIconButton) : renderIconButton(iconProp))} {!hideClearButton && ( {clearLabel} {platform === 'ios' ? : } )} {adaptiveDensity.compact && onFindButtonClick && ( )}
)}
{hasAfter && (
)}
); };