Skip to content

Commit

Permalink
Merge pull request #51667 from callstack-internal/feat/step-3-ui
Browse files Browse the repository at this point in the history
feat: Step 3 UI
  • Loading branch information
madmax330 authored Nov 7, 2024
2 parents 7a605ec + ce274cb commit 2ef91b8
Show file tree
Hide file tree
Showing 35 changed files with 1,609 additions and 138 deletions.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@
"date-fns-tz": "^3.2.0",
"dom-serializer": "^0.2.2",
"domhandler": "^4.3.0",
"expensify-common": "2.0.103",
"expensify-common": "2.0.106",
"expo": "51.0.31",
"expo-av": "14.0.7",
"expo-image": "1.12.15",
Expand Down
4 changes: 3 additions & 1 deletion src/components/AddressSearch/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ function AddressSearch(
onBlur,
onInputChange,
onPress,
onCountryChange,
predefinedPlaces = [],
preferredLocale,
renamedInputKeys = {
Expand Down Expand Up @@ -195,7 +196,7 @@ function AddressSearch(

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

Expand Down Expand Up @@ -244,6 +245,7 @@ function AddressSearch(
onInputChange?.(values);
}

onCountryChange?.(values.country);
onPress?.(values);
};

Expand Down
3 changes: 3 additions & 0 deletions src/components/AddressSearch/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ type AddressSearchProps = {

/** The user's preferred locale e.g. 'en', 'es-ES' */
preferredLocale?: Locale;

/** Callback to be called when the country is changed */
onCountryChange?: (country: unknown) => void;
};

type IsCurrentTargetInsideContainerType = (event: FocusEvent | NativeSyntheticEvent<TextInputFocusEventData>, containerRef: RefObject<View | HTMLElement>) => boolean;
Expand Down
8 changes: 4 additions & 4 deletions src/components/CountryPicker/CountrySelectorModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import RadioListItem from '@components/SelectionList/RadioListItem';
import useDebouncedState from '@hooks/useDebouncedState';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import searchCountryOptions from '@libs/searchCountryOptions';
import type {CountryData} from '@libs/searchCountryOptions';
import searchOptions from '@libs/searchOptions';
import type {Option} from '@libs/searchOptions';
import StringUtils from '@libs/StringUtils';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
Expand All @@ -27,7 +27,7 @@ type CountrySelectorModalProps = {
currentCountry: string;

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

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

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

const styles = useThemeStyles();
Expand Down
4 changes: 2 additions & 2 deletions src/components/CountryPicker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, {useState} from 'react';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import useLocalize from '@hooks/useLocalize';
import Navigation from '@libs/Navigation/Navigation';
import type {CountryData} from '@libs/searchCountryOptions';
import type {Option} from '@libs/searchOptions';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import CountrySelectorModal from './CountrySelectorModal';
Expand All @@ -26,7 +26,7 @@ function CountryPicker({value, errorText, onInputChange = () => {}}: CountryPick
setIsPickerVisible(false);
};

const updateInput = (item: CountryData) => {
const updateInput = (item: Option) => {
onInputChange?.(item.value);
hidePickerModal();
};
Expand Down
12 changes: 12 additions & 0 deletions src/components/Form/FormProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import CONST from '@src/CONST';
import type {OnyxFormDraftKey, OnyxFormKey} from '@src/ONYXKEYS';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Form} from '@src/types/form';
import type {Errors} from '@src/types/onyx/OnyxCommon';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type {RegisterInput} from './FormContext';
import FormContext from './FormContext';
Expand Down Expand Up @@ -244,9 +245,20 @@ function FormProvider(
setErrors({});
}, [formID]);

const resetFormFieldError = useCallback(
(inputID: keyof Form) => {
const newErrors = {...errors};
delete newErrors[inputID];
FormActions.setErrors(formID, newErrors as Errors);
setErrors(newErrors);
},
[errors, formID],
);

useImperativeHandle(forwardedRef, () => ({
resetForm,
resetErrors,
resetFormFieldError,
}));

const registerInput = useCallback<RegisterInput>(
Expand Down
2 changes: 2 additions & 0 deletions src/components/Form/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import type NetSuiteCustomListPicker from '@pages/workspace/accounting/netsuite/
import type NetSuiteMenuWithTopDescriptionForm from '@pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteMenuWithTopDescriptionForm';
import type {Country} from '@src/CONST';
import type {OnyxFormKey, OnyxValues} from '@src/ONYXKEYS';
import type {Form} from '@src/types/form';
import type {BaseForm} from '@src/types/form/Form';

/**
Expand Down Expand Up @@ -164,6 +165,7 @@ type FormProps<TFormID extends OnyxFormKey = OnyxFormKey> = {
type FormRef<TFormID extends OnyxFormKey = OnyxFormKey> = {
resetForm: (optionalValue: FormOnyxValues<TFormID>) => void;
resetErrors: () => void;
resetFormFieldError: (fieldID: keyof Form) => void;
};

type InputRefs = Record<string, MutableRefObject<InputComponentBaseProps>>;
Expand Down
61 changes: 24 additions & 37 deletions src/components/PushRowWithModal/PushRowModal.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import React, {useEffect, useState} from 'react';
import React, {useMemo} from 'react';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import Modal from '@components/Modal';
import ScreenWrapper from '@components/ScreenWrapper';
import SelectionList from '@components/SelectionList';
import RadioListItem from '@components/SelectionList/RadioListItem';
import useDebouncedState from '@hooks/useDebouncedState';
import useLocalize from '@hooks/useLocalize';
import searchOptions from '@libs/searchOptions';
import StringUtils from '@libs/StringUtils';
import CONST from '@src/CONST';

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

const allOptions = Object.entries(optionsList).map(([key, value]) => ({
value: key,
text: value,
keyForList: key,
isSelected: key === selectedOption,
}));
const [searchbarInputText, setSearchbarInputText] = useState('');
const [optionListItems, setOptionListItems] = useState(allOptions);

useEffect(() => {
setOptionListItems((prevOptionListItems) =>
prevOptionListItems.map((option) => ({
...option,
isSelected: option.value === selectedOption,
const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState('');

const options = useMemo(
() =>
Object.entries(optionsList).map(([key, value]) => ({
value: key,
text: value,
keyForList: key,
isSelected: key === selectedOption,
searchValue: StringUtils.sanitizeString(value),
})),
);
}, [selectedOption]);

const filterShownOptions = (searchText: string) => {
setSearchbarInputText(searchText);
const searchWords = searchText.toLowerCase().match(/[a-z0-9]+/g) ?? [];
setOptionListItems(
allOptions.filter((option) =>
searchWords.every((word) =>
option.text
.toLowerCase()
.replace(/[^a-z0-9]/g, ' ')
.includes(word),
),
),
);
};
[optionsList, selectedOption],
);

const handleSelectRow = (option: ListItemType) => {
onOptionChange(option.value);
onClose();
};

const searchResults = searchOptions(debouncedSearchValue, options);
const headerMessage = debouncedSearchValue.trim() && !searchResults.length ? translate('common.noResultsFound') : '';

return (
<Modal
onClose={onClose}
Expand All @@ -97,13 +84,13 @@ function PushRowModal({isVisible, selectedOption, onOptionChange, onClose, optio
onBackButtonPress={onClose}
/>
<SelectionList
headerMessage={searchbarInputText.trim().length && !optionListItems.length ? translate('common.noResultsFound') : ''}
headerMessage={headerMessage}
textInputLabel={searchInputTitle}
textInputValue={searchbarInputText}
onChangeText={filterShownOptions}
textInputValue={searchValue}
onChangeText={setSearchValue}
onSelectRow={handleSelectRow}
sections={[{data: optionListItems}]}
initiallyFocusedOptionKey={optionListItems.find((option) => option.value === selectedOption)?.keyForList}
sections={[{data: searchResults}]}
initiallyFocusedOptionKey={selectedOption}
showScrollIndicator
shouldShowTooltips={false}
ListItem={RadioListItem}
Expand Down
29 changes: 16 additions & 13 deletions src/components/PushRowWithModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ type PushRowWithModalProps = {
/** The list of options that we want to display where key is option code and value is option name */
optionsList: Record<string, string>;

/** The currently selected option */
selectedOption: string;
/** Current value of the selected item */
value?: string;

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

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

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

function PushRowWithModal({
selectedOption,
onOptionChange,
value,
optionsList,
wrapperStyles,
description,
Expand All @@ -47,6 +46,7 @@ function PushRowWithModal({
shouldAllowChange = true,
errorText,
onInputChange = () => {},
stateInputIDToReset,
}: PushRowWithModalProps) {
const [isModalVisible, setIsModalVisible] = useState(false);

Expand All @@ -58,16 +58,19 @@ function PushRowWithModal({
setIsModalVisible(true);
};

const handleOptionChange = (value: string) => {
onOptionChange(value);
onInputChange(value);
const handleOptionChange = (optionValue: string) => {
onInputChange(optionValue);

if (stateInputIDToReset) {
onInputChange('', stateInputIDToReset);
}
};

return (
<>
<MenuItemWithTopDescription
description={description}
title={optionsList[selectedOption]}
title={value ? optionsList[value] : ''}
shouldShowRightIcon={shouldAllowChange}
onPress={handleModalOpen}
wrapperStyle={wrapperStyles}
Expand All @@ -77,7 +80,7 @@ function PushRowWithModal({
/>
<PushRowModal
isVisible={isModalVisible}
selectedOption={selectedOption}
selectedOption={value ?? ''}
onOptionChange={handleOptionChange}
onClose={handleModalClose}
optionsList={optionsList}
Expand Down
8 changes: 4 additions & 4 deletions src/components/StatePicker/StateSelectorModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import RadioListItem from '@components/SelectionList/RadioListItem';
import useDebouncedState from '@hooks/useDebouncedState';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import searchCountryOptions from '@libs/searchCountryOptions';
import type {CountryData} from '@libs/searchCountryOptions';
import searchOptions from '@libs/searchOptions';
import type {Option} from '@libs/searchOptions';
import StringUtils from '@libs/StringUtils';
import CONST from '@src/CONST';

Expand All @@ -29,7 +29,7 @@ type StateSelectorModalProps = {
currentState: string;

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

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

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

const styles = useThemeStyles();
Expand Down
4 changes: 2 additions & 2 deletions src/components/StatePicker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React, {useState} from 'react';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import useLocalize from '@hooks/useLocalize';
import Navigation from '@libs/Navigation/Navigation';
import type {CountryData} from '@libs/searchCountryOptions';
import type {Option} from '@libs/searchOptions';
import CONST from '@src/CONST';
import StateSelectorModal from './StateSelectorModal';

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

const updateInput = (item: CountryData) => {
const updateInput = (item: Option) => {
onInputChange?.(item.value);
hidePickerModal();
};
Expand Down
Loading

0 comments on commit 2ef91b8

Please sign in to comment.