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 date picker to be compatible with form #7627

Closed
wants to merge 56 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
c3e298b
modified datepicker props
sig5 Feb 4, 2022
6cf0b80
remove hasError usages
sig5 Feb 4, 2022
f4ef1eb
added ref to ios and android
sig5 Feb 4, 2022
37c8733
pivoted values to defaultValue for Form Compatiblity
sig5 Feb 4, 2022
6020b72
Passed onBlur down to TextInput.
sig5 Feb 4, 2022
0bcf38e
Passed onBlur down to TextInput.
sig5 Feb 4, 2022
e20c7da
adjusted styling of error messages
sig5 Feb 4, 2022
82f8c87
Added prop to handle inline errors
sig5 Feb 4, 2022
906d7f4
Added default value of error text
sig5 Feb 5, 2022
345f711
Added value handling in desktop
sig5 Feb 7, 2022
78d1897
Added value handling in desktop
sig5 Feb 7, 2022
9b6f0f9
passed down new props
sig5 Feb 7, 2022
19f4a6b
fixed desktop bugs
sig5 Feb 8, 2022
8d3d32c
fixed desktop bugs
sig5 Feb 8, 2022
0021a62
removed duplicate proptypes
sig5 Feb 8, 2022
77c9d1e
fixed issues
sig5 Feb 8, 2022
d9b4a82
conditional styling
sig5 Feb 8, 2022
bd46454
Fixed web bug
sig5 Feb 8, 2022
9a7e686
remove old comment
sig5 Feb 8, 2022
beaf471
renamed additionalstyle
sig5 Feb 8, 2022
5e16990
Applied requested changes
sig5 Feb 8, 2022
faefd21
removed extra newline
sig5 Feb 8, 2022
6e4f5b7
renamed name to el
sig5 Feb 8, 2022
5653c91
fixed ref forwarding
sig5 Feb 8, 2022
cef12cc
reformatted
sig5 Feb 9, 2022
895b204
removing value prop completely
sig5 Mar 1, 2022
e048d09
removed value prop completely.
sig5 Mar 1, 2022
b40bc70
removing value from android datepicker
sig5 Mar 2, 2022
b5c8e8c
merged main
sig5 Mar 2, 2022
75df8d5
Added defaultValue support in datePicker/BaseTextInput
sig5 Mar 2, 2022
74ccc9e
Removed value prop
sig5 Mar 2, 2022
c33917c
remove extraline
sig5 Mar 2, 2022
304c08d
remove value from ios
sig5 Mar 8, 2022
d833d3d
removing value prop
sig5 Mar 8, 2022
e06dba3
removing value prop
sig5 Mar 8, 2022
12af0d6
added placeholder in ios
sig5 Mar 8, 2022
f9e0147
Adding ios changes
sig5 Mar 12, 2022
6115c67
merged main
sig5 Mar 13, 2022
0f711eb
removed value prop from desktop
sig5 Mar 13, 2022
d50f8ab
removed value prop from ios
sig5 Mar 13, 2022
a01b40e
removed value prop from ios
sig5 Mar 13, 2022
e95bd28
removed value prop from ios
sig5 Mar 13, 2022
6b53090
removed value prop from ios
sig5 Mar 13, 2022
0b886e8
removed value prop from ios
sig5 Mar 13, 2022
4959d11
Removed value prop from android
sig5 Mar 14, 2022
83dfe08
Removed value prop
sig5 Mar 15, 2022
3a2153d
remove this.defaultValue
sig5 Mar 15, 2022
fd0f219
fixed componentdidUpdate
sig5 Mar 15, 2022
b623066
rename raiseDateChange
sig5 Mar 15, 2022
1fa8dcc
fix lint errors
sig5 Mar 15, 2022
87ee30a
fixed bind
sig5 Mar 16, 2022
a386321
Merge branch 'Expensify:main' into refactorDatePicker
sig5 Mar 29, 2022
f266233
addressed comments
sig5 Mar 29, 2022
47d7542
fixed android state
sig5 Apr 1, 2022
f38e10d
fixed linting
sig5 Apr 1, 2022
92a58da
Merge remote-tracking branch 'origin/main' into refactorDatePicker
sig5 Apr 1, 2022
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
3 changes: 3 additions & 0 deletions src/components/DatePicker/datepickerPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};
Expand Down
59 changes: 41 additions & 18 deletions src/components/DatePicker/index.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link
Contributor

Choose a reason for hiding this comment

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

we renamed onChange to onInputChange passed down by the form, so we should update it accordingly within DatePicker and in all usages of DatePicker

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure thing! I will update it.

}
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 (
<>
<TextInput
label={this.props.label}
value={dateAsText}
ref={(el) => {
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 && (
<RNDatePicker
value={this.props.value ? moment(this.props.value).toDate() : new Date()}
value={this.state.selectedDate ? moment(this.state.selectedDate).toDate() : new Date()}
mode="date"
onChange={this.raiseDateChange}
onChange={this.setDate}
maximumDate={this.props.maximumDate}
/>
)}
Expand All @@ -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 */
<DatePicker {...props} forwardedRef={ref} />
)));
41 changes: 34 additions & 7 deletions src/components/DatePicker/index.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
*/
Expand All @@ -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)});
}

/**
Expand All @@ -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 (
<>
<TextInput
label={this.props.label}
value={dateAsText}
ref={(el) => {
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}
/>
<Popover
isVisible={this.state.isPickerVisible}
Expand Down Expand Up @@ -125,4 +148,8 @@ Datepicker.defaultProps = defaultProps;
* 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} forwardedRef={ref} />
)));
54 changes: 35 additions & 19 deletions src/components/DatePicker/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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;
Expand All @@ -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 (
<TextInput
forceActiveLabel={!canUseTouchScreen()}
ref={input => 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}
/>
);
}
Expand All @@ -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 */
<Datepicker {...props} forwardedRef={ref} />
)));
8 changes: 8 additions & 0 deletions src/libs/DateUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -135,6 +142,7 @@ const throttledUpdateTimezone = _.throttle(() => updateTimezone(), 1000 * 60 * 5
* @namespace DateUtils
*/
const DateUtils = {
getDateAsText,
timestampToRelative,
timestampToDateTime,
startCurrentDateUpdater,
Expand Down
2 changes: 0 additions & 2 deletions src/stories/Datepicker.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,9 @@ export default {
onChange: {action: 'date changed'},
},
args: {
value: '',
label: 'Select Date',
placeholder: 'Date Placeholder',
errorText: '',
hasError: false,
},
};

Expand Down
Loading