Skip to content

Commit

Permalink
Merge pull request #29062 from waterim/feat-19810-Replace-datepicker
Browse files Browse the repository at this point in the history
Feat: Remove moment from datepicker
  • Loading branch information
mountiny authored Oct 17, 2023
2 parents bb22e06 + 9a2b55f commit 12d3669
Show file tree
Hide file tree
Showing 9 changed files with 81 additions and 53 deletions.
1 change: 0 additions & 1 deletion src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,6 @@ const CONST = {
DESKTOP: `${ACTIVE_EXPENSIFY_URL}NewExpensify.dmg`,
},
DATE: {
MOMENT_FORMAT_STRING: 'YYYY-MM-DD',
SQL_DATE_TIME: 'YYYY-MM-DD HH:mm:ss',
FNS_FORMAT_STRING: 'yyyy-MM-dd',
LOCAL_TIME_FORMAT: 'h:mm a',
Expand Down
4 changes: 2 additions & 2 deletions src/components/DatePicker/datepickerPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ const propTypes = {
...fieldPropTypes,

/**
* The datepicker supports any value that `moment` can parse.
* The datepicker supports any value that `new Date()` can parse.
* `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.
* The datepicker supports any defaultValue that `new Date()` can parse.
* `onInputChange` would always be called with a Date (or null)
*/
defaultValue: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.string]),
Expand Down
10 changes: 5 additions & 5 deletions src/components/DatePicker/index.android.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import {Keyboard} from 'react-native';
import RNDatePicker from '@react-native-community/datetimepicker';
import moment from 'moment';
import {format} from 'date-fns';
import _ from 'underscore';
import TextInput from '../TextInput';
import CONST from '../../CONST';
Expand All @@ -28,8 +28,7 @@ class DatePicker extends React.Component {
this.setState({isPickerVisible: false});

if (event.type === 'set') {
const asMoment = moment(selectedDate, true);
this.props.onInputChange(asMoment.format(CONST.DATE.MOMENT_FORMAT_STRING));
this.props.onInputChange(format(selectedDate, CONST.DATE.FNS_FORMAT_STRING));
}
}

Expand All @@ -39,7 +38,8 @@ class DatePicker extends React.Component {
}

