@trackunit/react-form-components 1.22.28 → 1.23.0

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
@@ -696,7 +696,7 @@ function parseToDate(v) {
696
696
  *
697
697
  * NOTE: If shown with a label, please use the `DateField` component instead.
698
698
  */
699
- const DateBaseInput = ({ min, max, defaultValue, value, ref, onChange, suffix: suffixProp, "data-testid": dataTestId, ...rest }) => {
699
+ const DateBaseInput = ({ min, max, defaultValue, value, ref, onChange, suffix: suffixProp, "data-testid": dataTestId, onBlur, ...rest }) => {
700
700
  const timeZoneId = Intl.DateTimeFormat().resolvedOptions().timeZone;
701
701
  const isControlled = value !== undefined;
702
702
  const [internalValue, setInternalValue] = react.useState(() => dateToCalendarDayString({ date: parseToDate(defaultValue), timeZoneId }));
@@ -766,7 +766,27 @@ const DateBaseInput = ({ min, max, defaultValue, value, ref, onChange, suffix: s
766
766
  onChange?.(e);
767
767
  }
768
768
  }, [createInputChangeEvent, isControlled, locale, min, max, onChange, timeZoneId]);
769
- 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 => {
769
+ const handleBlur = react.useCallback((e) => {
770
+ if (!onBlur)
771
+ return;
772
+ const storedValue = isControlled
773
+ ? typeof value === "string"
774
+ ? value
775
+ : dateToCalendarDayString({ date: parseToDate(value), timeZoneId })
776
+ : internalValue;
777
+ // When the stored value is a committed calendar day (YYYY-MM-DD) we expose it on blur, even though the
778
+ // displayed input value is localized.
779
+ const shouldExposeStoredValue = storedValue === "" || dateAndTimeUtils.parseYYYYMMDDUtil(storedValue) !== null;
780
+ if (!shouldExposeStoredValue) {
781
+ onBlur(e);
782
+ return;
783
+ }
784
+ const prev = e.currentTarget.value;
785
+ e.currentTarget.value = storedValue;
786
+ onBlur(e);
787
+ e.currentTarget.value = prev;
788
+ }, [internalValue, isControlled, onBlur, timeZoneId, value]);
789
+ 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, onBlur: handleBlur, 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 => {
770
790
  const displayDate = pendingDate ?? selectedDate ?? null;
771
791
  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 => {
772
792
  const next = val instanceof Date ? val : Array.isArray(val) ? (val[0] instanceof Date ? val[0] : null) : null;
@@ -2551,10 +2571,9 @@ ColorField.displayName = "ColorField";
2551
2571
 
2552
2572
  /**
2553
2573
  * The date field component is used for entering date values with a calendar picker (same UI as DayPicker).
2554
- * The input shows a localized short date; when the user commits a valid day, `onChange` provides `YYYY-MM-DD`
2555
- * on `event.target.value` (use `parseYYYYMMDDUtil` from `@trackunit/date-and-time-utils` for a local-midnight
2556
- * `Date`). Do not parse `event.target.value` from `onBlur` — the visible string may be localized while
2557
- * `onChange` stays canonical.
2574
+ * The input shows a localized short date; when the user commits a valid day, both `onChange` and `onBlur`
2575
+ * expose `YYYY-MM-DD` on `event.target.value` (use `parseYYYYMMDDUtil` from `@trackunit/date-and-time-utils`
2576
+ * for a local-midnight `Date`).
2558
2577
  *
2559
2578
  * ### When to use
2560
2579
  * Use DateField for selecting calendar dates such as birthdates, deadlines, or scheduling.
package/index.esm.js CHANGED
@@ -695,7 +695,7 @@ function parseToDate(v) {
695
695
  *
696
696
  * NOTE: If shown with a label, please use the `DateField` component instead.
697
697
  */
698
- const DateBaseInput = ({ min, max, defaultValue, value, ref, onChange, suffix: suffixProp, "data-testid": dataTestId, ...rest }) => {
698
+ const DateBaseInput = ({ min, max, defaultValue, value, ref, onChange, suffix: suffixProp, "data-testid": dataTestId, onBlur, ...rest }) => {
699
699
  const timeZoneId = Intl.DateTimeFormat().resolvedOptions().timeZone;
700
700
  const isControlled = value !== undefined;
701
701
  const [internalValue, setInternalValue] = useState(() => dateToCalendarDayString({ date: parseToDate(defaultValue), timeZoneId }));
@@ -765,7 +765,27 @@ const DateBaseInput = ({ min, max, defaultValue, value, ref, onChange, suffix: s
765
765
  onChange?.(e);
766
766
  }
767
767
  }, [createInputChangeEvent, isControlled, locale, min, max, onChange, timeZoneId]);
768
- 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 => {
768
+ const handleBlur = useCallback((e) => {
769
+ if (!onBlur)
770
+ return;
771
+ const storedValue = isControlled
772
+ ? typeof value === "string"
773
+ ? value
774
+ : dateToCalendarDayString({ date: parseToDate(value), timeZoneId })
775
+ : internalValue;
776
+ // When the stored value is a committed calendar day (YYYY-MM-DD) we expose it on blur, even though the
777
+ // displayed input value is localized.
778
+ const shouldExposeStoredValue = storedValue === "" || parseYYYYMMDDUtil(storedValue) !== null;
779
+ if (!shouldExposeStoredValue) {
780
+ onBlur(e);
781
+ return;
782
+ }
783
+ const prev = e.currentTarget.value;
784
+ e.currentTarget.value = storedValue;
785
+ onBlur(e);
786
+ e.currentTarget.value = prev;
787
+ }, [internalValue, isControlled, onBlur, timeZoneId, value]);
788
+ 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, onBlur: handleBlur, 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 => {
769
789
  const displayDate = pendingDate ?? selectedDate ?? null;
770
790
  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 => {
771
791
  const next = val instanceof Date ? val : Array.isArray(val) ? (val[0] instanceof Date ? val[0] : null) : null;
@@ -2550,10 +2570,9 @@ ColorField.displayName = "ColorField";
2550
2570
 
2551
2571
  /**
2552
2572
  * The date field component is used for entering date values with a calendar picker (same UI as DayPicker).
2553
- * The input shows a localized short date; when the user commits a valid day, `onChange` provides `YYYY-MM-DD`
2554
- * on `event.target.value` (use `parseYYYYMMDDUtil` from `@trackunit/date-and-time-utils` for a local-midnight
2555
- * `Date`). Do not parse `event.target.value` from `onBlur` — the visible string may be localized while
2556
- * `onChange` stays canonical.
2573
+ * The input shows a localized short date; when the user commits a valid day, both `onChange` and `onBlur`
2574
+ * expose `YYYY-MM-DD` on `event.target.value` (use `parseYYYYMMDDUtil` from `@trackunit/date-and-time-utils`
2575
+ * for a local-midnight `Date`).
2557
2576
  *
2558
2577
  * ### When to use
2559
2578
  * Use DateField for selecting calendar dates such as birthdates, deadlines, or scheduling.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/react-form-components",
3
- "version": "1.22.28",
3
+ "version": "1.23.0",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "engines": {
@@ -19,7 +19,7 @@
19
19
  "@trackunit/ui-icons": "1.11.111",
20
20
  "@trackunit/shared-utils": "1.13.115",
21
21
  "@trackunit/ui-design-tokens": "1.11.112",
22
- "@trackunit/i18n-library-translation": "1.18.23",
22
+ "@trackunit/i18n-library-translation": "1.19.0",
23
23
  "string-ts": "^2.0.0",
24
24
  "es-toolkit": "^1.39.10"
25
25
  },
@@ -27,5 +27,5 @@ export interface DateBaseInputProps extends BaseInputExposedProps {
27
27
  *
28
28
  * NOTE: If shown with a label, please use the `DateField` component instead.
29
29
  */
30
- export declare const DateBaseInput: ({ min, max, defaultValue, value, ref, onChange, suffix: suffixProp, "data-testid": dataTestId, ...rest }: DateBaseInputProps) => ReactElement;
30
+ export declare const DateBaseInput: ({ min, max, defaultValue, value, ref, onChange, suffix: suffixProp, "data-testid": dataTestId, onBlur, ...rest }: DateBaseInputProps) => ReactElement;
31
31
  export {};
@@ -10,10 +10,9 @@ export interface DateFieldProps extends DateBaseInputProps, FormGroupExposedProp
10
10
  }
11
11
  /**
12
12
  * The date field component is used for entering date values with a calendar picker (same UI as DayPicker).
13
- * The input shows a localized short date; when the user commits a valid day, `onChange` provides `YYYY-MM-DD`
14
- * on `event.target.value` (use `parseYYYYMMDDUtil` from `@trackunit/date-and-time-utils` for a local-midnight
15
- * `Date`). Do not parse `event.target.value` from `onBlur` — the visible string may be localized while
16
- * `onChange` stays canonical.
13
+ * The input shows a localized short date; when the user commits a valid day, both `onChange` and `onBlur`
14
+ * expose `YYYY-MM-DD` on `event.target.value` (use `parseYYYYMMDDUtil` from `@trackunit/date-and-time-utils`
15
+ * for a local-midnight `Date`).
17
16
  *
18
17
  * ### When to use
19
18
  * Use DateField for selecting calendar dates such as birthdates, deadlines, or scheduling.