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

Feat: Remove moment from datepicker #29062

Merged
merged 7 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 0 additions & 1 deletion src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,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
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
// eslint-disable-next-line no-restricted-imports
import {Button, View, Keyboard} from 'react-native';
import RNDatePicker from '@react-native-community/datetimepicker';
import moment from 'moment';
import {format} from 'date-fns';
import _ from 'underscore';
import compose from '../../libs/compose';
import TextInput from '../TextInput';
Expand All @@ -26,7 +26,7 @@ class DatePicker extends React.Component {

this.state = {
isPickerVisible: false,
selectedDate: props.value || props.defaultValue ? moment(props.value || props.defaultValue).toDate() : new Date(),
selectedDate: props.value || props.defaultValue ? new Date(props.value || props.defaultValue) : new Date(),
waterim marked this conversation as resolved.
Show resolved Hide resolved
};

this.showPicker = this.showPicker.bind(this);
Expand Down Expand Up @@ -64,8 +64,7 @@ class DatePicker extends React.Component {
*/
selectDate() {
this.setState({isPickerVisible: false});
const asMoment = moment(this.state.selectedDate, true);
this.props.onInputChange(asMoment.format(CONST.DATE.MOMENT_FORMAT_STRING));
this.props.onInputChange(format(this.state.selectedDate, CONST.DATE.FNS_FORMAT_STRING));
}

/**
Expand All @@ -77,7 +76,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 (
<>
<TextInput
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').startOf('day') > moment(this.state.currentDateView).add(1, 'months');
const hasAvailableDatesPrevMonth = moment(this.props.minDate).startOf('day') < moment(this.state.currentDateView).subtract(1, 'months').endOf('month');
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 from 'react';
import {View} from 'react-native';
import moment from 'moment';
import {setYear} from 'date-fns';
import PropTypes from 'prop-types';
import TextInput from '../TextInput';
import CONST from '../../CONST';
Expand All @@ -12,13 +12,13 @@ import CalendarPicker from './CalendarPicker';

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 @@ -35,8 +35,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
Loading