Skip to content

Commit 1cfa2ba

Browse files
committed
push to page menu item
1 parent 82ce35f commit 1cfa2ba

File tree

19 files changed

+573
-154
lines changed

19 files changed

+573
-154
lines changed

src/ROUTES.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,10 @@ export default {
7070
getReportShareCodeRoute: (reportID) => `r/${reportID}/details/shareCode`,
7171
SELECT_YEAR: 'select-year',
7272
getYearSelectionRoute: (minYear, maxYear, currYear, backTo) => `select-year?min=${minYear}&max=${maxYear}&year=${currYear}&backTo=${backTo}`,
73-
73+
SETTINGS_SELECT_COUNTRY: 'select-country',
74+
getCountrySelectionRoute: (countryISO, backTo) => `select-country?countryISO=${countryISO}&backTo=${backTo}`,
75+
SETTINGS_USA_STATES: 'select-usa-states',
76+
getUsaStateSelectionRoute: (stateISO, backTo) => `select-usa-states?stateISO=${stateISO}&backTo=${backTo}`,
7477
/** This is a utility route used to go to the user's concierge chat, or the sign-in page if the user's not authenticated */
7578
CONCIERGE: 'concierge',
7679

src/components/AddressSearch/index.js

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ const propTypes = {
4848
/** A callback function when the value of this field has changed */
4949
onInputChange: PropTypes.func.isRequired,
5050

51+
/** A callback when a new country has changed */
52+
onCountryChange: PropTypes.func,
53+
54+
/** A callback when a new state has changed */
55+
onStateChange: PropTypes.func,
56+
5157
/** Customize the TextInput container */
5258
// eslint-disable-next-line react/forbid-prop-types
5359
containerStyles: PropTypes.arrayOf(PropTypes.object),
@@ -73,6 +79,8 @@ const defaultProps = {
7379
inputID: undefined,
7480
shouldSaveDraft: false,
7581
onBlur: () => {},
82+
onCountryChange: () => {},
83+
onStateChange: () => {},
7684
errorText: '',
7785
hint: '',
7886
value: undefined,
@@ -160,9 +168,18 @@ const AddressSearch = (props) => {
160168
state: state || stateAutoCompleteFallback,
161169
};
162170

171+
const isValidCountryCode = lodashGet(CONST.ALL_COUNTRIES, country);
172+
if (isValidCountryCode) {
173+
values.country = country;
174+
if (props.onCountryChange) {
175+
props.onCountryChange(country);
176+
}
177+
}
178+
163179
// If the address is not in the US, use the full length state name since we're displaying the address's
164180
// state / province in a TextInput instead of in a picker.
165-
if (country !== CONST.COUNTRY.US) {
181+
const isUS = country === CONST.COUNTRY.US;
182+
if (isUS) {
166183
values.state = longStateName;
167184
}
168185

@@ -172,17 +189,16 @@ const AddressSearch = (props) => {
172189
values.state = stateFallback;
173190
}
174191

192+
if (props.onStateChange) {
193+
props.onStateChange(isUS ? state : longStateName, isUS);
194+
}
195+
175196
// Not all pages define the Address Line 2 field, so in that case we append any additional address details
176197
// (e.g. Apt #) to Address Line 1
177198
if (subpremise && typeof props.renamedInputKeys.street2 === 'undefined') {
178199
values.street += `, ${subpremise}`;
179200
}
180201

181-
const isValidCountryCode = lodashGet(CONST.ALL_COUNTRIES, country);
182-
if (isValidCountryCode) {
183-
values.country = country;
184-
}
185-
186202
if (props.inputID) {
187203
_.each(values, (value, key) => {
188204
const inputKey = lodashGet(props.renamedInputKeys, key, key);

src/components/CountryPicker.js

Lines changed: 57 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,79 @@
1-
import _ from 'underscore';
2-
import React, {forwardRef} from 'react';
1+
import React, {useCallback, useRef, useEffect} from 'react';
2+
import {View} from 'react-native';
33
import PropTypes from 'prop-types';
4-
import Picker from './Picker';
4+
import sizes from '../styles/variables';
55
import withLocalize, {withLocalizePropTypes} from './withLocalize';
6+
import MenuItemWithTopDescription from './MenuItemWithTopDescription';
7+
import * as PersonalDetails from '../libs/actions/PersonalDetails';
8+
import Navigation from '../libs/Navigation/Navigation';
9+
import ROUTES from '../ROUTES';
10+
import FormHelpMessage from './FormHelpMessage';
611

712
const propTypes = {
8-
/** The label for the field */
9-
label: PropTypes.string,
13+
/** The ISO code of the country */
14+
countryISO: PropTypes.string,
1015

11-
/** A callback method that is called when the value changes and it receives the selected value as an argument. */
12-
onInputChange: PropTypes.func.isRequired,
16+
/** The ISO selected from CountrySelector */
17+
selectedCountryISO: PropTypes.string,
1318

14-
/** The value that needs to be selected */
15-
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
16-
17-
/** The ID used to uniquely identify the input in a form */
18-
inputID: PropTypes.string,
19-
20-
/** Saves a draft of the input value when used in a form */
21-
shouldSaveDraft: PropTypes.bool,
22-
23-
/** Callback that is called when the text input is blurred */
24-
onBlur: PropTypes.func,
25-
26-
/** Error text to display */
19+
/** Form Error description */
2720
errorText: PropTypes.string,
2821

2922
...withLocalizePropTypes,
3023
};
3124

3225
const defaultProps = {
33-
label: '',
34-
value: undefined,
26+
countryISO: '',
27+
selectedCountryISO: undefined,
3528
errorText: '',
36-
shouldSaveDraft: false,
37-
inputID: undefined,
38-
onBlur: () => {},
3929
};
4030

41-
const CountryPicker = forwardRef((props, ref) => {
42-
const COUNTRIES = _.map(props.translate('allCountries'), (countryName, countryISO) => ({
43-
value: countryISO,
44-
label: countryName,
45-
}));
31+
function BaseCountryPicker(props) {
32+
const countryTitle = useRef({title: '', iso: ''});
33+
const countryISO = props.countryISO;
34+
const selectedCountryISO = props.selectedCountryISO;
35+
const onInputChange = props.onInputChange;
4636

37+
useEffect(() => {
38+
if (!selectedCountryISO || selectedCountryISO === countryTitle.current.iso) {
39+
return;
40+
}
41+
countryTitle.current = {title: PersonalDetails.getCountryNameBy(selectedCountryISO || countryISO), iso: selectedCountryISO || countryISO};
42+
43+
// Needed to call onInputChange, so Form can update the validation and values
44+
onInputChange(countryTitle.current.iso);
45+
}, [countryISO, selectedCountryISO, onInputChange]);
46+
47+
const navigateToCountrySelector = useCallback(() => {
48+
Navigation.navigate(ROUTES.getCountrySelectionRoute(selectedCountryISO || countryISO, Navigation.getActiveRoute()));
49+
}, [countryISO, selectedCountryISO]);
50+
const descStyle = countryTitle.current.title.length === 0 ? {fontSize: sizes.fontSizeNormal} : null;
4751
return (
48-
<Picker
49-
ref={ref}
50-
inputID={props.inputID}
51-
placeholder={{value: '', label: '-'}}
52-
items={COUNTRIES}
53-
onInputChange={props.onInputChange}
54-
value={props.value}
55-
label={props.label || props.translate('common.country')}
56-
errorText={props.errorText}
57-
onBlur={props.onBlur}
58-
shouldSaveDraft={props.shouldSaveDraft}
59-
/>
52+
<View>
53+
<MenuItemWithTopDescription
54+
ref={props.forwardedRef}
55+
shouldShowRightIcon
56+
title={countryTitle.current.title}
57+
descriptionTextStyle={descStyle}
58+
description={props.translate('common.country')}
59+
onPress={navigateToCountrySelector}
60+
/>
61+
<FormHelpMessage message={props.errorText} />
62+
</View>
6063
);
61-
});
64+
}
65+
66+
BaseCountryPicker.propTypes = propTypes;
67+
BaseCountryPicker.defaultProps = defaultProps;
68+
69+
const CountryPicker = React.forwardRef((props, ref) => (
70+
<BaseCountryPicker
71+
// eslint-disable-next-line react/jsx-props-no-spreading
72+
{...props}
73+
forwardedRef={ref}
74+
/>
75+
));
6276

63-
CountryPicker.propTypes = propTypes;
64-
CountryPicker.defaultProps = defaultProps;
6577
CountryPicker.displayName = 'CountryPicker';
6678

6779
export default withLocalize(CountryPicker);

src/components/MenuItemWithTopDescription.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ const propTypes = {
66
...menuItemPropTypes,
77
};
88

9-
const MenuItemWithTopDescription = (props) => (
9+
const MenuItemWithTopDescription = React.forwardRef((props, ref) => (
1010
<MenuItem
1111
// eslint-disable-next-line react/jsx-props-no-spreading
1212
{...props}
13+
ref={ref}
1314
shouldShowBasicTitle
1415
shouldShowDescriptionOnTop
1516
/>
16-
);
17+
));
1718

1819
MenuItemWithTopDescription.propTypes = propTypes;
1920
MenuItemWithTopDescription.displayName = 'MenuItemWithTopDescription';
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import _ from 'underscore';
2+
import React, {useState} from 'react';
3+
import PropTypes from 'prop-types';
4+
import withLocalize, {withLocalizePropTypes} from './withLocalize';
5+
import ScreenWrapper from './ScreenWrapper';
6+
import HeaderWithBackButton from './HeaderWithBackButton';
7+
import * as Expensicons from './Icon/Expensicons';
8+
import themeColors from '../styles/themes/default';
9+
import OptionsSelector from './OptionsSelector';
10+
import styles from '../styles/styles';
11+
12+
const propTypes = {
13+
/** Title of the page */
14+
title: PropTypes.string.isRequired,
15+
16+
/** Function to call when the back button is pressed */
17+
onBackButtonPress: PropTypes.func.isRequired,
18+
19+
/** Text to display in the search input label */
20+
textSearchLabel: PropTypes.string.isRequired,
21+
22+
/** Placeholder text to display in the search input */
23+
placeholder: PropTypes.string.isRequired,
24+
25+
/** Function to call when a row is selected */
26+
onSelectRow: PropTypes.func.isRequired,
27+
28+
/** Initial option to display as selected */
29+
initialOption: PropTypes.string,
30+
31+
data: PropTypes.arrayOf(
32+
PropTypes.shape({
33+
/** Text to display for the option */
34+
text: PropTypes.string.isRequired,
35+
36+
/** Value of the option */
37+
value: PropTypes.string.isRequired,
38+
39+
/** Key to use for the option in the list */
40+
keyForList: PropTypes.string.isRequired,
41+
}),
42+
).isRequired,
43+
44+
...withLocalizePropTypes,
45+
};
46+
47+
const defaultProps = {
48+
initialOption: '',
49+
};
50+
51+
const greenCheckmark = {src: Expensicons.Checkmark, color: themeColors.success};
52+
53+
function filterOptions(searchValue, data) {
54+
const trimmedSearchValue = searchValue.trim();
55+
if (trimmedSearchValue.length === 0) {
56+
return [];
57+
}
58+
59+
return _.filter(data, (country) => country.text.toLowerCase().includes(searchValue.toLowerCase()));
60+
}
61+
62+
function OptionsSelectorWithSearch(props) {
63+
const [searchValue, setSearchValue] = useState('');
64+
const translate = props.translate;
65+
const initialOption = props.initialOption;
66+
67+
const filteredData = filterOptions(searchValue, props.data);
68+
const headerMessage = searchValue.trim() && !filteredData.length ? translate('common.noResultsFound') : '';
69+
70+
return (
71+
<ScreenWrapper includeSafeAreaPaddingBottom={false}>
72+
{({safeAreaPaddingBottomStyle}) => (
73+
<>
74+
<HeaderWithBackButton
75+
title={props.title}
76+
shouldShowBackButton
77+
onBackButtonPress={props.onBackButtonPress}
78+
/>
79+
<OptionsSelector
80+
textInputLabel={props.textSearchLabel}
81+
placeholderText={props.placeholder}
82+
headerMessage={headerMessage}
83+
sections={[{data: filteredData, indexOffset: 0}]}
84+
value={searchValue}
85+
onSelectRow={props.onSelectRow}
86+
onChangeText={setSearchValue}
87+
optionHoveredStyle={styles.hoveredComponentBG}
88+
safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle}
89+
shouldFocusOnSelectRow
90+
shouldHaveOptionSeparator
91+
initiallyFocusedOptionKey={initialOption}
92+
/>
93+
</>
94+
)}
95+
</ScreenWrapper>
96+
);
97+
}
98+
99+
OptionsSelectorWithSearch.propTypes = propTypes;
100+
OptionsSelectorWithSearch.defaultProps = defaultProps;
101+
OptionsSelectorWithSearch.displayName = 'OptionsSelectorWithSearch';
102+
103+
export {greenCheckmark};
104+
105+
export default withLocalize(OptionsSelectorWithSearch);

0 commit comments

Comments
 (0)