Skip to content

Commit 2ef91b8

Browse files
authored
Merge pull request #51667 from callstack-internal/feat/step-3-ui
feat: Step 3 UI
2 parents 7a605ec + ce274cb commit 2ef91b8

35 files changed

+1609
-138
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@
108108
"date-fns-tz": "^3.2.0",
109109
"dom-serializer": "^0.2.2",
110110
"domhandler": "^4.3.0",
111-
"expensify-common": "2.0.103",
111+
"expensify-common": "2.0.106",
112112
"expo": "51.0.31",
113113
"expo-av": "14.0.7",
114114
"expo-image": "1.12.15",

src/components/AddressSearch/index.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ function AddressSearch(
6363
onBlur,
6464
onInputChange,
6565
onPress,
66+
onCountryChange,
6667
predefinedPlaces = [],
6768
preferredLocale,
6869
renamedInputKeys = {
@@ -195,7 +196,7 @@ function AddressSearch(
195196

196197
// If the address is not in the US, use the full length state name since we're displaying the address's
197198
// state / province in a TextInput instead of in a picker.
198-
if (country !== CONST.COUNTRY.US) {
199+
if (country !== CONST.COUNTRY.US && country !== CONST.COUNTRY.CA) {
199200
values.state = longStateName;
200201
}
201202

@@ -244,6 +245,7 @@ function AddressSearch(
244245
onInputChange?.(values);
245246
}
246247

248+
onCountryChange?.(values.country);
247249
onPress?.(values);
248250
};
249251

src/components/AddressSearch/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ type AddressSearchProps = {
8787

8888
/** The user's preferred locale e.g. 'en', 'es-ES' */
8989
preferredLocale?: Locale;
90+
91+
/** Callback to be called when the country is changed */
92+
onCountryChange?: (country: unknown) => void;
9093
};
9194

9295
type IsCurrentTargetInsideContainerType = (event: FocusEvent | NativeSyntheticEvent<TextInputFocusEventData>, containerRef: RefObject<View | HTMLElement>) => boolean;

src/components/CountryPicker/CountrySelectorModal.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import RadioListItem from '@components/SelectionList/RadioListItem';
77
import useDebouncedState from '@hooks/useDebouncedState';
88
import useLocalize from '@hooks/useLocalize';
99
import useThemeStyles from '@hooks/useThemeStyles';
10-
import searchCountryOptions from '@libs/searchCountryOptions';
11-
import type {CountryData} from '@libs/searchCountryOptions';
10+
import searchOptions from '@libs/searchOptions';
11+
import type {Option} from '@libs/searchOptions';
1212
import StringUtils from '@libs/StringUtils';
1313
import CONST from '@src/CONST';
1414
import type {TranslationPaths} from '@src/languages/types';
@@ -27,7 +27,7 @@ type CountrySelectorModalProps = {
2727
currentCountry: string;
2828

2929
/** Function to call when the user selects a country */
30-
onCountrySelected: (value: CountryData) => void;
30+
onCountrySelected: (value: Option) => void;
3131

3232
/** Function to call when the user presses on the modal backdrop */
3333
onBackdropPress?: () => void;
@@ -52,7 +52,7 @@ function CountrySelectorModal({isVisible, currentCountry, onCountrySelected, onC
5252
[translate, currentCountry],
5353
);
5454

55-
const searchResults = searchCountryOptions(debouncedSearchValue, countries);
55+
const searchResults = searchOptions(debouncedSearchValue, countries);
5656
const headerMessage = debouncedSearchValue.trim() && !searchResults.length ? translate('common.noResultsFound') : '';
5757

5858
const styles = useThemeStyles();

src/components/CountryPicker/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, {useState} from 'react';
22
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
33
import useLocalize from '@hooks/useLocalize';
44
import Navigation from '@libs/Navigation/Navigation';
5-
import type {CountryData} from '@libs/searchCountryOptions';
5+
import type {Option} from '@libs/searchOptions';
66
import CONST from '@src/CONST';
77
import type {TranslationPaths} from '@src/languages/types';
88
import CountrySelectorModal from './CountrySelectorModal';
@@ -26,7 +26,7 @@ function CountryPicker({value, errorText, onInputChange = () => {}}: CountryPick
2626
setIsPickerVisible(false);
2727
};
2828

29-
const updateInput = (item: CountryData) => {
29+
const updateInput = (item: Option) => {
3030
onInputChange?.(item.value);
3131
hidePickerModal();
3232
};

src/components/Form/FormProvider.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import CONST from '@src/CONST';
1212
import type {OnyxFormDraftKey, OnyxFormKey} from '@src/ONYXKEYS';
1313
import ONYXKEYS from '@src/ONYXKEYS';
1414
import type {Form} from '@src/types/form';
15+
import type {Errors} from '@src/types/onyx/OnyxCommon';
1516
import {isEmptyObject} from '@src/types/utils/EmptyObject';
1617
import type {RegisterInput} from './FormContext';
1718
import FormContext from './FormContext';
@@ -244,9 +245,20 @@ function FormProvider(
244245
setErrors({});
245246
}, [formID]);
246247

248+
const resetFormFieldError = useCallback(
249+
(inputID: keyof Form) => {
250+
const newErrors = {...errors};
251+
delete newErrors[inputID];
252+
FormActions.setErrors(formID, newErrors as Errors);
253+
setErrors(newErrors);
254+
},
255+
[errors, formID],
256+
);
257+
247258
useImperativeHandle(forwardedRef, () => ({
248259
resetForm,
249260
resetErrors,
261+
resetFormFieldError,
250262
}));
251263

252264
const registerInput = useCallback<RegisterInput>(

src/components/Form/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import type NetSuiteCustomListPicker from '@pages/workspace/accounting/netsuite/
3333
import type NetSuiteMenuWithTopDescriptionForm from '@pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteMenuWithTopDescriptionForm';
3434
import type {Country} from '@src/CONST';
3535
import type {OnyxFormKey, OnyxValues} from '@src/ONYXKEYS';
36+
import type {Form} from '@src/types/form';
3637
import type {BaseForm} from '@src/types/form/Form';
3738

3839
/**
@@ -164,6 +165,7 @@ type FormProps<TFormID extends OnyxFormKey = OnyxFormKey> = {
164165
type FormRef<TFormID extends OnyxFormKey = OnyxFormKey> = {
165166
resetForm: (optionalValue: FormOnyxValues<TFormID>) => void;
166167
resetErrors: () => void;
168+
resetFormFieldError: (fieldID: keyof Form) => void;
167169
};
168170

169171
type InputRefs = Record<string, MutableRefObject<InputComponentBaseProps>>;

src/components/PushRowWithModal/PushRowModal.tsx

Lines changed: 24 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
import React, {useEffect, useState} from 'react';
1+
import React, {useMemo} from 'react';
22
import HeaderWithBackButton from '@components/HeaderWithBackButton';
33
import Modal from '@components/Modal';
44
import ScreenWrapper from '@components/ScreenWrapper';
55
import SelectionList from '@components/SelectionList';
66
import RadioListItem from '@components/SelectionList/RadioListItem';
7+
import useDebouncedState from '@hooks/useDebouncedState';
78
import useLocalize from '@hooks/useLocalize';
9+
import searchOptions from '@libs/searchOptions';
10+
import StringUtils from '@libs/StringUtils';
811
import CONST from '@src/CONST';
912

1013
type PushRowModalProps = {
@@ -40,44 +43,28 @@ type ListItemType = {
4043
function PushRowModal({isVisible, selectedOption, onOptionChange, onClose, optionsList, headerTitle, searchInputTitle}: PushRowModalProps) {
4144
const {translate} = useLocalize();
4245

43-
const allOptions = Object.entries(optionsList).map(([key, value]) => ({
44-
value: key,
45-
text: value,
46-
keyForList: key,
47-
isSelected: key === selectedOption,
48-
}));
49-
const [searchbarInputText, setSearchbarInputText] = useState('');
50-
const [optionListItems, setOptionListItems] = useState(allOptions);
51-
52-
useEffect(() => {
53-
setOptionListItems((prevOptionListItems) =>
54-
prevOptionListItems.map((option) => ({
55-
...option,
56-
isSelected: option.value === selectedOption,
46+
const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState('');
47+
48+
const options = useMemo(
49+
() =>
50+
Object.entries(optionsList).map(([key, value]) => ({
51+
value: key,
52+
text: value,
53+
keyForList: key,
54+
isSelected: key === selectedOption,
55+
searchValue: StringUtils.sanitizeString(value),
5756
})),
58-
);
59-
}, [selectedOption]);
60-
61-
const filterShownOptions = (searchText: string) => {
62-
setSearchbarInputText(searchText);
63-
const searchWords = searchText.toLowerCase().match(/[a-z0-9]+/g) ?? [];
64-
setOptionListItems(
65-
allOptions.filter((option) =>
66-
searchWords.every((word) =>
67-
option.text
68-
.toLowerCase()
69-
.replace(/[^a-z0-9]/g, ' ')
70-
.includes(word),
71-
),
72-
),
73-
);
74-
};
57+
[optionsList, selectedOption],
58+
);
7559

7660
const handleSelectRow = (option: ListItemType) => {
7761
onOptionChange(option.value);
7862
onClose();
7963
};
8064

65+
const searchResults = searchOptions(debouncedSearchValue, options);
66+
const headerMessage = debouncedSearchValue.trim() && !searchResults.length ? translate('common.noResultsFound') : '';
67+
8168
return (
8269
<Modal
8370
onClose={onClose}
@@ -97,13 +84,13 @@ function PushRowModal({isVisible, selectedOption, onOptionChange, onClose, optio
9784
onBackButtonPress={onClose}
9885
/>
9986
<SelectionList
100-
headerMessage={searchbarInputText.trim().length && !optionListItems.length ? translate('common.noResultsFound') : ''}
87+
headerMessage={headerMessage}
10188
textInputLabel={searchInputTitle}
102-
textInputValue={searchbarInputText}
103-
onChangeText={filterShownOptions}
89+
textInputValue={searchValue}
90+
onChangeText={setSearchValue}
10491
onSelectRow={handleSelectRow}
105-
sections={[{data: optionListItems}]}
106-
initiallyFocusedOptionKey={optionListItems.find((option) => option.value === selectedOption)?.keyForList}
92+
sections={[{data: searchResults}]}
93+
initiallyFocusedOptionKey={selectedOption}
10794
showScrollIndicator
10895
shouldShowTooltips={false}
10996
ListItem={RadioListItem}

src/components/PushRowWithModal/index.tsx

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ type PushRowWithModalProps = {
88
/** The list of options that we want to display where key is option code and value is option name */
99
optionsList: Record<string, string>;
1010

11-
/** The currently selected option */
12-
selectedOption: string;
11+
/** Current value of the selected item */
12+
value?: string;
1313

14-
/** Function to call when the user selects an option */
15-
onOptionChange: (option: string) => void;
14+
/** Function called whenever list item is selected */
15+
onInputChange?: (value: string, key?: string) => void;
1616

1717
/** Additional styles to apply to container */
1818
wrapperStyles?: StyleProp<ViewStyle>;
@@ -32,13 +32,12 @@ type PushRowWithModalProps = {
3232
/** Text to display on error message */
3333
errorText?: string;
3434

35-
/** Function called whenever option changes */
36-
onInputChange?: (value: string) => void;
35+
/** The ID of the input that should be reset when the value changes */
36+
stateInputIDToReset?: string;
3737
};
3838

3939
function PushRowWithModal({
40-
selectedOption,
41-
onOptionChange,
40+
value,
4241
optionsList,
4342
wrapperStyles,
4443
description,
@@ -47,6 +46,7 @@ function PushRowWithModal({
4746
shouldAllowChange = true,
4847
errorText,
4948
onInputChange = () => {},
49+
stateInputIDToReset,
5050
}: PushRowWithModalProps) {
5151
const [isModalVisible, setIsModalVisible] = useState(false);
5252

@@ -58,16 +58,19 @@ function PushRowWithModal({
5858
setIsModalVisible(true);
5959
};
6060

61-
const handleOptionChange = (value: string) => {
62-
onOptionChange(value);
63-
onInputChange(value);
61+
const handleOptionChange = (optionValue: string) => {
62+
onInputChange(optionValue);
63+
64+
if (stateInputIDToReset) {
65+
onInputChange('', stateInputIDToReset);
66+
}
6467
};
6568

6669
return (
6770
<>
6871
<MenuItemWithTopDescription
6972
description={description}
70-
title={optionsList[selectedOption]}
73+
title={value ? optionsList[value] : ''}
7174
shouldShowRightIcon={shouldAllowChange}
7275
onPress={handleModalOpen}
7376
wrapperStyle={wrapperStyles}
@@ -77,7 +80,7 @@ function PushRowWithModal({
7780
/>
7881
<PushRowModal
7982
isVisible={isModalVisible}
80-
selectedOption={selectedOption}
83+
selectedOption={value ?? ''}
8184
onOptionChange={handleOptionChange}
8285
onClose={handleModalClose}
8386
optionsList={optionsList}

src/components/StatePicker/StateSelectorModal.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import RadioListItem from '@components/SelectionList/RadioListItem';
88
import useDebouncedState from '@hooks/useDebouncedState';
99
import useLocalize from '@hooks/useLocalize';
1010
import useThemeStyles from '@hooks/useThemeStyles';
11-
import searchCountryOptions from '@libs/searchCountryOptions';
12-
import type {CountryData} from '@libs/searchCountryOptions';
11+
import searchOptions from '@libs/searchOptions';
12+
import type {Option} from '@libs/searchOptions';
1313
import StringUtils from '@libs/StringUtils';
1414
import CONST from '@src/CONST';
1515

@@ -29,7 +29,7 @@ type StateSelectorModalProps = {
2929
currentState: string;
3030

3131
/** Function to call when the user selects a state */
32-
onStateSelected: (value: CountryData) => void;
32+
onStateSelected: (value: Option) => void;
3333

3434
/** Function to call when the user presses on the modal backdrop */
3535
onBackdropPress?: () => void;
@@ -56,7 +56,7 @@ function StateSelectorModal({isVisible, currentState, onStateSelected, onClose,
5656
[translate, currentState],
5757
);
5858

59-
const searchResults = searchCountryOptions(debouncedSearchValue, countryStates);
59+
const searchResults = searchOptions(debouncedSearchValue, countryStates);
6060
const headerMessage = debouncedSearchValue.trim() && !searchResults.length ? translate('common.noResultsFound') : '';
6161

6262
const styles = useThemeStyles();

src/components/StatePicker/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import React, {useState} from 'react';
33
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
44
import useLocalize from '@hooks/useLocalize';
55
import Navigation from '@libs/Navigation/Navigation';
6-
import type {CountryData} from '@libs/searchCountryOptions';
6+
import type {Option} from '@libs/searchOptions';
77
import CONST from '@src/CONST';
88
import StateSelectorModal from './StateSelectorModal';
99

@@ -28,7 +28,7 @@ function StatePicker({value, errorText, onInputChange = () => {}}: StatePickerPr
2828
setIsPickerVisible(false);
2929
};
3030

31-
const updateInput = (item: CountryData) => {
31+
const updateInput = (item: Option) => {
3232
onInputChange?.(item.value);
3333
hidePickerModal();
3434
};

0 commit comments

Comments
 (0)