Skip to content

Commit

Permalink
feat: new timeZone prop (experimental) (#2467)
Browse files Browse the repository at this point in the history
  • Loading branch information
gpbl authored Sep 19, 2024
1 parent 80a5f14 commit b5e554a
Show file tree
Hide file tree
Showing 22 changed files with 169 additions and 101 deletions.
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

0 comments on commit b5e554a

Please sign in to comment.