Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 7 additions & 15 deletions src/components/SelectionList/BaseSelectionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -249,29 +249,19 @@ function BaseSelectionList<TItem extends ListItem>({
isActive: !disableKeyboardShortcuts && isFocused && !confirmButtonConfig?.isDisabled,
},
);

const textInputKeyPress = useCallback((event: TextInputKeyPressEvent) => {
const key = event.nativeEvent.key;
if (key === CONST.KEYBOARD_SHORTCUTS.TAB.shortcutKey) {
focusedItemRef?.focus();
}
}, []);

const handleTextInputRef = (element: BaseTextInputRef | null) => {
innerTextInputRef.current = element;

const textInputRef = textInputOptions?.ref;
if (!textInputRef) {
const focusTextInput = useCallback(() => {
if (!innerTextInputRef.current) {
return;
}

if (typeof textInputRef === 'function') {
textInputRef(element);
} else {
// eslint-disable-next-line react-compiler/react-compiler
textInputRef.current = element;
}
};
innerTextInputRef.current?.focus();
}, []);

const textInputComponent = ({shouldBeInsideList}: {shouldBeInsideList?: boolean}) => {
if (shouldBeInsideList !== (textInputOptions?.shouldBeInsideList ?? false)) {
Expand All @@ -280,17 +270,19 @@ function BaseSelectionList<TItem extends ListItem>({

return (
<TextInput
ref={innerTextInputRef}
focusTextInput={focusTextInput}
shouldShowTextInput={shouldShowTextInput}
onKeyPress={textInputKeyPress}
accessibilityLabel={textInputOptions?.label}
ref={handleTextInputRef}
options={textInputOptions}
onSubmit={selectFocusedOption}
dataLength={data.length}
isLoading={isLoadingNewOptions}
onFocusChange={(v: boolean) => (isTextInputFocusedRef.current = v)}
showLoadingPlaceholder={showLoadingPlaceholder}
isLoadingNewOptions={isLoadingNewOptions}
setFocusedIndex={setFocusedIndex}
/>
);
};
Expand Down
100 changes: 71 additions & 29 deletions src/components/SelectionList/components/TextInput.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<BaseTextInputRef> | null;
ref?: React.RefObject<BaseTextInputRef | null> | null;

/** Configuration options for the text input including label, placeholder, validation, etc. */
options?: TextInputOptions;
Expand Down Expand Up @@ -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({
Expand All @@ -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<NodeJS.Timeout | null>(null);
const mergedRef = mergeRefs<BaseTextInputRef>(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]),
);

if (!shouldShowTextInput) {
return null;
}

return (
<View style={[styles.ph5, styles.pb3]}>
<BaseTextInput
ref={ref}
onKeyPress={onKeyPress}
onFocus={() => 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}
/>
<>
<View style={[styles.ph5, styles.pb3]}>
<BaseTextInput
ref={mergedRef}
onKeyPress={onKeyPress}
onFocus={() => 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}
/>
</View>
{shouldShowHeaderMessage && (
<View style={[styles.ph5, styles.pb5]}>
<Text style={[styles.textLabel, styles.colorMuted, styles.minHeight5]}>{headerMessage}</Text>
</View>
)}
</View>
</>
);
}

Expand Down
5 changes: 4 additions & 1 deletion src/components/SelectionList/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<BaseTextInputRef | null> | ((ref: BaseTextInputRef | null) => void);
ref?: RefObject<BaseTextInputRef | null>;
};

type ConfirmButtonOptions<TItem extends ListItem> = {
Expand Down
24 changes: 16 additions & 8 deletions src/pages/Debug/Report/DebugReportActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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, setSearchValue, translate],
);

return (
<ScrollView style={styles.mv3}>
<Button
Expand All @@ -113,12 +124,9 @@ function DebugReportActions({reportID}: DebugReportActionsProps) {
style={[styles.pb3, styles.ph3]}
/>
<SelectionList
sections={[{data: searchedReportActions}]}
listItemTitleStyles={styles.fontWeightNormal}
textInputValue={searchValue}
textInputLabel={translate('common.search')}
headerMessage={getHeaderMessageForNonUserList(searchedReportActions.length > 0, debouncedSearchValue)}
onChangeText={setSearchValue}
data={searchedReportActions}
style={{listItemTitleStyles: styles.fontWeightNormal}}
textInputOptions={textInputOptions}
onSelectRow={(item) => Navigation.navigate(ROUTES.DEBUG_REPORT_ACTION.getRoute(reportID, item.reportActionID))}
ListItem={RadioListItem}
/>
Expand Down