Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new timeZone prop (experimental) #2467

Merged
merged 15 commits into from
Sep 19, 2024
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ DayPicker is a [React](https://react.dev) component for creating date pickers, c
- 🛠 Extensive set of props for [customizing](./docs/customization.mdx) the calendar.
- 🎨 Minimal design that can be [easily styled](./docs/styling.mdx) with CSS or any CSS framework.
- 📅 Supports [selections](./docs/selection-modes.mdx) of single days, multiple days, ranges of days, or [custom selections](./guides/custom-selections.mdx).
- 🌍 Can be [localized](./docs/localization.mdx) into any language, supports [ISO 8601 dates](./docs/localization.mdx#iso-week-dates), [UTC dates](./docs/localization.mdx#utc-dates), and the [Jalali calendar](./docs/localization.mdx#jalali-calendar).
- 🌍 Can be [localized](./docs/localization.mdx) into any language, supports [ISO 8601 dates](./docs/localization.mdx#iso-week-dates), [time zones](./docs/localization.mdx#time-zone), and the [Jalali calendar](./docs/localization.mdx#jalali-calendar).
- 🦮 Complies with WCAG 2.1 AA requirements for [accessibility](./docs/accessibility.mdx).
- ⚙️ [Customizable components](./guides/custom-components.mdx) to extend the rendered elements.
- 🔤 Easy integration [with input fields](./guides/input-fields.mdx).
Expand Down
19 changes: 19 additions & 0 deletions examples/TimeZone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React, { useState } from "react";

import { DayPicker, TZDate } from "react-day-picker";

export function TimeZone() {
const timezone = "Pacific/Kiritimati";
const [selected, setSelected] = useState<Date | undefined>(
TZDate.tz(timezone)
);
return (
<DayPicker
mode="single"
timeZone={timezone}
selected={selected}
onSelect={setSelected}
footer={selected ? `Selected: ${selected}` : "Pick a day."}
/>
);
}
1 change: 1 addition & 0 deletions examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export * from "./TailwindCSS";
export * from "./Testcase1567";
export * from "./TestCase2047";
export * from "./TestCase2389";
export * from "./TimeZone";
export * from "./Utc";
export * from "./WeekIso";
export * from "./Weeknumber";
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@
"tsconfig.json"
],
"dependencies": {
"date-fns": "^4.1.0"
"date-fns": "^4.1.0",
"@date-fns/tz": "^1.0.2"
},
"devDependencies": {
"@date-fns/utc": "^2.1.0",
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 9 additions & 2 deletions src/DayPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,15 @@ export function DayPicker(props: DayPickerProps) {
} = labels;

const weekdays = useMemo(
() => getWeekdays(locale, props.weekStartsOn, props.ISOWeek, dateLib),
[dateLib, locale, props.ISOWeek, props.weekStartsOn]
() =>
getWeekdays(
locale,
props.weekStartsOn,
props.ISOWeek,
props.timeZone,
dateLib
),
[dateLib, locale, props.ISOWeek, props.timeZone, props.weekStartsOn]
);

const isInteractive = mode !== undefined || onDayClick !== undefined;
Expand Down
4 changes: 2 additions & 2 deletions src/helpers/getDates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function getDates(
maxDate: Date | undefined,
props: Pick<
DayPickerProps,
"ISOWeek" | "fixedWeeks" | "locale" | "weekStartsOn"
"ISOWeek" | "fixedWeeks" | "locale" | "weekStartsOn" | "timeZone"
>,
dateLib: DateLib
): Date[] {
Expand Down Expand Up @@ -53,7 +53,7 @@ export function getDates(
if (maxDate && isAfter(date, maxDate)) {
break;
}
dates.push(new Date(date));
dates.push(date);
}

// If fixed weeks is enabled, add the extra dates to the array
Expand Down
5 changes: 4 additions & 1 deletion src/helpers/getInitialMonth.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { TZDate } from "@date-fns/tz";

import type { DateLib, DayPickerProps } from "../index.js";

/** Return the start month based on the props passed to DayPicker. */
Expand All @@ -12,13 +14,14 @@ export function getInitialMonth(
| "defaultMonth"
| "today"
| "numberOfMonths"
| "timeZone"
>,
dateLib: DateLib
): Date {
const {
month,
defaultMonth,
today = new dateLib.Date(),
today = props.timeZone ? TZDate.tz(props.timeZone) : new dateLib.Date(),
numberOfMonths = 1,
endMonth,
startMonth
Expand Down
1 change: 1 addition & 0 deletions src/helpers/getMonths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export function getMonths(
| "weekStartsOn"
| "reverseMonths"
| "firstWeekContainsDate"
| "timeZone"
>,
dateLib: DateLib
): CalendarMonth[] {
Expand Down
13 changes: 11 additions & 2 deletions src/helpers/getNavMonth.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { TZDate } from "@date-fns/tz";

import type { DateLib, DayPickerProps } from "../types/index.js";

/** Return the start and end months for the calendar navigation. */
Expand All @@ -8,6 +10,7 @@ export function getNavMonths(
| "endMonth"
| "startMonth"
| "today"
| "timeZone"
// Deprecated:
| "fromMonth"
| "fromYear"
Expand Down Expand Up @@ -49,14 +52,20 @@ export function getNavMonths(
} else if (fromYear) {
startMonth = new Date(fromYear, 0, 1);
} else if (!startMonth && hasDropdowns) {
startMonth = startOfYear(addYears(props.today ?? new Date(), -100));
const today =
props.today ??
(props.timeZone ? TZDate.tz(props.timeZone) : new dateLib.Date());
startMonth = startOfYear(addYears(today, -100));
}
if (endMonth) {
endMonth = endOfMonth(endMonth);
} else if (toYear) {
endMonth = new Date(toYear, 11, 31);
} else if (!endMonth && hasDropdowns) {
endMonth = endOfYear(props.today ?? new Date());
const today =
props.today ??
(props.timeZone ? TZDate.tz(props.timeZone) : new dateLib.Date());
endMonth = endOfYear(today);
}
return [
startMonth ? startOfDay(startMonth) : startMonth,
Expand Down
8 changes: 7 additions & 1 deletion src/helpers/getNextFocus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ export function getNextFocus(
calendarEndMonth: Date | undefined,
props: Pick<
DayPickerProps,
"disabled" | "hidden" | "modifiers" | "locale" | "ISOWeek" | "weekStartsOn"
| "disabled"
| "hidden"
| "modifiers"
| "locale"
| "ISOWeek"
| "weekStartsOn"
| "timeZone"
>,
dateLib: DateLib,
attempt: number = 0
Expand Down
2 changes: 1 addition & 1 deletion src/helpers/getWeekdays.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe.each<0 | 1 | 2 | 3 | 4 | 5 | 6>([0, 1, 2, 3, 4, 5, 6])(

describe("when using ISO week", () => {
beforeEach(() => {
result = getWeekdays(es, 3, true, dateLib);
result = getWeekdays(es, 3, true, undefined, dateLib);
});
test("should return Monday as first day", () => {
expect(result[0]).toBeMonday();
Expand Down
8 changes: 6 additions & 2 deletions src/helpers/getWeekdays.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { TZDate } from "@date-fns/tz";

import type { Locale } from "../lib/dateLib.js";
import { dateLib as defaultDateLib } from "../lib/index.js";
import type { DateLib } from "../types/index.js";
Expand All @@ -12,12 +14,14 @@ export function getWeekdays(
weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined,
/** Use ISOWeek instead of locale/ */
ISOWeek?: boolean | undefined,
timeZone?: string | undefined,
/** @ignore */
dateLib: DateLib = defaultDateLib
): Date[] {
const date = timeZone ? TZDate.tz(timeZone) : new dateLib.Date();
const start = ISOWeek
? dateLib.startOfISOWeek(new dateLib.Date())
: dateLib.startOfWeek(new dateLib.Date(), { locale, weekStartsOn });
? dateLib.startOfISOWeek(date)
: dateLib.startOfWeek(date, { locale, weekStartsOn });

const days = [];
for (let i = 0; i < 7; i++) {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/dateLib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export type { Month as DateFnsMonth } from "date-fns";
*/
export const dateLib = {
/** The constructor of the date object. */
Date: Date as GenericDateConstructor,
Date: Date as GenericDateConstructor<Date>,
addDays,
addMonths,
addWeeks,
Expand Down
1 change: 1 addition & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./dateLib.js";
export { TZDate } from "@date-fns/tz";
15 changes: 15 additions & 0 deletions src/types/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,21 @@ export interface PropsBase {
* @see https://en.wikipedia.org/wiki/ISO_week_date
*/
ISOWeek?: boolean;
/**
* The time zone (IANA or UTC offset) to use in the calendar (experimental).
* See
* [Wikipedia](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)
* for the possible values.
*
* Time zones are supported by the `TZDate` object by the
* [@date-fns/tz](https://github.com/date-fns/tz) package. Please refer to the
* package documentation for more information.
*
* @since 9.1.1
* @experimental
* @see https://daypicker.dev/docs/localization#time-zone
*/
timeZone?: string | undefined;
/**
* Change the components used for rendering the calendar elements.
*
Expand Down
11 changes: 9 additions & 2 deletions src/useCalendar.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useEffect } from "react";

import type {
CalendarWeek,
CalendarDay,
Expand Down Expand Up @@ -79,6 +81,7 @@ export function useCalendar(
| "onMonthChange"
| "month"
| "defaultMonth"
| "timeZone"
// Deprecated:
| "fromMonth"
| "fromYear"
Expand All @@ -90,14 +93,18 @@ export function useCalendar(
const [navStart, navEnd] = getNavMonths(props, dateLib);

const { startOfMonth, endOfMonth } = dateLib;

const initialMonth = getInitialMonth(props, dateLib);

const [firstMonth, setFirstMonth] = useControlledValue(
initialMonth,
props.month ? startOfMonth(props.month) : undefined
);

useEffect(() => {
const newInitialMonth = getInitialMonth(props, dateLib);
setFirstMonth(newInitialMonth);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.timeZone]);

/** The months displayed in the calendar. */
const displayMonths = getDisplayMonths(firstMonth, navEnd, props, dateLib);

Expand Down
9 changes: 7 additions & 2 deletions src/useGetModifiers.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { TZDate } from "@date-fns/tz";

import { DayFlag, SelectionState } from "./UI.js";
import { CalendarDay } from "./classes/index.js";
import type { DateLib, DayPickerProps, Modifiers } from "./types/index.js";
Expand All @@ -15,7 +17,7 @@ export function useGetModifiers(
) {
const { disabled, hidden, modifiers, showOutsideDays, today } = props;

const { isSameDay, isSameMonth, Date } = dateLib;
const { isSameDay, isSameMonth } = dateLib;

const internalModifiersMap: Record<DayFlag, CalendarDay[]> = {
[DayFlag.focused]: [],
Expand Down Expand Up @@ -47,7 +49,10 @@ export function useGetModifiers(
Boolean(hidden && dateMatchModifiers(date, hidden, dateLib)) ||
(!showOutsideDays && isOutside);

const isToday = isSameDay(date, today ?? new Date());
const isToday = isSameDay(
date,
today ?? (props.timeZone ? TZDate.tz(props.timeZone) : new dateLib.Date())
);

if (isOutside) internalModifiersMap.outside.push(day);
if (isDisabled) internalModifiersMap.disabled.push(day);
Expand Down
4 changes: 1 addition & 3 deletions src/utc.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import React from "react";

import { UTCDate } from "@date-fns/utc";

import {
DayPicker as DayPickerComponent,
type DayPickerProps
} from "./index.js";

export function DayPicker(props: DayPickerProps) {
return <DayPickerComponent dateLib={{ Date: UTCDate }} {...props} />;
return <DayPickerComponent timeZone="utc" {...props} />;
}
Loading