Skip to content

Commit

Permalink
Merge pull request #8770 from Expensify/cmartins-detaPickerRefactor
Browse files Browse the repository at this point in the history
Refactor Datepicker to work with Form.js
  • Loading branch information
Luke9389 authored Apr 27, 2022
2 parents a8dbb3c + 0425aeb commit ffe26f9
Show file tree
Hide file tree
Showing 9 changed files with 97 additions and 49 deletions.
8 changes: 7 additions & 1 deletion src/components/DatePicker/datepickerPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,16 @@ const propTypes = {

/**
* The datepicker supports any value that `moment` can parse.
* `onChange` would always be called with a Date (or null)
* `onInputChange` would always be called with a Date (or null)
*/
value: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.string]),

/**
* The datepicker supports any defaultValue that `moment` can parse.
* `onInputChange` would always be called with a Date (or null)
*/
defaultValue: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.string]),

/* Restricts for selectable max date range for the picker */
maximumDate: PropTypes.instanceOf(Date),
};
Expand Down
42 changes: 26 additions & 16 deletions src/components/DatePicker/index.android.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import RNDatePicker from '@react-native-community/datetimepicker';
import moment from 'moment';
import _ from 'underscore';
import TextInput from '../TextInput';
import CONST from '../../CONST';
import {propTypes, defaultProps} from './datepickerPropTypes';
Expand All @@ -14,50 +15,56 @@ class DatePicker extends React.Component {
};

this.showPicker = this.showPicker.bind(this);
this.raiseDateChange = this.raiseDateChange.bind(this);
}

/**
* @param {Event} event
*/
showPicker(event) {
this.setState({isPickerVisible: true});
event.preventDefault();
this.setDate = this.setDate.bind(this);
}

