From cd9a326cae60a9e6d3f9a341cf4f3ab1985d2ca3 Mon Sep 17 00:00:00 2001 From: Siddharth Singh Date: Sat, 12 Apr 2025 03:55:37 +0530 Subject: [PATCH] add rotating placeholders --- src/components/fuzzy-search-input.tsx | 65 +++++++++++++++++++++++++-- src/features/admin/policy-manager.tsx | 15 ++++++- 2 files changed, 75 insertions(+), 5 deletions(-) diff --git a/src/components/fuzzy-search-input.tsx b/src/components/fuzzy-search-input.tsx index c434ce05f..50008e463 100644 --- a/src/components/fuzzy-search-input.tsx +++ b/src/components/fuzzy-search-input.tsx @@ -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 { /** The array of objects or strings to search through. */ data: T[]; @@ -12,8 +15,12 @@ interface FuzzySearchInputProps { 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 | string>({ onSelection, displayKey, placeholder = 'Search...', + placeholders, + placeholderChangeInterval = 5000, searchFn = defaultSearch, className = '', baseId, inputClassName = '', renderSuggestion: FuzzySearchSuggestion, }: FuzzySearchInputProps) { + const intl = useIntl(); const [inputValue, setInputValue] = useState(''); const [suggestions, setSuggestions] = useState([]); const [showSuggestions, setShowSuggestions] = useState(false); const [activeIndex, setActiveIndex] = useState(-1); + // dynamic placeholder state + const [currentPlaceholder, setCurrentPlaceholder] = useState( + typeof placeholder === 'string' ? placeholder : intl.formatMessage(placeholder), + ); + const placeholderIntervalRef = useRef(null); + const containerRef = useRef(null); // Generate unique IDs for ARIA attributes if no baseId is provided const generatedId = useId(); @@ -70,6 +86,49 @@ function FuzzySearchInput | 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 | string>({ onKeyDown={handleKeyDown} onFocus={handleFocus} onBlur={handleBlur} - placeholder={placeholder} + placeholder={currentPlaceholder} autoComplete='off' aria-autocomplete='list' aria-controls={showSuggestions && suggestions.length > 0 ? listboxId : undefined} diff --git a/src/features/admin/policy-manager.tsx b/src/features/admin/policy-manager.tsx index 73ca375a5..5ad28d9c5 100644 --- a/src/features/admin/policy-manager.tsx +++ b/src/features/admin/policy-manager.tsx @@ -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} />