Skip to content

Commit

Permalink
feat: Mark dates as disabled after range start if min or max range co…
Browse files Browse the repository at this point in the history
…nfig is provided (resolves #1039)
  • Loading branch information
Jasenkoo committed Jan 4, 2025
1 parent a1fc18b commit f809f79
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 18 deletions.
4 changes: 2 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export interface GeneralConfig {
closeOnAutoApply?: boolean;
noSwipe?: boolean;
keepActionRow?: boolean;
onClickOutside?: (validate: () => boolean) => void;
onClickOutside?: (validate: () => boolean, evt: PointerEvent) => void;
tabOutClosesMenu?: boolean;
arrowLeft?: string;
keepViewOnOffsetClick?: boolean;
Expand Down Expand Up @@ -300,7 +300,7 @@ export interface VueDatePickerProps {
disableYearSelect?: boolean;
focusStartDate?: boolean;
disabledTimes?:
| ((time: TimeObj | TimeObj[] | (TimeObj | undefined)[]) => boolean)
| ((time: TimeObj | (TimeObj | undefined)[]) => boolean)
| DisabledTime[]
| [DisabledTime[], DisabledTime[]];
timePickerInline?: boolean;
Expand Down
46 changes: 42 additions & 4 deletions src/VueDatePicker/composables/calendar-class.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import { ref } from 'vue';
import { addDays } from 'date-fns';
import { addDays, isAfter, isBefore } from 'date-fns';

import { useDefaults, useValidation } from '@/composables/index';
import { isModelAuto, matchDate } from '@/utils/util';
import { isDateAfter, isDateBefore, isDateBetween, isDateEqual, getDate, getWeekFromDate } from '@/utils/date-utils';
import {
isDateAfter,
isDateBefore,
isDateBetween,
isDateEqual,
getDate,
getWeekFromDate,
getBeforeAndAfterInRange,
} from '@/utils/date-utils';
import { localToTz } from '@/utils/timezone';

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

const isDateAfterMaxRange = (day: ICalendarDay) => {
if (Array.isArray(modelValue.value) && modelValue.value.length === 1) {
const { before, after } = getBeforeAndAfterInRange(+defaultedRange.value.maxRange!, modelValue.value[0]);
return isBefore(day.value, before) || isAfter(day.value, after);
}
return false;
};

const isDateBeforeMinRange = (day: ICalendarDay) => {
if (Array.isArray(modelValue.value) && modelValue.value.length === 1) {
const { before, after } = getBeforeAndAfterInRange(+defaultedRange.value.minRange!, modelValue.value[0]);
return isDateBetween([before, after], modelValue.value[0], day.value);
}
return false;
};

const minMaxRangeDate = (day: ICalendarDay) => {
if (defaultedRange.value.enabled && (defaultedRange.value.maxRange || defaultedRange.value.minRange)) {
if (defaultedRange.value.maxRange && defaultedRange.value.minRange) {
return isDateAfterMaxRange(day) || isDateBeforeMinRange(day);
}
return defaultedRange.value.maxRange ? isDateAfterMaxRange(day) : isDateBeforeMinRange(day);
}
return false;
};

// Common classes to be checked for any mode
const sharedClasses = (day: ICalendarDay): Record<string, boolean> => {
const { isRangeStart, isRangeEnd } = rangeStartEnd(day);
const isRangeStartEnd = defaultedRange.value.enabled ? isRangeStart || isRangeEnd : false;
return {
dp__cell_offset: !day.current,
dp__pointer: !props.disabled && !(!day.current && props.hideOffsetDates) && !isDisabled(day.value),
dp__cell_disabled: isDisabled(day.value),
dp__pointer:
!props.disabled &&
!(!day.current && props.hideOffsetDates) &&
!isDisabled(day.value) &&
!minMaxRangeDate(day),
dp__cell_disabled: isDisabled(day.value) || minMaxRangeDate(day),
dp__cell_highlight:
!disableHighlight(day) &&
(highlighted(day) || highlightedWeekDay(day)) &&
Expand Down
2 changes: 1 addition & 1 deletion src/VueDatePicker/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ export interface Config {
closeOnAutoApply: boolean;
noSwipe: boolean;
keepActionRow: boolean;
onClickOutside?: (validate: () => boolean) => void;
onClickOutside?: (validate: () => boolean, evt: PointerEvent) => void;
tabOutClosesMenu: boolean;
arrowLeft?: string;
keepViewOnOffsetClick?: boolean;
Expand Down
8 changes: 8 additions & 0 deletions src/VueDatePicker/utils/date-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
subMonths,
format,
startOfMonth,
subDays,
addDays,
} from 'date-fns';
import { errors } from '@/utils/util';

Expand Down Expand Up @@ -443,3 +445,9 @@ export const checkHighlightYear = (defaultedHighlight: Highlight | HighlightFn,
export const getCellId = (date: Date) => {
return format(date, 'yyyy-MM-dd');
};

export const getBeforeAndAfterInRange = (range: number, date: Date) => {
const before = subDays(resetDateTime(date), range);
const after = addDays(resetDateTime(date), range);
return { before, after };
};
31 changes: 22 additions & 9 deletions tests/unit/behaviour.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ import {
startOfMonth,
startOfQuarter,
startOfYear,
setDate,
} from 'date-fns';

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

import {
clickCalendarDate,
clickSelectBtn,
getCellClasses,
getMonthName,
hoverCalendarDate,
openMenu,
Expand Down Expand Up @@ -153,19 +155,12 @@ describe('It should validate various picker scenarios', () => {
const disabledDates = [addDays(today, 1)];
const dp = await openMenu({ disabledDates });

const getCellClasses = (date: Date) => {
const el = dp.find(`[data-test-id="${date}"]`);
const innerCell = el.find('.dp__cell_inner');

return innerCell.classes();
};

expect(getCellClasses(resetDateTime(disabledDates[0]))).toContain('dp__cell_disabled');
expect(getCellClasses(dp, disabledDates[0])).toContain('dp__cell_disabled');

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

await dp.setProps({ disabledDates: updatedDisabledDates });
expect(getCellClasses(resetDateTime(updatedDisabledDates[1]))).toContain('dp__cell_disabled');
expect(getCellClasses(dp, updatedDisabledDates[1])).toContain('dp__cell_disabled');
dp.unmount();
});

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

it('Should trigger @text-input event when typing date #909', async () => {
Expand All @@ -537,5 +533,22 @@ describe('It should validate various picker scenarios', () => {
await dp.find('[data-test-id="dp-input"]').setValue('02');
expect(dp.emitted()).toHaveProperty('text-input');
expect(dp.emitted()['text-input']).toHaveLength(2);
dp.unmount();
});

it('Should disable invalid dates when min and max range options are provided', async () => {
const disabledClass = 'dp__cell_disabled';
const dp = await openMenu({ range: { minRange: 3, maxRange: 10 } });
const start = setDate(new Date(), 15);
await clickCalendarDate(dp, start);
const disabledBeforeMin = addDays(start, 2);
const disabledAfterMax = addDays(start, 11);
const inRange = addDays(start, 7);

expect(getCellClasses(dp, disabledBeforeMin)).toContain(disabledClass);
expect(getCellClasses(dp, disabledAfterMax)).toContain(disabledClass);
const empty = getCellClasses(dp, inRange).find((className) => className === disabledClass);
expect(empty).toBeFalsy();
dp.unmount();
});
});
15 changes: 13 additions & 2 deletions tests/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,26 @@ export const getMonthName = (date: Date) => {
};

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

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

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

export const padZero = (val: number) => (val < 10 ? `0${val}` : val);

export const getCalendarCell = (dp: VueWrapper<any>, date: Date) => {
return dp.find(`[data-test-id="${resetDateTime(date)}"]`);
};

export const getCellClasses = (dp: VueWrapper<any>, date: Date) => {
const el = getCalendarCell(dp, date);
const innerCell = el.find('.dp__cell_inner');

return innerCell.classes();
};

0 comments on commit f809f79

Please sign in to comment.