Skip to content

Commit

Permalink
Migrate SearchRouterInput to RNMarkdownTextInput
Browse files Browse the repository at this point in the history
  • Loading branch information
289Adam289 committed Dec 20, 2024
1 parent 5d38a05 commit 44c8aae
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 54 deletions.
16 changes: 14 additions & 2 deletions src/components/Search/SearchPageHeaderInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ function SearchPageHeaderInput({queryJSON, children}: SearchPageHeaderInputProps
const [textInputValue, setTextInputValue] = useState(queryText);
// The input text that was last used for autocomplete; needed for the SearchRouterList when browsing list via arrow keys
const [autocompleteQueryValue, setAutocompleteQueryValue] = useState(queryText);
const [selection, setSelection] = useState({start: textInputValue.length, end: textInputValue.length});

const [autocompleteSubstitutions, setAutocompleteSubstitutions] = useState<SubstitutionMap>({});
const [isAutocompleteListVisible, setIsAutocompleteListVisible] = useState(false);
Expand Down Expand Up @@ -158,7 +159,9 @@ function SearchPageHeaderInput({queryJSON, children}: SearchPageHeaderInputProps

if (item.searchItemType === CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.AUTOCOMPLETE_SUGGESTION && textInputValue) {
const trimmedUserSearchQuery = SearchAutocompleteUtils.getQueryWithoutAutocompletedPart(textInputValue);
onSearchQueryChange(`${trimmedUserSearchQuery}${SearchQueryUtils.sanitizeSearchValue(item.searchQuery)} `);
const newSearchQuery = `${trimmedUserSearchQuery}${SearchQueryUtils.sanitizeSearchValue(item.searchQuery)}\u00A0`;
onSearchQueryChange(newSearchQuery);
setSelection({start: newSearchQuery.length, end: newSearchQuery.length});

if (item.mapKey && item.autocompleteID) {
const substitutions = {...autocompleteSubstitutions, [item.mapKey]: item.autocompleteID};
Expand Down Expand Up @@ -189,6 +192,14 @@ function SearchPageHeaderInput({queryJSON, children}: SearchPageHeaderInputProps
[autocompleteSubstitutions],
);

const setTextAndUpdateSelection = useCallback(
(text: string) => {
setTextInputValue(text);
setSelection({start: text.length, end: text.length});
},
[setSelection, setTextInputValue],
);

if (isCannedQuery) {
const headerIcon = getHeaderContent(type).icon;

Expand Down Expand Up @@ -267,13 +278,14 @@ function SearchPageHeaderInput({queryJSON, children}: SearchPageHeaderInputProps
rightComponent={children}
routerListRef={listRef}
ref={textInputRef}
selection={selection}
/>
<View style={[styles.mh85vh, !isAutocompleteListVisible && styles.dNone]}>
<SearchRouterList
autocompleteQueryValue={autocompleteQueryValue}
searchQueryItem={searchQueryItem}
onListItemPress={onListItemPress}
setTextQuery={setTextInputValue}
setTextQuery={setTextAndUpdateSelection}
updateAutocompleteSubstitutions={updateAutocompleteSubstitutions}
ref={listRef}
/>
Expand Down
20 changes: 17 additions & 3 deletions src/components/Search/SearchRouter/SearchRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret}: SearchRouterProps)
const [textInputValue, , setTextInputValue] = useDebouncedState('', 500);
// The input text that was last used for autocomplete; needed for the SearchRouterList when browsing list via arrow keys
const [autocompleteQueryValue, setAutocompleteQueryValue] = useState(textInputValue);
const [selection, setSelection] = useState({start: textInputValue.length, end: textInputValue.length});
const [autocompleteSubstitutions, setAutocompleteSubstitutions] = useState<SubstitutionMap>({});
const textInputRef = useRef<AnimatedTextInputRef>(null);

Expand Down Expand Up @@ -202,6 +203,14 @@ function SearchRouter({onRouterClose, shouldHideInputCaret}: SearchRouterProps)
[autocompleteSubstitutions, onRouterClose, setTextInputValue, activeWorkspaceID],
);

const setTextAndUpdateSelection = useCallback(
(text: string) => {
setTextInputValue(text);
setSelection({start: text.length, end: text.length});
},
[setSelection, setTextInputValue],
);

const onListItemPress = useCallback(
(item: OptionData | SearchQueryItem) => {
if (isSearchQueryItem(item)) {
Expand All @@ -211,7 +220,9 @@ function SearchRouter({onRouterClose, shouldHideInputCaret}: SearchRouterProps)

if (item.searchItemType === CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.CONTEXTUAL_SUGGESTION) {
const searchQuery = getContextualSearchQuery(item);
onSearchQueryChange(`${searchQuery} `, true);
const newSearchQuery = `${searchQuery}\u00A0`;
onSearchQueryChange(newSearchQuery, true);
setSelection({start: newSearchQuery.length, end: newSearchQuery.length});

const autocompleteKey = getContextualSearchAutocompleteKey(item);
if (autocompleteKey && item.autocompleteID) {
Expand All @@ -221,7 +232,9 @@ function SearchRouter({onRouterClose, shouldHideInputCaret}: SearchRouterProps)
}
} else if (item.searchItemType === CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.AUTOCOMPLETE_SUGGESTION && textInputValue) {
const trimmedUserSearchQuery = SearchAutocompleteUtils.getQueryWithoutAutocompletedPart(textInputValue);
onSearchQueryChange(`${trimmedUserSearchQuery}${SearchQueryUtils.sanitizeSearchValue(item.searchQuery)} `);
const newSearchQuery = `${trimmedUserSearchQuery}${SearchQueryUtils.sanitizeSearchValue(item.searchQuery)}\u00A0`;
onSearchQueryChange(newSearchQuery);
setSelection({start: newSearchQuery.length, end: newSearchQuery.length});

if (item.mapKey && item.autocompleteID) {
const substitutions = {...autocompleteSubstitutions, [item.mapKey]: item.autocompleteID};
Expand Down Expand Up @@ -292,14 +305,15 @@ function SearchRouter({onRouterClose, shouldHideInputCaret}: SearchRouterProps)
outerWrapperStyle={[shouldUseNarrowLayout ? styles.mv3 : styles.mv2, shouldUseNarrowLayout ? styles.mh5 : styles.mh2]}
wrapperFocusedStyle={[styles.borderColorFocus]}
isSearchingForReports={isSearchingForReports}
selection={selection}
ref={textInputRef}
/>
<SearchRouterList
autocompleteQueryValue={autocompleteQueryValue || textInputValue}
searchQueryItem={searchQueryItem}
additionalSections={additionalSections}
onListItemPress={onListItemPress}
setTextQuery={setTextInputValue}
setTextQuery={setTextAndUpdateSelection}
updateAutocompleteSubstitutions={updateAutocompleteSubstitutions}
ref={listRef}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,61 +1,17 @@
import type {ForwardedRef, ReactNode, RefObject} from 'react';
import type {ForwardedRef} from 'react';
import React, {forwardRef, useState} from 'react';
import type {StyleProp, TextInputProps, ViewStyle} from 'react-native';
import {View} from 'react-native';
import FormHelpMessage from '@components/FormHelpMessage';
import type {SelectionListHandle} from '@components/SelectionList/types';
import TextInput from '@components/TextInput';
import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
import {workletizedParser} from '@libs/SearchAutocompleteUtils';
import shouldDelayFocus from '@libs/shouldDelayFocus';
import variables from '@styles/variables';
import CONST from '@src/CONST';

type SearchRouterInputProps = {
/** Value of TextInput */
value: string;

/** Callback to update search in SearchRouter */
onSearchQueryChange: (searchTerm: string) => void;

/** Callback invoked when the user submits the input */
onSubmit?: () => void;

/** SearchRouterList ref for managing TextInput and SearchRouterList focus */
routerListRef?: RefObject<SelectionListHandle>;

/** Whether the input is full width */
isFullWidth: boolean;

/** Whether the input is disabled */
disabled?: boolean;

/** Whether the offline message should be shown */
shouldShowOfflineMessage?: boolean;

/** Callback to call when the input gets focus */
onFocus?: () => void;

/** Callback to call when the input gets blur */
onBlur?: () => void;

/** Any additional styles to apply */
wrapperStyle?: StyleProp<ViewStyle>;

/** Any additional styles to apply when input is focused */
wrapperFocusedStyle?: StyleProp<ViewStyle>;

/** Any additional styles to apply to text input along with FormHelperMessage */
outerWrapperStyle?: StyleProp<ViewStyle>;

/** Component to be displayed on the right */
rightComponent?: ReactNode;

/** Whether the search reports API call is running */
isSearchingForReports?: boolean;
} & Pick<TextInputProps, 'caretHidden' | 'autoFocus'>;
import type SearchRouterInputProps from './types';

function SearchRouterInput(
{
Expand Down Expand Up @@ -122,6 +78,9 @@ function SearchRouterInput(
}}
isLoading={!!isSearchingForReports}
ref={ref}
isMarkdownEnabled
multiline={false}
parser={workletizedParser}
/>
</View>
{!!rightComponent && <View style={styles.pr3}>{rightComponent}</View>}
Expand Down
101 changes: 101 additions & 0 deletions src/components/Search/SearchRouter/SearchRouterInput/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import type {ForwardedRef} from 'react';
import React, {forwardRef, useState} from 'react';
import {View} from 'react-native';
import FormHelpMessage from '@components/FormHelpMessage';
import TextInput from '@components/TextInput';
import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
import {workletizedParser} from '@libs/SearchAutocompleteUtils';
import shouldDelayFocus from '@libs/shouldDelayFocus';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import type SearchRouterInputProps from './types';

function SearchRouterInput(
{
value,
onSearchQueryChange,
onSubmit = () => {},
routerListRef,
isFullWidth,
disabled = false,
shouldShowOfflineMessage = false,
autoFocus = true,
onFocus,
onBlur,
caretHidden = false,
wrapperStyle,
wrapperFocusedStyle,
outerWrapperStyle,
rightComponent,
isSearchingForReports,
selection,
}: SearchRouterInputProps,
ref: ForwardedRef<BaseTextInputRef>,
) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const [isFocused, setIsFocused] = useState<boolean>(false);
const {isOffline} = useNetwork();
const offlineMessage: string = isOffline && shouldShowOfflineMessage ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : '';

const inputWidth = isFullWidth ? styles.w100 : {width: variables.popoverWidth};

return (
<View style={[outerWrapperStyle]}>
<View style={[styles.flexRow, styles.alignItemsCenter, wrapperStyle ?? styles.searchRouterTextInputContainer, isFocused && wrapperFocusedStyle]}>
<View style={styles.flex1}>
<TextInput
testID="search-router-text-input"
value={value}
onChangeText={onSearchQueryChange}
autoFocus={autoFocus}
shouldDelayFocus={shouldDelayFocus}
caretHidden={caretHidden}
loadingSpinnerStyle={[styles.mt0, styles.mr2]}
role={CONST.ROLE.PRESENTATION}
placeholder={translate('search.searchPlaceholder')}
autoCapitalize="none"
autoCorrect={false}
spellCheck={false}
enterKeyHint="search"
accessibilityLabel={translate('search.searchPlaceholder')}
disabled={disabled}
onSubmitEditing={onSubmit}
shouldUseDisabledStyles={false}
textInputContainerStyles={[styles.borderNone, styles.pb0]}
inputStyle={[inputWidth, styles.p3]}
onFocus={() => {
setIsFocused(true);
routerListRef?.current?.updateExternalTextInputFocus(true);
onFocus?.();
}}
onBlur={() => {
setIsFocused(false);
routerListRef?.current?.updateExternalTextInputFocus(false);
onBlur?.();
}}
isLoading={!!isSearchingForReports}
ref={ref}
isMarkdownEnabled
multiline={false}
parser={workletizedParser}
selection={selection}
/>
</View>
{!!rightComponent && <View style={styles.pr3}>{rightComponent}</View>}
</View>
<FormHelpMessage
style={styles.ph3}
isError={false}
message={offlineMessage}
/>
</View>
);
}

SearchRouterInput.displayName = 'SearchRouterInput';

export default forwardRef(SearchRouterInput);
49 changes: 49 additions & 0 deletions src/components/Search/SearchRouter/SearchRouterInput/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type {ReactNode, RefObject} from 'react';
import type {StyleProp, TextInputProps, ViewStyle} from 'react-native';
import type {SelectionListHandle} from '@components/SelectionList/types';

type SearchRouterInputProps = {
/** Value of TextInput */
value: string;

/** Callback to update search in SearchRouter */
onSearchQueryChange: (searchTerm: string) => void;

/** Callback invoked when the user submits the input */
onSubmit?: () => void;

/** SearchRouterList ref for managing TextInput and SearchRouterList focus */
routerListRef?: RefObject<SelectionListHandle>;

/** Whether the input is full width */
isFullWidth: boolean;

/** Whether the input is disabled */
disabled?: boolean;

/** Whether the offline message should be shown */
shouldShowOfflineMessage?: boolean;

/** Callback to call when the input gets focus */
onFocus?: () => void;

/** Callback to call when the input gets blur */
onBlur?: () => void;

/** Any additional styles to apply */
wrapperStyle?: StyleProp<ViewStyle>;

/** Any additional styles to apply when input is focused */
wrapperFocusedStyle?: StyleProp<ViewStyle>;

/** Any additional styles to apply to text input along with FormHelperMessage */
outerWrapperStyle?: StyleProp<ViewStyle>;

/** Component to be displayed on the right */
rightComponent?: ReactNode;

/** Whether the search reports API call is running */
isSearchingForReports?: boolean;
} & Pick<TextInputProps, 'caretHidden' | 'autoFocus' | 'selection'>;

export default SearchRouterInputProps;
2 changes: 1 addition & 1 deletion src/components/Search/SearchRouter/SearchRouterList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@ function SearchRouterList(
}

const trimmedUserSearchQuery = SearchAutocompleteUtils.getQueryWithoutAutocompletedPart(autocompleteQueryValue);
setTextQuery(`${trimmedUserSearchQuery}${SearchQueryUtils.sanitizeSearchValue(focusedItem.searchQuery)} `);
setTextQuery(`${trimmedUserSearchQuery}${SearchQueryUtils.sanitizeSearchValue(focusedItem.searchQuery)}\u00A0`);
updateAutocompleteSubstitutions(focusedItem);
},
[autocompleteQueryValue, setTextQuery, updateAutocompleteSubstitutions],
Expand Down
5 changes: 4 additions & 1 deletion src/components/TextInput/BaseTextInput/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {MarkdownStyle} from '@expensify/react-native-live-markdown';
import type {MarkdownRange, MarkdownStyle} from '@expensify/react-native-live-markdown';
import type {GestureResponderEvent, StyleProp, TextInputProps, TextStyle, ViewStyle} from 'react-native';
import type {AnimatedTextInputRef} from '@components/RNTextInput';
import type IconAsset from '@src/types/utils/IconAsset';
Expand Down Expand Up @@ -119,6 +119,9 @@ type CustomBaseTextInputProps = {
/** List of markdowns that won't be styled as a markdown */
excludedMarkdownStyles?: Array<keyof MarkdownStyle>;

/** Custom parser function for RNMarkdownTextInput */
parser?: (input: string) => MarkdownRange[];

/** Whether the clear button should be displayed */
shouldShowClearButton?: boolean;

Expand Down

0 comments on commit 44c8aae

Please sign in to comment.