Skip to content

Commit 80f1e44

Browse files
authored
Merge pull request #54403 from software-mansion-labs/289Adam289/50949-highlight-autocomplete
Highlight autocomplete value
2 parents 4e631b4 + e86a8ad commit 80f1e44

20 files changed

+244
-89
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
"react-compiler-healthcheck": "react-compiler-healthcheck --verbose",
7272
"react-compiler-healthcheck-test": "react-compiler-healthcheck --verbose &> react-compiler-output.txt",
7373
"generate-search-parser": "peggy --format es -o src/libs/SearchParser/searchParser.js src/libs/SearchParser/searchParser.peggy src/libs/SearchParser/baseRules.peggy",
74-
"generate-autocomplete-parser": "peggy --format es -o src/libs/SearchParser/autocompleteParser.js src/libs/SearchParser/autocompleteParser.peggy src/libs/SearchParser/baseRules.peggy",
74+
"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",
7575
"web:prod": "http-server ./dist --cors"
7676
},
7777
"dependencies": {

scripts/parser-workletization.sh

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/bin/bash
2+
###
3+
# This script modifies the autocompleteParser.js file to be compatible with worklets.
4+
# autocompleteParser.js is generated by PeggyJS and uses some parts of syntax not supported by worklets.
5+
# This script runs each time the parser is generated by the `generate-autocomplete-parser` command.
6+
###
7+
8+
filePath=$1
9+
10+
if [ ! -f "$filePath" ]; then
11+
echo "$filePath does not exist."
12+
exit 1
13+
fi
14+
# shellcheck disable=SC2016
15+
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
16+
mv tmp.txt "$filePath"
17+
echo "Successfully updated $filePath"
18+
else
19+
echo "An error occurred while modifying the file."
20+
rm -f tmp.txt
21+
exit 1
22+
fi

src/components/Search/SearchRouter/SearchRouterInput.tsx src/components/Search/SearchAutocompleteInput.tsx

+33-14
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
import type {ForwardedRef, ReactNode, RefObject} from 'react';
22
import React, {forwardRef, useState} from 'react';
3-
import type {StyleProp, TextInputProps, ViewStyle} from 'react-native';
43
import {View} from 'react-native';
4+
import type {StyleProp, TextInputProps, ViewStyle} from 'react-native';
5+
import {useOnyx} from 'react-native-onyx';
56
import FormHelpMessage from '@components/FormHelpMessage';
67
import type {SelectionListHandle} from '@components/SelectionList/types';
78
import TextInput from '@components/TextInput';
89
import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types';
10+
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
911
import useLocalize from '@hooks/useLocalize';
1012
import useNetwork from '@hooks/useNetwork';
1113
import useThemeStyles from '@hooks/useThemeStyles';
14+
import {parseForLiveMarkdown} from '@libs/SearchAutocompleteUtils';
1215
import handleKeyPress from '@libs/SearchInputOnKeyPress';
1316
import shouldDelayFocus from '@libs/shouldDelayFocus';
1417
import variables from '@styles/variables';
1518
import CONST from '@src/CONST';
19+
import ONYXKEYS from '@src/ONYXKEYS';
1620

17-
type SearchRouterInputProps = {
21+
type SearchAutocompleteInputProps = {
1822
/** Value of TextInput */
1923
value: string;
2024

@@ -24,8 +28,8 @@ type SearchRouterInputProps = {
2428
/** Callback invoked when the user submits the input */
2529
onSubmit?: () => void;
2630

27-
/** SearchRouterList ref for managing TextInput and SearchRouterList focus */
28-
routerListRef?: RefObject<SelectionListHandle>;
31+
/** SearchAutocompleteList ref for managing TextInput and SearchAutocompleteList focus */
32+
autocompleteListRef?: RefObject<SelectionListHandle>;
2933

3034
/** Whether the input is full width */
3135
isFullWidth: boolean;
@@ -56,14 +60,14 @@ type SearchRouterInputProps = {
5660

5761
/** Whether the search reports API call is running */
5862
isSearchingForReports?: boolean;
59-
} & Pick<TextInputProps, 'caretHidden' | 'autoFocus'>;
63+
} & Pick<TextInputProps, 'caretHidden' | 'autoFocus' | 'selection'>;
6064

61-
function SearchRouterInput(
65+
function SearchAutocompleteInput(
6266
{
6367
value,
6468
onSearchQueryChange,
6569
onSubmit = () => {},
66-
routerListRef,
70+
autocompleteListRef,
6771
isFullWidth,
6872
disabled = false,
6973
shouldShowOfflineMessage = false,
@@ -76,13 +80,19 @@ function SearchRouterInput(
7680
outerWrapperStyle,
7781
rightComponent,
7882
isSearchingForReports,
79-
}: SearchRouterInputProps,
83+
selection,
84+
}: SearchAutocompleteInputProps,
8085
ref: ForwardedRef<BaseTextInputRef>,
8186
) {
8287
const styles = useThemeStyles();
8388
const {translate} = useLocalize();
8489
const [isFocused, setIsFocused] = useState<boolean>(false);
8590
const {isOffline} = useNetwork();
91+
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
92+
93+
const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST);
94+
const emailList = Object.keys(loginList ?? {});
95+
8696
const offlineMessage: string = isOffline && shouldShowOfflineMessage ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : '';
8797

8898
const inputWidth = isFullWidth ? styles.w100 : {width: variables.popoverWidth};
@@ -95,7 +105,7 @@ function SearchRouterInput(
95105
fsClass="fs-unmask"
96106
>
97107
<TextInput
98-
testID="search-router-text-input"
108+
testID="search-autocomplete-text-input"
99109
value={value}
100110
onChangeText={onSearchQueryChange}
101111
autoFocus={autoFocus}
@@ -113,21 +123,29 @@ function SearchRouterInput(
113123
maxLength={CONST.SEARCH_QUERY_LIMIT}
114124
onSubmitEditing={onSubmit}
115125
shouldUseDisabledStyles={false}
116-
textInputContainerStyles={[styles.borderNone, styles.pb0]}
126+
textInputContainerStyles={[styles.borderNone, styles.pb0, styles.pr3]}
117127
inputStyle={[inputWidth, styles.pl3, styles.pr3]}
118128
onFocus={() => {
119129
setIsFocused(true);
120-
routerListRef?.current?.updateExternalTextInputFocus(true);
130+
autocompleteListRef?.current?.updateExternalTextInputFocus(true);
121131
onFocus?.();
122132
}}
123133
onBlur={() => {
124134
setIsFocused(false);
125-
routerListRef?.current?.updateExternalTextInputFocus(false);
135+
autocompleteListRef?.current?.updateExternalTextInputFocus(false);
126136
onBlur?.();
127137
}}
128138
isLoading={!!isSearchingForReports}
129139
ref={ref}
130140
onKeyPress={handleKeyPress(onSubmit)}
141+
isMarkdownEnabled
142+
multiline={false}
143+
parser={(input: string) => {
144+
'worklet';
145+
146+
return parseForLiveMarkdown(input, emailList, currentUserPersonalDetails.displayName ?? '');
147+
}}
148+
selection={selection}
131149
/>
132150
</View>
133151
{!!rightComponent && <View style={styles.pr3}>{rightComponent}</View>}
@@ -141,6 +159,7 @@ function SearchRouterInput(
141159
);
142160
}
143161

144-
SearchRouterInput.displayName = 'SearchRouterInput';
162+
SearchAutocompleteInput.displayName = 'SearchAutocompleteInput';
145163

146-
export default forwardRef(SearchRouterInput);
164+
export type {SearchAutocompleteInputProps};
165+
export default forwardRef(SearchAutocompleteInput);

src/components/Search/SearchRouter/SearchRouterList.tsx src/components/Search/SearchAutocompleteList.tsx

+7-8
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {useOnyx} from 'react-native-onyx';
55
import * as Expensicons from '@components/Icon/Expensicons';
66
import {usePersonalDetails} from '@components/OnyxProvider';
77
import {useOptionsList} from '@components/OptionListContextProvider';
8-
import type {SearchFilterKey, UserFriendlyKey} from '@components/Search/types';
98
import SelectionList from '@components/SelectionList';
109
import SearchQueryListItem, {isSearchQueryItem} from '@components/SelectionList/Search/SearchQueryListItem';
1110
import type {SearchQueryItem, SearchQueryListItemProps} from '@components/SelectionList/Search/SearchQueryListItem';
@@ -38,7 +37,8 @@ import Timing from '@userActions/Timing';
3837
import CONST from '@src/CONST';
3938
import ONYXKEYS from '@src/ONYXKEYS';
4039
import type PersonalDetails from '@src/types/onyx/PersonalDetails';
41-
import {getSubstitutionMapKey} from './getQueryWithSubstitutions';
40+
import {getSubstitutionMapKey} from './SearchRouter/getQueryWithSubstitutions';
41+
import type {SearchFilterKey, UserFriendlyKey} from './types';
4242

4343
type AutocompleteItemData = {
4444
filterKey: UserFriendlyKey;
@@ -49,7 +49,7 @@ type AutocompleteItemData = {
4949

5050
type GetAdditionalSectionsCallback = (options: Options) => Array<SectionListDataType<OptionData | SearchQueryItem>> | undefined;
5151

52-
type SearchRouterListProps = {
52+
type SearchAutocompleteListProps = {
5353
/** Value of TextInput */
5454
autocompleteQueryValue: string;
5555

@@ -117,9 +117,8 @@ function SearchRouterItem(props: UserListItemProps<OptionData> | SearchQueryList
117117
);
118118
}
119119

120-
// Todo rename to SearchAutocompleteList once it's used in both Router and SearchPage
121-
function SearchRouterList(
122-
{autocompleteQueryValue, searchQueryItem, getAdditionalSections, onListItemPress, setTextQuery, updateAutocompleteSubstitutions}: SearchRouterListProps,
120+
function SearchAutocompleteList(
121+
{autocompleteQueryValue, searchQueryItem, getAdditionalSections, onListItemPress, setTextQuery, updateAutocompleteSubstitutions}: SearchAutocompleteListProps,
123122
ref: ForwardedRef<SelectionListHandle>,
124123
) {
125124
const styles = useThemeStyles();
@@ -465,7 +464,7 @@ function SearchRouterList(
465464
}
466465

467466
const trimmedUserSearchQuery = getQueryWithoutAutocompletedPart(autocompleteQueryValue);
468-
setTextQuery(`${trimmedUserSearchQuery}${sanitizeSearchValue(focusedItem.searchQuery)} `);
467+
setTextQuery(`${trimmedUserSearchQuery}${sanitizeSearchValue(focusedItem.searchQuery)}\u00A0`);
469468
updateAutocompleteSubstitutions(focusedItem);
470469
},
471470
[autocompleteQueryValue, setTextQuery, updateAutocompleteSubstitutions],
@@ -495,6 +494,6 @@ function SearchRouterList(
495494
);
496495
}
497496

498-
export default forwardRef(SearchRouterList);
497+
export default forwardRef(SearchAutocompleteList);
499498
export {SearchRouterItem};
500499
export type {GetAdditionalSectionsCallback};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type {ForwardedRef} from 'react';
2+
import React, {forwardRef} from 'react';
3+
import SearchAutocompleteInput from '@components/Search/SearchAutocompleteInput';
4+
import type {SearchAutocompleteInputProps} from '@components/Search/SearchAutocompleteInput';
5+
import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types';
6+
7+
function SearchInputSelectionWrapper(props: SearchAutocompleteInputProps, ref: ForwardedRef<BaseTextInputRef>) {
8+
return (
9+
<SearchAutocompleteInput
10+
ref={ref}
11+
// eslint-disable-next-line react/jsx-props-no-spreading
12+
{...props}
13+
selection={undefined}
14+
/>
15+
);
16+
}
17+
18+
SearchInputSelectionWrapper.displayName = 'SearchInputSelectionWrapper';
19+
20+
export default forwardRef(SearchInputSelectionWrapper);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type {ForwardedRef} from 'react';
2+
import React, {forwardRef} from 'react';
3+
import SearchAutocompleteInput from '@components/Search/SearchAutocompleteInput';
4+
import type {SearchAutocompleteInputProps} from '@components/Search/SearchAutocompleteInput';
5+
import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types';
6+
7+
function SearchInputSelectionWrapper({selection, ...props}: SearchAutocompleteInputProps, ref: ForwardedRef<BaseTextInputRef>) {
8+
return (
9+
<SearchAutocompleteInput
10+
selection={selection}
11+
ref={ref}
12+
// eslint-disable-next-line react/jsx-props-no-spreading
13+
{...props}
14+
/>
15+
);
16+
}
17+
18+
SearchInputSelectionWrapper.displayName = 'SearchInputSelectionWrapper';
19+
20+
export default forwardRef(SearchInputSelectionWrapper);

src/components/Search/SearchPageHeader.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ function SearchPageHeader({queryJSON}: SearchPageHeaderProps) {
395395
onTooltipPress={onFiltersButtonPress}
396396
>
397397
<Button
398-
innerStyles={!isCannedQuery && [styles.searchRouterInputResults, styles.borderNone]}
398+
innerStyles={!isCannedQuery && [styles.searchAutocompleteInputResults, styles.borderNone]}
399399
text={translate('search.filtersHeader')}
400400
icon={Expensicons.Filters}
401401
onPress={onFiltersButtonPress}

src/components/Search/SearchPageHeaderInput.tsx

+22-10
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@ import ONYXKEYS from '@src/ONYXKEYS';
2929
import ROUTES from '@src/ROUTES';
3030
import type {SearchDataTypes} from '@src/types/onyx/SearchResults';
3131
import type IconAsset from '@src/types/utils/IconAsset';
32+
import SearchAutocompleteList from './SearchAutocompleteList';
33+
import SearchInputSelectionWrapper from './SearchInputSelectionWrapper';
3234
import {buildSubstitutionsMap} from './SearchRouter/buildSubstitutionsMap';
3335
import {getQueryWithSubstitutions} from './SearchRouter/getQueryWithSubstitutions';
3436
import type {SubstitutionMap} from './SearchRouter/getQueryWithSubstitutions';
3537
import {getUpdatedSubstitutionsMap} from './SearchRouter/getUpdatedSubstitutionsMap';
3638
import SearchButton from './SearchRouter/SearchButton';
3739
import {useSearchRouterContext} from './SearchRouter/SearchRouterContext';
38-
import SearchRouterInput from './SearchRouter/SearchRouterInput';
39-
import SearchRouterList from './SearchRouter/SearchRouterList';
4040
import type {SearchQueryJSON, SearchQueryString} from './types';
4141

4242
// When counting absolute positioning, we need to account for borders
@@ -83,8 +83,9 @@ function SearchPageHeaderInput({queryJSON, children}: SearchPageHeaderInputProps
8383

8484
// The actual input text that the user sees
8585
const [textInputValue, setTextInputValue] = useState(queryText);
86-
// The input text that was last used for autocomplete; needed for the SearchRouterList when browsing list via arrow keys
86+
// The input text that was last used for autocomplete; needed for the SearchAutocompleteList when browsing list via arrow keys
8787
const [autocompleteQueryValue, setAutocompleteQueryValue] = useState(queryText);
88+
const [selection, setSelection] = useState({start: textInputValue.length, end: textInputValue.length});
8889

8990
const [autocompleteSubstitutions, setAutocompleteSubstitutions] = useState<SubstitutionMap>({});
9091
const [isAutocompleteListVisible, setIsAutocompleteListVisible] = useState(false);
@@ -165,7 +166,9 @@ function SearchPageHeaderInput({queryJSON, children}: SearchPageHeaderInputProps
165166

166167
if (item.searchItemType === CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.AUTOCOMPLETE_SUGGESTION && textInputValue) {
167168
const trimmedUserSearchQuery = getQueryWithoutAutocompletedPart(textInputValue);
168-
onSearchQueryChange(`${trimmedUserSearchQuery}${sanitizeSearchValue(item.searchQuery)} `);
169+
const newSearchQuery = `${trimmedUserSearchQuery}${sanitizeSearchValue(item.searchQuery)}\u00A0`;
170+
onSearchQueryChange(newSearchQuery);
171+
setSelection({start: newSearchQuery.length, end: newSearchQuery.length});
169172

170173
if (item.mapKey && item.autocompleteID) {
171174
const substitutions = {...autocompleteSubstitutions, [item.mapKey]: item.autocompleteID};
@@ -196,6 +199,14 @@ function SearchPageHeaderInput({queryJSON, children}: SearchPageHeaderInputProps
196199
[autocompleteSubstitutions],
197200
);
198201

202+
const setTextAndUpdateSelection = useCallback(
203+
(text: string) => {
204+
setTextInputValue(text);
205+
setSelection({start: text.length, end: text.length});
206+
},
207+
[setSelection, setTextInputValue],
208+
);
209+
199210
if (isCannedQuery) {
200211
const headerIcon = getHeaderContent(type).icon;
201212

@@ -258,7 +269,7 @@ function SearchPageHeaderInput({queryJSON, children}: SearchPageHeaderInputProps
258269
style={[styles.searchResultsHeaderBar, isAutocompleteListVisible && styles.ph3]}
259270
>
260271
<View style={[styles.appBG, ...autocompleteInputStyle]}>
261-
<SearchRouterInput
272+
<SearchInputSelectionWrapper
262273
value={textInputValue}
263274
onSearchQueryChange={onSearchQueryChange}
264275
isFullWidth
@@ -272,19 +283,20 @@ function SearchPageHeaderInput({queryJSON, children}: SearchPageHeaderInputProps
272283
autoFocus={false}
273284
onFocus={showAutocompleteList}
274285
onBlur={hideAutocompleteList}
275-
wrapperStyle={[styles.searchRouterInputResults, styles.br2]}
276-
wrapperFocusedStyle={styles.searchRouterInputResultsFocused}
286+
wrapperStyle={[styles.searchAutocompleteInputResults, styles.br2]}
287+
wrapperFocusedStyle={styles.searchAutocompleteInputResultsFocused}
277288
outerWrapperStyle={[inputWrapperActiveStyle, styles.pb2]}
278289
rightComponent={children}
279-
routerListRef={listRef}
290+
autocompleteListRef={listRef}
280291
ref={textInputRef}
292+
selection={selection}
281293
/>
282294
<View style={[styles.mh85vh, !isAutocompleteListVisible && styles.dNone]}>
283-
<SearchRouterList
295+
<SearchAutocompleteList
284296
autocompleteQueryValue={autocompleteQueryValue}
285297
searchQueryItem={searchQueryItem}
286298
onListItemPress={onListItemPress}
287-
setTextQuery={setTextInputValue}
299+
setTextQuery={setTextAndUpdateSelection}
288300
updateAutocompleteSubstitutions={updateAutocompleteSubstitutions}
289301
ref={listRef}
290302
/>

0 commit comments

Comments
 (0)