diff --git a/src/components/DatePicker/datepickerPropTypes.js b/src/components/DatePicker/datepickerPropTypes.js index 8f3bd03c75e0..a4009cee6ccf 100644 --- a/src/components/DatePicker/datepickerPropTypes.js +++ b/src/components/DatePicker/datepickerPropTypes.js @@ -13,6 +13,9 @@ const propTypes = { */ value: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.string]), + /* Stores the drafted date/ default value to be set for the user. */ + defaultValue: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.string]), + /* Restricts for selectable max date range for the picker */ maximumDate: PropTypes.instanceOf(Date), }; diff --git a/src/components/DatePicker/index.android.js b/src/components/DatePicker/index.android.js index bc6a8ea54222..a4d7938710bd 100644 --- a/src/components/DatePicker/index.android.js +++ b/src/components/DatePicker/index.android.js @@ -4,60 +4,80 @@ import moment from 'moment'; import TextInput from '../TextInput'; import CONST from '../../CONST'; import {propTypes, defaultProps} from './datepickerPropTypes'; +import DateUtils from '../../libs/DateUtils'; class DatePicker extends React.Component { constructor(props) { super(props); - this.state = { isPickerVisible: false, + selectedDate: props.value || props.defaultValue, }; - this.showPicker = this.showPicker.bind(this); - this.raiseDateChange = this.raiseDateChange.bind(this); + this.setDate = this.setDate.bind(this); } - /** - * @param {Event} event - */ - showPicker(event) { - this.setState({isPickerVisible: true}); - event.preventDefault(); + componentDidUpdate() { + const dateValue = DateUtils.getDateAsText(this.props.value); + if (this.props.value === undefined || DateUtils.getDateAsText(this.state.selectedDate) === dateValue) { + return; + } + // eslint-disable-next-line react/no-did-update-set-state + this.setState({selectedDate: this.props.value}); + this.textInput.setNativeProps({text: dateValue}); } /** * @param {Event} event * @param {Date} selectedDate */ - raiseDateChange(event, selectedDate) { + setDate(event, selectedDate) { + this.setState({isPickerVisible: false}); if (event.type === 'set') { this.props.onChange(selectedDate); } + this.setState({selectedDate}); - this.setState({isPickerVisible: false}); + // Updates the value of TextInput on Date Change + this.textInput.setNativeProps({text: DateUtils.getDateAsText(selectedDate)}); } - render() { - const dateAsText = this.props.value ? moment(this.props.value).format(CONST.DATE.MOMENT_FORMAT_STRING) : ''; + /** + * @param {Event} event + */ + showPicker(event) { + this.setState({isPickerVisible: true}); + event.preventDefault(); + } + render() { return ( <> { + this.textInput = el; + if (typeof this.props.forwardRef === 'function') { + this.props.forwardedRef(el); + } + }} + defaultValue={DateUtils.getDateAsText(this.state.selectedDate) || CONST.DATE.MOMENT_FORMAT_STRING} 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} + shouldSaveDraft={this.props.shouldSaveDraft} + isFormInput={this.props.isFormInput} + inputID={this.props.inputID} /> {this.state.isPickerVisible && ( )} @@ -69,4 +89,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 */ + +))); diff --git a/src/components/DatePicker/index.ios.js b/src/components/DatePicker/index.ios.js index 6a1bd98de987..79e8276b6bf5 100644 --- a/src/components/DatePicker/index.ios.js +++ b/src/components/DatePicker/index.ios.js @@ -3,13 +3,14 @@ import React from 'react'; import {Button, View} from 'react-native'; import RNDatePicker from '@react-native-community/datetimepicker'; import moment from 'moment'; +import {CONST} from 'expensify-common/lib/CONST'; import TextInput from '../TextInput'; import withLocalize, {withLocalizePropTypes} from '../withLocalize'; import Popover from '../Popover'; -import CONST from '../../CONST'; import styles from '../../styles/styles'; import themeColors from '../../styles/themes/default'; import {propTypes, defaultProps} from './datepickerPropTypes'; +import DateUtils from '../../libs/DateUtils'; const datepickerPropTypes = { ...propTypes, @@ -19,18 +20,29 @@ const datepickerPropTypes = { class Datepicker extends React.Component { constructor(props) { super(props); - + const value = DateUtils.getDateAsText(props.value) || DateUtils.getDateAsText(props.defaultValue) || CONST.DATE.MOMENT_FORMAT_STRING; this.state = { isPickerVisible: false, selectedDate: props.value ? moment(props.value).toDate() : new Date(), + value, }; - this.showPicker = this.showPicker.bind(this); this.reset = this.reset.bind(this); this.selectDate = this.selectDate.bind(this); this.updateLocalDate = this.updateLocalDate.bind(this); } + componentDidUpdate() { + const dateValue = DateUtils.getDateAsText(this.props.value); + if (this.props.value === undefined || this.state.value === dateValue) { + return; + } + + // eslint-disable-next-line react/no-did-update-set-state + this.setState({value: dateValue}); + this.textInput.setNativeProps({text: dateValue}); + } + /** * @param {Event} event */ @@ -54,6 +66,9 @@ class Datepicker extends React.Component { selectDate() { this.setState({isPickerVisible: false}); this.props.onChange(this.state.selectedDate); + + // Updates the value of TextInput on Date Change + this.textInput.setNativeProps({text: DateUtils.getDateAsText(this.state.selectedDate)}); } /** @@ -65,19 +80,27 @@ class Datepicker extends React.Component { } render() { - const dateAsText = this.props.value ? moment(this.props.value).format(CONST.DATE.MOMENT_FORMAT_STRING) : ''; return ( <> { + this.textInput = el; + if (typeof this.props.forwardRef === 'function') { + this.props.forwardedRef(el); + } + }} 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} + defaultValue={this.state.value} + shouldSaveDraft={this.props.shouldSaveDraft} + isFormInput={this.props.isFormInput} + inputID={this.props.inputID} /> ( + /* eslint-disable-next-line react/jsx-props-no-spreading */ + +))); diff --git a/src/components/DatePicker/index.js b/src/components/DatePicker/index.js index a324c25ae878..986ecc3f3d11 100644 --- a/src/components/DatePicker/index.js +++ b/src/components/DatePicker/index.js @@ -5,6 +5,7 @@ import CONST from '../../CONST'; import {propTypes, defaultProps} from './datepickerPropTypes'; import withWindowDimensions, {windowDimensionsPropTypes} from '../withWindowDimensions'; import canUseTouchScreen from '../../libs/canUseTouchscreen'; +import DateUtils from '../../libs/DateUtils'; import './styles.css'; const datePickerPropTypes = { @@ -16,31 +17,36 @@ 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) - : ''; + const value = DateUtils.getDateAsText(props.value) || DateUtils.getDateAsText(props.defaultValue) || ''; + this.state = {value}; } componentDidMount() { // Adds nice native datepicker on web/desktop. Not possible to set this through props - this.inputRef.setAttribute('type', 'date'); - this.inputRef.classList.add('expensify-datepicker'); + this.textInput.setAttribute('type', 'date'); + this.textInput.classList.add('expensify-datepicker'); if (this.props.maximumDate) { - this.inputRef.setAttribute('max', moment(this.props.maximumDate).format(CONST.DATE.MOMENT_FORMAT_STRING)); + this.textInput.setAttribute('max', moment(this.props.maximumDate).format(CONST.DATE.MOMENT_FORMAT_STRING)); + } + } + + componentDidUpdate() { + const dateValue = DateUtils.getDateAsText(this.props.value); + if (this.props.value === undefined || this.state.value === dateValue) { + return; } + // eslint-disable-next-line react/no-did-update-set-state + this.setState({value: dateValue}); + this.textInput.setNativeProps({text: dateValue}); } /** * 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); return; @@ -58,27 +64,33 @@ class Datepicker extends React.Component { * don't make this very obvious. To avoid confusion we open the datepicker when the user focuses the field */ showDatepicker() { - if (!this.inputRef) { + if (!this.textInput) { return; } - this.inputRef.click(); + this.textInput.click(); } render() { return ( this.inputRef = input} + ref={(el) => { + this.textInput = el; + if (typeof this.props.forwardRef === 'function') { this.props.forwardedRef(el); } + }} onFocus={this.showDatepicker} label={this.props.label} - onChangeText={this.raiseDateChange} - defaultValue={this.defaultValue} + onChangeText={this.setDate} + onBlur={this.props.onBlur} + defaultValue={this.state.value} placeholder={this.props.placeholder} - hasError={this.props.hasError} errorText={this.props.errorText} containerStyles={this.props.containerStyles} disabled={this.props.disabled} + shouldSaveDraft={this.props.shouldSaveDraft} + isFormInput={this.props.isFormInput} + inputID={this.props.inputID} /> ); } @@ -87,4 +99,8 @@ class Datepicker extends React.Component { 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 */ + +))); diff --git a/src/libs/DateUtils.js b/src/libs/DateUtils.js index 791bd9e8c81c..db09e22033f7 100644 --- a/src/libs/DateUtils.js +++ b/src/libs/DateUtils.js @@ -126,6 +126,13 @@ function updateTimezone() { } } +function getDateAsText(date) { + if (!date) { + return date; + } + return moment(date).format(CONST.DATE.MOMENT_FORMAT_STRING); +} + /* * Returns a version of updateTimezone function throttled by 5 minutes */ @@ -135,6 +142,7 @@ const throttledUpdateTimezone = _.throttle(() => updateTimezone(), 1000 * 60 * 5 * @namespace DateUtils */ const DateUtils = { + getDateAsText, timestampToRelative, timestampToDateTime, startCurrentDateUpdater, diff --git a/src/stories/Datepicker.stories.js b/src/stories/Datepicker.stories.js index 0fb3cd31c2b6..3a043ca46c64 100644 --- a/src/stories/Datepicker.stories.js +++ b/src/stories/Datepicker.stories.js @@ -13,11 +13,9 @@ export default { onChange: {action: 'date changed'}, }, args: { - value: '', label: 'Select Date', placeholder: 'Date Placeholder', errorText: '', - hasError: false, }, }; diff --git a/src/stories/Form.stories.js b/src/stories/Form.stories.js index 7de80c6192af..6de226999fa7 100644 --- a/src/stories/Form.stories.js +++ b/src/stories/Form.stories.js @@ -5,6 +5,7 @@ import AddressSearch from '../components/AddressSearch'; import Form from '../components/Form'; import * as FormActions from '../libs/actions/FormActions'; import styles from '../styles/styles'; +import DatePicker from '../components/DatePicker'; import CheckboxWithLabel from '../components/CheckboxWithLabel'; import Text from '../components/Text'; @@ -16,7 +17,10 @@ import Text from '../components/Text'; const story = { title: 'Components/Form', component: Form, - subcomponents: {TextInput, AddressSearch, CheckboxWithLabel}, + subcomponents: { + TextInput, DatePicker, AddressSearch, CheckboxWithLabel, + }, + }; const Template = (args) => { @@ -44,6 +48,13 @@ const Template = (args) => { containerStyles={[styles.mt4]} isFormInput /> +