kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
add rotating placeholders
rodzic
a2ba5f85b8
commit
cd9a326cae
|
@ -1,8 +1,11 @@
|
|||
import FuzzySearch from 'fuzzy-search';
|
||||
import React, { useState, useRef, useCallback, useEffect, useId } from 'react';
|
||||
import { useIntl, MessageDescriptor } from 'react-intl';
|
||||
|
||||
import Input from 'soapbox/components/ui/input.tsx';
|
||||
|
||||
type PlaceholderText = string | MessageDescriptor;
|
||||
|
||||
interface FuzzySearchInputProps<T> {
|
||||
/** The array of objects or strings to search through. */
|
||||
data: T[];
|
||||
|
@ -12,8 +15,12 @@ interface FuzzySearchInputProps<T> {
|
|||
onSelection: (selection: T | null, clearField: () => void) => void;
|
||||
/** Optional: The key to display in the suggestion list. Defaults to the first key in the `keys` prop or the item itself if data is string[]. */
|
||||
displayKey?: keyof T;
|
||||
/** Optional: Placeholder text for the input field. */
|
||||
placeholder?: string;
|
||||
/** Optional: Placeholder text for the input field. If a string is provided, it will be used as a static placeholder. */
|
||||
placeholder?: PlaceholderText;
|
||||
/** Optional: Array of placeholders to rotate through. Takes precedence over placeholder if both are provided. */
|
||||
placeholders?: PlaceholderText[];
|
||||
/** Optional: Interval in milliseconds to change placeholders. Defaults to 5000ms (5 seconds). */
|
||||
placeholderChangeInterval?: number;
|
||||
/**
|
||||
* Optional: Custom search function to override the default fuzzy search. */
|
||||
searchFn?: SearchImpl;
|
||||
|
@ -51,17 +58,26 @@ function FuzzySearchInput<T extends Record<string, any> | string>({
|
|||
onSelection,
|
||||
displayKey,
|
||||
placeholder = 'Search...',
|
||||
placeholders,
|
||||
placeholderChangeInterval = 5000,
|
||||
searchFn = defaultSearch,
|
||||
className = '',
|
||||
baseId,
|
||||
inputClassName = '',
|
||||
renderSuggestion: FuzzySearchSuggestion,
|
||||
}: FuzzySearchInputProps<T>) {
|
||||
const intl = useIntl();
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [suggestions, setSuggestions] = useState<T[]>([]);
|
||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||
const [activeIndex, setActiveIndex] = useState<number>(-1);
|
||||
|
||||
// dynamic placeholder state
|
||||
const [currentPlaceholder, setCurrentPlaceholder] = useState<string>(
|
||||
typeof placeholder === 'string' ? placeholder : intl.formatMessage(placeholder),
|
||||
);
|
||||
const placeholderIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
// Generate unique IDs for ARIA attributes if no baseId is provided
|
||||
const generatedId = useId();
|
||||
|
@ -70,6 +86,49 @@ function FuzzySearchInput<T extends Record<string, any> | string>({
|
|||
const listboxId = `${componentBaseId}-listbox`;
|
||||
const getOptionId = (index: number) => `${componentBaseId}-option-${index}`;
|
||||
|
||||
// Helper function to format a placeholder (either string or MessageDescriptor)
|
||||
const formatPlaceholder = useCallback((text: PlaceholderText): string => {
|
||||
if (typeof text === 'string') {
|
||||
return text;
|
||||
}
|
||||
return intl.formatMessage(text);
|
||||
}, [intl]);
|
||||
|
||||
// Handle placeholder rotation if placeholders array is provided
|
||||
useEffect(() => {
|
||||
// Clear any existing interval
|
||||
if (placeholderIntervalRef.current) {
|
||||
clearInterval(placeholderIntervalRef.current);
|
||||
placeholderIntervalRef.current = null;
|
||||
}
|
||||
|
||||
// If we have multiple placeholders, set up rotation
|
||||
if (placeholders && placeholders.length > 1) {
|
||||
// Set initial placeholder
|
||||
const randomIndex = Math.floor(Math.random() * placeholders.length);
|
||||
setCurrentPlaceholder(formatPlaceholder(placeholders[randomIndex]));
|
||||
|
||||
// Set up interval to change placeholder
|
||||
placeholderIntervalRef.current = setInterval(() => {
|
||||
const randomIndex = Math.floor(Math.random() * placeholders.length);
|
||||
setCurrentPlaceholder(formatPlaceholder(placeholders[randomIndex]));
|
||||
}, placeholderChangeInterval);
|
||||
} else if (placeholders && placeholders.length === 1) {
|
||||
// If just one placeholder in the array, use it statically
|
||||
setCurrentPlaceholder(formatPlaceholder(placeholders[0]));
|
||||
} else if (placeholder) {
|
||||
// Fall back to the single placeholder prop
|
||||
setCurrentPlaceholder(formatPlaceholder(placeholder));
|
||||
}
|
||||
|
||||
// Clean up interval on unmount
|
||||
return () => {
|
||||
if (placeholderIntervalRef.current) {
|
||||
clearInterval(placeholderIntervalRef.current);
|
||||
}
|
||||
};
|
||||
}, [placeholder, placeholders, placeholderChangeInterval, formatPlaceholder]);
|
||||
|
||||
const getDisplayText = useCallback((item: T): string => {
|
||||
if (typeof item === 'string') {
|
||||
return item;
|
||||
|
@ -180,7 +239,7 @@ function FuzzySearchInput<T extends Record<string, any> | string>({
|
|||
onKeyDown={handleKeyDown}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
placeholder={placeholder}
|
||||
placeholder={currentPlaceholder}
|
||||
autoComplete='off'
|
||||
aria-autocomplete='list'
|
||||
aria-controls={showSuggestions && suggestions.length > 0 ? listboxId : undefined}
|
||||
|
|
|
@ -24,7 +24,7 @@ const messages = defineMessages({
|
|||
welcomeTitle: { id: 'admin.policies.welcome.title', defaultMessage: 'Welcome to Policy Manager' },
|
||||
welcomeGetStarted: { id: 'admin.policies.welcome.get_started', defaultMessage: 'Get Started' },
|
||||
helpTitle: { id: 'admin.policies.help.title', defaultMessage: 'Help' },
|
||||
helpButton: { id: 'admin.policies.help.button', defaultMessage: 'Help...' },
|
||||
helpButton: { id: 'admin.policies.help.button', defaultMessage: 'Help' },
|
||||
okay: { id: 'admin.policies.help.okay', defaultMessage: 'Okay' },
|
||||
});
|
||||
|
||||
|
@ -45,6 +45,17 @@ const PolicyManager: FC = () => {
|
|||
// get the current set of policies out of the API response
|
||||
const initialPolicies = storedPolicies?.spec?.policies ?? [];
|
||||
|
||||
// Generate dynamic placeholders from policy names
|
||||
const dynamicPlaceholders = useMemo(() => {
|
||||
if (allPolicies.length === 0) {
|
||||
return [messages.searchPlaceholder];
|
||||
}
|
||||
|
||||
return [
|
||||
...allPolicies.map(policy => policy.name.toLowerCase()),
|
||||
];
|
||||
}, [allPolicies]);
|
||||
|
||||
// initialFields is used to set up the reducer. stores the initial value of
|
||||
// all the fields from the current policy and falls back to the default if
|
||||
// the value isn't present.
|
||||
|
@ -256,7 +267,7 @@ const PolicyManager: FC = () => {
|
|||
keys={['name', 'description']}
|
||||
onSelection={handleSelection}
|
||||
displayKey='name'
|
||||
placeholder={intl.formatMessage(messages.searchPlaceholder)}
|
||||
placeholders={dynamicPlaceholders}
|
||||
className='w-full'
|
||||
renderSuggestion={PolicySuggestion}
|
||||
/>
|
||||
|
|
Ładowanie…
Reference in New Issue