/**
* @param {Event} event
* @param {Date} selectedDate
*/
raiseDateChange(event, selectedDate) {
setDate(event, selectedDate) {
if (event.type === 'set') {
this.props.onChange(selectedDate);
this.props.onInputChange(selectedDate);
}

this.setState({isPickerVisible: false});
}

/**
* @param {Event} event
*/
showPicker(event) {
this.setState({isPickerVisible: true});
event.preventDefault();
}

render() {
const dateAsText = this.props.value ? moment(this.props.value).format(CONST.DATE.MOMENT_FORMAT_STRING) : '';
const dateAsText = this.props.defaultValue ? moment(this.props.defaultValue).format(CONST.DATE.MOMENT_FORMAT_STRING) : '';

return (
<>
<TextInput
label={this.props.label}
value={dateAsText}
placeholder={this.props.placeholder}
hasError={this.props.hasError}
errorText={this.props.errorText}
containerStyles={this.props.containerStyles}
onPress={this.showPicker}
editable={false}
disabled={this.props.disabled}
onBlur={this.props.onBlur}
ref={(el) => {
if (!_.isFunction(this.props.innerRef)) {
return;
}
this.props.innerRef(el);
}}
/>
{this.state.isPickerVisible && (
<RNDatePicker
value={this.props.value ? moment(this.props.value).toDate() : new Date()}
value={this.props.defaultValue ? moment(this.props.defaultValue).toDate() : new Date()}
mode="date"
onChange={this.raiseDateChange}
onChange={this.setDate}
maximumDate={this.props.maximumDate}
/>
)}
Expand All @@ -69,4 +76,7 @@ class DatePicker extends React.Component {
DatePicker.propTypes = propTypes;
DatePicker.defaultProps = defaultProps;

export default DatePicker;
export default React.forwardRef((props, ref) => (
/* eslint-disable-next-line react/jsx-props-no-spreading */
<DatePicker {...props} innerRef={ref} />
));
28 changes: 19 additions & 9 deletions src/components/DatePicker/index.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React from 'react';
import {Button, View} from 'react-native';
import RNDatePicker from '@react-native-community/datetimepicker';
import moment from 'moment';
import _ from 'underscore';
import TextInput from '../TextInput';
import withLocalize, {withLocalizePropTypes} from '../withLocalize';
import Popover from '../Popover';
Expand All @@ -16,13 +17,13 @@ const datepickerPropTypes = {
...withLocalizePropTypes,
};

class Datepicker extends React.Component {
class DatePicker extends React.Component {
constructor(props) {
super(props);

this.state = {
isPickerVisible: false,
selectedDate: props.value ? moment(props.value).toDate() : new Date(),
selectedDate: props.defaultValue ? moment(props.defaultValue).toDate() : new Date(),
};

this.showPicker = this.showPicker.bind(this);
Expand All @@ -49,11 +50,11 @@ class Datepicker extends React.Component {

/**
* Accept the current spinner changes, close the spinner and propagate the change
* to the parent component (props.onChange)
* to the parent component (props.onInputChange)
*/
selectDate() {
this.setState({isPickerVisible: false});
this.props.onChange(this.state.selectedDate);
this.props.onInputChange(this.state.selectedDate);
}

/**
Expand All @@ -65,19 +66,25 @@ class Datepicker extends React.Component {
}

render() {
const dateAsText = this.props.value ? moment(this.props.value).format(CONST.DATE.MOMENT_FORMAT_STRING) : '';
const dateAsText = this.props.defaultValue ? moment(this.props.defaultValue).format(CONST.DATE.MOMENT_FORMAT_STRING) : '';
return (
<>
<TextInput
label={this.props.label}
value={dateAsText}
placeholder={this.props.placeholder}
hasError={this.props.hasError}
errorText={this.props.errorText}
containerStyles={this.props.containerStyles}
onPress={this.showPicker}
editable={false}
disabled={this.props.disabled}
onBlur={this.props.onBlur}
ref={(el) => {
if (!_.isFunction(this.props.innerRef)) {
return;
}
this.props.innerRef(el);
}}
/>
<Popover
isVisible={this.state.isPickerVisible}
Expand Down Expand Up @@ -116,13 +123,16 @@ class Datepicker extends React.Component {
}
}

Datepicker.propTypes = datepickerPropTypes;
Datepicker.defaultProps = defaultProps;
DatePicker.propTypes = datepickerPropTypes;
DatePicker.defaultProps = defaultProps;

/**
* We're applying localization here because we present a modal (with buttons) ourselves
* Furthermore we're passing the locale down so that the modal and the date spinner are in the same
* locale. Otherwise the spinner would be present in the system locale and it would be weird if it happens
* that the modal buttons are in one locale (app) while the (spinner) month names are another (system)
*/
export default withLocalize(Datepicker);
export default withLocalize(React.forwardRef((props, ref) => (
/* eslint-disable-next-line react/jsx-props-no-spreading */
<DatePicker {...props} innerRef={ref} />
)));
36 changes: 23 additions & 13 deletions src/components/DatePicker/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import moment from 'moment';
import _ from 'underscore';
import TextInput from '../TextInput';
import CONST from '../../CONST';
import {propTypes, defaultProps} from './datepickerPropTypes';
Expand All @@ -12,18 +13,18 @@ const datePickerPropTypes = {
...windowDimensionsPropTypes,
};

class Datepicker extends React.Component {
class DatePicker extends React.Component {
constructor(props) {
super(props);

this.raiseDateChange = this.raiseDateChange.bind(this);
this.setDate = this.setDate.bind(this);
this.showDatepicker = this.showDatepicker.bind(this);

/* We're using uncontrolled input otherwise it wont be possible to
* raise change events with a date value - each change will produce a date
* and make us reset the text input */
this.defaultValue = props.value
? moment(props.value).format(CONST.DATE.MOMENT_FORMAT_STRING)
this.defaultValue = props.defaultValue
? moment(props.defaultValue).format(CONST.DATE.MOMENT_FORMAT_STRING)
: '';
}

Expand All @@ -40,15 +41,15 @@ class Datepicker extends React.Component {
* Trigger the `onChange` handler when the user input has a complete date or is cleared
* @param {String} text
*/
raiseDateChange(text) {
setDate(text) {
if (!text) {
this.props.onChange(null);
this.props.onInputChange(null);
return;
}

const asMoment = moment(text);
if (asMoment.isValid()) {
this.props.onChange(asMoment.format(CONST.DATE.MOMENT_FORMAT_STRING));
this.props.onInputChange(asMoment.format(CONST.DATE.MOMENT_FORMAT_STRING));
}
}

Expand All @@ -69,22 +70,31 @@ class Datepicker extends React.Component {
return (
<TextInput
forceActiveLabel={!canUseTouchScreen()}
ref={input => this.inputRef = input}
ref={(el) => {
this.inputRef = el;

if (_.isFunction(this.props.innerRef)) {
this.props.innerRef(el);
}
}}
onFocus={this.showDatepicker}
label={this.props.label}
onChangeText={this.raiseDateChange}
onInputChange={this.setDate}
defaultValue={this.defaultValue}
placeholder={this.props.placeholder}
hasError={this.props.hasError}
errorText={this.props.errorText}
containerStyles={this.props.containerStyles}
disabled={this.props.disabled}
onBlur={this.props.onBlur}
/>
);
}
}

Datepicker.propTypes = datePickerPropTypes;
Datepicker.defaultProps = defaultProps;
DatePicker.propTypes = datePickerPropTypes;
DatePicker.defaultProps = defaultProps;

export default withWindowDimensions(Datepicker);
export default withWindowDimensions(React.forwardRef((props, ref) => (
/* eslint-disable-next-line react/jsx-props-no-spreading */
<DatePicker {...props} innerRef={ref} />
)));
4 changes: 2 additions & 2 deletions src/pages/EnablePayments/AdditionalDetailsStep.js
Original file line number Diff line number Diff line change
Expand Up @@ -320,8 +320,8 @@ class AdditionalDetailsStep extends React.Component {
<DatePicker
containerStyles={[styles.mt4]}
label={this.props.translate(this.fieldNameTranslationKeys.dob)}
onChange={val => this.clearErrorAndSetValue('dob', val)}
value={this.props.walletAdditionalDetailsDraft.dob || ''}
onInputChange={val => this.clearErrorAndSetValue('dob', val)}
defaultValue={this.props.walletAdditionalDetailsDraft.dob || ''}
placeholder={this.props.translate('common.dob')}
errorText={this.getErrorText('dob')}
maximumDate={new Date()}
Expand Down
7 changes: 3 additions & 4 deletions src/pages/ReimbursementAccount/CompanyStep.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,7 @@ class CompanyStep extends React.Component {
* @param {String} value
*/
clearDateErrorsAndSetValue(value) {
this.clearError('incorporationDate');
this.clearError('incorporationDateFuture');
this.clearErrors(['incorporationDate', 'incorporationDateFuture']);
this.setValue({incorporationDate: value});
}

Expand Down Expand Up @@ -277,8 +276,8 @@ class CompanyStep extends React.Component {
<View style={styles.mt4}>
<DatePicker
label={this.props.translate('companyStep.incorporationDate')}
onChange={this.clearDateErrorsAndSetValue}
value={this.state.incorporationDate}
onInputChange={this.clearDateErrorsAndSetValue}
defaultValue={this.state.incorporationDate}
placeholder={this.props.translate('companyStep.incorporationDatePlaceholder')}
errorText={this.getErrorText('incorporationDate') || this.getErrorText('incorporationDateFuture')}
maximumDate={new Date()}
Expand Down
4 changes: 2 additions & 2 deletions src/pages/ReimbursementAccount/IdentityForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ const IdentityForm = (props) => {
label={`${props.translate('common.dob')}`}
containerStyles={[styles.mt4]}
placeholder={props.translate('common.dateFormat')}
value={props.values.dob}
onChange={value => props.onFieldChange({dob: value})}
defaultValue={props.values.dob}
onInputChange={value => props.onFieldChange({dob: value})}
errorText={dobErrorText}
maximumDate={new Date()}
/>
Expand Down
4 changes: 2 additions & 2 deletions src/stories/Datepicker.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default {
onChange: {action: 'date changed'},
},
args: {
value: '',
defaultValue: '',
label: 'Select Date',
placeholder: 'Date Placeholder',
errorText: '',
Expand All @@ -34,7 +34,7 @@ Default.args = {

PreFilled.args = {
label: 'Select Date',
value: new Date(2018, 7, 21),
defaultValue: new Date(2018, 7, 21),
};

export {
Expand Down
13 changes: 13 additions & 0 deletions src/stories/Form.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {View} from 'react-native';
import TextInput from '../components/TextInput';
import Picker from '../components/Picker';
import AddressSearch from '../components/AddressSearch';
import DatePicker from '../components/DatePicker';
import Form from '../components/Form';
import * as FormActions from '../libs/actions/FormActions';
import styles from '../styles/styles';
Expand All @@ -22,6 +23,7 @@ const story = {
AddressSearch,
CheckboxWithLabel,
Picker,
DatePicker,
},
};

Expand Down Expand Up @@ -54,6 +56,12 @@ const Template = (args) => {
containerStyles={[styles.mt4]}
isFormInput
/>
<DatePicker
label="Date of birth"
inputID="dob"
containerStyles={[styles.mt4]}
isFormInput
/>
<View>
<Picker
label="Fruit"
Expand Down Expand Up @@ -158,6 +166,9 @@ const defaultArgs = {
if (!values.accountNumber) {
errors.accountNumber = 'Please enter an account number';
}
if (!values.dob) {
errors.dob = 'Please enter your date of birth';
}
if (!values.pickFruit) {
errors.pickFruit = 'Please select a fruit';
}
Expand All @@ -182,6 +193,7 @@ const defaultArgs = {
draftValues: {
routingNumber: '00001',
accountNumber: '1111222233331111',
dob: '1990-01-01',
pickFruit: 'orange',
pickAnotherFruit: 'apple',
checkbox: false,
Expand All @@ -197,6 +209,7 @@ InputError.args = {
routingNumber: '',
accountNumber: '',
pickFruit: '',
dob: '',
pickAnotherFruit: '',
checkbox: false,
},
Expand Down

0 comments on commit ffe26f9

Please sign in to comment.