Skip to content
Merged
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
109 changes: 109 additions & 0 deletions package/src/day-cell.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { Temporal } from "@js-temporal/polyfill";
import { describe, it, expect } from "vitest";
import {
dayCellInstancePropsAreEqual,
dayButtonInnerPropsAreEqual,
} from "./day-cell";

const date = Temporal.PlainDate.from("2026-03-15");

/** Full derived-state object the comparators read, with field overrides. */
function makeDerived(overrides: Record<string, unknown> = {}) {
return {
root: {},
date,
columnIndex: 0,
orientation: "horizontal",
outsideMonth: false,
hidden: false,
selected: false,
today: false,
disabled: false,
focused: false,
rangeStart: false,
rangeEnd: false,
rangeBoundary: false,
inRange: false,
rangeIndex: false,
rangeLength: false,
rangeHasStart: false,
rangeHasEnd: false,
rangePreviewStart: false,
rangePreviewEnd: false,
rangePreviewBoundary: false,
rangePreviewInRange: false,
rangePreviewIndex: false,
rangePreviewLength: false,
rangePreviewHasStart: false,
rangePreviewHasEnd: false,
isTabTarget: false,
...overrides,
};
}

describe("dayCellInstancePropsAreEqual", () => {
it("returns false when a pass-through prop (className) changes", () => {
const _derivedState = makeDerived();
const prev = { date, _derivedState, className: "a" };
const next = { date, _derivedState, className: "b" };
expect(dayCellInstancePropsAreEqual(prev as never, next as never)).toBe(
false,
);
});

it("returns false when a pass-through handler identity changes", () => {
const _derivedState = makeDerived();
const prev = { date, _derivedState, onPointerDown: () => {} };
const next = { date, _derivedState, onPointerDown: () => {} };
expect(dayCellInstancePropsAreEqual(prev as never, next as never)).toBe(
false,
);
});

it("returns true when pass-through props are unchanged", () => {
const _derivedState = makeDerived();
const onPointerDown = () => {};
const prev = { date, _derivedState, className: "a", onPointerDown };
const next = { date, _derivedState, className: "a", onPointerDown };
expect(dayCellInstancePropsAreEqual(prev as never, next as never)).toBe(
true,
);
});

it("still ignores children (fresh React elements every render)", () => {
const _derivedState = makeDerived();
const prev = { date, _derivedState, children: <span>a</span> };
const next = { date, _derivedState, children: <span>b</span> };
expect(dayCellInstancePropsAreEqual(prev as never, next as never)).toBe(
true,
);
});

it("returns false when a derived-state field changes", () => {
const prev = { date, _derivedState: makeDerived({ selected: false }) };
const next = { date, _derivedState: makeDerived({ selected: true }) };
expect(dayCellInstancePropsAreEqual(prev as never, next as never)).toBe(
false,
);
});
});

describe("dayButtonInnerPropsAreEqual", () => {
it("returns false when a pass-through prop changes", () => {
const _derivedState = makeDerived();
const prev = { date, _derivedState, "data-x": "1" };
const next = { date, _derivedState, "data-x": "2" };
expect(dayButtonInnerPropsAreEqual(prev as never, next as never)).toBe(
false,
);
});

it("returns true when pass-through props are unchanged", () => {
const _derivedState = makeDerived();
const prev = { date, _derivedState, "data-x": "1" };
const next = { date, _derivedState, "data-x": "1" };
expect(dayButtonInnerPropsAreEqual(prev as never, next as never)).toBe(
true,
);
});
});
43 changes: 40 additions & 3 deletions package/src/day-cell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,42 @@ function DayCellInstanceFn(props: DayCellInstanceProps) {
);
}

function dayCellInstancePropsAreEqual(
/**
* Shallow-compares the arbitrary pass-through props (className, style, event
* handlers, data-*, …) that day-cell components spread onto their element.
* The explicitly-handled keys (compared separately) are skipped, as is
* `children` — a fresh React element every render, never usefully comparable.
*/
function passThroughPropsEqual(
prev: Record<string, unknown>,
next: Record<string, unknown>,
handledKeys: ReadonlySet<string>,
): boolean {
const prevKeys = Object.keys(prev).filter((k) => !handledKeys.has(k));
const nextKeys = Object.keys(next).filter((k) => !handledKeys.has(k));
if (prevKeys.length !== nextKeys.length) return false;
for (const key of prevKeys) {
if (prev[key] !== next[key]) return false;
}
return true;
}

const DAY_CELL_HANDLED_KEYS: ReadonlySet<string> = new Set([
"render",
"date",
"columnIndex",
"children",
"_derivedState",
]);

const DAY_BUTTON_HANDLED_KEYS: ReadonlySet<string> = new Set([
"render",
"date",
"children",
"_derivedState",
]);

export function dayCellInstancePropsAreEqual(
prev: DayCellInstanceProps,
next: DayCellInstanceProps,
): boolean {
Expand Down Expand Up @@ -328,7 +363,8 @@ function dayCellInstancePropsAreEqual(
a.rangePreviewLength === b.rangePreviewLength &&
a.rangePreviewHasStart === b.rangePreviewHasStart &&
a.rangePreviewHasEnd === b.rangePreviewHasEnd &&
a.isTabTarget === b.isTabTarget
a.isTabTarget === b.isTabTarget &&
passThroughPropsEqual(prev, next, DAY_CELL_HANDLED_KEYS)
);
}

Expand Down Expand Up @@ -525,11 +561,12 @@ function DayButtonInnerFn(
);
}

function dayButtonInnerPropsAreEqual(
export function dayButtonInnerPropsAreEqual(
prev: DayButtonInnerProps,
next: DayButtonInnerProps,
): boolean {
if (prev.date !== next.date || prev.render !== next.render) return false;
if (!passThroughPropsEqual(prev, next, DAY_BUTTON_HANDLED_KEYS)) return false;
const a = prev._derivedState;
const b = next._derivedState;
if (!a || !b) return a === b;
Expand Down
Loading