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

Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
fa5c8bc
add script for parser workletization
289Adam289 Dec 19, 2024
cc24f25
update parser
289Adam289 Dec 19, 2024
8a7ea58
Improve parsers - handle NBSP
289Adam289 Dec 20, 2024
5d38a05
improve sanitize string and and workletized parser function
289Adam289 Dec 20, 2024
44c8aae
Migrate SearchRouterInput to RNMarkdownTextInput
289Adam289 Dec 20, 2024
b5d82de
fix script lint
289Adam289 Dec 20, 2024
2a65f02
fix shell lint and add styles
289Adam289 Dec 20, 2024
e320a65
Merge branch 'main' into 289Adam289/50949-highlight-autocomplete
289Adam289 Jan 7, 2025
9759ee9
fix lint on chnaged files
289Adam289 Jan 8, 2025
95c2690
changes current user mention color
289Adam289 Jan 8, 2025
9a243d2
Merge branch 'main' into 289Adam289/50949-highlight-autocomplete
289Adam289 Jan 8, 2025
c870653
Merge branch 'main' into 289Adam289/50949-highlight-autocomplete
289Adam289 Jan 10, 2025
9b80cd0
logic separation and names refactor
289Adam289 Jan 13, 2025
226eb60
Merge branch 'main' into 289Adam289/50949-highlight-autocomplete
289Adam289 Jan 14, 2025
4347c8d
fix failing parser tests
289Adam289 Jan 14, 2025
9197506
Merge branch 'main' into 289Adam289/50949-highlight-autocomplete
289Adam289 Jan 17, 2025
0c52b0f
fix: changed files lint
289Adam289 Jan 17, 2025
49ec771
Merge branch 'main' into 289Adam289/50949-highlight-autocomplete
289Adam289 Jan 20, 2025
e95cd01
Merge branch 'main' into 289Adam289/50949-highlight-autocomplete
289Adam289 Jan 23, 2025
514da4e
Merge branch 'main' into 289Adam289/50949-highlight-autocomplete
289Adam289 Jan 24, 2025
819f389
add necessary comments to parser files
289Adam289 Jan 24, 2025
f6c73f6
Merge branch 'main' into 289Adam289/50949-highlight-autocomplete
289Adam289 Jan 28, 2025
8a53425
fix parser failing tests
289Adam289 Jan 28, 2025
257e060
Merge branch 'main' into 289Adam289/50949-highlight-autocomplete
289Adam289 Jan 28, 2025
51df8e1
fix styles
289Adam289 Jan 28, 2025
ced2f32
fix typescript
289Adam289 Jan 28, 2025
83a963e
Merge branch 'main' into 289Adam289/50949-highlight-autocomplete
289Adam289 Jan 28, 2025
afe82b9
add support for all logins
289Adam289 Jan 29, 2025
601aeb9
fix lint
289Adam289 Jan 29, 2025
8f23199
Merge branch 'main' into 289Adam289/50949-highlight-autocomplete
289Adam289 Jan 31, 2025
e86a8ad
fix names
289Adam289 Jan 31, 2025
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 @@ -71,7 +71,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
22 changes: 22 additions & 0 deletions scripts/parser-workletization.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/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
# shellcheck disable=SC2016
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 @@ -123,6 +79,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, styles.dFlex, styles.alignItemsCenter]}
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 @@ -453,7 +453,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 @@ -122,6 +122,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