Skip to content

Commit f809f79

Browse files
committed
feat: Mark dates as disabled after range start if min or max range config is provided (resolves #1039)
1 parent a1fc18b commit f809f79

File tree

6 files changed

+88
-18
lines changed

6 files changed

+88
-18
lines changed

index.d.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ export interface GeneralConfig {
131131
closeOnAutoApply?: boolean;
132132
noSwipe?: boolean;
133133
keepActionRow?: boolean;
134-
onClickOutside?: (validate: () => boolean) => void;
134+
onClickOutside?: (validate: () => boolean, evt: PointerEvent) => void;
135135
tabOutClosesMenu?: boolean;
136136
arrowLeft?: string;
137137
keepViewOnOffsetClick?: boolean;
@@ -300,7 +300,7 @@ export interface VueDatePickerProps {
300300
disableYearSelect?: boolean;
301301
focusStartDate?: boolean;
302302
disabledTimes?:
303-
| ((time: TimeObj | TimeObj[] | (TimeObj | undefined)[]) => boolean)
303+
| ((time: TimeObj | (TimeObj | undefined)[]) => boolean)
304304
| DisabledTime[]
305305
| [DisabledTime[], DisabledTime[]];
306306
timePickerInline?: boolean;

src/VueDatePicker/composables/calendar-class.ts

+42-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
import { ref } from 'vue';
2-
import { addDays } from 'date-fns';
2+
import { addDays, isAfter, isBefore } from 'date-fns';
33

44
import { useDefaults, useValidation } from '@/composables/index';
55
import { isModelAuto, matchDate } from '@/utils/util';
6-
import { isDateAfter, isDateBefore, isDateBetween, isDateEqual, getDate, getWeekFromDate } from '@/utils/date-utils';
6+
import {
7+
isDateAfter,
8+
isDateBefore,
9+
isDateBetween,
10+
isDateEqual,
11+
getDate,
12+
getWeekFromDate,
13+
getBeforeAndAfterInRange,
14+
} from '@/utils/date-utils';
715
import { localToTz } from '@/utils/timezone';
816

917
import type { UnwrapRef, WritableComputedRef } from 'vue';
@@ -244,14 +252,44 @@ export const useCalendarClass = (modelValue: WritableComputedRef<InternalModuleV
244252
return false;
245253
};
246254

255+
const isDateAfterMaxRange = (day: ICalendarDay) => {
256+
if (Array.isArray(modelValue.value) && modelValue.value.length === 1) {
257+
const { before, after } = getBeforeAndAfterInRange(+defaultedRange.value.maxRange!, modelValue.value[0]);
258+
return isBefore(day.value, before) || isAfter(day.value, after);
259+
}
260+
return false;
261+
};
262+
263+
const isDateBeforeMinRange = (day: ICalendarDay) => {
264+
if (Array.isArray(modelValue.value) && modelValue.value.length === 1) {
265+
const { before, after } = getBeforeAndAfterInRange(+defaultedRange.value.minRange!, modelValue.value[0]);
266+
return isDateBetween([before, after], modelValue.value[0], day.value);
267+
}
268+
return false;
269+
};
270+
271+
const minMaxRangeDate = (day: ICalendarDay) => {
272+
if (defaultedRange.value.enabled && (defaultedRange.value.maxRange || defaultedRange.value.minRange)) {
273+
if (defaultedRange.value.maxRange && defaultedRange.value.minRange) {
274+
return isDateAfterMaxRange(day) || isDateBeforeMinRange(day);
275+
}
276+
return defaultedRange.value.maxRange ? isDateAfterMaxRange(day) : isDateBeforeMinRange(day);
277+
}
278+
return false;
279+
};
280+
247281
// Common classes to be checked for any mode
248282
const sharedClasses = (day: ICalendarDay): Record<string, boolean> => {
249283
const { isRangeStart, isRangeEnd } = rangeStartEnd(day);
250284
const isRangeStartEnd = defaultedRange.value.enabled ? isRangeStart || isRangeEnd : false;
251285
return {
252286
dp__cell_offset: !day.current,
253-
dp__pointer: !props.disabled && !(!day.current && props.hideOffsetDates) && !isDisabled(day.value),
254-
dp__cell_disabled: isDisabled(day.value),
287+
dp__pointer:
288+
!props.disabled &&
289+
!(!day.current && props.hideOffsetDates) &&
290+
!isDisabled(day.value) &&
291+
!minMaxRangeDate(day),
292+
dp__cell_disabled: isDisabled(day.value) || minMaxRangeDate(day),
255293
dp__cell_highlight:
256294
!disableHighlight(day) &&
257295
(highlighted(day) || highlightedWeekDay(day)) &&

src/VueDatePicker/interfaces.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ export interface Config {
282282
closeOnAutoApply: boolean;
283283
noSwipe: boolean;
284284
keepActionRow: boolean;
285-
onClickOutside?: (validate: () => boolean) => void;
285+
onClickOutside?: (validate: () => boolean, evt: PointerEvent) => void;
286286
tabOutClosesMenu: boolean;
287287
arrowLeft?: string;
288288
keepViewOnOffsetClick?: boolean;

src/VueDatePicker/utils/date-utils.ts

+8
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import {
2424
subMonths,
2525
format,
2626
startOfMonth,
27+
subDays,
28+
addDays,
2729
} from 'date-fns';
2830
import { errors } from '@/utils/util';
2931

@@ -443,3 +445,9 @@ export const checkHighlightYear = (defaultedHighlight: Highlight | HighlightFn,
443445
export const getCellId = (date: Date) => {
444446
return format(date, 'yyyy-MM-dd');
445447
};
448+
449+
export const getBeforeAndAfterInRange = (range: number, date: Date) => {
450+
const before = subDays(resetDateTime(date), range);
451+
const after = addDays(resetDateTime(date), range);
452+
return { before, after };
453+
};

tests/unit/behaviour.spec.ts

+22-9
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ import {
1616
startOfMonth,
1717
startOfQuarter,
1818
startOfYear,
19+
setDate,
1920
} from 'date-fns';
2021

2122
import { resetDateTime } from '@/utils/date-utils';
2223

2324
import {
2425
clickCalendarDate,
2526
clickSelectBtn,
27+
getCellClasses,
2628
getMonthName,
2729
hoverCalendarDate,
2830
openMenu,
@@ -153,19 +155,12 @@ describe('It should validate various picker scenarios', () => {
153155
const disabledDates = [addDays(today, 1)];
154156
const dp = await openMenu({ disabledDates });
155157

156-
const getCellClasses = (date: Date) => {
157-
const el = dp.find(`[data-test-id="${date}"]`);
158-
const innerCell = el.find('.dp__cell_inner');
159-
160-
return innerCell.classes();
161-
};
162-
163-
expect(getCellClasses(resetDateTime(disabledDates[0]))).toContain('dp__cell_disabled');
158+
expect(getCellClasses(dp, disabledDates[0])).toContain('dp__cell_disabled');
164159

165160
const updatedDisabledDates = [...disabledDates, addDays(today, 2)];
166161

167162
await dp.setProps({ disabledDates: updatedDisabledDates });
168-
expect(getCellClasses(resetDateTime(updatedDisabledDates[1]))).toContain('dp__cell_disabled');
163+
expect(getCellClasses(dp, updatedDisabledDates[1])).toContain('dp__cell_disabled');
169164
dp.unmount();
170165
});
171166

@@ -527,6 +522,7 @@ describe('It should validate various picker scenarios', () => {
527522
await dp.find('[data-test-id="dp-input"]').trigger('click');
528523
const menuShown = dp.find('[role="dialog"]');
529524
expect(menuShown.exists()).toBeTruthy();
525+
dp.unmount();
530526
});
531527

532528
it('Should trigger @text-input event when typing date #909', async () => {
@@ -537,5 +533,22 @@ describe('It should validate various picker scenarios', () => {
537533
await dp.find('[data-test-id="dp-input"]').setValue('02');
538534
expect(dp.emitted()).toHaveProperty('text-input');
539535
expect(dp.emitted()['text-input']).toHaveLength(2);
536+
dp.unmount();
537+
});
538+
539+
it('Should disable invalid dates when min and max range options are provided', async () => {
540+
const disabledClass = 'dp__cell_disabled';
541+
const dp = await openMenu({ range: { minRange: 3, maxRange: 10 } });
542+
const start = setDate(new Date(), 15);
543+
await clickCalendarDate(dp, start);
544+
const disabledBeforeMin = addDays(start, 2);
545+
const disabledAfterMax = addDays(start, 11);
546+
const inRange = addDays(start, 7);
547+
548+
expect(getCellClasses(dp, disabledBeforeMin)).toContain(disabledClass);
549+
expect(getCellClasses(dp, disabledAfterMax)).toContain(disabledClass);
550+
const empty = getCellClasses(dp, inRange).find((className) => className === disabledClass);
551+
expect(empty).toBeFalsy();
552+
dp.unmount();
540553
});
541554
});

tests/utils.ts

+13-2
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,26 @@ export const getMonthName = (date: Date) => {
3333
};
3434

3535
export const clickCalendarDate = async (dp: VueWrapper<any>, date: Date) => {
36-
await dp.find(`[data-test-id="${resetDateTime(date)}"]`).trigger('click');
36+
await getCalendarCell(dp, date).trigger('click');
3737
};
3838

3939
export const hoverCalendarDate = async (dp: VueWrapper<any>, date: Date) => {
40-
await dp.find(`[data-test-id="${resetDateTime(date)}"]`).trigger('mouseenter');
40+
await getCalendarCell(dp, date).trigger('mouseenter');
4141
};
4242

4343
export const clickSelectBtn = async (dp: VueWrapper<any>) => {
4444
await dp.find(`[data-test-id="select-button"]`).trigger('click');
4545
};
4646

4747
export const padZero = (val: number) => (val < 10 ? `0${val}` : val);
48+
49+
export const getCalendarCell = (dp: VueWrapper<any>, date: Date) => {
50+
return dp.find(`[data-test-id="${resetDateTime(date)}"]`);
51+
};
52+
53+
export const getCellClasses = (dp: VueWrapper<any>, date: Date) => {
54+
const el = getCalendarCell(dp, date);
55+
const innerCell = el.find('.dp__cell_inner');
56+
57+
return innerCell.classes();
58+
};

0 commit comments

Comments
 (0)