@carlonicora/nextjs-jsonapi 3.3.5 → 3.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,6 +9,39 @@ import { useI18nDateFnsLocale } from "../../i18n";
9
9
  import { Button, Calendar, Popover, PopoverContent, PopoverTrigger } from "../../shadcnui";
10
10
  import { cn } from "../../utils";
11
11
 
12
+ /**
13
+ * Decides the range to hold after react-day-picker reports a selection.
14
+ *
15
+ * react-day-picker's `addToRange` NEVER moves `from` when the current range is
16
+ * already COMPLETE and the clicked day falls after it — it returns
17
+ * `{ from, to: clickedDay }` (see its `from && to` branch). So "did `from`
18
+ * change?" cannot distinguish "the user is starting a new range" from "the user
19
+ * moved the end": it is true only for a click BEFORE `from`.
20
+ *
21
+ * That is the asymmetry this component shipped with. Its default range is the
22
+ * whole CURRENT month, so clicking any day in the current month landed after
23
+ * `from` and silently became the END date — the start could never be moved
24
+ * forward — while clicking a day in a PAST month fell before `from`, changed it,
25
+ * and appeared to work.
26
+ *
27
+ * A click on a complete range therefore starts a NEW range at the clicked day,
28
+ * which is the conventional range-picker behaviour. `triggerDate` is the day
29
+ * react-day-picker reports as the one that was clicked (`onSelect`'s second
30
+ * argument) — the only reliable source for it, since the computed range hides it.
31
+ */
32
+ export function nextDateRange(params: {
33
+ current: DateRange | undefined;
34
+ computed: DateRange | undefined;
35
+ triggerDate: Date;
36
+ }): DateRange | undefined {
37
+ const { current, computed, triggerDate } = params;
38
+ // react-day-picker returns undefined to DESELECT (clicking the only selected
39
+ // day of a single-day range). Honour that before anything else.
40
+ if (!computed) return undefined;
41
+ if (current?.from && current?.to) return { from: triggerDate, to: undefined };
42
+ return computed;
43
+ }
44
+
12
45
  type DateRangeSelectorProps = {
13
46
  onDateChange: (date?: DateRange) => void;
14
47
  avoidSettingDates?: boolean;
@@ -49,18 +82,9 @@ export function DateRangeSelector({ onDateChange, avoidSettingDates, showPreviou
49
82
  }
50
83
  }, [date, prevRange, onDateChange]);
51
84
 
52
- // Custom handler to reset end date if a new start date is picked
53
- const handleSelect = (range: DateRange | undefined) => {
54
- if (!range) {
55
- setDate(undefined);
56
- return;
57
- }
58
- // If a new start date is picked, reset end date
59
- if (range.from && (!date?.from || range.from.getTime() !== date.from.getTime())) {
60
- setDate({ from: range.from, to: undefined });
61
- } else {
62
- setDate(range);
63
- }
85
+ // A click on an already-complete range starts a new one see nextDateRange().
86
+ const handleSelect = (range: DateRange | undefined, triggerDate: Date) => {
87
+ setDate(nextDateRange({ current: date, computed: range, triggerDate }));
64
88
  };
65
89
 
66
90
  // Show placeholder button during SSR to prevent hydration mismatch
@@ -0,0 +1,74 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { nextDateRange } from "../DateRangeSelector";
3
+
4
+ // The dates below mirror the component's own default range: the whole current
5
+ // month. "Today" is irrelevant to nextDateRange() — only the shape of the range
6
+ // matters — so fixed dates are used rather than a faked clock.
7
+ const AUG_1 = new Date(2026, 7, 1);
8
+ const AUG_10 = new Date(2026, 7, 10);
9
+ const AUG_20 = new Date(2026, 7, 20);
10
+ const AUG_31 = new Date(2026, 7, 31);
11
+ const JUL_10 = new Date(2026, 6, 10);
12
+
13
+ const CURRENT_MONTH = { from: AUG_1, to: AUG_31 };
14
+
15
+ describe("nextDateRange", () => {
16
+ // The reported bug: with the default (complete) current-month range, every
17
+ // click in the current month lands AFTER `from`, so react-day-picker returns
18
+ // { from: AUG_1, to: clicked } and `from` never moves. The selector must read
19
+ // that as "start a new range at the clicked day", not "move the end".
20
+ it("starts a new range when a complete range is clicked inside the current month", () => {
21
+ expect(
22
+ nextDateRange({
23
+ current: CURRENT_MONTH,
24
+ computed: { from: AUG_1, to: AUG_10 }, // what addToRange returns
25
+ triggerDate: AUG_10,
26
+ }),
27
+ ).toEqual({ from: AUG_10, to: undefined });
28
+ });
29
+
30
+ // The half that always worked, and which must keep working: a click BEFORE
31
+ // `from` does move `from`, so react-day-picker returns { from: clicked, to }.
32
+ // Same outcome either way — the clicked day becomes the new start.
33
+ it("starts a new range when a complete range is clicked in a past month", () => {
34
+ expect(
35
+ nextDateRange({
36
+ current: CURRENT_MONTH,
37
+ computed: { from: JUL_10, to: AUG_31 },
38
+ triggerDate: JUL_10,
39
+ }),
40
+ ).toEqual({ from: JUL_10, to: undefined });
41
+ });
42
+
43
+ it("completes an in-progress range with the second click", () => {
44
+ expect(
45
+ nextDateRange({
46
+ current: { from: AUG_10, to: undefined },
47
+ computed: { from: AUG_10, to: AUG_20 },
48
+ triggerDate: AUG_20,
49
+ }),
50
+ ).toEqual({ from: AUG_10, to: AUG_20 });
51
+ });
52
+
53
+ it("keeps react-day-picker's swap when the second click precedes the start", () => {
54
+ expect(
55
+ nextDateRange({
56
+ current: { from: AUG_20, to: undefined },
57
+ computed: { from: AUG_10, to: AUG_20 },
58
+ triggerDate: AUG_10,
59
+ }),
60
+ ).toEqual({ from: AUG_10, to: AUG_20 });
61
+ });
62
+
63
+ it("opens a range from the empty state", () => {
64
+ expect(
65
+ nextDateRange({ current: undefined, computed: { from: AUG_10, to: AUG_10 }, triggerDate: AUG_10 }),
66
+ ).toEqual({ from: AUG_10, to: AUG_10 });
67
+ });
68
+
69
+ // react-day-picker returns undefined to DESELECT; that must survive the
70
+ // complete-range branch rather than being turned into a new start.
71
+ it("clears when react-day-picker deselects", () => {
72
+ expect(nextDateRange({ current: { from: AUG_10, to: AUG_10 }, computed: undefined, triggerDate: AUG_10 })).toBeUndefined();
73
+ });
74
+ });