Skip to content

Commit 5a53069

Browse files
committed
added MAX/MIN year disabled states
1 parent a694dc1 commit 5a53069

2 files changed

Lines changed: 101 additions & 7 deletions

File tree

src/components/DatePicker/CalendarPicker/index.tsx

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {addMonths, addYears, format, getYear, isSameDay, parseISO, setDate, setMonth, setYear, startOfDay, subMonths, subYears} from 'date-fns';
1+
import {addMonths, addYears, format, isSameDay, parseISO, setDate, setMonth, setYear, startOfDay, subMonths, subYears} from 'date-fns';
22
import {Str} from 'expensify-common';
33
import React, {useCallback, useEffect, useRef, useState} from 'react';
44
import type {StyleProp, ViewStyle} from 'react-native';
@@ -81,8 +81,8 @@ function CalendarPicker({
8181
const initialHeight = (calendarDaysMatrix?.length || CONST.MAX_CALENDAR_PICKER_ROWS) * CONST.CALENDAR_PICKER_DAY_HEIGHT;
8282
const heightValue = useSharedValue(initialHeight);
8383

84-
const minYear = Math.min(getYear(new Date(minDate)), CONST.CALENDAR_PICKER.MIN_YEAR);
85-
const maxYear = Math.max(getYear(new Date(maxDate)), CONST.CALENDAR_PICKER.MAX_YEAR);
84+
const minYear = CONST.CALENDAR_PICKER.MIN_YEAR;
85+
const maxYear = CONST.CALENDAR_PICKER.MAX_YEAR;
8686

8787
const [years, setYears] = useState<CalendarPickerListItem[]>(() =>
8888
Array.from({length: maxYear - minYear + 1}, (v, i) => i + minYear).map((year) => ({
@@ -122,12 +122,20 @@ function CalendarPicker({
122122
onSelected?.(format(newCurrentDateView, CONST.DATE.FNS_FORMAT_STRING));
123123
};
124124

125+
const isAtMinBoundary = currentYearView <= CONST.CALENDAR_PICKER.MIN_YEAR && currentMonthView === 0;
126+
const isAtMaxBoundary = currentYearView >= CONST.CALENDAR_PICKER.MAX_YEAR && currentMonthView === 11;
127+
const isAtMinYear = currentYearView <= CONST.CALENDAR_PICKER.MIN_YEAR;
128+
const isAtMaxYear = currentYearView >= CONST.CALENDAR_PICKER.MAX_YEAR;
129+
125130
/**
126131
* Handles the user pressing the previous month arrow of the calendar picker.
127132
*/
128133
const moveToPrevMonth = () => {
129134
setCurrentDateView((prev) => {
130135
const prevMonth = subMonths(new Date(prev), 1);
136+
if (prevMonth.getFullYear() < CONST.CALENDAR_PICKER.MIN_YEAR) {
137+
return prev;
138+
}
131139
// if year is subtracted, we need to update the years list
132140
if (prevMonth.getFullYear() < prev.getFullYear()) {
133141
setYears((prevYears) =>
@@ -147,6 +155,9 @@ function CalendarPicker({
147155
const moveToNextMonth = () => {
148156
setCurrentDateView((prev) => {
149157
const nextMonth = addMonths(new Date(prev), 1);
158+
if (nextMonth.getFullYear() > CONST.CALENDAR_PICKER.MAX_YEAR) {
159+
return prev;
160+
}
150161
// if year is added, we need to update the years list
151162
if (nextMonth.getFullYear() > prev.getFullYear()) {
152163
setYears((prevYears) =>
@@ -164,6 +175,9 @@ function CalendarPicker({
164175
const moveToPrevYear = () => {
165176
setCurrentDateView((prev) => {
166177
const prevYear = subYears(new Date(prev), 1);
178+
if (prevYear.getFullYear() < CONST.CALENDAR_PICKER.MIN_YEAR) {
179+
return prev;
180+
}
167181
setYears((prevYears) => prevYears.map((item) => ({...item, isSelected: item.value === prevYear.getFullYear()})));
168182
return prevYear;
169183
});
@@ -172,6 +186,9 @@ function CalendarPicker({
172186
const moveToNextYear = () => {
173187
setCurrentDateView((prev) => {
174188
const nextYear = addYears(new Date(prev), 1);
189+
if (nextYear.getFullYear() > CONST.CALENDAR_PICKER.MAX_YEAR) {
190+
return prev;
191+
}
175192
setYears((prevYears) => prevYears.map((item) => ({...item, isSelected: item.value === nextYear.getFullYear()})));
176193
return nextYear;
177194
});
@@ -218,13 +235,17 @@ function CalendarPicker({
218235
<PressableWithFeedback
219236
shouldUseAutoHitSlop={false}
220237
testID="prev-month-arrow"
238+
disabled={isAtMinBoundary}
221239
onPress={moveToPrevMonth}
222240
hoverDimmingValue={1}
223241
accessibilityLabel={translate('common.previousMonth')}
224242
role={CONST.ROLE.BUTTON}
225243
sentryLabel={CONST.SENTRY_LABEL.CALENDAR_PICKER.PREV_MONTH}
226244
>
227-
<ArrowIcon direction={CONST.DIRECTION.LEFT} />
245+
<ArrowIcon
246+
disabled={isAtMinBoundary}
247+
direction={CONST.DIRECTION.LEFT}
248+
/>
228249
</PressableWithFeedback>
229250
<View style={[themeStyles.flex1, themeStyles.alignItemsCenter]}>
230251
<PressableWithFeedback
@@ -253,26 +274,31 @@ function CalendarPicker({
253274
<PressableWithFeedback
254275
shouldUseAutoHitSlop={false}
255276
testID="next-month-arrow"
277+
disabled={isAtMaxBoundary}
256278
onPress={moveToNextMonth}
257279
hoverDimmingValue={1}
258280
accessibilityLabel={translate('common.nextMonth')}
259281
role={CONST.ROLE.BUTTON}
260282
sentryLabel={CONST.SENTRY_LABEL.CALENDAR_PICKER.NEXT_MONTH}
261283
>
262-
<ArrowIcon />
284+
<ArrowIcon disabled={isAtMaxBoundary} />
263285
</PressableWithFeedback>
264286
</View>
265287
<View style={[themeStyles.alignItemsCenter, themeStyles.flexRow, {flex: 2}]}>
266288
<PressableWithFeedback
267289
shouldUseAutoHitSlop={false}
268290
testID="prev-year-arrow"
291+
disabled={isAtMinYear}
269292
onPress={moveToPrevYear}
270293
hoverDimmingValue={1}
271294
accessibilityLabel={translate('common.previousYear')}
272295
role={CONST.ROLE.BUTTON}
273296
sentryLabel={CONST.SENTRY_LABEL.CALENDAR_PICKER.PREV_YEAR}
274297
>
275-
<ArrowIcon direction={CONST.DIRECTION.LEFT} />
298+
<ArrowIcon
299+
disabled={isAtMinYear}
300+
direction={CONST.DIRECTION.LEFT}
301+
/>
276302
</PressableWithFeedback>
277303
<View style={[themeStyles.flex1, themeStyles.alignItemsCenter]}>
278304
<PressableWithFeedback
@@ -300,13 +326,14 @@ function CalendarPicker({
300326
<PressableWithFeedback
301327
shouldUseAutoHitSlop={false}
302328
testID="next-year-arrow"
329+
disabled={isAtMaxYear}
303330
onPress={moveToNextYear}
304331
hoverDimmingValue={1}
305332
accessibilityLabel={translate('common.nextYear')}
306333
role={CONST.ROLE.BUTTON}
307334
sentryLabel={CONST.SENTRY_LABEL.CALENDAR_PICKER.NEXT_YEAR}
308335
>
309-
<ArrowIcon />
336+
<ArrowIcon disabled={isAtMaxYear} />
310337
</PressableWithFeedback>
311338
</View>
312339
</View>

tests/unit/CalendarPickerTest.tsx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {fireEvent, render, screen, userEvent, within} from '@testing-library/rea
33
import {addMonths, addYears, subMonths, subYears} from 'date-fns';
44
import CalendarPicker from '@components/DatePicker/CalendarPicker';
55
import DateUtils from '@libs/DateUtils';
6+
import CONST from '@src/CONST';
67

78
const monthNames = DateUtils.getMonthNames();
89

@@ -421,6 +422,72 @@ describe('CalendarPicker', () => {
421422
expect(screen.getByText(monthNames.at(11) ?? '')).toBeTruthy();
422423
});
423424

425+
test('next year arrow should not navigate above CONST.CALENDAR_PICKER.MAX_YEAR', () => {
426+
const value = new Date(CONST.CALENDAR_PICKER.MAX_YEAR, 5, 15);
427+
const maxDate = new Date(CONST.CALENDAR_PICKER.MAX_YEAR, 11, 31);
428+
render(
429+
<CalendarPicker
430+
value={value}
431+
maxDate={maxDate}
432+
/>,
433+
);
434+
435+
fireEvent.press(screen.getByTestId('next-year-arrow'));
436+
437+
// Year should be clamped at MAX_YEAR
438+
expect(within(screen.getByTestId('currentYearText')).getByText(CONST.CALENDAR_PICKER.MAX_YEAR.toString())).toBeTruthy();
439+
});
440+
441+
test('prev year arrow should not navigate below CONST.CALENDAR_PICKER.MIN_YEAR', () => {
442+
const value = new Date(CONST.CALENDAR_PICKER.MIN_YEAR, 5, 15);
443+
const minDate = new Date(CONST.CALENDAR_PICKER.MIN_YEAR, 0, 1);
444+
render(
445+
<CalendarPicker
446+
value={value}
447+
minDate={minDate}
448+
/>,
449+
);
450+
451+
fireEvent.press(screen.getByTestId('prev-year-arrow'));
452+
453+
// Year should be clamped at MIN_YEAR
454+
expect(within(screen.getByTestId('currentYearText')).getByText(CONST.CALENDAR_PICKER.MIN_YEAR.toString())).toBeTruthy();
455+
});
456+
457+
test('next month arrow should not navigate above December of CONST.CALENDAR_PICKER.MAX_YEAR', () => {
458+
const value = new Date(CONST.CALENDAR_PICKER.MAX_YEAR, 11, 15);
459+
const maxDate = new Date(CONST.CALENDAR_PICKER.MAX_YEAR, 11, 31);
460+
render(
461+
<CalendarPicker
462+
value={value}
463+
maxDate={maxDate}
464+
/>,
465+
);
466+
467+
fireEvent.press(screen.getByTestId('next-month-arrow'));
468+
469+
// Should remain on December of MAX_YEAR
470+
expect(within(screen.getByTestId('currentYearText')).getByText(CONST.CALENDAR_PICKER.MAX_YEAR.toString())).toBeTruthy();
471+
expect(within(screen.getByTestId('currentMonthText')).getByText(monthNames.at(11) ?? '')).toBeTruthy();
472+
});
473+
474+
test('prev month arrow should not navigate below January of CONST.CALENDAR_PICKER.MIN_YEAR', () => {
475+
const value = new Date(CONST.CALENDAR_PICKER.MIN_YEAR, 0, 15);
476+
const minDate = new Date(CONST.CALENDAR_PICKER.MIN_YEAR, 0, 1);
477+
render(
478+
<CalendarPicker
479+
value={value}
480+
minDate={minDate}
481+
/>,
482+
);
483+
484+
fireEvent.press(screen.getByTestId('prev-month-arrow'));
485+
486+
// Should remain on January of MIN_YEAR
487+
expect(within(screen.getByTestId('currentYearText')).getByText(CONST.CALENDAR_PICKER.MIN_YEAR.toString())).toBeTruthy();
488+
expect(within(screen.getByTestId('currentMonthText')).getByText(monthNames.at(0) ?? '')).toBeTruthy();
489+
});
490+
424491
test('month picker should always return all 12 months', () => {
425492
const allMonths = DateUtils.getFilteredMonthItems(monthNames, 6);
426493

0 commit comments

Comments
 (0)