Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Highlight autocomplete value #54403

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 6 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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
"react-compiler-healthcheck": "react-compiler-healthcheck --verbose",
"react-compiler-healthcheck-test": "react-compiler-healthcheck --verbose &> react-compiler-output.txt",
"generate-search-parser": "peggy --format es -o src/libs/SearchParser/searchParser.js src/libs/SearchParser/searchParser.peggy src/libs/SearchParser/baseRules.peggy",
"generate-autocomplete-parser": "peggy --format es -o src/libs/SearchParser/autocompleteParser.js src/libs/SearchParser/autocompleteParser.peggy src/libs/SearchParser/baseRules.peggy",
"generate-autocomplete-parser": "peggy --format es -o src/libs/SearchParser/autocompleteParser.js src/libs/SearchParser/autocompleteParser.peggy src/libs/SearchParser/baseRules.peggy && ./scripts/parser-workletization.sh src/libs/SearchParser/autocompleteParser.js",
"web:prod": "http-server ./dist --cors"
},
"dependencies": {
Expand Down
21 changes: 21 additions & 0 deletions scripts/parser-workletization.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/bin/bash
###
# This script modifies the autocompleteParser.js file to be compatible with worklets.
# autocompleteParser.js is generated by PeggyJS and uses syntax not supported by worklets.
# This script runs each time the parser is generated by the `generate-autocomplete-parser` command.
###

filePath=$1

if [ ! -f "$filePath" ]; then
echo "$filePath does not exist."
exit 1
fi
if awk 'BEGIN { print "\47worklet\47\n\nclass peg\$SyntaxError{}" } 1' "$filePath" | sed 's/function peg\$SyntaxError/function temporary/g' | sed 's/peg$subclass(peg$SyntaxError, Error);//g' > tmp.txt; then
mv tmp.txt "$filePath"
echo "Successfully updated $filePath"
else
echo "An error occurred while modifying the file."
rm -f tmp.txt
exit 1
fi
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 @@
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 @@ -122,7 +123,7 @@
}
if (reportForContextualSearch.isPolicyExpenseChat) {
roomType = CONST.SEARCH.DATA_TYPES.EXPENSE;
autocompleteID = reportForContextualSearch.policyID ?? '';

Check failure on line 126 in src/components/Search/SearchRouter/SearchRouter.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

}

additionalSections.push({
Expand Down Expand Up @@ -202,6 +203,14 @@
[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 @@

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 @@
}
} 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 @@
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 @@ -179,7 +179,7 @@
if (currentUser) {
autocompleteOptions.push({
name: currentUser.displayName ?? Str.removeSMSDomain(currentUser.login ?? ''),
accountID: currentUser.accountID?.toString() ?? '-1',

Check failure on line 182 in src/components/Search/SearchRouter/SearchRouterList.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

});
}

Expand Down Expand Up @@ -443,7 +443,7 @@
}

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
Loading
Loading