Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
15 changes: 9 additions & 6 deletions packages/react-day-picker/src/classes/CalendarDay.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createDateAdapter } from "../internal/dateAdapter.js";
import { type DateLib, defaultDateLib } from "./DateLib.js";

/**
Expand All @@ -13,15 +14,16 @@ export class CalendarDay {
displayMonth: Date,
dateLib: DateLib = defaultDateLib,
) {
const dateAdapter = createDateAdapter(dateLib);
this.date = date;
this.displayMonth = displayMonth;
this.outside = Boolean(
displayMonth && !dateLib.isSameMonth(date, displayMonth),
displayMonth && !dateAdapter.isSameMonth(date, displayMonth),
);
this.dateLib = dateLib;
this.isoDate = dateLib.format(date, "yyyy-MM-dd");
this.displayMonthId = dateLib.format(displayMonth, "yyyy-MM");
this.dateMonthId = dateLib.format(date, "yyyy-MM");
this.isoDate = dateAdapter.dayKey(date);
this.displayMonthId = dateAdapter.monthKey(displayMonth);
this.dateMonthId = dateAdapter.monthKey(date);
}

/**
Expand Down Expand Up @@ -80,9 +82,10 @@ export class CalendarDay {
* @returns `true` if the days are equal, otherwise `false`.
*/
isEqualTo(day: CalendarDay) {
const dateAdapter = createDateAdapter(this.dateLib);
return (
this.dateLib.isSameDay(day.date, this.date) &&
this.dateLib.isSameMonth(day.displayMonth, this.displayMonth)
dateAdapter.isSameDay(day.date, this.date) &&
dateAdapter.isSameMonth(day.displayMonth, this.displayMonth)
);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { DateLib } from "../classes/index.js";
import { createDateAdapter } from "../internal/dateAdapter.js";

const FIVE_WEEKS = 5;
const FOUR_WEEKS = 4;
Expand All @@ -16,12 +17,13 @@ const FOUR_WEEKS = 4;
* @returns The number of weeks in the broadcast calendar (4 or 5).
*/
export function getBroadcastWeeksInMonth(month: Date, dateLib: DateLib): 4 | 5 {
const dateAdapter = createDateAdapter(dateLib);
// Get the first day of the month
const firstDayOfMonth = dateLib.startOfMonth(month);

// Get the day of the week for the first day of the month (1-7, where 1 is Monday)
const firstDayOfWeek =
firstDayOfMonth.getDay() > 0 ? firstDayOfMonth.getDay() : 7;
const firstDay = dateAdapter.getDay(firstDayOfMonth);
const firstDayOfWeek = firstDay > 0 ? firstDay : 7;

const broadcastStartDate = dateLib.addDays(month, -firstDayOfWeek + 1);

Expand Down
8 changes: 7 additions & 1 deletion packages/react-day-picker/src/helpers/getDisplayMonths.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { DateLib } from "../classes/DateLib.js";
import {
createDateAdapter,
type DateAdapter,
} from "../internal/dateAdapter.js";
import type { DayPickerProps } from "../types/index.js";

/**
Expand All @@ -9,19 +13,21 @@ import type { DayPickerProps } from "../types/index.js";
* @param calendarEndMonth The latest month the user can navigate to.
* @param props The DayPicker props, including `numberOfMonths`.
* @param dateLib The date library to use for date manipulation.
* @param dateAdapter Internal date boundary used for range comparisons.
* @returns An array of dates representing the months to display.
*/
export function getDisplayMonths(
firstDisplayedMonth: Date,
calendarEndMonth: Date | undefined,
props: Pick<DayPickerProps, "numberOfMonths">,
dateLib: DateLib,
dateAdapter: DateAdapter<Date> = createDateAdapter(dateLib),
): Date[] {
const { numberOfMonths = 1 } = props;
const months: Date[] = [];
for (let i = 0; i < numberOfMonths; i++) {
const month = dateLib.addMonths(firstDisplayedMonth, i);
if (calendarEndMonth && month > calendarEndMonth) {
if (calendarEndMonth && dateAdapter.compare(month, calendarEndMonth) > 0) {
break;
}
months.push(month);
Expand Down
6 changes: 4 additions & 2 deletions packages/react-day-picker/src/helpers/getMonthOptions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { DateLib } from "../classes/DateLib.js";
import type { DropdownOption } from "../components/Dropdown.js";
import { createDateAdapter } from "../internal/dateAdapter.js";
import type { Formatters } from "../types/index.js";

/**
Expand Down Expand Up @@ -31,6 +32,7 @@ export function getMonthOptions(
eachMonthOfInterval,
getMonth,
} = dateLib;
const dateAdapter = createDateAdapter(dateLib);

const months = eachMonthOfInterval({
start: startOfYear(displayMonth),
Expand All @@ -41,8 +43,8 @@ export function getMonthOptions(
const label = formatters.formatMonthDropdown(month, dateLib);
const value = getMonth(month);
const disabled =
(navStart && month < startOfMonth(navStart)) ||
(navEnd && month > startOfMonth(navEnd)) ||
(navStart && dateAdapter.compare(month, startOfMonth(navStart)) < 0) ||
(navEnd && dateAdapter.compare(month, startOfMonth(navEnd)) > 0) ||
false;
return { value, label, disabled };
});
Expand Down
27 changes: 27 additions & 0 deletions packages/react-day-picker/src/helpers/getMonths.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,30 @@ test("should handle months with no dates", () => {
expect(result[0]).toBeInstanceOf(CalendarMonth);
expect(result[0].weeks).toHaveLength(0); // No dates should result in no weeks
});

describe("when dates fall on displayed week boundaries", () => {
const displayMonth = new Date(2024, 0, 1);
const firstBoundaryDate = dateLib.startOfWeek(displayMonth);
const lastBoundaryDate = dateLib.endOfWeek(dateLib.endOfMonth(displayMonth));
const outsideDates = [
dateLib.addDays(firstBoundaryDate, -1),
dateLib.addDays(lastBoundaryDate, 1),
];
let monthDates: Date[];

beforeEach(() => {
const result = getMonths(
[displayMonth],
[outsideDates[0], firstBoundaryDate, lastBoundaryDate, outsideDates[1]],
mockProps,
dateLib,
);
monthDates = result[0].weeks.flatMap((week) =>
week.days.map((day) => day.date),
);
});

test("includes only the boundary dates", () => {
expect(monthDates).toEqual([firstBoundaryDate, lastBoundaryDate]);
});
});
16 changes: 13 additions & 3 deletions packages/react-day-picker/src/helpers/getMonths.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { DateLib } from "../classes/DateLib.js";
import { CalendarDay, CalendarMonth, CalendarWeek } from "../classes/index.js";
import {
createDateAdapter,
type DateAdapter,
} from "../internal/dateAdapter.js";
import type { DayPickerProps } from "../types/index.js";

/**
Expand All @@ -13,6 +17,7 @@ import type { DayPickerProps } from "../types/index.js";
* @param dates The dates to display in the calendar.
* @param props Options from the DayPicker props context.
* @param dateLib The date library to use for date manipulation.
* @param dateAdapter Internal date boundary used for filtering calendar dates.
* @returns An array of `CalendarMonth` objects representing the months to
* display.
*/
Expand All @@ -24,6 +29,7 @@ export function getMonths(
"broadcastCalendar" | "fixedWeeks" | "ISOWeek" | "reverseMonths"
>,
dateLib: DateLib,
dateAdapter: DateAdapter<Date> = createDateAdapter(dateLib),
): CalendarMonth[] {
const {
addDays,
Expand Down Expand Up @@ -54,7 +60,10 @@ export function getMonths(

/** The dates to display in the month. */
const monthDates = dates.filter((date) => {
return date >= firstDateOfFirstWeek && date <= lastDateOfLastWeek;
return (
dateAdapter.compare(date, firstDateOfFirstWeek) >= 0 &&
dateAdapter.compare(date, lastDateOfLastWeek) <= 0
);
});

const nrOfDaysWithFixedWeeks = props.broadcastCalendar ? 35 : 42;
Expand All @@ -63,8 +72,9 @@ export function getMonths(
const extraDates = dates.filter((date) => {
const daysToAdd = nrOfDaysWithFixedWeeks - monthDates.length;
return (
date > lastDateOfLastWeek &&
date <= addDays(lastDateOfLastWeek, daysToAdd)
dateAdapter.compare(date, lastDateOfLastWeek) > 0 &&
dateAdapter.compare(date, addDays(lastDateOfLastWeek, daysToAdd)) <=
0
);
});
monthDates.push(...extraDates);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { DateLib } from "../classes/index.js";
import { createDateAdapter } from "../internal/dateAdapter.js";

/**
* Returns the start date of the week in the broadcast calendar.
Expand All @@ -13,8 +14,9 @@ import type { DateLib } from "../classes/index.js";
* @returns The start date of the broadcast week.
*/
export function startOfBroadcastWeek(date: Date, dateLib: DateLib): Date {
const dateAdapter = createDateAdapter(dateLib);
const firstOfMonth = dateLib.startOfMonth(date);
const dayOfWeek = firstOfMonth.getDay();
const dayOfWeek = dateAdapter.getDay(firstOfMonth);

if (dayOfWeek === 1) {
return firstOfMonth;
Expand Down
127 changes: 127 additions & 0 deletions packages/react-day-picker/src/internal/dateAdapter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { TZDate } from "@date-fns/tz";

import { CalendarDay } from "../classes/CalendarDay.js";
import { DateLib } from "../classes/DateLib.js";

import { createDateAdapter } from "./dateAdapter.js";

describe("createDateAdapter", () => {
describe("when creating a time key for a native Date", () => {
const date = new Date(2024, 0, 15);
let result: number;

beforeEach(() => {
const adapter = createDateAdapter(new DateLib());
result = adapter.timeKey(date);
});

test("uses getTime-compatible values", () => {
expect(result).toBe(date.getTime());
});
});

describe("when creating a time key for a TZDate", () => {
const date = new TZDate(2024, 0, 15, "Pacific/Honolulu");
let result: number;

beforeEach(() => {
const adapter = createDateAdapter(new DateLib());
result = adapter.timeKey(date);
});

test("uses getTime-compatible values", () => {
expect(result).toBe(date.getTime());
});
});

describe("when comparing dates", () => {
const earlier = new Date(2024, 0, 1);
const later = new Date(2024, 0, 2);
let earlierResult: number;
let laterResult: number;
let matchingResult: number;

beforeEach(() => {
const adapter = createDateAdapter(new DateLib());
earlierResult = adapter.compare(earlier, later);
laterResult = adapter.compare(later, earlier);
matchingResult = adapter.compare(earlier, new Date(earlier));
});

test("returns a negative value for earlier dates", () => {
expect(earlierResult).toBeLessThan(0);
});

test("returns a positive value for later dates", () => {
expect(laterResult).toBeGreaterThan(0);
});

test("returns zero for matching timestamps", () => {
expect(matchingResult).toBe(0);
});
});

describe("when creating stable keys", () => {
const date = new Date(2024, 0, 15);
const displayMonth = new Date(2024, 0, 1);
let calendarDay: CalendarDay;
let dayKey: string;
let displayMonthKey: string;
let dateMonthKey: string;

beforeEach(() => {
const dateLib = new DateLib();
const adapter = createDateAdapter(dateLib);
calendarDay = new CalendarDay(date, displayMonth, dateLib);
dayKey = adapter.dayKey(date);
displayMonthKey = adapter.monthKey(displayMonth);
dateMonthKey = adapter.monthKey(date);
});

test("matches the CalendarDay day key", () => {
expect(dayKey).toBe(calendarDay.isoDate);
});

test("matches the CalendarDay display month key", () => {
expect(displayMonthKey).toBe(calendarDay.displayMonthId);
});

test("matches the CalendarDay date month key", () => {
expect(dateMonthKey).toBe(calendarDay.dateMonthId);
});
});

describe("when DateLib overrides are provided", () => {
const nextDate = new Date(2024, 0, 20);
let addDaysResult: Date;
let isSameDayResult: boolean;
let dayKeyResult: string;

beforeEach(() => {
const dateLib = new DateLib(undefined, {
addDays: () => nextDate,
format: (_date, formatStr) => `formatted:${formatStr}`,
isSameDay: () => true,
});
const adapter = createDateAdapter(dateLib);
addDaysResult = adapter.addDays(new Date(2024, 0, 15), 5);
isSameDayResult = adapter.isSameDay(
new Date(2024, 0, 15),
new Date(2024, 0, 16),
);
dayKeyResult = adapter.dayKey(new Date(2024, 0, 15));
});

test("delegates date math", () => {
expect(addDaysResult).toBe(nextDate);
});

test("delegates date equality", () => {
expect(isSameDayResult).toBe(true);
});

test("delegates stable key formatting", () => {
expect(dayKeyResult).toBe("formatted:yyyy-MM-dd");
});
});
});
Loading
Loading