render() {
const dateAsText = this.props.value || this.props.defaultValue ? moment(this.props.value || this.props.defaultValue).format(CONST.DATE.MOMENT_FORMAT_STRING) : '';
const date = this.props.value || this.props.defaultValue;
const dateAsText = date ? format(new Date(date), CONST.DATE.FNS_FORMAT_STRING) : '';

return (
<>
Expand Down Expand Up @@ -73,7 +73,7 @@ class DatePicker extends React.Component {
/>
{this.state.isPickerVisible && (
<RNDatePicker
value={this.props.value || this.props.defaultValue ? moment(this.props.value || this.props.defaultValue).toDate() : new Date()}
value={date ? new Date(date) : new Date()}
mode="date"
onChange={this.setDate}
maximumDate={this.props.maxDate}
Expand Down
10 changes: 5 additions & 5 deletions src/components/DatePicker/index.ios.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, {useState, useRef, useCallback, useEffect} from 'react';
import {Button, View, Keyboard} from 'react-native';
import RNDatePicker from '@react-native-community/datetimepicker';
import moment from 'moment';
import {format} from 'date-fns';
import isFunction from 'lodash/isFunction';
import TextInput from '../TextInput';
import Popover from '../Popover';
Expand All @@ -13,8 +13,9 @@ import useKeyboardState from '../../hooks/useKeyboardState';
import useLocalize from '../../hooks/useLocalize';

function DatePicker({value, defaultValue, innerRef, onInputChange, preferredLocale, minDate, maxDate, label, disabled, onBlur, placeholder, containerStyles, errorText}) {
const dateValue = value || defaultValue;
const [isPickerVisible, setIsPickerVisible] = useState(false);
const [selectedDate, setSelectedDate] = useState(moment(value || defaultValue).toDate());
const [selectedDate, setSelectedDate] = useState(dateValue ? new Date(dateValue) : new Date());
const {isKeyboardShown} = useKeyboardState();
const {translate} = useLocalize();
const initialValue = useRef(null);
Expand Down Expand Up @@ -65,8 +66,7 @@ function DatePicker({value, defaultValue, innerRef, onInputChange, preferredLoca
*/
const selectDate = () => {
setIsPickerVisible(false);
const asMoment = moment(selectedDate, true);
onInputChange(asMoment.format(CONST.DATE.MOMENT_FORMAT_STRING));
onInputChange(format(selectedDate, CONST.DATE.FNS_FORMAT_STRING));
};

/**
Expand All @@ -77,7 +77,7 @@ function DatePicker({value, defaultValue, innerRef, onInputChange, preferredLoca
setSelectedDate(date);
};

const dateAsText = value || defaultValue ? moment(value || defaultValue).format(CONST.DATE.MOMENT_FORMAT_STRING) : '';
const dateAsText = dateValue ? format(new Date(dateValue), CONST.DATE.FNS_FORMAT_STRING) : '';

return (
<>
Expand Down
12 changes: 6 additions & 6 deletions src/components/DatePicker/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, {useEffect, useRef} from 'react';
import moment from 'moment';
import {format, isValid} from 'date-fns';
import _ from 'underscore';
import TextInput from '../TextInput';
import CONST from '../../CONST';
Expand All @@ -13,8 +13,8 @@ function DatePicker({maxDate, minDate, onInputChange, innerRef, label, value, pl
useEffect(() => {
// Adds nice native datepicker on web/desktop. Not possible to set this through props
inputRef.current.setAttribute('type', 'date');
inputRef.current.setAttribute('max', moment(maxDate).format(CONST.DATE.MOMENT_FORMAT_STRING));
inputRef.current.setAttribute('min', moment(minDate).format(CONST.DATE.MOMENT_FORMAT_STRING));
inputRef.current.setAttribute('max', format(new Date(maxDate), CONST.DATE.FNS_FORMAT_STRING));
inputRef.current.setAttribute('min', format(new Date(minDate), CONST.DATE.FNS_FORMAT_STRING));
inputRef.current.classList.add('expensify-datepicker');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
Expand All @@ -29,9 +29,9 @@ function DatePicker({maxDate, minDate, onInputChange, innerRef, label, value, pl
return;
}

const asMoment = moment(text, true);
if (asMoment.isValid()) {
onInputChange(asMoment.format(CONST.DATE.MOMENT_FORMAT_STRING));
const date = new Date(text);
if (isValid(date)) {
onInputChange(format(date, CONST.DATE.FNS_FORMAT_STRING));
}
};

Expand Down
40 changes: 20 additions & 20 deletions src/components/NewDatePicker/CalendarPicker/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import _ from 'underscore';
import React from 'react';
import {View} from 'react-native';
import moment from 'moment';
import {setYear, format, getYear, subMonths, addMonths, startOfDay, endOfMonth, setDate, isSameDay} from 'date-fns';
import PropTypes from 'prop-types';
import Str from 'expensify-common/lib/str';
import Text from '../../Text';
Expand All @@ -11,6 +11,7 @@ import styles from '../../../styles/styles';
import generateMonthMatrix from './generateMonthMatrix';
import withLocalize, {withLocalizePropTypes} from '../../withLocalize';
import CONST from '../../../CONST';
import DateUtils from '../../../libs/DateUtils';
import getButtonState from '../../../libs/getButtonState';
import * as StyleUtils from '../../../styles/StyleUtils';
import PressableWithFeedback from '../../Pressable/PressableWithFeedback';
Expand All @@ -34,8 +35,8 @@ const propTypes = {

const defaultProps = {
value: new Date(),
minDate: moment().year(CONST.CALENDAR_PICKER.MIN_YEAR).toDate(),
maxDate: moment().year(CONST.CALENDAR_PICKER.MAX_YEAR).toDate(),
minDate: setYear(new Date(), CONST.CALENDAR_PICKER.MIN_YEAR),
maxDate: setYear(new Date(), CONST.CALENDAR_PICKER.MAX_YEAR),
onSelected: () => {},
};

Expand All @@ -46,16 +47,15 @@ class CalendarPicker extends React.PureComponent {
if (props.minDate >= props.maxDate) {
throw new Error('Minimum date cannot be greater than the maximum date.');
}

let currentDateView = moment(props.value, CONST.DATE.MOMENT_FORMAT_STRING).toDate();
let currentDateView = new Date(props.value);
if (props.maxDate < currentDateView) {
currentDateView = props.maxDate;
} else if (props.minDate > currentDateView) {
currentDateView = props.minDate;
}

const minYear = moment(this.props.minDate).year();
const maxYear = moment(this.props.maxDate).year();
const minYear = getYear(new Date(this.props.minDate));
const maxYear = getYear(new Date(this.props.maxDate));

this.state = {
currentDateView,
Expand All @@ -79,7 +79,7 @@ class CalendarPicker extends React.PureComponent {

onYearSelected(year) {
this.setState((prev) => {
const newCurrentDateView = moment(prev.currentDateView).set('year', year).toDate();
const newCurrentDateView = setYear(new Date(prev.currentDateView), year);

return {
currentDateView: newCurrentDateView,
Expand All @@ -99,34 +99,34 @@ class CalendarPicker extends React.PureComponent {
onDayPressed(day) {
this.setState(
(prev) => ({
currentDateView: moment(prev.currentDateView).set('date', day).toDate(),
currentDateView: setDate(new Date(prev.currentDateView), day),
}),
() => this.props.onSelected(moment(this.state.currentDateView).format('YYYY-MM-DD')),
() => this.props.onSelected(format(new Date(this.state.currentDateView), CONST.DATE.FNS_FORMAT_STRING)),
);
}

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

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

render() {
const monthNames = _.map(moment.localeData(this.props.preferredLocale).months(), Str.recapitalize);
const daysOfWeek = _.map(moment.localeData(this.props.preferredLocale).weekdays(), (day) => day.toUpperCase());
const monthNames = _.map(DateUtils.getMonthNames(this.props.preferredLocale), Str.recapitalize);
const daysOfWeek = _.map(DateUtils.getDaysOfWeek(this.props.preferredLocale), (day) => day.toUpperCase());
const currentMonthView = this.state.currentDateView.getMonth();
const currentYearView = this.state.currentDateView.getFullYear();
const calendarDaysMatrix = generateMonthMatrix(currentYearView, currentMonthView);
const hasAvailableDatesNextMonth = moment(this.props.maxDate).endOf('month').endOf('day') >= moment(this.state.currentDateView).add(1, 'months');
const hasAvailableDatesPrevMonth = moment(this.props.minDate).startOf('month').startOf('day') <= moment(this.state.currentDateView).subtract(1, 'months');
const hasAvailableDatesNextMonth = startOfDay(endOfMonth(new Date(this.props.maxDate))) > addMonths(new Date(this.state.currentDateView), 1);
const hasAvailableDatesPrevMonth = startOfDay(new Date(this.props.minDate)) < endOfMonth(subMonths(new Date(this.state.currentDateView), 1));

return (
<View>
Expand Down Expand Up @@ -201,11 +201,11 @@ class CalendarPicker extends React.PureComponent {
style={styles.flexRow}
>
{_.map(week, (day, index) => {
const currentDate = moment([currentYearView, currentMonthView, day]);
const isBeforeMinDate = currentDate < moment(this.props.minDate).startOf('day');
const isAfterMaxDate = currentDate > moment(this.props.maxDate).startOf('day');
const currentDate = new Date(currentYearView, currentMonthView, day);
const isBeforeMinDate = currentDate < startOfDay(new Date(this.props.minDate));
const isAfterMaxDate = currentDate > startOfDay(new Date(this.props.maxDate));
const isDisabled = !day || isBeforeMinDate || isAfterMaxDate;
const isSelected = moment(this.props.value).isSame(moment([currentYearView, currentMonthView, day]), 'day');
const isSelected = isSameDay(new Date(this.props.value), new Date(currentYearView, currentMonthView, day));

return (
<PressableWithoutFeedback
Expand Down
10 changes: 5 additions & 5 deletions src/components/NewDatePicker/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, {useEffect, useState} from 'react';
import {View} from 'react-native';
import moment from 'moment';
import {setYear} from 'date-fns';
import PropTypes from 'prop-types';
import _ from 'lodash';
import TextInput from '../TextInput';
Expand All @@ -14,13 +14,13 @@ import InputWrapper from '../Form/InputWrapper';

const propTypes = {
/**
* The datepicker supports any value that `moment` can parse.
* The datepicker supports any value that `new Date()` can parse.
* `onInputChange` would always be called with a Date (or null)
*/
value: PropTypes.string,

/**
* The datepicker supports any defaultValue that `moment` can parse.
* The datepicker supports any defaultValue that `new Date()` can parse.
* `onInputChange` would always be called with a Date (or null)
*/
defaultValue: PropTypes.string,
Expand All @@ -39,8 +39,8 @@ const propTypes = {

const datePickerDefaultProps = {
...defaultBaseTextInputPropTypes,
minDate: moment().year(CONST.CALENDAR_PICKER.MIN_YEAR).toDate(),
maxDate: moment().year(CONST.CALENDAR_PICKER.MAX_YEAR).toDate(),
minDate: setYear(new Date(), CONST.CALENDAR_PICKER.MIN_YEAR),
maxDate: setYear(new Date(), CONST.CALENDAR_PICKER.MAX_YEAR),
value: undefined,
};

Expand Down
36 changes: 36 additions & 0 deletions src/libs/DateUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
isSameDay,
isAfter,
isSameYear,
eachMonthOfInterval,
eachDayOfInterval,
} from 'date-fns';

import Onyx from 'react-native-onyx';
Expand Down Expand Up @@ -255,6 +257,38 @@ function getCurrentTimezone(): Required<Timezone> {
return timezone;
}

/**
* @returns [January, Fabruary, March, April, May, June, July, August, ...]
*/
function getMonthNames(preferredLocale: string): string[] {
if (preferredLocale) {
setLocale(preferredLocale);
}
const fullYear = new Date().getFullYear();
const monthsArray = eachMonthOfInterval({
start: new Date(fullYear, 0, 1), // January 1st of the current year
end: new Date(fullYear, 11, 31), // December 31st of the current year
});

// eslint-disable-next-line rulesdir/prefer-underscore-method
return monthsArray.map((monthDate) => format(monthDate, CONST.DATE.MONTH_FORMAT));
}

/**
* @returns [Monday, Thuesday, Wednesday, ...]
*/
function getDaysOfWeek(preferredLocale: string): string[] {
if (preferredLocale) {
setLocale(preferredLocale);
}
const startOfCurrentWeek = startOfWeek(new Date(), {weekStartsOn: 1}); // Assuming Monday is the start of the week
const endOfCurrentWeek = endOfWeek(new Date(), {weekStartsOn: 1}); // Assuming Monday is the start of the week
const daysOfWeek = eachDayOfInterval({start: startOfCurrentWeek, end: endOfCurrentWeek});

// eslint-disable-next-line rulesdir/prefer-underscore-method
return daysOfWeek.map((date) => format(date, 'eeee'));
}

// Used to throttle updates to the timezone when necessary
let lastUpdatedTimezoneTime = new Date();

Expand Down Expand Up @@ -357,6 +391,8 @@ const DateUtils = {
isToday,
isTomorrow,
isYesterday,
getMonthNames,
getDaysOfWeek,
};

export default DateUtils;
11 changes: 2 additions & 9 deletions tests/unit/CalendarPickerTest.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
import {render, fireEvent, within} from '@testing-library/react-native';
import {format, eachMonthOfInterval, subYears, addYears} from 'date-fns';
import {subYears, addYears} from 'date-fns';
import DateUtils from '../../src/libs/DateUtils';
import CalendarPicker from '../../src/components/NewDatePicker/CalendarPicker';
import CONST from '../../src/CONST';

DateUtils.setLocale(CONST.LOCALES.EN);
const fullYear = new Date().getFullYear();
const monthsArray = eachMonthOfInterval({
start: new Date(fullYear, 0, 1), // January 1st of the current year
end: new Date(fullYear, 11, 31), // December 31st of the current year
});
// eslint-disable-next-line rulesdir/prefer-underscore-method
const monthNames = monthsArray.map((monthDate) => format(monthDate, CONST.DATE.MONTH_FORMAT));
const monthNames = DateUtils.getMonthNames(CONST.LOCALES.EN);

jest.mock('@react-navigation/native', () => ({
useNavigation: () => ({navigate: jest.fn()}),
Expand Down

0 comments on commit 12d3669

Please sign in to comment.