diff --git a/package/src/day-cell.test.tsx b/package/src/day-cell.test.tsx new file mode 100644 index 0000000..4063952 --- /dev/null +++ b/package/src/day-cell.test.tsx @@ -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 = {}) { + 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: a }; + const next = { date, _derivedState, children: b }; + 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, + ); + }); +}); diff --git a/package/src/day-cell.tsx b/package/src/day-cell.tsx index 846ba39..2fdfe96 100644 --- a/package/src/day-cell.tsx +++ b/package/src/day-cell.tsx @@ -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, + next: Record, + handledKeys: ReadonlySet, +): 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 = new Set([ + "render", + "date", + "columnIndex", + "children", + "_derivedState", +]); + +const DAY_BUTTON_HANDLED_KEYS: ReadonlySet = new Set([ + "render", + "date", + "children", + "_derivedState", +]); + +export function dayCellInstancePropsAreEqual( prev: DayCellInstanceProps, next: DayCellInstanceProps, ): boolean { @@ -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) ); } @@ -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;