diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 60934952ad42..80c7e0bf78f0 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -249,7 +249,6 @@ function BaseSelectionList({ isActive: !disableKeyboardShortcuts && isFocused && !confirmButtonConfig?.isDisabled, }, ); - const textInputKeyPress = useCallback((event: TextInputKeyPressEvent) => { const key = event.nativeEvent.key; if (key === CONST.KEYBOARD_SHORTCUTS.TAB.shortcutKey) { @@ -257,21 +256,12 @@ function BaseSelectionList({ } }, []); - const handleTextInputRef = (element: BaseTextInputRef | null) => { - innerTextInputRef.current = element; - - const textInputRef = textInputOptions?.ref; - if (!textInputRef) { + const focusTextInput = useCallback(() => { + if (!innerTextInputRef) { return; } - - if (typeof textInputRef === 'function') { - textInputRef(element); - } else { - // eslint-disable-next-line react-compiler/react-compiler - textInputRef.current = element; - } - }; + innerTextInputRef.current?.focus(); + }, [innerTextInputRef]); const textInputComponent = ({shouldBeInsideList}: {shouldBeInsideList?: boolean}) => { if (shouldBeInsideList !== (textInputOptions?.shouldBeInsideList ?? false)) { @@ -280,10 +270,11 @@ function BaseSelectionList({ return ( ({ onFocusChange={(v: boolean) => (isTextInputFocusedRef.current = v)} showLoadingPlaceholder={showLoadingPlaceholder} isLoadingNewOptions={isLoadingNewOptions} + setFocusedIndex={setFocusedIndex} /> ); }; diff --git a/src/components/SelectionList/components/TextInput.tsx b/src/components/SelectionList/components/TextInput.tsx index da63b1a14edf..4f0bfa76a2d7 100644 --- a/src/components/SelectionList/components/TextInput.tsx +++ b/src/components/SelectionList/components/TextInput.tsx @@ -1,4 +1,5 @@ -import React from 'react'; +import {useFocusEffect} from '@react-navigation/native'; +import React, {useCallback, useRef} from 'react'; import type {TextInputKeyPressEvent} from 'react-native'; import {View} from 'react-native'; import type {TextInputOptions} from '@components/SelectionList/types'; @@ -7,11 +8,12 @@ import BaseTextInput from '@components/TextInput'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import mergeRefs from '@libs/mergeRefs'; import CONST from '@src/CONST'; type TextInputProps = { /** Reference to the BaseTextInput component */ - ref?: React.Ref | null; + ref?: React.RefObject | null; /** Configuration options for the text input including label, placeholder, validation, etc. */ options?: TextInputOptions; @@ -42,6 +44,12 @@ type TextInputProps = { /** Whether to show the loading indicator for new options */ isLoadingNewOptions?: boolean; + + /** Function to update the focused index in the list */ + setFocusedIndex: (index: number) => void; + + /** Function to focus text input component */ + focusTextInput: () => void; }; function TextInput({ @@ -56,48 +64,82 @@ function TextInput({ showLoadingPlaceholder, isLoadingNewOptions, shouldShowTextInput, + setFocusedIndex, + focusTextInput, }: TextInputProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const headerMessage = options?.headerMessage; + const {label, value, onChangeText, errorText, headerMessage, hint, disableAutoFocus, placeholder, maxLength, inputMode, ref: optionsRef} = options ?? {}; const resultsFound = headerMessage !== translate('common.noResultsFound'); const noData = dataLength === 0 && !showLoadingPlaceholder; - const shouldShowHeaderMessage = !!headerMessage && !isLoadingNewOptions && resultsFound && !noData; + const shouldShowHeaderMessage = !!headerMessage && (!isLoadingNewOptions || resultsFound || !noData); + + const focusTimeoutRef = useRef(null); + const mergedRef = mergeRefs(ref, optionsRef); + + const handleTextInputChange = useCallback( + (text: string) => { + onChangeText?.(text); + setFocusedIndex(0); + }, + [onChangeText, setFocusedIndex], + ); + + useFocusEffect( + useCallback(() => { + if (!shouldShowTextInput || disableAutoFocus) { + return; + } + + focusTimeoutRef.current = setTimeout(focusTextInput, CONST.ANIMATED_TRANSITION); + + return () => { + if (!focusTimeoutRef.current) { + return; + } + clearTimeout(focusTimeoutRef.current); + focusTimeoutRef.current = null; + }; + }, [shouldShowTextInput, disableAutoFocus, focusTextInput]), + ); if (!shouldShowTextInput) { return null; } + return ( - - onFocusChange?.(true)} - onBlur={() => onFocusChange?.(false)} - label={options?.label} - accessibilityLabel={accessibilityLabel} - hint={options?.hint} - role={CONST.ROLE.PRESENTATION} - value={options?.value} - placeholder={options?.placeholder} - maxLength={options?.maxLength} - onChangeText={options?.onChangeText} - inputMode={options?.inputMode} - selectTextOnFocus - spellCheck={false} - onSubmitEditing={onSubmit} - submitBehavior={dataLength ? 'blurAndSubmit' : 'submit'} - isLoading={isLoading} - testID="selection-list-text-input" - errorText={options?.errorText} - shouldInterceptSwipe={false} - /> + <> + + onFocusChange?.(true)} + onBlur={() => onFocusChange?.(false)} + label={label} + accessibilityLabel={accessibilityLabel} + hint={hint} + role={CONST.ROLE.PRESENTATION} + value={value} + placeholder={placeholder} + maxLength={maxLength} + onChangeText={handleTextInputChange} + inputMode={inputMode} + selectTextOnFocus + spellCheck={false} + onSubmitEditing={onSubmit} + submitBehavior={dataLength ? 'blurAndSubmit' : 'submit'} + isLoading={isLoading} + testID="selection-list-text-input" + errorText={errorText} + shouldInterceptSwipe={false} + /> + {shouldShowHeaderMessage && ( {headerMessage} )} - + ); } diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index eece8f12754c..fddf503cdf2a 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -176,8 +176,11 @@ type TextInputOptions = { /** Whether the text input should be shown as a header inside list */ shouldBeInsideList?: boolean; + /** Whether the text input autofocus should be disabled */ + disableAutoFocus?: boolean; + /** Reference to the text input component */ - ref?: RefObject | ((ref: BaseTextInputRef | null) => void); + ref?: RefObject; }; type ConfirmButtonOptions = { diff --git a/src/pages/Debug/Report/DebugReportActions.tsx b/src/pages/Debug/Report/DebugReportActions.tsx index c548842227a4..1dcafa255477 100644 --- a/src/pages/Debug/Report/DebugReportActions.tsx +++ b/src/pages/Debug/Report/DebugReportActions.tsx @@ -3,8 +3,8 @@ import React, {useCallback, useMemo} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import Button from '@components/Button'; import ScrollView from '@components/ScrollView'; -import SelectionList from '@components/SelectionListWithSections'; -import RadioListItem from '@components/SelectionListWithSections/RadioListItem'; +import SelectionList from '@components/SelectionList'; +import RadioListItem from '@components/SelectionList/ListItem/RadioListItem'; import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -100,9 +100,20 @@ function DebugReportActions({reportID}: DebugReportActionsProps) { reportActionID: reportAction.reportActionID, text: getReportActionDebugText(reportAction), alternateText: `${reportAction.reportActionID} | ${datetimeToCalendarTime(reportAction.created, false, false)}`, + keyForList: reportAction.reportActionID, })); }, [sortedAllReportActions, debouncedSearchValue, getReportActionDebugText, datetimeToCalendarTime]); + const textInputOptions = useMemo( + () => ({ + value: searchValue, + label: translate('common.search'), + onChangeText: setSearchValue, + headerMessage: getHeaderMessageForNonUserList(searchedReportActions.length > 0, debouncedSearchValue), + }), + [debouncedSearchValue, searchValue, searchedReportActions.length, setSearchValue, translate], + ); + return (