Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion examples/AsiaSaigonTimezone.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ beforeEach(() => {
render(<AsiaSaigonTimezone />);
});

test.skip("the first row should display 7 days", () => {
test("the first row should display 7 days", () => {
expect(
screen.getAllByRole("row")[0].querySelectorAll("[role='gridcell']"),
).toHaveLength(7);
Expand Down
14 changes: 12 additions & 2 deletions examples/AsiaSaigonTimezone.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import React from "react";

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

export function AsiaSaigonTimezone() {
return <DayPicker defaultMonth={new Date(1900, 11)} timeZone="Asia/Saigon" />;
const timeZone = "Asia/Saigon";

return (
<DayPicker
defaultMonth={new TZDate(1900, 11, 1, timeZone)}
timeZone={timeZone}
showOutsideDays
fixedWeeks
noonSafe
/>
);
}
70 changes: 70 additions & 0 deletions examples/PersianNoonSafe.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React from "react";
import { DayPicker, enUS, faIR } from "react-day-picker/persian";

import { render, screen, within } from "@/test/render";

test("Persian noonSafe keeps full weeks in historical time zones", () => {
render(
<DayPicker
timeZone="Asia/Dubai"
noonSafe
fixedWeeks
showOutsideDays
defaultMonth={new Date(1900, 11, 1)}
/>,
);

const grid = screen.getByRole("grid");
const rows = within(grid).getAllByRole("row");
const dayRows = rows.filter(
(row) => within(row).queryAllByRole("gridcell").length > 0,
);

expect(within(dayRows[0]).getAllByRole("gridcell")).toHaveLength(7);
expect(
within(dayRows[dayRows.length - 1]).getAllByRole("gridcell"),
).toHaveLength(7);
});

test("Persian noonSafe renders full first week for historical Saigon month", () => {
render(
<DayPicker
fixedWeeks
hideWeekdays
mode="single"
month={new Date(1497, 11, 1)}
noonSafe
numerals="latn"
timeZone="Asia/Saigon"
locale={faIR}
/>,
);

const grid = screen.getByRole("grid");
const rows = within(grid).getAllByRole("row");
const dayRows = rows.filter(
(row) => within(row).queryAllByRole("gridcell").length > 0,
);

expect(within(dayRows[0]).getAllByRole("gridcell")).toHaveLength(7);
});

test("month dropdown does not repeat month labels when noonSafe is set", () => {
render(
<DayPicker
captionLayout="dropdown"
month={new Date(1300, 11, 1)}
noonSafe
numerals="latn"
timeZone="Asia/Tehran"
locale={enUS}
/>,
);

const monthSelect = screen.getAllByRole("combobox")[0];
const monthLabels = within(monthSelect)
.getAllByRole("option")
.map((option) => option.textContent);

expect(new Set(monthLabels).size).toBe(monthLabels.length);
});
157 changes: 157 additions & 0 deletions examples/TimeZoneNoonSafe.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import userEvent from "@testing-library/user-event";
import React from "react";
import { DayPicker } from "react-day-picker";
import { dateButton, grid } from "@/test/elements";
import { render, screen, within } from "@/test/render";
import { TimeZoneNoonSafe } from "./TimeZoneNoonSafe";

test("the first row should display 7 days", () => {
render(<TimeZoneNoonSafe />);
const grid = screen.getByRole("grid");
const rows = within(grid).getAllByRole("row");
const firstDayRow = rows.find((row) =>
row.querySelector("[role='gridcell']"),
);
expect(firstDayRow?.querySelectorAll("[role='gridcell']")).toHaveLength(7);
});

test("week numbers remain valid when using noonSafe", () => {
render(<TimeZoneNoonSafe showWeekNumber />);
const weekNumbers = screen
.getAllByRole("rowheader")
.map((cell) => Number(cell.textContent))
.filter((value) => Number.isFinite(value));

expect(weekNumbers.length).toBeGreaterThan(0);
weekNumbers.forEach((value) => {
expect(value).toBeGreaterThan(0);
expect(value).toBeLessThan(60);
});
});

test("the last row should display 7 days", () => {
render(<TimeZoneNoonSafe />);
const grid = screen.getByRole("grid");
const rows = within(grid).getAllByRole("row");
const lastDayRow = [...rows]
.reverse()
.find((row) => row.querySelector("[role='gridcell']"));
expect(lastDayRow?.querySelectorAll("[role='gridcell']")).toHaveLength(7);
});

describe("TimeZoneNoonSafe navigation", () => {
test("previous and next month buttons render full weeks", async () => {
render(<TimeZoneNoonSafe />);
const user = userEvent.setup();

const assertFirstAndLastRowHave7Cells = () => {
const [grid] = screen.getAllByRole("grid");
const rows = within(grid).getAllByRole("row");
const dayRows = rows.filter(
(row) => within(row).queryAllByRole("gridcell").length > 0,
);
const firstCells = within(dayRows[0]).getAllByRole("gridcell");
const lastCells = within(dayRows[dayRows.length - 1]).getAllByRole(
"gridcell",
);
expect(firstCells.length).toBe(7);
expect(lastCells.length).toBe(7);
};

// Current month
assertFirstAndLastRowHave7Cells();

// Move to previous month
await user.click(screen.getByRole("button", { name: /previous month/i }));
assertFirstAndLastRowHave7Cells();

// Move to next month twice (back to original and forward one)
await user.click(screen.getByRole("button", { name: /next month/i }));
await user.click(screen.getByRole("button", { name: /next month/i }));
assertFirstAndLastRowHave7Cells();
});
});

test("year dropdown starts at the fromMonth year", () => {
const timeZone = "Asia/Dubai";
const fromMonth = new Date(1880, 0, 1);
render(
<TimeZoneNoonSafe
captionLayout="dropdown"
timeZone={timeZone}
fromMonth={fromMonth}
toMonth={new Date(1885, 11, 31)}
/>,
);
const selectYear = screen.getAllByRole("combobox")[1];
const firstYearOption = within(selectYear).getAllByRole("option")[0];

expect(Number(firstYearOption.getAttribute("value"))).toBe(1880);
});

describe("when props are midnight UTC dates with noonSafe and a time zone", () => {
const originalTz = process.env.TZ;
const isoDate = new Date("2024-03-01T00:00:00.000Z");

beforeAll(() => {
process.env.TZ = "America/Los_Angeles";
});

afterAll(() => {
process.env.TZ = originalTz;
});

test("the month prop is interpreted in the target zone", () => {
render(
<TimeZoneNoonSafe timeZone="Europe/Berlin" noonSafe month={isoDate} />,
);

expect(grid("March 2024")).toBeInTheDocument();
});

test("selected/disabled dates are interpreted in the target zone", () => {
render(
<TimeZoneNoonSafe
timeZone="Europe/Berlin"
noonSafe
month={isoDate}
selected={isoDate}
disabled={isoDate}
/>,
);

const marchFirst = dateButton(new Date(2024, 2, 1));
expect(marchFirst).toBeInTheDocument();
expect(marchFirst).toBeDisabled();
});
});

describe("when the system zone is Honolulu and the target zone is historical Auckland", () => {
const originalTz = process.env.TZ;

beforeAll(() => {
process.env.TZ = "Pacific/Honolulu";
});

afterAll(() => {
process.env.TZ = originalTz;
});

test("noonSafe keeps the full month grid", () => {
render(
<DayPicker
mode="single"
month={new Date(1900, 10, 30)}
noonSafe
timeZone="Pacific/Auckland"
/>,
);

expect(
document.querySelector('[data-day="1900-11-01"]'),
).toBeInTheDocument();
expect(
document.querySelector('[data-day="1900-11-30"]'),
).toBeInTheDocument();
});
});
64 changes: 64 additions & 0 deletions examples/TimeZoneNoonSafe.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React, { useState } from "react";

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

type TimeZoneNoonSafeProps = Partial<DayPickerProps> & {
selected?: Date;
onSelect?: PropsSingle["onSelect"];
};

export function TimeZoneNoonSafe(props: TimeZoneNoonSafeProps = {}) {
const {
timeZone: timeZoneProp,
weekStartsOn: weekStartsOnProp,
selected: selectedProp,
onSelect: onSelectProp,
defaultMonth,
startMonth,
footer,
mode: _mode,
...rest
} = props;

const timeZone = timeZoneProp ?? "Asia/Dubai";
const weekStartsOn = (weekStartsOnProp ??
1) as DayPickerProps["weekStartsOn"];
const [selected, setSelected] = useState<Date | undefined>(
selectedProp ?? new TZDate(1900, 11, 1, timeZone),
);
const onSelect: PropsSingle["onSelect"] =
onSelectProp ??
((nextSelected) => {
setSelected(nextSelected ?? undefined);
});
const selectedValue = selectedProp ?? selected;

return (
<DayPicker
mode="single"
captionLayout="dropdown"
defaultMonth={defaultMonth ?? new TZDate(1900, 11, 1, timeZone)}
timeZone={timeZone}
noonSafe
weekStartsOn={weekStartsOn}
showOutsideDays={rest.showOutsideDays ?? true}
fixedWeeks={rest.fixedWeeks ?? true}
selected={selectedValue}
onSelect={onSelect}
startMonth={startMonth ?? new Date(1880, 0, 1)}
toYear={2025}
footer={
footer ??
(selected
? selected.toString()
: `Pick a day to see it in ${timeZone} time zone.`)
}
{...rest}
/>
);
}
41 changes: 41 additions & 0 deletions examples/TimeZoneNoonSafeSimple.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React, { useState } from "react";

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

export function TimeZoneNoonSafeSimple() {
const timeZone = "Asia/Dubai";
const [selected, setSelected] = useState<Date | undefined>(
new TZDate(1900, 11, 1, timeZone),
);
const [noonSafeEnabled, setNoonSafeEnabled] = useState(true);
const formatter = new Intl.DateTimeFormat("en-US", {
dateStyle: "full",
timeStyle: "short",
timeZone,
});

return (
<div>
<button
aria-pressed={noonSafeEnabled}
onClick={() => setNoonSafeEnabled((current) => !current)}
type="button"
>
{noonSafeEnabled ? "Disable noonSafe" : "Enable noonSafe"}
</button>
<DayPicker
month={new Date(1900, 11, 1)}
mode="single"
timeZone={timeZone}
noonSafe={noonSafeEnabled}
selected={selected}
onSelect={setSelected}
footer={
selected
? `Selected: ${formatter.format(selected)} (${timeZone})`
: `Pick a day to see it in ${timeZone}`
}
/>
</div>
);
}
2 changes: 2 additions & 0 deletions examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ export * from "./TestCase2843";
export * from "./TestCase2864";
export * from "./Testcase1567";
export * from "./TimeZone";
export * from "./TimeZoneNoonSafe";
export * from "./TimeZoneNoonSafeSimple";
export * from "./timezone/TestCase2833";
export * from "./Utc";
export * from "./WeekIso";
Expand Down
Loading