@trackunit/react-form-components 1.22.15 → 1.22.18

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
@@ -623,6 +623,50 @@ const dateFieldStoredValueToDisplayString = (stored, locale) => {
623
623
  return stored;
624
624
  };
625
625
 
626
+ /**
627
+ * Converts a Date instant to the canonical YYYY-MM-DD for a specific IANA time zone.
628
+ * This preserves the calendar day the user selected in their local zone.
629
+ */
630
+ const dateToCalendarDayString = ({ date, timeZoneId }) => {
631
+ if (!date || Number.isNaN(date.getTime()))
632
+ return "";
633
+ const resolvedTimeZoneId = timeZoneId ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
634
+ return dateAndTimeUtils.toZonedDateTimeUtil(date, resolvedTimeZoneId).toPlainDate().toString();
635
+ };
636
+ /**
637
+ * Clamps a date to [minDate, maxDate] by comparing calendar days in a specific timezone.
638
+ * Returns the original `date` when it's in range, or the relevant boundary date when out of range.
639
+ */
640
+ const clampDateToCalendarDayRange = ({ date, minDate, maxDate, timeZoneId, }) => {
641
+ if (date === null || date === undefined)
642
+ return null;
643
+ const day = dateToCalendarDayString({ date, timeZoneId });
644
+ if (day === "")
645
+ return null;
646
+ if (minDate !== undefined) {
647
+ const minDay = dateToCalendarDayString({ date: minDate, timeZoneId });
648
+ if (minDay !== "" && day < minDay)
649
+ return minDate;
650
+ }
651
+ if (maxDate !== undefined) {
652
+ const maxDay = dateToCalendarDayString({ date: maxDate, timeZoneId });
653
+ if (maxDay !== "" && day > maxDay)
654
+ return maxDate;
655
+ }
656
+ return date;
657
+ };
658
+ /**
659
+ * Returns true when a date falls outside [minDate, maxDate] by calendar day in a specific timezone.
660
+ */
661
+ const isCalendarDayOutOfRange = ({ date, minDate, maxDate, timeZoneId, }) => {
662
+ if (!date)
663
+ return false;
664
+ const clamped = clampDateToCalendarDayRange({ date, minDate, maxDate, timeZoneId });
665
+ if (!clamped)
666
+ return false;
667
+ return clamped.getTime() !== date.getTime();
668
+ };
669
+
626
670
  function parseToDate(v) {
627
671
  if (v === undefined || v === "")
628
672
  return undefined;
@@ -639,20 +683,6 @@ function parseToDate(v) {
639
683
  const fallback = new Date(str);
640
684
  return Number.isNaN(fallback.getTime()) ? undefined : fallback;
641
685
  }
642
- function startOfDayMs(d) {
643
- return dateAndTimeUtils.toDateUtil(dateAndTimeUtils.startOfDayUtil(d)).getTime();
644
- }
645
- /** Clamp a date to [minDate, maxDate]; returns the same date if in range or no bounds. */
646
- function clampToRange(date, minDate, maxDate) {
647
- if (date === null || date === undefined)
648
- return null;
649
- const dayStart = startOfDayMs(date);
650
- if (minDate !== undefined && dayStart < startOfDayMs(minDate))
651
- return minDate;
652
- if (maxDate !== undefined && dayStart > startOfDayMs(maxDate))
653
- return maxDate;
654
- return date;
655
- }
656
686
  /**
657
687
  * A wrapper around BaseInput with a pop-up day picker using the same calendar UI as DayPicker.
658
688
  *
@@ -662,8 +692,9 @@ function clampToRange(date, minDate, maxDate) {
662
692
  * NOTE: If shown with a label, please use the `DateField` component instead.
663
693
  */
664
694
  const DateBaseInput = ({ min, max, defaultValue, value, ref, onChange, suffix: suffixProp, "data-testid": dataTestId, ...rest }) => {
695
+ const timeZoneId = Intl.DateTimeFormat().resolvedOptions().timeZone;
665
696
  const isControlled = value !== undefined;
666
- const [internalValue, setInternalValue] = react.useState(() => dateAndTimeUtils.dateToYYYYMMDDUtil(parseToDate(defaultValue)));
697
+ const [internalValue, setInternalValue] = react.useState(() => dateToCalendarDayString({ date: parseToDate(defaultValue), timeZoneId }));
667
698
  const [pendingDate, setPendingDate] = react.useState(null);
668
699
  const [t, i18n] = useTranslation();
669
700
  const locale = i18n.resolvedLanguage ?? i18n.language;
@@ -671,7 +702,7 @@ const DateBaseInput = ({ min, max, defaultValue, value, ref, onChange, suffix: s
671
702
  const resolvedValue = isControlled
672
703
  ? typeof value === "string"
673
704
  ? dateFieldStoredValueToDisplayString(value, locale)
674
- : dateFieldStoredValueToDisplayString(dateAndTimeUtils.dateToYYYYMMDDUtil(parseToDate(value)), locale)
705
+ : dateFieldStoredValueToDisplayString(dateToCalendarDayString({ date: parseToDate(value), timeZoneId }), locale)
675
706
  : dateFieldStoredValueToDisplayString(internalValue, locale);
676
707
  const selectedDate = isControlled && typeof value === "string"
677
708
  ? (dateAndTimeUtils.parseYYYYMMDDUtil(value) ?? undefined)
@@ -686,13 +717,8 @@ const DateBaseInput = ({ min, max, defaultValue, value, ref, onChange, suffix: s
686
717
  return false;
687
718
  const minDate = parseToDate(min);
688
719
  const maxDate = parseToDate(max);
689
- const dayStart = startOfDayMs(date);
690
- if (minDate !== undefined && dayStart < startOfDayMs(minDate))
691
- return true;
692
- if (maxDate !== undefined && dayStart > startOfDayMs(maxDate))
693
- return true;
694
- return false;
695
- }, [min, max]);
720
+ return isCalendarDayOutOfRange({ date, minDate, maxDate, timeZoneId });
721
+ }, [min, max, timeZoneId]);
696
722
  const handleCalendarChange = react.useCallback((next) => {
697
723
  setPendingDate(next);
698
724
  }, []);
@@ -709,21 +735,21 @@ const DateBaseInput = ({ min, max, defaultValue, value, ref, onChange, suffix: s
709
735
  const handleApply = react.useCallback((closePopover) => {
710
736
  const minDate = parseToDate(min);
711
737
  const maxDate = parseToDate(max);
712
- const clamped = clampToRange(pendingDate, minDate, maxDate);
713
- const str = clamped ? dateAndTimeUtils.dateToYYYYMMDDUtil(clamped) : "";
738
+ const clamped = clampDateToCalendarDayRange({ date: pendingDate, minDate, maxDate, timeZoneId });
739
+ const str = clamped ? dateToCalendarDayString({ date: clamped, timeZoneId }) : "";
714
740
  if (!isControlled)
715
741
  setInternalValue(str);
716
742
  onChange?.(createInputChangeEvent(str, inputRef.current));
717
743
  closePopover();
718
- }, [isControlled, min, max, onChange, pendingDate]);
744
+ }, [isControlled, min, max, onChange, pendingDate, timeZoneId]);
719
745
  const handleInputChange = react.useCallback((e) => {
720
746
  const raw = e.target.value;
721
747
  const parsed = parseDateFieldInputForChange(raw, locale);
722
748
  const minDate = parseToDate(min);
723
749
  const maxDate = parseToDate(max);
724
750
  if (parsed !== null) {
725
- const clamped = clampToRange(parsed, minDate, maxDate);
726
- const str = clamped ? dateAndTimeUtils.dateToYYYYMMDDUtil(clamped) : "";
751
+ const clamped = clampDateToCalendarDayRange({ date: parsed, minDate, maxDate, timeZoneId });
752
+ const str = clamped ? dateToCalendarDayString({ date: clamped, timeZoneId }) : "";
727
753
  if (!isControlled)
728
754
  setInternalValue(str);
729
755
  onChange?.(createInputChangeEvent(str, inputRef.current));
@@ -733,7 +759,7 @@ const DateBaseInput = ({ min, max, defaultValue, value, ref, onChange, suffix: s
733
759
  setInternalValue(raw);
734
760
  onChange?.(e);
735
761
  }
736
- }, [isControlled, locale, min, max, onChange]);
762
+ }, [isControlled, locale, min, max, onChange, timeZoneId]);
737
763
  return (jsxRuntime.jsxs(reactComponents.Popover, { onOpenStateChange: open => open && syncPendingFromValue(), placement: "bottom-start", children: [jsxRuntime.jsx(reactComponents.PopoverTrigger, { children: jsxRuntime.jsx("div", { className: tailwindMerge.twMerge("flex w-full min-w-0 cursor-pointer items-center", (Boolean(rest.disabled) || Boolean(rest.readOnly)) && "pointer-events-none"), children: jsxRuntime.jsx(BaseInput, { ...rest, "aria-readonly": true, className: tailwindMerge.twMerge("w-full min-w-0", rest.className), "data-testid": dataTestId ? `${dataTestId}-input` : undefined, onChange: handleInputChange, placeholder: rest.placeholder ?? t("dateField.placeholder"), ref: inputRef, suffix: suffixProp ?? (jsxRuntime.jsx(reactComponents.Icon, { "aria-label": undefined, className: Boolean(rest.disabled) || Boolean(rest.readOnly) ? "text-neutral-500" : undefined, "data-testid": dataTestId ? `${dataTestId}-calendar` : "calendar", name: "Calendar", size: "medium", type: "solid" })), type: "text", value: resolvedValue }) }) }), jsxRuntime.jsx(reactComponents.PopoverContent, { children: closePopover => {
738
764
  const displayDate = pendingDate ?? selectedDate ?? null;
739
765
  return (jsxRuntime.jsxs("div", { className: tailwindMerge.twMerge("flex w-min flex-col overflow-hidden rounded-md border border-neutral-300 bg-white p-0"), children: [jsxRuntime.jsx(ReactCalendar, { allowPartialRange: true, className: tailwindMerge.twMerge("custom-day-picker", "range-picker", "p-0"), defaultActiveStartDate: displayDate ?? undefined, defaultView: "month", locale: locale, onChange: val => {
package/index.esm.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
2
  import { registerTranslations, useNamespaceTranslation, NamespaceTrans } from '@trackunit/i18n-library-translation';
3
- import { parseYYYYMMDDUtil, formatShortDateUtil, parseValidDate, toDateUtil, startOfDayUtil, dateToYYYYMMDDUtil } from '@trackunit/date-and-time-utils';
3
+ import { parseYYYYMMDDUtil, formatShortDateUtil, parseValidDate, toDateUtil, startOfDayUtil, toZonedDateTimeUtil } from '@trackunit/date-and-time-utils';
4
4
  import { IconButton, Icon, Tooltip, Popover, PopoverTrigger, PopoverContent, Button, cvaMenu, cvaMenuList, Tag, useIsTextTruncated, ZStack, MenuItem, useMeasure, useDebounce, useMergeRefs, Spinner, useScrollBlock, Text, Heading, useIsFirstRender } from '@trackunit/react-components';
5
5
  import { useRef, useEffect, useImperativeHandle, useState, useCallback, cloneElement, isValidElement, useLayoutEffect, useMemo, useReducer, createContext, useContext, useId } from 'react';
6
6
  import ReactCalendar from 'react-calendar';
@@ -622,6 +622,50 @@ const dateFieldStoredValueToDisplayString = (stored, locale) => {
622
622
  return stored;
623
623
  };
624
624
 
625
+ /**
626
+ * Converts a Date instant to the canonical YYYY-MM-DD for a specific IANA time zone.
627
+ * This preserves the calendar day the user selected in their local zone.
628
+ */
629
+ const dateToCalendarDayString = ({ date, timeZoneId }) => {
630
+ if (!date || Number.isNaN(date.getTime()))
631
+ return "";
632
+ const resolvedTimeZoneId = timeZoneId ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
633
+ return toZonedDateTimeUtil(date, resolvedTimeZoneId).toPlainDate().toString();
634
+ };
635
+ /**
636
+ * Clamps a date to [minDate, maxDate] by comparing calendar days in a specific timezone.
637
+ * Returns the original `date` when it's in range, or the relevant boundary date when out of range.
638
+ */
639
+ const clampDateToCalendarDayRange = ({ date, minDate, maxDate, timeZoneId, }) => {
640
+ if (date === null || date === undefined)
641
+ return null;
642
+ const day = dateToCalendarDayString({ date, timeZoneId });
643
+ if (day === "")
644
+ return null;
645
+ if (minDate !== undefined) {
646
+ const minDay = dateToCalendarDayString({ date: minDate, timeZoneId });
647
+ if (minDay !== "" && day < minDay)
648
+ return minDate;
649
+ }
650
+ if (maxDate !== undefined) {
651
+ const maxDay = dateToCalendarDayString({ date: maxDate, timeZoneId });
652
+ if (maxDay !== "" && day > maxDay)
653
+ return maxDate;
654
+ }
655
+ return date;
656
+ };
657
+ /**
658
+ * Returns true when a date falls outside [minDate, maxDate] by calendar day in a specific timezone.
659
+ */
660
+ const isCalendarDayOutOfRange = ({ date, minDate, maxDate, timeZoneId, }) => {
661
+ if (!date)
662
+ return false;
663
+ const clamped = clampDateToCalendarDayRange({ date, minDate, maxDate, timeZoneId });
664
+ if (!clamped)
665
+ return false;
666
+ return clamped.getTime() !== date.getTime();
667
+ };
668
+
625
669
  function parseToDate(v) {
626
670
  if (v === undefined || v === "")
627
671
  return undefined;
@@ -638,20 +682,6 @@ function parseToDate(v) {
638
682
  const fallback = new Date(str);
639
683
  return Number.isNaN(fallback.getTime()) ? undefined : fallback;
640
684
  }
641
- function startOfDayMs(d) {
642
- return toDateUtil(startOfDayUtil(d)).getTime();
643
- }
644
- /** Clamp a date to [minDate, maxDate]; returns the same date if in range or no bounds. */
645
- function clampToRange(date, minDate, maxDate) {
646
- if (date === null || date === undefined)
647
- return null;
648
- const dayStart = startOfDayMs(date);
649
- if (minDate !== undefined && dayStart < startOfDayMs(minDate))
650
- return minDate;
651
- if (maxDate !== undefined && dayStart > startOfDayMs(maxDate))
652
- return maxDate;
653
- return date;
654
- }
655
685
  /**
656
686
  * A wrapper around BaseInput with a pop-up day picker using the same calendar UI as DayPicker.
657
687
  *
@@ -661,8 +691,9 @@ function clampToRange(date, minDate, maxDate) {
661
691
  * NOTE: If shown with a label, please use the `DateField` component instead.
662
692
  */
663
693
  const DateBaseInput = ({ min, max, defaultValue, value, ref, onChange, suffix: suffixProp, "data-testid": dataTestId, ...rest }) => {
694
+ const timeZoneId = Intl.DateTimeFormat().resolvedOptions().timeZone;
664
695
  const isControlled = value !== undefined;
665
- const [internalValue, setInternalValue] = useState(() => dateToYYYYMMDDUtil(parseToDate(defaultValue)));
696
+ const [internalValue, setInternalValue] = useState(() => dateToCalendarDayString({ date: parseToDate(defaultValue), timeZoneId }));
666
697
  const [pendingDate, setPendingDate] = useState(null);
667
698
  const [t, i18n] = useTranslation();
668
699
  const locale = i18n.resolvedLanguage ?? i18n.language;
@@ -670,7 +701,7 @@ const DateBaseInput = ({ min, max, defaultValue, value, ref, onChange, suffix: s
670
701
  const resolvedValue = isControlled
671
702
  ? typeof value === "string"
672
703
  ? dateFieldStoredValueToDisplayString(value, locale)
673
- : dateFieldStoredValueToDisplayString(dateToYYYYMMDDUtil(parseToDate(value)), locale)
704
+ : dateFieldStoredValueToDisplayString(dateToCalendarDayString({ date: parseToDate(value), timeZoneId }), locale)
674
705
  : dateFieldStoredValueToDisplayString(internalValue, locale);
675
706
  const selectedDate = isControlled && typeof value === "string"
676
707
  ? (parseYYYYMMDDUtil(value) ?? undefined)
@@ -685,13 +716,8 @@ const DateBaseInput = ({ min, max, defaultValue, value, ref, onChange, suffix: s
685
716
  return false;
686
717
  const minDate = parseToDate(min);
687
718
  const maxDate = parseToDate(max);
688
- const dayStart = startOfDayMs(date);
689
- if (minDate !== undefined && dayStart < startOfDayMs(minDate))
690
- return true;
691
- if (maxDate !== undefined && dayStart > startOfDayMs(maxDate))
692
- return true;
693
- return false;
694
- }, [min, max]);
719
+ return isCalendarDayOutOfRange({ date, minDate, maxDate, timeZoneId });
720
+ }, [min, max, timeZoneId]);
695
721
  const handleCalendarChange = useCallback((next) => {
696
722
  setPendingDate(next);
697
723
  }, []);
@@ -708,21 +734,21 @@ const DateBaseInput = ({ min, max, defaultValue, value, ref, onChange, suffix: s
708
734
  const handleApply = useCallback((closePopover) => {
709
735
  const minDate = parseToDate(min);
710
736
  const maxDate = parseToDate(max);
711
- const clamped = clampToRange(pendingDate, minDate, maxDate);
712
- const str = clamped ? dateToYYYYMMDDUtil(clamped) : "";
737
+ const clamped = clampDateToCalendarDayRange({ date: pendingDate, minDate, maxDate, timeZoneId });
738
+ const str = clamped ? dateToCalendarDayString({ date: clamped, timeZoneId }) : "";
713
739
  if (!isControlled)
714
740
  setInternalValue(str);
715
741
  onChange?.(createInputChangeEvent(str, inputRef.current));
716
742
  closePopover();
717
- }, [isControlled, min, max, onChange, pendingDate]);
743
+ }, [isControlled, min, max, onChange, pendingDate, timeZoneId]);
718
744
  const handleInputChange = useCallback((e) => {
719
745
  const raw = e.target.value;
720
746
  const parsed = parseDateFieldInputForChange(raw, locale);
721
747
  const minDate = parseToDate(min);
722
748
  const maxDate = parseToDate(max);
723
749
  if (parsed !== null) {
724
- const clamped = clampToRange(parsed, minDate, maxDate);
725
- const str = clamped ? dateToYYYYMMDDUtil(clamped) : "";
750
+ const clamped = clampDateToCalendarDayRange({ date: parsed, minDate, maxDate, timeZoneId });
751
+ const str = clamped ? dateToCalendarDayString({ date: clamped, timeZoneId }) : "";
726
752
  if (!isControlled)
727
753
  setInternalValue(str);
728
754
  onChange?.(createInputChangeEvent(str, inputRef.current));
@@ -732,7 +758,7 @@ const DateBaseInput = ({ min, max, defaultValue, value, ref, onChange, suffix: s
732
758
  setInternalValue(raw);
733
759
  onChange?.(e);
734
760
  }
735
- }, [isControlled, locale, min, max, onChange]);
761
+ }, [isControlled, locale, min, max, onChange, timeZoneId]);
736
762
  return (jsxs(Popover, { onOpenStateChange: open => open && syncPendingFromValue(), placement: "bottom-start", children: [jsx(PopoverTrigger, { children: jsx("div", { className: twMerge("flex w-full min-w-0 cursor-pointer items-center", (Boolean(rest.disabled) || Boolean(rest.readOnly)) && "pointer-events-none"), children: jsx(BaseInput, { ...rest, "aria-readonly": true, className: twMerge("w-full min-w-0", rest.className), "data-testid": dataTestId ? `${dataTestId}-input` : undefined, onChange: handleInputChange, placeholder: rest.placeholder ?? t("dateField.placeholder"), ref: inputRef, suffix: suffixProp ?? (jsx(Icon, { "aria-label": undefined, className: Boolean(rest.disabled) || Boolean(rest.readOnly) ? "text-neutral-500" : undefined, "data-testid": dataTestId ? `${dataTestId}-calendar` : "calendar", name: "Calendar", size: "medium", type: "solid" })), type: "text", value: resolvedValue }) }) }), jsx(PopoverContent, { children: closePopover => {
737
763
  const displayDate = pendingDate ?? selectedDate ?? null;
738
764
  return (jsxs("div", { className: twMerge("flex w-min flex-col overflow-hidden rounded-md border border-neutral-300 bg-white p-0"), children: [jsx(ReactCalendar, { allowPartialRange: true, className: twMerge("custom-day-picker", "range-picker", "p-0"), defaultActiveStartDate: displayDate ?? undefined, defaultView: "month", locale: locale, onChange: val => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/react-form-components",
3
- "version": "1.22.15",
3
+ "version": "1.22.18",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "engines": {
@@ -9,17 +9,17 @@
9
9
  "dependencies": {
10
10
  "react-calendar": "^6.0.0",
11
11
  "react-select": "^5.10.2",
12
- "@trackunit/date-and-time-utils": "1.11.109",
12
+ "@trackunit/date-and-time-utils": "1.11.111",
13
13
  "usehooks-ts": "^3.1.0",
14
14
  "libphonenumber-js": "^1.12.22",
15
15
  "zod": "^3.25.76",
16
16
  "tailwind-merge": "^2.0.0",
17
- "@trackunit/css-class-variance-utilities": "1.11.106",
18
- "@trackunit/react-components": "1.22.11",
19
- "@trackunit/ui-icons": "1.11.102",
20
- "@trackunit/shared-utils": "1.13.106",
21
- "@trackunit/ui-design-tokens": "1.11.103",
22
- "@trackunit/i18n-library-translation": "1.18.12",
17
+ "@trackunit/css-class-variance-utilities": "1.11.108",
18
+ "@trackunit/react-components": "1.22.14",
19
+ "@trackunit/ui-icons": "1.11.104",
20
+ "@trackunit/shared-utils": "1.13.108",
21
+ "@trackunit/ui-design-tokens": "1.11.105",
22
+ "@trackunit/i18n-library-translation": "1.18.14",
23
23
  "string-ts": "^2.0.0",
24
24
  "es-toolkit": "^1.39.10"
25
25
  },
@@ -0,0 +1,25 @@
1
+ interface DateToCalendarDayStringParams {
2
+ readonly date: Date | undefined;
3
+ readonly timeZoneId: string | undefined;
4
+ }
5
+ /**
6
+ * Converts a Date instant to the canonical YYYY-MM-DD for a specific IANA time zone.
7
+ * This preserves the calendar day the user selected in their local zone.
8
+ */
9
+ export declare const dateToCalendarDayString: ({ date, timeZoneId }: DateToCalendarDayStringParams) => string;
10
+ interface DateRangeByCalendarDayParams {
11
+ readonly date: Date | null | undefined;
12
+ readonly minDate: Date | undefined;
13
+ readonly maxDate: Date | undefined;
14
+ readonly timeZoneId: string | undefined;
15
+ }
16
+ /**
17
+ * Clamps a date to [minDate, maxDate] by comparing calendar days in a specific timezone.
18
+ * Returns the original `date` when it's in range, or the relevant boundary date when out of range.
19
+ */
20
+ export declare const clampDateToCalendarDayRange: ({ date, minDate, maxDate, timeZoneId, }: DateRangeByCalendarDayParams) => Date | null;
21
+ /**
22
+ * Returns true when a date falls outside [minDate, maxDate] by calendar day in a specific timezone.
23
+ */
24
+ export declare const isCalendarDayOutOfRange: ({ date, minDate, maxDate, timeZoneId, }: DateRangeByCalendarDayParams) => boolean;
25
+ export {};