Files
MemoryBear/web/src/components/SearchInput/index.tsx
2026-03-25 13:58:25 +08:00

126 lines
3.7 KiB
TypeScript

/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:24:23
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-25 11:15:26
*/
/**
* SearchInput Component
*
* A search input component with debounce and throttle support:
* - Configurable debounce delay for search optimization
* - Optional throttle mode for rate limiting
* - Search icon prefix
* - Clear button
* - Internationalized placeholder
*
* @component
*/
import { useState, type FC, useCallback, useRef } from 'react';
import { Input, type InputProps } from 'antd';
import { useTranslation } from 'react-i18next';
/** Props interface for SearchInput component */
interface SearchInputProps extends InputProps {
/** Placeholder text */
placeholder?: string;
/** Callback fired when search value changes */
onSearch?: (value: string) => void;
/** Debounce delay in milliseconds (default: 300) */
debounceDelay?: number;
/** Throttle delay in milliseconds (overrides debounce if set) */
throttleDelay?: number;
/** Default input value */
defaultValue?: string;
/** Custom styles */
style?: Record<string, string | number>;
/** Additional CSS classes */
className?: string;
/** Input size */
size?: InputProps['size']
/** Maximum length of the input value */
maxLength?: number;
}
/** Search input component with debounce and throttle support */
const SearchInput: FC<SearchInputProps> = ({
placeholder,
onSearch,
debounceDelay = 300,
throttleDelay,
defaultValue = undefined,
className = '',
variant = 'filled',
...props
}) => {
const { t } = useTranslation();
const [value, setValue] = useState(defaultValue);
const timerRef = useRef<number | null>(null);
const throttleRef = useRef<boolean>(false);
const lastCallRef = useRef<number>(0);
/** Debounce function - delays callback execution until after delay period */
const debounce = useCallback(<T extends (...args: any[]) => void>(callback: T, delay: number) => {
return (...args: Parameters<T>) => {
if (timerRef.current) {
window.clearTimeout(timerRef.current);
}
timerRef.current = window.setTimeout(() => {
callback(...args);
}, delay);
};
}, []);
/** Throttle function - limits callback execution to once per delay period */
const throttle = useCallback(<T extends (...args: any[]) => void>(callback: T, delay: number) => {
return (...args: Parameters<T>) => {
const now = Date.now();
if (!throttleRef.current && now - lastCallRef.current >= delay) {
lastCallRef.current = now;
throttleRef.current = true;
callback(...args);
window.setTimeout(() => {
throttleRef.current = false;
}, delay);
}
};
}, []);
/** Handle input change with debounce or throttle based on configuration */
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
setValue(newValue);
/** Decide whether to use debounce or throttle based on throttleDelay setting */
if (onSearch) {
if (throttleDelay) {
const throttledSearch = throttle(() => {
onSearch(newValue);
}, throttleDelay);
throttledSearch();
} else {
const debouncedSearch = debounce(() => {
onSearch(newValue);
}, debounceDelay);
debouncedSearch();
}
}
};
return (
<Input
allowClear
prefix={<div className="rb:size-4 rb:bg-[url('@/assets/images/search.svg')] rb:mr-1"></div>}
placeholder={placeholder || t('user.searchPlaceholder')}
value={value}
onChange={handleChange}
style={{ width: '300px' }}
className={className}
variant={variant}
{...props}
/>
);
};
export default SearchInput;