@trackunit/react-date-and-time-components 2.17.7 → 2.17.9
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.
package/index.cjs.js
CHANGED
|
@@ -580,6 +580,21 @@ const useParseTemporalPeriodFromText = () => {
|
|
|
580
580
|
}, [t, findTemporalDirection, findTemporalUnit]);
|
|
581
581
|
};
|
|
582
582
|
|
|
583
|
+
/**
|
|
584
|
+
* Returns a function that checks if two dates fall on the same calendar day
|
|
585
|
+
* in the user's current timezone.
|
|
586
|
+
*/
|
|
587
|
+
const useSameCalendarDay = () => {
|
|
588
|
+
const { startOf } = reactDateAndTimeHooks.useDateAndTime();
|
|
589
|
+
return react.useCallback((left, right) => {
|
|
590
|
+
if (left === undefined || right === undefined) {
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
// Compare the start-of-day timestamps in the user's timezone
|
|
594
|
+
return startOf(left, "day").getTime() === startOf(right, "day").getTime();
|
|
595
|
+
}, [startOf]);
|
|
596
|
+
};
|
|
597
|
+
|
|
583
598
|
const DEFAULT_DIRECTIONS = ["last", "next"];
|
|
584
599
|
const DEFAULT_COUNT = 7;
|
|
585
600
|
/**
|
|
@@ -657,6 +672,11 @@ const isTemporalDirection = (direction) => {
|
|
|
657
672
|
return typeof direction === "string" && TEMPORAL_DIRECTIONS.includes(direction);
|
|
658
673
|
};
|
|
659
674
|
|
|
675
|
+
/**
|
|
676
|
+
* Checks if two date ranges are identical by comparing their start and end timestamps.
|
|
677
|
+
*/
|
|
678
|
+
const sameDateRange = (left, right) => left?.start?.getTime() === right?.start?.getTime() && left?.end?.getTime() === right?.end?.getTime();
|
|
679
|
+
|
|
660
680
|
/**
|
|
661
681
|
* DayRangeSelect is a popover-based date range picker with preset temporal options (e.g., "Last 7 days", "Next 30 days")
|
|
662
682
|
* and an optional custom calendar picker. Users can search for presets by typing natural language queries like "last 3 months".
|
|
@@ -688,6 +708,7 @@ const DayRangeSelect = ({ className, "data-testid": dataTestId, style, ref, disa
|
|
|
688
708
|
const filterTemporalPeriodsInRange = useFilterTemporalPeriodsInRange();
|
|
689
709
|
const formatTemporalPeriodLabel = useFormatTemporalPeriodLabel();
|
|
690
710
|
const calculateDateRange = useCalculateDateRange();
|
|
711
|
+
const sameCalendarDay = useSameCalendarDay();
|
|
691
712
|
const calculateCustomDateRangeDisabledDays = useCalculateCustomDateRangeDisabledDays();
|
|
692
713
|
const customDateRangeDisabledDays = react.useMemo(() => calculateCustomDateRangeDisabledDays({ maxDaysInRange, allowedDirection, disabledDays }), [calculateCustomDateRangeDisabledDays, maxDaysInRange, allowedDirection, disabledDays]);
|
|
693
714
|
const defaultTemporalPeriods = react.useMemo(() => {
|
|
@@ -707,6 +728,34 @@ const DayRangeSelect = ({ className, "data-testid": dataTestId, style, ref, disa
|
|
|
707
728
|
// input from uncontrolled to controlled, which React warns about.
|
|
708
729
|
const [dateRangeSearchValue, setDateRangeSearchValue] = react.useState("");
|
|
709
730
|
const [selectedRange, setSelectedRange] = react.useState();
|
|
731
|
+
const [emittedDateRange, setEmittedDateRange] = react.useState();
|
|
732
|
+
const controlledDateRange = react.useMemo(() => {
|
|
733
|
+
if (!selectedDateRange) {
|
|
734
|
+
return undefined;
|
|
735
|
+
}
|
|
736
|
+
if (isTemporalPeriod(selectedDateRange)) {
|
|
737
|
+
return calculateDateRange(selectedDateRange);
|
|
738
|
+
}
|
|
739
|
+
return selectedDateRange;
|
|
740
|
+
}, [calculateDateRange, selectedDateRange]);
|
|
741
|
+
const [trackedDateRange, setTrackedDateRange] = react.useState(controlledDateRange);
|
|
742
|
+
// Parents often normalize the range we just emitted (start of day, or "now" when the range ends today)
|
|
743
|
+
// before passing it back. That write-back is still the user's selection.
|
|
744
|
+
const isEchoOfEmission = emittedDateRange !== undefined &&
|
|
745
|
+
controlledDateRange !== undefined &&
|
|
746
|
+
sameCalendarDay(emittedDateRange.start, controlledDateRange.start) &&
|
|
747
|
+
sameCalendarDay(emittedDateRange.end, controlledDateRange.end);
|
|
748
|
+
if (!sameDateRange(trackedDateRange, controlledDateRange)) {
|
|
749
|
+
setTrackedDateRange(controlledDateRange);
|
|
750
|
+
if (emittedDateRange) {
|
|
751
|
+
setEmittedDateRange(undefined);
|
|
752
|
+
}
|
|
753
|
+
if (!isEchoOfEmission && !sameDateRange(selectedRange?.dateRange, controlledDateRange)) {
|
|
754
|
+
setDateRangeSearchValue("");
|
|
755
|
+
setTemporalPeriods(defaultTemporalPeriods);
|
|
756
|
+
setSelectedRange(undefined);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
710
759
|
const currentSelectedRange = react.useMemo(() => {
|
|
711
760
|
// Internal selected range takes precedence
|
|
712
761
|
if (selectedRange) {
|
|
@@ -758,12 +807,14 @@ const DayRangeSelect = ({ className, "data-testid": dataTestId, style, ref, disa
|
|
|
758
807
|
const handleReset = react.useCallback(() => {
|
|
759
808
|
setTemporalPeriods(defaultTemporalPeriods);
|
|
760
809
|
setSelectedRange(undefined);
|
|
810
|
+
setEmittedDateRange(undefined);
|
|
761
811
|
onRangeSelect(undefined);
|
|
762
812
|
handleDateRangeSearch("");
|
|
763
813
|
}, [handleDateRangeSearch, onRangeSelect, defaultTemporalPeriods]);
|
|
764
814
|
const handleOptionsSelect = react.useCallback((value, closeMenu) => {
|
|
765
815
|
closeMenu();
|
|
766
816
|
setSelectedRange(value);
|
|
817
|
+
setEmittedDateRange(value.dateRange);
|
|
767
818
|
onRangeSelect(value);
|
|
768
819
|
}, [onRangeSelect]);
|
|
769
820
|
const handleCustomDateRangeSelect = react.useCallback((dateRange, closeMenu) => {
|
|
@@ -777,9 +828,20 @@ const DayRangeSelect = ({ className, "data-testid": dataTestId, style, ref, disa
|
|
|
777
828
|
temporalPeriod: undefined,
|
|
778
829
|
};
|
|
779
830
|
setSelectedRange(_selectedDateRange);
|
|
831
|
+
setEmittedDateRange(_selectedDateRange.dateRange);
|
|
780
832
|
onRangeSelect(_selectedDateRange);
|
|
781
833
|
}, [onRangeSelect, handleReset]);
|
|
782
|
-
return (jsxRuntime.jsxs(reactComponents.Popover, { onOpenStateChange:
|
|
834
|
+
return (jsxRuntime.jsxs(reactComponents.Popover, { onOpenStateChange: isOpen => {
|
|
835
|
+
if (isOpen) {
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
setIsCustomDateRangeOpen(false);
|
|
839
|
+
// Clear emitted range so a later external change landing on the same
|
|
840
|
+
// calendar days is not mistakenly treated as an echo of the user's selection.
|
|
841
|
+
// Note: search and periods are NOT cleared on close (original behavior).
|
|
842
|
+
// They are only cleared when the controlled range changes externally.
|
|
843
|
+
setEmittedDateRange(undefined);
|
|
844
|
+
}, placement: popoverPlacement, children: [jsxRuntime.jsx(reactComponents.PopoverTrigger, { ref: ref, children: !actionButton ? (jsxRuntime.jsx(reactComponents.Button, { className: cvaTriggerButton({ active: !!selectedDateRange, className }), "data-testid": dataTestId, disabled: disabled, fullWidth: fullWidth, prefix: jsxRuntime.jsx(reactComponents.Icon, { ariaLabel: t("input.icon.tooltip.calendar"), color: disabled ? "neutral" : selectedDateRange ? "primary" : undefined, name: "Calendar", size: size === "large" ? "medium" : "small" }), size: size, style: style, variant: "secondary", children: popoverTriggerLabel ?? t("trigger.selectDateRange") })) : (jsxRuntime.jsx("div", { className: className, "data-testid": dataTestId, style: style, children: actionButton })) }), jsxRuntime.jsx(reactComponents.PopoverContent, { "data-testid": dataTestId ? `${dataTestId}-popover-content` : undefined, children: closeMenu => {
|
|
783
845
|
return !isCustomDateRangeOpen ? (jsxRuntime.jsxs(reactComponents.MenuContent, { className: "min-w-[280px]", children: [showPopoverHeader ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [showDateRangeSearch ? (jsxRuntime.jsx(reactFormComponents.Search, { className: "w-full", "data-testid": dataTestId ? `${dataTestId}-search-input` : undefined, onChange: event => handleDateRangeSearch(event.target.value), onClear: () => handleDateRangeSearch(""), placeholder: allowedDirection === "last"
|
|
784
846
|
? t("input.placeholder.customRange.last")
|
|
785
847
|
: t("input.placeholder.customRange.next"), value: dateRangeSearchValue })) : null, showResetButton ? (jsxRuntime.jsx(reactComponents.Button, { className: "ml-auto", "data-testid": dataTestId ? `${dataTestId}-reset-button` : undefined, disabled: !selectedDateRange, onClick: handleReset, size: "small", variant: "ghost", children: t("layout.actions.reset") })) : null] }), jsxRuntime.jsx("hr", { className: "my-1 border-neutral-200", role: "separator" })] })) : null, jsxRuntime.jsx(DayRangeSelectOptions, { "data-testid": dataTestId ? `${dataTestId}-options` : undefined, onSelect: value => handleOptionsSelect(value, closeMenu), selectedDateRange: currentSelectedRange, temporalPeriods: temporalPeriods }), showCustomDateRangeOption ? (jsxRuntime.jsxs("section", { children: [jsxRuntime.jsx(reactComponents.MenuDivider, {}), jsxRuntime.jsx(reactComponents.MenuItem, { "data-testid": dataTestId ? `${dataTestId}-custom-range` : undefined, label: t("trigger.customRange"), onClick: () => setIsCustomDateRangeOpen(true), selected: isCustomDateRangeSelected, suffix: jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [isCustomDateRangeSelected ? jsxRuntime.jsx(reactComponents.Icon, { color: "primary", name: "Check", size: "small" }) : null, jsxRuntime.jsx(reactComponents.Icon, { name: "ChevronRight", size: "small" })] }) })] })) : null] })) : (jsxRuntime.jsx(reactComponents.Card, { children: jsxRuntime.jsx(DayRangePicker, { cancelButtonLabel: t("layout.actions.back"), disabledDays: customDateRangeDisabledDays, language: "en", onClose: () => setIsCustomDateRangeOpen(false), onRangeSelect: dateRange => handleCustomDateRangeSelect(dateRange
|
package/index.esm.js
CHANGED
|
@@ -578,6 +578,21 @@ const useParseTemporalPeriodFromText = () => {
|
|
|
578
578
|
}, [t, findTemporalDirection, findTemporalUnit]);
|
|
579
579
|
};
|
|
580
580
|
|
|
581
|
+
/**
|
|
582
|
+
* Returns a function that checks if two dates fall on the same calendar day
|
|
583
|
+
* in the user's current timezone.
|
|
584
|
+
*/
|
|
585
|
+
const useSameCalendarDay = () => {
|
|
586
|
+
const { startOf } = useDateAndTime();
|
|
587
|
+
return useCallback((left, right) => {
|
|
588
|
+
if (left === undefined || right === undefined) {
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
// Compare the start-of-day timestamps in the user's timezone
|
|
592
|
+
return startOf(left, "day").getTime() === startOf(right, "day").getTime();
|
|
593
|
+
}, [startOf]);
|
|
594
|
+
};
|
|
595
|
+
|
|
581
596
|
const DEFAULT_DIRECTIONS = ["last", "next"];
|
|
582
597
|
const DEFAULT_COUNT = 7;
|
|
583
598
|
/**
|
|
@@ -655,6 +670,11 @@ const isTemporalDirection = (direction) => {
|
|
|
655
670
|
return typeof direction === "string" && TEMPORAL_DIRECTIONS.includes(direction);
|
|
656
671
|
};
|
|
657
672
|
|
|
673
|
+
/**
|
|
674
|
+
* Checks if two date ranges are identical by comparing their start and end timestamps.
|
|
675
|
+
*/
|
|
676
|
+
const sameDateRange = (left, right) => left?.start?.getTime() === right?.start?.getTime() && left?.end?.getTime() === right?.end?.getTime();
|
|
677
|
+
|
|
658
678
|
/**
|
|
659
679
|
* DayRangeSelect is a popover-based date range picker with preset temporal options (e.g., "Last 7 days", "Next 30 days")
|
|
660
680
|
* and an optional custom calendar picker. Users can search for presets by typing natural language queries like "last 3 months".
|
|
@@ -686,6 +706,7 @@ const DayRangeSelect = ({ className, "data-testid": dataTestId, style, ref, disa
|
|
|
686
706
|
const filterTemporalPeriodsInRange = useFilterTemporalPeriodsInRange();
|
|
687
707
|
const formatTemporalPeriodLabel = useFormatTemporalPeriodLabel();
|
|
688
708
|
const calculateDateRange = useCalculateDateRange();
|
|
709
|
+
const sameCalendarDay = useSameCalendarDay();
|
|
689
710
|
const calculateCustomDateRangeDisabledDays = useCalculateCustomDateRangeDisabledDays();
|
|
690
711
|
const customDateRangeDisabledDays = useMemo(() => calculateCustomDateRangeDisabledDays({ maxDaysInRange, allowedDirection, disabledDays }), [calculateCustomDateRangeDisabledDays, maxDaysInRange, allowedDirection, disabledDays]);
|
|
691
712
|
const defaultTemporalPeriods = useMemo(() => {
|
|
@@ -705,6 +726,34 @@ const DayRangeSelect = ({ className, "data-testid": dataTestId, style, ref, disa
|
|
|
705
726
|
// input from uncontrolled to controlled, which React warns about.
|
|
706
727
|
const [dateRangeSearchValue, setDateRangeSearchValue] = useState("");
|
|
707
728
|
const [selectedRange, setSelectedRange] = useState();
|
|
729
|
+
const [emittedDateRange, setEmittedDateRange] = useState();
|
|
730
|
+
const controlledDateRange = useMemo(() => {
|
|
731
|
+
if (!selectedDateRange) {
|
|
732
|
+
return undefined;
|
|
733
|
+
}
|
|
734
|
+
if (isTemporalPeriod(selectedDateRange)) {
|
|
735
|
+
return calculateDateRange(selectedDateRange);
|
|
736
|
+
}
|
|
737
|
+
return selectedDateRange;
|
|
738
|
+
}, [calculateDateRange, selectedDateRange]);
|
|
739
|
+
const [trackedDateRange, setTrackedDateRange] = useState(controlledDateRange);
|
|
740
|
+
// Parents often normalize the range we just emitted (start of day, or "now" when the range ends today)
|
|
741
|
+
// before passing it back. That write-back is still the user's selection.
|
|
742
|
+
const isEchoOfEmission = emittedDateRange !== undefined &&
|
|
743
|
+
controlledDateRange !== undefined &&
|
|
744
|
+
sameCalendarDay(emittedDateRange.start, controlledDateRange.start) &&
|
|
745
|
+
sameCalendarDay(emittedDateRange.end, controlledDateRange.end);
|
|
746
|
+
if (!sameDateRange(trackedDateRange, controlledDateRange)) {
|
|
747
|
+
setTrackedDateRange(controlledDateRange);
|
|
748
|
+
if (emittedDateRange) {
|
|
749
|
+
setEmittedDateRange(undefined);
|
|
750
|
+
}
|
|
751
|
+
if (!isEchoOfEmission && !sameDateRange(selectedRange?.dateRange, controlledDateRange)) {
|
|
752
|
+
setDateRangeSearchValue("");
|
|
753
|
+
setTemporalPeriods(defaultTemporalPeriods);
|
|
754
|
+
setSelectedRange(undefined);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
708
757
|
const currentSelectedRange = useMemo(() => {
|
|
709
758
|
// Internal selected range takes precedence
|
|
710
759
|
if (selectedRange) {
|
|
@@ -756,12 +805,14 @@ const DayRangeSelect = ({ className, "data-testid": dataTestId, style, ref, disa
|
|
|
756
805
|
const handleReset = useCallback(() => {
|
|
757
806
|
setTemporalPeriods(defaultTemporalPeriods);
|
|
758
807
|
setSelectedRange(undefined);
|
|
808
|
+
setEmittedDateRange(undefined);
|
|
759
809
|
onRangeSelect(undefined);
|
|
760
810
|
handleDateRangeSearch("");
|
|
761
811
|
}, [handleDateRangeSearch, onRangeSelect, defaultTemporalPeriods]);
|
|
762
812
|
const handleOptionsSelect = useCallback((value, closeMenu) => {
|
|
763
813
|
closeMenu();
|
|
764
814
|
setSelectedRange(value);
|
|
815
|
+
setEmittedDateRange(value.dateRange);
|
|
765
816
|
onRangeSelect(value);
|
|
766
817
|
}, [onRangeSelect]);
|
|
767
818
|
const handleCustomDateRangeSelect = useCallback((dateRange, closeMenu) => {
|
|
@@ -775,9 +826,20 @@ const DayRangeSelect = ({ className, "data-testid": dataTestId, style, ref, disa
|
|
|
775
826
|
temporalPeriod: undefined,
|
|
776
827
|
};
|
|
777
828
|
setSelectedRange(_selectedDateRange);
|
|
829
|
+
setEmittedDateRange(_selectedDateRange.dateRange);
|
|
778
830
|
onRangeSelect(_selectedDateRange);
|
|
779
831
|
}, [onRangeSelect, handleReset]);
|
|
780
|
-
return (jsxs(Popover, { onOpenStateChange:
|
|
832
|
+
return (jsxs(Popover, { onOpenStateChange: isOpen => {
|
|
833
|
+
if (isOpen) {
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
setIsCustomDateRangeOpen(false);
|
|
837
|
+
// Clear emitted range so a later external change landing on the same
|
|
838
|
+
// calendar days is not mistakenly treated as an echo of the user's selection.
|
|
839
|
+
// Note: search and periods are NOT cleared on close (original behavior).
|
|
840
|
+
// They are only cleared when the controlled range changes externally.
|
|
841
|
+
setEmittedDateRange(undefined);
|
|
842
|
+
}, placement: popoverPlacement, children: [jsx(PopoverTrigger, { ref: ref, children: !actionButton ? (jsx(Button, { className: cvaTriggerButton({ active: !!selectedDateRange, className }), "data-testid": dataTestId, disabled: disabled, fullWidth: fullWidth, prefix: jsx(Icon, { ariaLabel: t("input.icon.tooltip.calendar"), color: disabled ? "neutral" : selectedDateRange ? "primary" : undefined, name: "Calendar", size: size === "large" ? "medium" : "small" }), size: size, style: style, variant: "secondary", children: popoverTriggerLabel ?? t("trigger.selectDateRange") })) : (jsx("div", { className: className, "data-testid": dataTestId, style: style, children: actionButton })) }), jsx(PopoverContent, { "data-testid": dataTestId ? `${dataTestId}-popover-content` : undefined, children: closeMenu => {
|
|
781
843
|
return !isCustomDateRangeOpen ? (jsxs(MenuContent, { className: "min-w-[280px]", children: [showPopoverHeader ? (jsxs("div", { children: [jsxs("div", { className: "flex flex-col gap-1", children: [showDateRangeSearch ? (jsx(Search, { className: "w-full", "data-testid": dataTestId ? `${dataTestId}-search-input` : undefined, onChange: event => handleDateRangeSearch(event.target.value), onClear: () => handleDateRangeSearch(""), placeholder: allowedDirection === "last"
|
|
782
844
|
? t("input.placeholder.customRange.last")
|
|
783
845
|
: t("input.placeholder.customRange.next"), value: dateRangeSearchValue })) : null, showResetButton ? (jsx(Button, { className: "ml-auto", "data-testid": dataTestId ? `${dataTestId}-reset-button` : undefined, disabled: !selectedDateRange, onClick: handleReset, size: "small", variant: "ghost", children: t("layout.actions.reset") })) : null] }), jsx("hr", { className: "my-1 border-neutral-200", role: "separator" })] })) : null, jsx(DayRangeSelectOptions, { "data-testid": dataTestId ? `${dataTestId}-options` : undefined, onSelect: value => handleOptionsSelect(value, closeMenu), selectedDateRange: currentSelectedRange, temporalPeriods: temporalPeriods }), showCustomDateRangeOption ? (jsxs("section", { children: [jsx(MenuDivider, {}), jsx(MenuItem, { "data-testid": dataTestId ? `${dataTestId}-custom-range` : undefined, label: t("trigger.customRange"), onClick: () => setIsCustomDateRangeOpen(true), selected: isCustomDateRangeSelected, suffix: jsxs(Fragment, { children: [isCustomDateRangeSelected ? jsx(Icon, { color: "primary", name: "Check", size: "small" }) : null, jsx(Icon, { name: "ChevronRight", size: "small" })] }) })] })) : null] })) : (jsx(Card, { children: jsx(DayRangePicker, { cancelButtonLabel: t("layout.actions.back"), disabledDays: customDateRangeDisabledDays, language: "en", onClose: () => setIsCustomDateRangeOpen(false), onRangeSelect: dateRange => handleCustomDateRangeSelect(dateRange
|
package/package.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/react-date-and-time-components",
|
|
3
|
-
"version": "2.17.
|
|
3
|
+
"version": "2.17.9",
|
|
4
4
|
"repository": "https://github.com/Trackunit/manager",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=24.x"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@trackunit/react-components": "6.0.
|
|
11
|
-
"@trackunit/date-and-time-utils": "1.20.
|
|
12
|
-
"@trackunit/react-date-and-time-hooks": "2.17.
|
|
13
|
-
"@trackunit/css-class-variance-utilities": "2.6.
|
|
14
|
-
"@trackunit/ui-icons": "1.20.
|
|
15
|
-
"@trackunit/shared-utils": "1.22.
|
|
16
|
-
"@trackunit/i18n-library-translation": "2.14.
|
|
17
|
-
"@trackunit/react-form-components": "2.17.
|
|
10
|
+
"@trackunit/react-components": "6.0.7",
|
|
11
|
+
"@trackunit/date-and-time-utils": "1.20.17",
|
|
12
|
+
"@trackunit/react-date-and-time-hooks": "2.17.8",
|
|
13
|
+
"@trackunit/css-class-variance-utilities": "2.6.17",
|
|
14
|
+
"@trackunit/ui-icons": "1.20.17",
|
|
15
|
+
"@trackunit/shared-utils": "1.22.17",
|
|
16
|
+
"@trackunit/i18n-library-translation": "2.14.7",
|
|
17
|
+
"@trackunit/react-form-components": "2.17.8",
|
|
18
18
|
"string-ts": "^2.0.0",
|
|
19
19
|
"tailwind-merge": "^3.7.0",
|
|
20
20
|
"react-calendar": "^6.0.1"
|