Skip to content

Commit 5a85843

Browse files
committed
fix: timezone-matcher-support
- Add matchersWithTimeZone utility for timezone-aware date conversions - Integrate timezone conversion for disabled, hidden, and custom modifiers - Ensure consistent date comparisons across different timezones
1 parent aeb782b commit 5a85843

File tree

4 files changed

+436
-0
lines changed

4 files changed

+436
-0
lines changed

src/DayPicker.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { useCalendar } from "./useCalendar.js";
2828
import { type DayPickerContext, dayPickerContext } from "./useDayPicker.js";
2929
import { useFocus } from "./useFocus.js";
3030
import { useSelection } from "./useSelection.js";
31+
import { matchersWithTimeZone } from "./utils/matcherWithTimeZone.js";
3132
import { rangeIncludesDate } from "./utils/rangeIncludesDate.js";
3233
import { isDateRange } from "./utils/typeguards.js";
3334

@@ -77,6 +78,36 @@ export function DayPicker(initialProps: DayPickerProps) {
7778
: undefined,
7879
};
7980
}
81+
82+
// Convert disabled and hidden matchers to use timezone-aware dates
83+
const dateLibInstance = new DateLib({ timeZone: props.timeZone });
84+
if (props.disabled && props.timeZone) {
85+
props.disabled = matchersWithTimeZone(
86+
props.disabled,
87+
props.timeZone,
88+
dateLibInstance,
89+
);
90+
}
91+
if (props.hidden && props.timeZone) {
92+
props.hidden = matchersWithTimeZone(
93+
props.hidden,
94+
props.timeZone,
95+
dateLibInstance,
96+
);
97+
}
98+
99+
// Convert custom modifiers to use timezone-aware dates
100+
if (props.modifiers && props.timeZone) {
101+
const convertedModifiers: typeof props.modifiers = {};
102+
for (const [key, value] of Object.entries(props.modifiers)) {
103+
convertedModifiers[key] = matchersWithTimeZone(
104+
value,
105+
props.timeZone,
106+
dateLibInstance,
107+
);
108+
}
109+
props.modifiers = convertedModifiers;
110+
}
80111
}
81112
const { components, formatters, labels, dateLib, locale, classNames } =
82113
useMemo(() => {

src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from "./addToRange.js";
22
export * from "./dateMatchModifiers.js";
3+
export * from "./matcherWithTimeZone.js";
34
export * from "./rangeContainsDayOfWeek.js";
45
export * from "./rangeContainsModifiers.js";
56
export * from "./rangeIncludesDate.js";
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
import { TZDate } from "@date-fns/tz";
2+
3+
import { DateLib } from "../classes/DateLib.js";
4+
import type {
5+
DateAfter,
6+
DateBefore,
7+
DateInterval,
8+
DateRange,
9+
DayOfWeek,
10+
Matcher,
11+
} from "../types/index.js";
12+
13+
import {
14+
matchersWithTimeZone,
15+
matcherWithTimeZone,
16+
} from "./matcherWithTimeZone.js";
17+
18+
const testDate = new Date(2023, 5, 15, 12, 0, 0); // June 15, 2023, 12:00
19+
const timeZone = "America/New_York";
20+
const dateLib = new DateLib({ timeZone });
21+
22+
describe("matcherWithTimeZone", () => {
23+
describe("when matcher is a boolean", () => {
24+
test("should return the boolean unchanged", () => {
25+
expect(matcherWithTimeZone(true, timeZone, dateLib)).toBe(true);
26+
expect(matcherWithTimeZone(false, timeZone, dateLib)).toBe(false);
27+
});
28+
});
29+
30+
describe("when matcher is a function", () => {
31+
test("should return the function unchanged", () => {
32+
const matcher = () => true;
33+
expect(matcherWithTimeZone(matcher, timeZone, dateLib)).toBe(matcher);
34+
});
35+
});
36+
37+
describe("when matcher is a single Date", () => {
38+
test("should convert to TZDate with specified timezone", () => {
39+
const result = matcherWithTimeZone(testDate, timeZone, dateLib);
40+
expect(result).toBeInstanceOf(TZDate);
41+
expect((result as TZDate).withTimeZone(timeZone).getTime()).toBe(
42+
testDate.getTime(),
43+
);
44+
});
45+
});
46+
47+
describe("when matcher is an array of Dates", () => {
48+
test("should convert all dates to TZDate with specified timezone", () => {
49+
const dates = [testDate, new Date(2023, 5, 16), new Date(2023, 5, 17)];
50+
const result = matcherWithTimeZone(dates, timeZone, dateLib) as TZDate[];
51+
52+
expect(Array.isArray(result)).toBe(true);
53+
expect(result).toHaveLength(3);
54+
result.forEach((date, index) => {
55+
expect(date).toBeInstanceOf(TZDate);
56+
expect(date.withTimeZone(timeZone).getTime()).toBe(
57+
dates[index].getTime(),
58+
);
59+
});
60+
});
61+
});
62+
63+
describe("when matcher is a DateRange", () => {
64+
test("should convert both from and to dates to TZDate", () => {
65+
const dateRange: DateRange = {
66+
from: testDate,
67+
to: new Date(2023, 5, 20),
68+
};
69+
const result = matcherWithTimeZone(
70+
dateRange,
71+
timeZone,
72+
dateLib,
73+
) as DateRange;
74+
75+
expect(result.from).toBeInstanceOf(TZDate);
76+
expect(result.to).toBeInstanceOf(TZDate);
77+
expect((result.from as TZDate).withTimeZone(timeZone).getTime()).toBe(
78+
dateRange.from?.getTime(),
79+
);
80+
expect((result.to as TZDate).withTimeZone(timeZone).getTime()).toBe(
81+
dateRange.to?.getTime(),
82+
);
83+
});
84+
85+
test("should handle undefined from and to dates", () => {
86+
const dateRange: DateRange = {
87+
from: testDate,
88+
to: undefined,
89+
};
90+
const result = matcherWithTimeZone(
91+
dateRange,
92+
timeZone,
93+
dateLib,
94+
) as DateRange;
95+
96+
expect(result.from).toBeInstanceOf(TZDate);
97+
expect(result.to).toBeUndefined();
98+
});
99+
100+
test("should handle completely undefined DateRange", () => {
101+
const dateRange: DateRange = {
102+
from: undefined,
103+
to: undefined,
104+
};
105+
const result = matcherWithTimeZone(
106+
dateRange,
107+
timeZone,
108+
dateLib,
109+
) as DateRange;
110+
111+
expect(result.from).toBeUndefined();
112+
expect(result.to).toBeUndefined();
113+
});
114+
});
115+
116+
describe("when matcher is a DateBefore", () => {
117+
test("should convert before date to TZDate", () => {
118+
const dateBefore: DateBefore = {
119+
before: testDate,
120+
};
121+
const result = matcherWithTimeZone(
122+
dateBefore,
123+
timeZone,
124+
dateLib,
125+
) as DateBefore;
126+
127+
expect(result.before).toBeInstanceOf(TZDate);
128+
expect((result.before as TZDate).withTimeZone(timeZone).getTime()).toBe(
129+
dateBefore.before.getTime(),
130+
);
131+
});
132+
});
133+
134+
describe("when matcher is a DateAfter", () => {
135+
test("should convert after date to TZDate", () => {
136+
const dateAfter: DateAfter = {
137+
after: testDate,
138+
};
139+
const result = matcherWithTimeZone(
140+
dateAfter,
141+
timeZone,
142+
dateLib,
143+
) as DateAfter;
144+
145+
expect(result.after).toBeInstanceOf(TZDate);
146+
expect((result.after as TZDate).withTimeZone(timeZone).getTime()).toBe(
147+
dateAfter.after.getTime(),
148+
);
149+
});
150+
});
151+
152+
describe("when matcher is a DateInterval", () => {
153+
test("should convert both before and after dates to TZDate", () => {
154+
const dateInterval: DateInterval = {
155+
before: new Date(2023, 5, 20), // June 20, 2023
156+
after: new Date(2023, 5, 10), // June 10, 2023 (after should be before "before" for open interval)
157+
};
158+
159+
const result = matcherWithTimeZone(
160+
dateInterval,
161+
timeZone,
162+
dateLib,
163+
) as DateInterval;
164+
165+
expect(result.before).toBeInstanceOf(TZDate);
166+
expect(result.after).toBeInstanceOf(TZDate);
167+
expect((result.before as TZDate).withTimeZone(timeZone).getTime()).toBe(
168+
dateInterval.before.getTime(),
169+
);
170+
expect((result.after as TZDate).withTimeZone(timeZone).getTime()).toBe(
171+
dateInterval.after.getTime(),
172+
);
173+
});
174+
});
175+
176+
describe("when matcher is a DayOfWeek", () => {
177+
test("should return DayOfWeek unchanged", () => {
178+
const dayOfWeek: DayOfWeek = {
179+
dayOfWeek: [0, 6], // Sunday and Saturday
180+
};
181+
const result = matcherWithTimeZone(dayOfWeek, timeZone, dateLib);
182+
183+
expect(result).toBe(dayOfWeek);
184+
});
185+
});
186+
187+
describe("when matcher is an unknown type", () => {
188+
test("should return the matcher unchanged", () => {
189+
const unknownMatcher = { unknown: "property" };
190+
const result = matcherWithTimeZone(
191+
unknownMatcher as unknown as Matcher,
192+
timeZone,
193+
dateLib,
194+
);
195+
196+
expect(result).toBe(unknownMatcher);
197+
});
198+
});
199+
});
200+
201+
describe("matchersWithTimeZone", () => {
202+
describe("when matchers is undefined", () => {
203+
test("should return undefined", () => {
204+
const result = matchersWithTimeZone(undefined, timeZone, dateLib);
205+
expect(result).toBeUndefined();
206+
});
207+
});
208+
209+
describe("when matchers is a single matcher", () => {
210+
test("should convert single date matcher", () => {
211+
const result = matchersWithTimeZone(testDate, timeZone, dateLib);
212+
expect(result).toBeInstanceOf(TZDate);
213+
});
214+
215+
test("should convert single DateRange matcher", () => {
216+
const dateRange: DateRange = {
217+
from: testDate,
218+
to: new Date(2023, 5, 20),
219+
};
220+
const result = matchersWithTimeZone(
221+
dateRange,
222+
timeZone,
223+
dateLib,
224+
) as DateRange;
225+
226+
expect(result.from).toBeInstanceOf(TZDate);
227+
expect(result.to).toBeInstanceOf(TZDate);
228+
});
229+
});
230+
231+
describe("when matchers is an array", () => {
232+
test("should convert all matchers in array", () => {
233+
const matchers: Matcher[] = [
234+
testDate,
235+
{ before: new Date(2023, 5, 20) },
236+
{ after: new Date(2023, 5, 10) },
237+
true,
238+
];
239+
const result = matchersWithTimeZone(
240+
matchers,
241+
timeZone,
242+
dateLib,
243+
) as Matcher[];
244+
245+
expect(Array.isArray(result)).toBe(true);
246+
expect(result).toHaveLength(4);
247+
248+
// First matcher (Date) should be converted to TZDate
249+
expect(result[0]).toBeInstanceOf(TZDate);
250+
251+
// Second matcher (DateBefore) should have before converted to TZDate
252+
expect((result[1] as DateBefore).before).toBeInstanceOf(TZDate);
253+
254+
// Third matcher (DateAfter) should have after converted to TZDate
255+
expect((result[2] as DateAfter).after).toBeInstanceOf(TZDate);
256+
257+
// Fourth matcher (boolean) should remain unchanged
258+
expect(result[3]).toBe(true);
259+
});
260+
261+
test("should handle empty array", () => {
262+
const result = matchersWithTimeZone([], timeZone, dateLib);
263+
expect(result).toEqual([]);
264+
});
265+
});
266+
267+
describe("timezone handling", () => {
268+
test("should work with different timezones", () => {
269+
const pacificTimeZone = "America/Los_Angeles";
270+
const result = matchersWithTimeZone(
271+
testDate,
272+
pacificTimeZone,
273+
new DateLib({ timeZone: pacificTimeZone }),
274+
);
275+
276+
expect(result).toBeInstanceOf(TZDate);
277+
expect((result as TZDate).withTimeZone(pacificTimeZone).getTime()).toBe(
278+
testDate.getTime(),
279+
);
280+
});
281+
282+
test("should work with UTC timezone", () => {
283+
const utcTimeZone = "UTC";
284+
const result = matchersWithTimeZone(
285+
testDate,
286+
utcTimeZone,
287+
new DateLib({ timeZone: utcTimeZone }),
288+
);
289+
290+
expect(result).toBeInstanceOf(TZDate);
291+
expect((result as TZDate).withTimeZone(utcTimeZone).getTime()).toBe(
292+
testDate.getTime(),
293+
);
294+
});
295+
});
296+
});

0 commit comments

Comments
 (0)