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 FuzzySearch from 'fuzzy-search';
|
||||||
import React, { useState, useRef, useCallback, useEffect, useId } from 'react';
|
import React, { useState, useRef, useCallback, useEffect, useId } from 'react';
|
||||||
|
import { useIntl, MessageDescriptor } from 'react-intl';
|
||||||
|
|
||||||
import Input from 'soapbox/components/ui/input.tsx';
|
import Input from 'soapbox/components/ui/input.tsx';
|
||||||
|
|
||||||
|
type PlaceholderText = string | MessageDescriptor;
|
||||||
|
|
||||||
interface FuzzySearchInputProps<T> {
|
interface FuzzySearchInputProps<T> {
|
||||||
/** The array of objects or strings to search through. */
|
/** The array of objects or strings to search through. */
|
||||||
data: T[];
|
data: T[];
|
||||||
|
@ -12,8 +15,12 @@ interface FuzzySearchInputProps<T> {
|
||||||
onSelection: (selection: T | null, clearField: () => void) => void;
|
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[]. */
|
/** 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;
|
displayKey?: keyof T;
|
||||||
/** Optional: Placeholder text for the input field. */
|
/** Optional: Placeholder text for the input field. If a string is provided, it will be used as a static placeholder. */
|
||||||
placeholder?: string;
|
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. */
|
* Optional: Custom search function to override the default fuzzy search. */
|
||||||
searchFn?: SearchImpl;
|
searchFn?: SearchImpl;
|
||||||
|
@ -51,17 +58,26 @@ function FuzzySearchInput<T extends Record<string, any> | string>({
|
||||||
onSelection,
|
onSelection,
|
||||||
displayKey,
|
displayKey,
|
||||||
placeholder = 'Search...',
|
placeholder = 'Search...',
|
||||||
|
placeholders,
|
||||||
|
placeholderChangeInterval = 5000,
|
||||||
searchFn = defaultSearch,
|
searchFn = defaultSearch,
|
||||||
className = '',
|
className = '',
|
||||||
baseId,
|
baseId,
|
||||||
inputClassName = '',
|
inputClassName = '',
|
||||||
renderSuggestion: FuzzySearchSuggestion,
|
renderSuggestion: FuzzySearchSuggestion,
|
||||||
}: FuzzySearchInputProps<T>) {
|
}: FuzzySearchInputProps<T>) {
|
||||||
|
const intl = useIntl();
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
const [suggestions, setSuggestions] = useState<T[]>([]);
|
const [suggestions, setSuggestions] = useState<T[]>([]);
|
||||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||||
const [activeIndex, setActiveIndex] = useState<number>(-1);
|
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);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
// Generate unique IDs for ARIA attributes if no baseId is provided
|
// Generate unique IDs for ARIA attributes if no baseId is provided
|
||||||
const generatedId = useId();
|
const generatedId = useId();
|
||||||
|
@ -70,6 +86,49 @@ function FuzzySearchInput<T extends Record<string, any> | string>({
|
||||||
const listboxId = `${componentBaseId}-listbox`;
|
const listboxId = `${componentBaseId}-listbox`;
|
||||||
const getOptionId = (index: number) => `${componentBaseId}-option-${index}`;
|
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 => {
|
const getDisplayText = useCallback((item: T): string => {
|
||||||
if (typeof item === 'string') {
|
if (typeof item === 'string') {
|
||||||
return item;
|
return item;
|
||||||
|
@ -180,7 +239,7 @@ function FuzzySearchInput<T extends Record<string, any> | string>({
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
placeholder={placeholder}
|
placeholder={currentPlaceholder}
|
||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
aria-autocomplete='list'
|
aria-autocomplete='list'
|
||||||
aria-controls={showSuggestions && suggestions.length > 0 ? listboxId : undefined}
|
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' },
|
welcomeTitle: { id: 'admin.policies.welcome.title', defaultMessage: 'Welcome to Policy Manager' },
|
||||||
welcomeGetStarted: { id: 'admin.policies.welcome.get_started', defaultMessage: 'Get Started' },
|
welcomeGetStarted: { id: 'admin.policies.welcome.get_started', defaultMessage: 'Get Started' },
|
||||||
helpTitle: { id: 'admin.policies.help.title', defaultMessage: 'Help' },
|
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' },
|
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
|
// get the current set of policies out of the API response
|
||||||
const initialPolicies = storedPolicies?.spec?.policies ?? [];
|
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
|
// 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
|
// all the fields from the current policy and falls back to the default if
|
||||||
// the value isn't present.
|
// the value isn't present.
|
||||||
|
@ -256,7 +267,7 @@ const PolicyManager: FC = () => {
|
||||||
keys={['name', 'description']}
|
keys={['name', 'description']}
|
||||||
onSelection={handleSelection}
|
onSelection={handleSelection}
|
||||||
displayKey='name'
|
displayKey='name'
|
||||||
placeholder={intl.formatMessage(messages.searchPlaceholder)}
|
placeholders={dynamicPlaceholders}
|
||||||
className='w-full'
|
className='w-full'
|
||||||
renderSuggestion={PolicySuggestion}
|
renderSuggestion={PolicySuggestion}
|
||||||
/>
|
/>
|
||||||
|
|
Ładowanie…
Reference in New Issue