|
@@ -12,6 +12,11 @@ import {
|
|
|
toIsoDateYmdFromDate,
|
|
toIsoDateYmdFromDate,
|
|
|
} from "@/lib/frontend/search/dateRange";
|
|
} from "@/lib/frontend/search/dateRange";
|
|
|
|
|
|
|
|
|
|
+import {
|
|
|
|
|
+ normalizeDayClickArgs,
|
|
|
|
|
+ buildCalendarState,
|
|
|
|
|
+} from "@/lib/frontend/search/dateRangePickerUtils";
|
|
|
|
|
+
|
|
|
const ACTIVE_FIELD = Object.freeze({
|
|
const ACTIVE_FIELD = Object.freeze({
|
|
|
FROM: "from",
|
|
FROM: "from",
|
|
|
TO: "to",
|
|
TO: "to",
|
|
@@ -25,66 +30,6 @@ function focusRef(ref) {
|
|
|
requestAnimationFrame(() => ref?.current?.focus?.());
|
|
requestAnimationFrame(() => ref?.current?.focus?.());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function normalizeDayClickArgs(args) {
|
|
|
|
|
- // react-day-picker handler signatures have differed across versions.
|
|
|
|
|
- // We normalize both of these variants:
|
|
|
|
|
- // - onDayClick(day, modifiers, event)
|
|
|
|
|
- // - onDayClick(event, day, modifiers)
|
|
|
|
|
- const a0 = args?.[0];
|
|
|
|
|
- const a1 = args?.[1];
|
|
|
|
|
- const a2 = args?.[2];
|
|
|
|
|
-
|
|
|
|
|
- // Common: (day, modifiers)
|
|
|
|
|
- if (a0 instanceof Date) return { day: a0, modifiers: a1 || null };
|
|
|
|
|
-
|
|
|
|
|
- // Event-first: (event, day, modifiers)
|
|
|
|
|
- if (a1 instanceof Date) return { day: a1, modifiers: a2 || null };
|
|
|
|
|
-
|
|
|
|
|
- return { day: null, modifiers: null };
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function buildCalendarState({ fromDate, toDate, isRangeInvalid }) {
|
|
|
|
|
- let calendarSelected = undefined;
|
|
|
|
|
- let invalidInterval = null;
|
|
|
|
|
-
|
|
|
|
|
- if (fromDate && toDate) {
|
|
|
|
|
- if (isRangeInvalid) {
|
|
|
|
|
- const min = fromDate < toDate ? fromDate : toDate;
|
|
|
|
|
- const max = fromDate < toDate ? toDate : fromDate;
|
|
|
|
|
-
|
|
|
|
|
- calendarSelected = { from: min, to: max };
|
|
|
|
|
- invalidInterval = { from: min, to: max };
|
|
|
|
|
- } else {
|
|
|
|
|
- calendarSelected = { from: fromDate, to: toDate };
|
|
|
|
|
- }
|
|
|
|
|
- } else if (fromDate) {
|
|
|
|
|
- calendarSelected = { from: fromDate, to: undefined };
|
|
|
|
|
- } else if (toDate) {
|
|
|
|
|
- // "to only" -> visually represent as a single-day range
|
|
|
|
|
- calendarSelected = { from: toDate, to: toDate };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const calendarModifiers =
|
|
|
|
|
- isRangeInvalid && invalidInterval
|
|
|
|
|
- ? {
|
|
|
|
|
- invalid_range: invalidInterval,
|
|
|
|
|
- invalid_range_edge: [fromDate, toDate].filter(Boolean),
|
|
|
|
|
- }
|
|
|
|
|
- : undefined;
|
|
|
|
|
-
|
|
|
|
|
- const calendarModifiersClassNames =
|
|
|
|
|
- isRangeInvalid && invalidInterval
|
|
|
|
|
- ? {
|
|
|
|
|
- invalid_range:
|
|
|
|
|
- "bg-destructive/10 text-destructive dark:bg-destructive/20 dark:text-destructive",
|
|
|
|
|
- invalid_range_edge:
|
|
|
|
|
- "!bg-destructive !text-white hover:!bg-destructive/90",
|
|
|
|
|
- }
|
|
|
|
|
- : undefined;
|
|
|
|
|
-
|
|
|
|
|
- return { calendarSelected, calendarModifiers, calendarModifiersClassNames };
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
export function useSearchDateRangePicker({
|
|
export function useSearchDateRangePicker({
|
|
|
from,
|
|
from,
|
|
|
to,
|
|
to,
|
|
@@ -108,8 +53,13 @@ export function useSearchDateRangePicker({
|
|
|
|
|
|
|
|
const isRangeInvalid = validation?.code === "VALIDATION_SEARCH_RANGE";
|
|
const isRangeInvalid = validation?.code === "VALIDATION_SEARCH_RANGE";
|
|
|
|
|
|
|
|
- const now = React.useMemo(() => new Date(), []);
|
|
|
|
|
- const presets = React.useMemo(() => buildDatePresets({ now }), [now]);
|
|
|
|
|
|
|
+ // Presets should reflect the current day even if the app is open across midnight.
|
|
|
|
|
+ // We refresh presets on popover open, and also compute a fresh preset on click.
|
|
|
|
|
+ const [presetsNow, setPresetsNow] = React.useState(() => new Date());
|
|
|
|
|
+
|
|
|
|
|
+ const presets = React.useMemo(() => {
|
|
|
|
|
+ return buildDatePresets({ now: presetsNow });
|
|
|
|
|
+ }, [presetsNow]);
|
|
|
|
|
|
|
|
const presetsRow1 = React.useMemo(() => presets.slice(0, 4), [presets]);
|
|
const presetsRow1 = React.useMemo(() => presets.slice(0, 4), [presets]);
|
|
|
const presetsRow2 = React.useMemo(() => presets.slice(4), [presets]);
|
|
const presetsRow2 = React.useMemo(() => presets.slice(4), [presets]);
|
|
@@ -121,6 +71,9 @@ export function useSearchDateRangePicker({
|
|
|
React.useEffect(() => {
|
|
React.useEffect(() => {
|
|
|
if (!open) return;
|
|
if (!open) return;
|
|
|
|
|
|
|
|
|
|
+ // Refresh preset reference "now" whenever the popover opens.
|
|
|
|
|
+ setPresetsNow(new Date());
|
|
|
|
|
+
|
|
|
// Only run on open transition.
|
|
// Only run on open transition.
|
|
|
// We intentionally do not depend on from/to to avoid focus jumps while clicking.
|
|
// We intentionally do not depend on from/to to avoid focus jumps while clicking.
|
|
|
if (from && !to) {
|
|
if (from && !to) {
|
|
@@ -143,11 +96,11 @@ export function useSearchDateRangePicker({
|
|
|
const hasTo = hasOwn(patch, "to");
|
|
const hasTo = hasOwn(patch, "to");
|
|
|
|
|
|
|
|
onDateRangeChange({
|
|
onDateRangeChange({
|
|
|
- from: hasFrom ? patch.from : from ?? null,
|
|
|
|
|
- to: hasTo ? patch.to : to ?? null,
|
|
|
|
|
|
|
+ from: hasFrom ? patch.from : (from ?? null),
|
|
|
|
|
+ to: hasTo ? patch.to : (to ?? null),
|
|
|
});
|
|
});
|
|
|
},
|
|
},
|
|
|
- [onDateRangeChange, from, to]
|
|
|
|
|
|
|
+ [onDateRangeChange, from, to],
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
const { calendarSelected, calendarModifiers, calendarModifiersClassNames } =
|
|
const { calendarSelected, calendarModifiers, calendarModifiersClassNames } =
|
|
@@ -188,7 +141,7 @@ export function useSearchDateRangePicker({
|
|
|
setActiveField(ACTIVE_FIELD.TO);
|
|
setActiveField(ACTIVE_FIELD.TO);
|
|
|
focusRef(toRef);
|
|
focusRef(toRef);
|
|
|
},
|
|
},
|
|
|
- [disabled, activeField, safeOnDateRangeChange]
|
|
|
|
|
|
|
+ [disabled, activeField, safeOnDateRangeChange],
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
const handleClearFrom = React.useCallback(
|
|
const handleClearFrom = React.useCallback(
|
|
@@ -201,7 +154,7 @@ export function useSearchDateRangePicker({
|
|
|
setActiveField(ACTIVE_FIELD.FROM);
|
|
setActiveField(ACTIVE_FIELD.FROM);
|
|
|
focusRef(fromRef);
|
|
focusRef(fromRef);
|
|
|
},
|
|
},
|
|
|
- [disabled, safeOnDateRangeChange]
|
|
|
|
|
|
|
+ [disabled, safeOnDateRangeChange],
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
const handleClearTo = React.useCallback(
|
|
const handleClearTo = React.useCallback(
|
|
@@ -214,7 +167,7 @@ export function useSearchDateRangePicker({
|
|
|
setActiveField(ACTIVE_FIELD.TO);
|
|
setActiveField(ACTIVE_FIELD.TO);
|
|
|
focusRef(toRef);
|
|
focusRef(toRef);
|
|
|
},
|
|
},
|
|
|
- [disabled, safeOnDateRangeChange]
|
|
|
|
|
|
|
+ [disabled, safeOnDateRangeChange],
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
const handleReset = React.useCallback(() => {
|
|
const handleReset = React.useCallback(() => {
|
|
@@ -229,20 +182,26 @@ export function useSearchDateRangePicker({
|
|
|
const applyPreset = React.useCallback(
|
|
const applyPreset = React.useCallback(
|
|
|
(preset) => {
|
|
(preset) => {
|
|
|
if (disabled) return;
|
|
if (disabled) return;
|
|
|
- if (!preset?.from || !preset?.to) return;
|
|
|
|
|
|
|
+ if (!preset?.key) return;
|
|
|
|
|
+
|
|
|
|
|
+ // Defensive: compute a fresh preset at click-time (midnight edge-case).
|
|
|
|
|
+ const freshList = buildDatePresets({ now: new Date() });
|
|
|
|
|
+ const fresh = freshList.find((p) => p.key === preset.key) || preset;
|
|
|
|
|
+
|
|
|
|
|
+ if (!fresh?.from || !fresh?.to) return;
|
|
|
|
|
|
|
|
- safeOnDateRangeChange({ from: preset.from, to: preset.to });
|
|
|
|
|
|
|
+ safeOnDateRangeChange({ from: fresh.from, to: fresh.to });
|
|
|
|
|
|
|
|
const nextMonth =
|
|
const nextMonth =
|
|
|
- toDateFromIsoDateYmd(preset.from) ||
|
|
|
|
|
- toDateFromIsoDateYmd(preset.to) ||
|
|
|
|
|
|
|
+ toDateFromIsoDateYmd(fresh.from) ||
|
|
|
|
|
+ toDateFromIsoDateYmd(fresh.to) ||
|
|
|
new Date();
|
|
new Date();
|
|
|
|
|
|
|
|
setMonth(nextMonth);
|
|
setMonth(nextMonth);
|
|
|
setActiveField(ACTIVE_FIELD.TO);
|
|
setActiveField(ACTIVE_FIELD.TO);
|
|
|
focusRef(toRef);
|
|
focusRef(toRef);
|
|
|
},
|
|
},
|
|
|
- [disabled, safeOnDateRangeChange]
|
|
|
|
|
|
|
+ [disabled, safeOnDateRangeChange],
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
@@ -264,6 +223,7 @@ export function useSearchDateRangePicker({
|
|
|
calendarSelected,
|
|
calendarSelected,
|
|
|
calendarModifiers,
|
|
calendarModifiers,
|
|
|
calendarModifiersClassNames,
|
|
calendarModifiersClassNames,
|
|
|
|
|
+ isRangeInvalid,
|
|
|
handlePickDay,
|
|
handlePickDay,
|
|
|
handleClearFrom,
|
|
handleClearFrom,
|
|
|
handleClearTo,
|
|
handleClearTo,
|