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

Personal details - DOB field updating once year or month has been changed #18577

Merged
merged 16 commits into from
May 26, 2023
Merged
1 change: 1 addition & 0 deletions contributingGuides/FORMS.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ Form.js will automatically provide the following props to any input with the inp
- value: The input value.
- errorText: The translated error text that is returned by validate for that specific input.
- onBlur: An onBlur handler that calls validate.
- onTouched: An onTouched handler that marks the input as touched.
- onInputChange: An onChange handler that saves draft values and calls validate for that input (inputA). Passing an inputID as a second param allows inputA to manipulate the input value of the provided inputID (inputB).

## Dynamic Form Inputs
Expand Down
4 changes: 2 additions & 2 deletions src/components/CalendarPicker/calendarPickerPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import moment from 'moment';
import CONST from '../../CONST';

const propTypes = {
/** An initial value of date */
value: PropTypes.objectOf(Date),
/** An initial value of date string */
value: PropTypes.string,

/** A minimum date (oldest) allowed to select */
minDate: PropTypes.objectOf(Date),
Expand Down
119 changes: 112 additions & 7 deletions src/components/CalendarPicker/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ class CalendarPicker extends React.PureComponent {
constructor(props) {
super(props);

let currentDateView = props.value;
let currentSelection = moment(props.value, CONST.DATE.MOMENT_FORMAT_STRING);
let currentDateView = currentSelection.toDate();

if (props.selectedYear) {
currentDateView = moment(currentDateView).set('year', props.selectedYear).toDate();
}
Expand All @@ -28,12 +30,17 @@ class CalendarPicker extends React.PureComponent {
}
if (props.maxDate < currentDateView) {
currentDateView = props.maxDate;
currentSelection = moment(props.maxDate);
} else if (props.minDate > currentDateView) {
currentDateView = props.minDate;
currentSelection = moment(props.minDate);
}

this.state = {
currentDateView,
selectedYear: currentSelection.get('year').toString(),
selectedMonth: this.getNumberStringWithLeadingZero(currentSelection.get('month') + 1),
selectedDay: this.getNumberStringWithLeadingZero(currentSelection.get('date')),
};

this.moveToPrevMonth = this.moveToPrevMonth.bind(this);
Expand All @@ -56,7 +63,19 @@ class CalendarPicker extends React.PureComponent {
}

// If the selectedYear prop has changed, update the currentDateView state with the new year value
this.setState((prev) => ({currentDateView: moment(prev.currentDateView).set('year', this.props.selectedYear).toDate()}));
this.setState(
(prev) => {
const newMomentDate = moment(prev.currentDateView).set('year', this.props.selectedYear);

return {
selectedYear: this.props.selectedYear,
currentDateView: this.clampDate(newMomentDate.toDate()),
};
},
() => {
this.props.onSelected(this.getSelectedDateString());
},
);
}

/**
Expand All @@ -67,7 +86,7 @@ class CalendarPicker extends React.PureComponent {
onYearPickerPressed() {
const minYear = moment(this.props.minDate).year();
const maxYear = moment(this.props.maxDate).year();
const currentYear = this.state.currentDateView.getFullYear();
const currentYear = parseInt(this.state.selectedYear, 10);
Navigation.navigate(ROUTES.getYearSelectionRoute(minYear, maxYear, currentYear, Navigation.getActiveRoute()));
this.props.onYearPickerOpen(this.state.currentDateView);
}
Expand All @@ -77,16 +96,102 @@ class CalendarPicker extends React.PureComponent {
* @param {Number} day - The day of the month that was selected.
*/
onDayPressed(day) {
const selectedDate = new Date(this.state.currentDateView.getFullYear(), this.state.currentDateView.getMonth(), day);
this.props.onSelected(selectedDate);
this.setState(
(prev) => {
const momentDate = moment(prev.currentDateView).date(day);

return {
selectedDay: this.getNumberStringWithLeadingZero(day),
currentDateView: this.clampDate(momentDate.toDate()),
};
},
() => {
this.props.onSelected(this.getSelectedDateString());
},
);
}

/**
* Gets the date string build from state values of selected year, month and day.
* @returns {string} - Date string in the 'YYYY-MM-DD' format.
*/
getSelectedDateString() {
// can't use moment.format() method here because it won't allow incorrect dates
return `${this.state.selectedYear}-${this.state.selectedMonth}-${this.state.selectedDay}`;
}

/**
* Returns the string converted from the given number. If the number is lower than 10,
* it will add zero at the beginning of the string.
* @param {Number} number - The number to be converted.
* @returns {string} - Converted string prefixed by zero if necessary.
*/
getNumberStringWithLeadingZero(number) {
return `${number < 10 ? `0${number}` : number}`;
}

/**
* Gives the new version of the state object,
* changing both selected month and year based on the given moment date.
* @param {moment.Moment} momentDate - Moment date object.
* @returns {{currentDateView: Date, selectedMonth: string, selectedYear: string}} - The new version of the state.
*/
getMonthState(momentDate) {
const clampedDate = this.clampDate(momentDate.toDate());
const month = clampedDate.getMonth() + 1;

return {
selectedMonth: this.getNumberStringWithLeadingZero(month),
selectedYear: clampedDate.getFullYear().toString(), // year might have changed too
currentDateView: clampedDate,
};
}

/**
* Handles the user pressing the previous month arrow of the calendar picker.
*/
moveToPrevMonth() {
this.setState((prev) => ({currentDateView: moment(prev.currentDateView).subtract(1, 'M').toDate()}));
this.setState(
(prev) => {
const momentDate = moment(prev.currentDateView).subtract(1, 'M');

return this.getMonthState(momentDate);
},
() => {
this.props.onSelected(this.getSelectedDateString());
},
);
}

/**
* Handles the user pressing the next month arrow of the calendar picker.
*/
moveToNextMonth() {
this.setState((prev) => ({currentDateView: moment(prev.currentDateView).add(1, 'M').toDate()}));
this.setState(
(prev) => {
const momentDate = moment(prev.currentDateView).add(1, 'M');

return this.getMonthState(momentDate);
},
() => {
this.props.onSelected(this.getSelectedDateString());
},
);
}

/**
* Checks whether the given date is in the min/max date range and returns the limit value if not.
* @param {Date} date - The date object to check.
* @returns {Date} - The date that is within the min/max date range.
*/
clampDate(date) {
if (this.props.maxDate < date) {
return this.props.maxDate;
}
if (this.props.minDate > date) {
return this.props.minDate;
}
return date;
}

render() {
Expand Down
3 changes: 3 additions & 0 deletions src/components/Form.js
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,9 @@ class Form extends React.Component {
child.props.onBlur(event);
}
},
onTouched: () => {
this.setTouchedInput(inputID);
},
onInputChange: (value, key) => {
const inputKey = key || inputID;
this.setState(
Expand Down
4 changes: 2 additions & 2 deletions src/components/NewDatePicker/datePickerPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ const propTypes = {
* The datepicker supports any value that `moment` can parse.
* `onInputChange` would always be called with a Date (or null)
*/
value: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.string]),
value: 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]),
defaultValue: PropTypes.string,

/** A minimum date of calendar to select */
minDate: PropTypes.objectOf(Date),
Expand Down
9 changes: 5 additions & 4 deletions src/components/NewDatePicker/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ class NewDatePicker extends React.Component {

this.state = {
selectedMonth: null,
selectedDate: moment(props.value || props.defaultValue || undefined).toDate(),
selectedDate: props.value || props.defaultValue || undefined,
};

this.setDate = this.setDate.bind(this);
this.setCurrentSelectedMonth = this.setCurrentSelectedMonth.bind(this);

// We're using uncontrolled input otherwise it wont be possible to
// We're using uncontrolled input otherwise it won't 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.defaultValue ? moment(props.defaultValue).format(CONST.DATE.MOMENT_FORMAT_STRING) : '';
Expand All @@ -47,11 +47,12 @@ class NewDatePicker extends React.Component {

/**
* Trigger the `onInputChange` handler when the user input has a complete date or is cleared
* @param {Date} selectedDate
* @param {string} selectedDate
*/
setDate(selectedDate) {
this.setState({selectedDate}, () => {
this.props.onInputChange(moment(selectedDate).format(CONST.DATE.MOMENT_FORMAT_STRING));
this.props.onTouched();
this.props.onInputChange(selectedDate);
});
}

Expand Down
3 changes: 1 addition & 2 deletions src/styles/styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -3224,12 +3224,11 @@ const styles = {
},

datePickerPopover: {
position: 'absolute',
backgroundColor: themeColors.appBG,
width: '100%',
alignSelf: 'center',
top: 60,
zIndex: 100,
marginTop: 8,
},

loginHeroHeader: {
Expand Down
28 changes: 14 additions & 14 deletions tests/unit/CalendarPickerTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ describe('CalendarPicker', () => {
const onSelectedMock = jest.fn();
const minDate = new Date('2022-01-01');
const maxDate = new Date('2030-01-01');
const value = new Date('2023-01-01');
const value = '2023-01-01';
const {getByText} = render(
<MockedCalendarPicker
value={value}
Expand All @@ -84,13 +84,13 @@ describe('CalendarPicker', () => {

fireEvent.press(getByText('15'));

expect(onSelectedMock).toHaveBeenCalledWith(new Date('2023-01-15'));
expect(onSelectedMock).toHaveBeenCalledWith('2023-01-15');
expect(onSelectedMock).toHaveBeenCalledTimes(1);
});

test('clicking previous month arrow and selecting day updates the selected date', () => {
const onSelectedMock = jest.fn();
const value = new Date('2022-01-01');
const value = '2022-01-01';
const minDate = new Date('2022-01-01');
const maxDate = new Date('2030-01-01');
const {getByText, getByTestId} = render(
Expand All @@ -105,7 +105,7 @@ describe('CalendarPicker', () => {
fireEvent.press(getByTestId('next-month-arrow'));
fireEvent.press(getByText('15'));

expect(onSelectedMock).toHaveBeenCalledWith(new Date('2022-02-15'));
expect(onSelectedMock).toHaveBeenCalledWith('2022-02-15');
});

test('should block the back arrow when there is no available dates in the previous month', () => {
Expand All @@ -123,7 +123,7 @@ describe('CalendarPicker', () => {

test('should block the next arrow when there is no available dates in the next month', () => {
const maxDate = new Date('2003-02-24');
const value = new Date('2003-02-17');
const value = '2003-02-17';
const {getByTestId} = render(
<MockedCalendarPicker
maxDate={maxDate}
Expand All @@ -146,7 +146,7 @@ describe('CalendarPicker', () => {

fireEvent.press(getByText('1'));

expect(onSelectedMock).toHaveBeenCalledWith(new Date('2011-03-01'));
expect(onSelectedMock).toHaveBeenCalledWith('2011-03-01');
});

test('should open the calendar on a year from max date if it is earlier than current year', () => {
Expand All @@ -170,51 +170,51 @@ describe('CalendarPicker', () => {
});

test('should not allow to press earlier day than minDate', () => {
const date = new Date('2003-02-17');
const value = '2003-02-17';
const minDate = new Date('2003-02-16');
const {getByLabelText} = render(
<MockedCalendarPicker
minDate={minDate}
value={date}
value={value}
/>,
);

expect(getByLabelText('15')).toBeDisabled();
});

test('should not allow to press later day than max', () => {
const date = new Date('2003-02-17');
const value = '2003-02-17';
const maxDate = new Date('2003-02-24');
const {getByLabelText} = render(
<MockedCalendarPicker
maxDate={maxDate}
value={date}
value={value}
/>,
);

expect(getByLabelText('25')).toBeDisabled();
});

test('should allow to press min date', () => {
const date = new Date('2003-02-17');
const value = '2003-02-17';
const minDate = new Date('2003-02-16');
const {getByLabelText} = render(
<MockedCalendarPicker
minDate={minDate}
value={date}
value={value}
/>,
);

expect(getByLabelText('16')).not.toBeDisabled();
});

test('should not allow to press max date', () => {
const date = new Date('2003-02-17');
const value = '2003-02-17';
const maxDate = new Date('2003-02-24');
const {getByLabelText} = render(
<MockedCalendarPicker
maxDate={maxDate}
value={date}
value={value}
/>,
);

Expand Down