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

Refactor Datepicker to work with Form.js #8770

Merged
merged 21 commits into from
Apr 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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();
}
Copy link
Contributor Author

@luacmartins luacmartins Apr 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was a lint warning saying that showPicker should come after setDate, that's why I moved this.


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;
}
luacmartins marked this conversation as resolved.
Show resolved Hide resolved
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;
}
luacmartins marked this conversation as resolved.
Show resolved Hide resolved
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