@trackunit/react-form-components 1.18.15 → 1.18.17
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 +239 -17
- package/index.esm.js +239 -20
- package/package.json +9 -8
- package/src/components/DateField/DateBaseInput/DateBaseInput.d.ts +3 -2
- package/src/components/DateField/DateField.d.ts +3 -2
- package/src/index.d.ts +1 -0
- package/src/translation.d.ts +2 -2
- package/src/utilities/createInputChangeEvent.d.ts +7 -0
- package/src/utilities/parseDateFieldValue.d.ts +26 -0
package/index.cjs.js
CHANGED
|
@@ -4,8 +4,10 @@ var jsxRuntime = require('react/jsx-runtime');
|
|
|
4
4
|
var i18nLibraryTranslation = require('@trackunit/i18n-library-translation');
|
|
5
5
|
var dateAndTimeUtils = require('@trackunit/date-and-time-utils');
|
|
6
6
|
var reactComponents = require('@trackunit/react-components');
|
|
7
|
-
var uiDesignTokens = require('@trackunit/ui-design-tokens');
|
|
8
7
|
var react = require('react');
|
|
8
|
+
var ReactCalendar = require('react-calendar');
|
|
9
|
+
var tailwindMerge = require('tailwind-merge');
|
|
10
|
+
var uiDesignTokens = require('@trackunit/ui-design-tokens');
|
|
9
11
|
var cssClassVarianceUtilities = require('@trackunit/css-class-variance-utilities');
|
|
10
12
|
var stringTs = require('string-ts');
|
|
11
13
|
var usehooksTs = require('usehooks-ts');
|
|
@@ -13,7 +15,6 @@ var parsePhoneNumberFromString = require('libphonenumber-js');
|
|
|
13
15
|
var ReactSelect = require('react-select');
|
|
14
16
|
var ReactAsyncSelect = require('react-select/async');
|
|
15
17
|
var esToolkit = require('es-toolkit');
|
|
16
|
-
var tailwindMerge = require('tailwind-merge');
|
|
17
18
|
var ReactAsyncCreatableSelect = require('react-select/async-creatable');
|
|
18
19
|
var ReactCreatableSelect = require('react-select/creatable');
|
|
19
20
|
var sharedUtils = require('@trackunit/shared-utils');
|
|
@@ -26,6 +27,10 @@ var defaultTranslations = {
|
|
|
26
27
|
"colorField.error.INVALID_HEX_CODE": "Please enter a valid color before continuing.",
|
|
27
28
|
"colorField.error.REQUIRED": "This field is required",
|
|
28
29
|
"colorField.tooltip": "Select color",
|
|
30
|
+
"dateField.actions.apply": "Apply",
|
|
31
|
+
"dateField.actions.cancel": "Cancel",
|
|
32
|
+
"dateField.actions.clear": "Clear",
|
|
33
|
+
"dateField.placeholder": "yyyy-mm-dd",
|
|
29
34
|
"dropzone.input.title": "Drag-and-drop file input",
|
|
30
35
|
"dropzone.label.default": "<clickable>Browse</clickable> or drag files here...",
|
|
31
36
|
"emailField.error.INVALID_EMAIL": "Please enter a valid email address",
|
|
@@ -102,6 +107,110 @@ const setupLibraryTranslations = () => {
|
|
|
102
107
|
i18nLibraryTranslation.registerTranslations(translations);
|
|
103
108
|
};
|
|
104
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Builds a synthetic change event for an input with a given value.
|
|
112
|
+
* Use when calling onChange from code (e.g. clear, apply, clamped value) so consumers
|
|
113
|
+
* that use `onChange={e => setValue(e.target.value)}` still receive the expected shape.
|
|
114
|
+
*/
|
|
115
|
+
function createInputChangeEvent(value, sourceInput) {
|
|
116
|
+
const target = document.createElement("input");
|
|
117
|
+
target.value = value;
|
|
118
|
+
if (sourceInput) {
|
|
119
|
+
target.name = sourceInput.name;
|
|
120
|
+
target.id = sourceInput.id;
|
|
121
|
+
}
|
|
122
|
+
const native = new Event("change", { bubbles: true });
|
|
123
|
+
return {
|
|
124
|
+
target,
|
|
125
|
+
currentTarget: target,
|
|
126
|
+
type: "change",
|
|
127
|
+
bubbles: native.bubbles,
|
|
128
|
+
cancelable: native.cancelable,
|
|
129
|
+
defaultPrevented: native.defaultPrevented,
|
|
130
|
+
eventPhase: native.eventPhase,
|
|
131
|
+
isTrusted: native.isTrusted,
|
|
132
|
+
nativeEvent: native,
|
|
133
|
+
timeStamp: native.timeStamp,
|
|
134
|
+
preventDefault: () => native.preventDefault(),
|
|
135
|
+
stopPropagation: () => native.stopPropagation(),
|
|
136
|
+
persist: () => {
|
|
137
|
+
return;
|
|
138
|
+
},
|
|
139
|
+
isDefaultPrevented: () => native.defaultPrevented,
|
|
140
|
+
isPropagationStopped: () => false,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const ISO_DATE_REGEX = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
145
|
+
function parseAndValidateYYYYMMDD(value) {
|
|
146
|
+
if (value === "")
|
|
147
|
+
return null;
|
|
148
|
+
const isoMatch = ISO_DATE_REGEX.exec(value);
|
|
149
|
+
if (!isoMatch)
|
|
150
|
+
return null;
|
|
151
|
+
const [, y, m, d] = isoMatch;
|
|
152
|
+
if (y === undefined || m === undefined || d === undefined)
|
|
153
|
+
return null;
|
|
154
|
+
const year = Number.parseInt(y, 10);
|
|
155
|
+
const month = Number.parseInt(m, 10) - 1;
|
|
156
|
+
const day = Number.parseInt(d, 10);
|
|
157
|
+
if (month < 0 || month > 11 || day < 1 || day > 31)
|
|
158
|
+
return null;
|
|
159
|
+
const local = new Date(year, month, day);
|
|
160
|
+
if (Number.isNaN(local.getTime()))
|
|
161
|
+
return null;
|
|
162
|
+
if (local.getFullYear() !== year || local.getMonth() !== month || local.getDate() !== day) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
return { year, month: month + 1, day };
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Parses the value from a DateField change event (YYYY-MM-DD string) to a Date at local midnight.
|
|
169
|
+
* Use this when you need a Date from event.target.value to avoid timezone shifts that occur
|
|
170
|
+
* with `new Date(value)` (which interprets the string as UTC).
|
|
171
|
+
*
|
|
172
|
+
* @param value - The string from event.target.value (e.g. "2025-03-07" or "")
|
|
173
|
+
* @returns {Date | null} - Date at local midnight for the given day, or null if value is empty or invalid
|
|
174
|
+
*/
|
|
175
|
+
function parseDateFieldValue(value) {
|
|
176
|
+
const parsed = parseAndValidateYYYYMMDD(value);
|
|
177
|
+
if (!parsed)
|
|
178
|
+
return null;
|
|
179
|
+
return new Date(parsed.year, parsed.month - 1, parsed.day);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Converts a YYYY-MM-DD string (e.g. from DateField) to an ISO string at UTC midnight for that calendar day.
|
|
183
|
+
* Use this when storing or sending date-only values so that the calendar day is preserved regardless of
|
|
184
|
+
* user timezone (unlike calling .toISOString() on a local-midnight Date, which shifts the day for UTC+).
|
|
185
|
+
*
|
|
186
|
+
* @param value - The string from event.target.value (e.g. "2025-03-07" or "")
|
|
187
|
+
* @returns {string | null} - "YYYY-MM-DDT00:00:00.000Z" or null if value is empty or invalid
|
|
188
|
+
*/
|
|
189
|
+
function toISODateStringUTC(value) {
|
|
190
|
+
const parsed = parseAndValidateYYYYMMDD(value);
|
|
191
|
+
if (!parsed)
|
|
192
|
+
return null;
|
|
193
|
+
const { year, month, day } = parsed;
|
|
194
|
+
const mm = String(month).padStart(2, "0");
|
|
195
|
+
const dd = String(day).padStart(2, "0");
|
|
196
|
+
return `${year}-${mm}-${dd}T00:00:00.000Z`;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Converts a Date (e.g. at local midnight) to an ISO string at UTC midnight for the same calendar day.
|
|
200
|
+
* Use when sending a date-only value to an API that expects UTC midnight (e.g. booking date).
|
|
201
|
+
*
|
|
202
|
+
* @param date - A Date instance (typically local midnight from a date picker)
|
|
203
|
+
* @returns {string} "YYYY-MM-DDT00:00:00.000Z" for that calendar day
|
|
204
|
+
*/
|
|
205
|
+
function dateToISODateUTC(date) {
|
|
206
|
+
const year = date.getFullYear();
|
|
207
|
+
const month = date.getMonth() + 1;
|
|
208
|
+
const day = date.getDate();
|
|
209
|
+
const mm = String(month).padStart(2, "0");
|
|
210
|
+
const dd = String(day).padStart(2, "0");
|
|
211
|
+
return `${year}-${mm}-${dd}T00:00:00.000Z`;
|
|
212
|
+
}
|
|
213
|
+
|
|
105
214
|
const cvaInputBase = cssClassVarianceUtilities.cvaMerge([
|
|
106
215
|
"component-baseInput-shadow",
|
|
107
216
|
"component-baseInput-border",
|
|
@@ -487,26 +596,136 @@ const BaseInput = ({ className, isInvalid = false, "data-testid": dataTestId, pr
|
|
|
487
596
|
};
|
|
488
597
|
BaseInput.displayName = "BaseInput";
|
|
489
598
|
|
|
599
|
+
function parseToDate(v) {
|
|
600
|
+
if (v === undefined || v === "")
|
|
601
|
+
return undefined;
|
|
602
|
+
if (v instanceof Date)
|
|
603
|
+
return v;
|
|
604
|
+
if (typeof v === "number") {
|
|
605
|
+
const d = new Date(v);
|
|
606
|
+
return Number.isNaN(d.getTime()) ? undefined : d;
|
|
607
|
+
}
|
|
608
|
+
const str = String(v);
|
|
609
|
+
const fromField = parseDateFieldValue(str);
|
|
610
|
+
if (fromField !== null)
|
|
611
|
+
return fromField;
|
|
612
|
+
const fallback = new Date(str);
|
|
613
|
+
return Number.isNaN(fallback.getTime()) ? undefined : fallback;
|
|
614
|
+
}
|
|
615
|
+
function formatToInputString(d) {
|
|
616
|
+
if (!d)
|
|
617
|
+
return "";
|
|
618
|
+
return dateAndTimeUtils.Temporal.PlainDateTime.from({
|
|
619
|
+
year: d.getFullYear(),
|
|
620
|
+
month: d.getMonth() + 1,
|
|
621
|
+
day: d.getDate(),
|
|
622
|
+
})
|
|
623
|
+
.toPlainDate()
|
|
624
|
+
.toString();
|
|
625
|
+
}
|
|
626
|
+
function startOfDayMs(d) {
|
|
627
|
+
return dateAndTimeUtils.toDateUtil(dateAndTimeUtils.startOfDayUtil(d)).getTime();
|
|
628
|
+
}
|
|
629
|
+
/** Clamp a date to [minDate, maxDate]; returns the same date if in range or no bounds. */
|
|
630
|
+
function clampToRange(date, minDate, maxDate) {
|
|
631
|
+
if (date === null || date === undefined)
|
|
632
|
+
return null;
|
|
633
|
+
const dayStart = startOfDayMs(date);
|
|
634
|
+
if (minDate !== undefined && dayStart < startOfDayMs(minDate))
|
|
635
|
+
return minDate;
|
|
636
|
+
if (maxDate !== undefined && dayStart > startOfDayMs(maxDate))
|
|
637
|
+
return maxDate;
|
|
638
|
+
return date;
|
|
639
|
+
}
|
|
490
640
|
/**
|
|
491
|
-
* A wrapper around BaseInput with a pop-up day picker.
|
|
641
|
+
* A wrapper around BaseInput with a pop-up day picker using the same calendar UI as DayPicker.
|
|
492
642
|
*
|
|
493
643
|
* The value is formatted to an ISO date string (YYYY-MM-DD)
|
|
494
644
|
*
|
|
495
645
|
* NOTE: If shown with a label, please use the `DateField` component instead.
|
|
496
646
|
*/
|
|
497
|
-
const DateBaseInput = ({ min, max, defaultValue, value, ref, ...rest }) => {
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
647
|
+
const DateBaseInput = ({ min, max, defaultValue, value, ref, onChange, suffix: suffixProp, "data-testid": dataTestId, ...rest }) => {
|
|
648
|
+
const isControlled = value !== undefined;
|
|
649
|
+
const [internalValue, setInternalValue] = react.useState(() => formatToInputString(parseToDate(defaultValue)));
|
|
650
|
+
const [pendingDate, setPendingDate] = react.useState(null);
|
|
651
|
+
// For controlled string value, only normalize when it's strict YYYY-MM-DD so partial input (e.g. "2025-03") is preserved.
|
|
652
|
+
const resolvedValue = isControlled
|
|
653
|
+
? typeof value === "string"
|
|
654
|
+
? (() => {
|
|
655
|
+
const strict = parseDateFieldValue(value);
|
|
656
|
+
return strict !== null ? formatToInputString(strict) : value;
|
|
657
|
+
})()
|
|
658
|
+
: formatToInputString(parseToDate(value))
|
|
659
|
+
: internalValue;
|
|
660
|
+
const selectedDate = isControlled && typeof value === "string"
|
|
661
|
+
? (parseDateFieldValue(value) ?? undefined)
|
|
662
|
+
: parseToDate(isControlled ? value : internalValue);
|
|
663
|
+
const inputRef = react.useRef(null);
|
|
664
|
+
const [t] = useTranslation();
|
|
665
|
+
react.useImperativeHandle(ref, () => inputRef.current ?? document.createElement("input"), []);
|
|
666
|
+
const syncPendingFromValue = react.useCallback(() => {
|
|
667
|
+
setPendingDate(selectedDate ?? null);
|
|
668
|
+
}, [selectedDate]);
|
|
669
|
+
const tileDisabled = react.useCallback(({ date, view }) => {
|
|
670
|
+
if (view !== "month")
|
|
671
|
+
return false;
|
|
672
|
+
const minDate = parseToDate(min);
|
|
673
|
+
const maxDate = parseToDate(max);
|
|
674
|
+
const dayStart = startOfDayMs(date);
|
|
675
|
+
if (minDate !== undefined && dayStart < startOfDayMs(minDate))
|
|
676
|
+
return true;
|
|
677
|
+
if (maxDate !== undefined && dayStart > startOfDayMs(maxDate))
|
|
678
|
+
return true;
|
|
679
|
+
return false;
|
|
680
|
+
}, [min, max]);
|
|
681
|
+
const handleCalendarChange = react.useCallback((next) => {
|
|
682
|
+
setPendingDate(next);
|
|
683
|
+
}, []);
|
|
684
|
+
const handleClear = react.useCallback((closePopover) => {
|
|
685
|
+
setPendingDate(null);
|
|
686
|
+
if (!isControlled)
|
|
687
|
+
setInternalValue("");
|
|
688
|
+
onChange?.(createInputChangeEvent("", inputRef.current));
|
|
689
|
+
closePopover();
|
|
690
|
+
}, [isControlled, onChange]);
|
|
691
|
+
const handleCancel = react.useCallback((closePopover) => {
|
|
692
|
+
closePopover();
|
|
693
|
+
}, []);
|
|
694
|
+
const handleApply = react.useCallback((closePopover) => {
|
|
695
|
+
const minDate = parseToDate(min);
|
|
696
|
+
const maxDate = parseToDate(max);
|
|
697
|
+
const clamped = clampToRange(pendingDate, minDate, maxDate);
|
|
698
|
+
const str = clamped ? formatToInputString(clamped) : "";
|
|
699
|
+
if (!isControlled)
|
|
700
|
+
setInternalValue(str);
|
|
701
|
+
onChange?.(createInputChangeEvent(str, inputRef.current));
|
|
702
|
+
closePopover();
|
|
703
|
+
}, [isControlled, min, max, onChange, pendingDate]);
|
|
704
|
+
const handleInputChange = react.useCallback((e) => {
|
|
705
|
+
const raw = e.target.value;
|
|
706
|
+
const parsed = parseDateFieldValue(raw);
|
|
707
|
+
const minDate = parseToDate(min);
|
|
708
|
+
const maxDate = parseToDate(max);
|
|
709
|
+
if (parsed !== null) {
|
|
710
|
+
const clamped = clampToRange(parsed, minDate, maxDate);
|
|
711
|
+
const str = clamped ? formatToInputString(clamped) : "";
|
|
712
|
+
if (!isControlled)
|
|
713
|
+
setInternalValue(str);
|
|
714
|
+
onChange?.(createInputChangeEvent(str, inputRef.current));
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
if (!isControlled)
|
|
718
|
+
setInternalValue(raw);
|
|
719
|
+
onChange?.(e);
|
|
720
|
+
}
|
|
721
|
+
}, [isControlled, min, max, onChange]);
|
|
722
|
+
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 => {
|
|
723
|
+
const displayDate = pendingDate ?? selectedDate ?? null;
|
|
724
|
+
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", onChange: val => {
|
|
725
|
+
const next = val instanceof Date ? val : Array.isArray(val) ? (val[0] instanceof Date ? val[0] : null) : null;
|
|
726
|
+
handleCalendarChange(next);
|
|
727
|
+
}, selectRange: false, tileDisabled: tileDisabled, value: displayDate }), jsxRuntime.jsx("hr", {}), jsxRuntime.jsxs("div", { className: "flex w-full justify-between gap-2 px-4 py-3", children: [jsxRuntime.jsx(reactComponents.Button, { className: "mr-auto", "data-testid": dataTestId ? `${dataTestId}-clear-button` : undefined, onClick: () => handleClear(closePopover), size: "small", variant: "secondary", children: t("dateField.actions.clear") }), jsxRuntime.jsxs("div", { className: "flex gap-2", children: [jsxRuntime.jsx(reactComponents.Button, { "data-testid": dataTestId ? `${dataTestId}-cancel-button` : undefined, onClick: () => handleCancel(closePopover), size: "small", variant: "ghost-neutral", children: t("dateField.actions.cancel") }), jsxRuntime.jsx(reactComponents.Button, { "data-testid": dataTestId ? `${dataTestId}-apply-button` : undefined, onClick: () => handleApply(closePopover), size: "small", children: t("dateField.actions.apply") })] })] })] }));
|
|
728
|
+
} })] }));
|
|
510
729
|
};
|
|
511
730
|
|
|
512
731
|
/**
|
|
@@ -2284,7 +2503,7 @@ const ColorField = ({ label, id, tip, helpText, errorMessage, helpAddon, classNa
|
|
|
2284
2503
|
ColorField.displayName = "ColorField";
|
|
2285
2504
|
|
|
2286
2505
|
/**
|
|
2287
|
-
* The date field component is used for entering date values with a
|
|
2506
|
+
* The date field component is used for entering date values with a calendar picker (same UI as DayPicker).
|
|
2288
2507
|
*
|
|
2289
2508
|
* ### When to use
|
|
2290
2509
|
* Use DateField for selecting calendar dates such as birthdates, deadlines, or scheduling.
|
|
@@ -4776,14 +4995,17 @@ exports.cvaSelectPlaceholder = cvaSelectPlaceholder;
|
|
|
4776
4995
|
exports.cvaSelectPrefixSuffix = cvaSelectPrefixSuffix;
|
|
4777
4996
|
exports.cvaSelectSingleValue = cvaSelectSingleValue;
|
|
4778
4997
|
exports.cvaSelectValueContainer = cvaSelectValueContainer;
|
|
4998
|
+
exports.dateToISODateUTC = dateToISODateUTC;
|
|
4779
4999
|
exports.getCountryAbbreviation = getCountryAbbreviation;
|
|
4780
5000
|
exports.getPhoneNumberWithPlus = getPhoneNumberWithPlus;
|
|
4781
5001
|
exports.isInvalidCountryCode = isInvalidCountryCode;
|
|
4782
5002
|
exports.isInvalidPhoneNumber = isInvalidPhoneNumber;
|
|
4783
5003
|
exports.isValidHEXColor = isValidHEXColor;
|
|
5004
|
+
exports.parseDateFieldValue = parseDateFieldValue;
|
|
4784
5005
|
exports.parseSchedule = parseSchedule;
|
|
4785
5006
|
exports.phoneErrorMessage = phoneErrorMessage;
|
|
4786
5007
|
exports.serializeSchedule = serializeSchedule;
|
|
5008
|
+
exports.toISODateStringUTC = toISODateStringUTC;
|
|
4787
5009
|
exports.useCreatableSelect = useCreatableSelect;
|
|
4788
5010
|
exports.useCustomComponents = useCustomComponents;
|
|
4789
5011
|
exports.useGetPhoneValidationRules = useGetPhoneValidationRules;
|
package/index.esm.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
2
2
|
import { registerTranslations, useNamespaceTranslation, NamespaceTrans } from '@trackunit/i18n-library-translation';
|
|
3
|
-
import { Temporal } from '@trackunit/date-and-time-utils';
|
|
4
|
-
import { IconButton, Icon, Tooltip, cvaMenu, cvaMenuList, Tag, useIsTextTruncated, ZStack, MenuItem, useMeasure, useDebounce, useMergeRefs, Spinner, useScrollBlock, Text, Heading, useIsFirstRender } from '@trackunit/react-components';
|
|
3
|
+
import { Temporal, toDateUtil, startOfDayUtil } from '@trackunit/date-and-time-utils';
|
|
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
|
+
import { useRef, useEffect, useImperativeHandle, useState, useCallback, cloneElement, isValidElement, useLayoutEffect, useReducer, useMemo, createContext, useContext, useId } from 'react';
|
|
6
|
+
import ReactCalendar from 'react-calendar';
|
|
7
|
+
import { twMerge } from 'tailwind-merge';
|
|
5
8
|
import { themeSpacing } from '@trackunit/ui-design-tokens';
|
|
6
|
-
import { useRef, useEffect, useImperativeHandle, useCallback, useState, cloneElement, isValidElement, useLayoutEffect, useReducer, useMemo, createContext, useContext, useId } from 'react';
|
|
7
9
|
import { cvaMerge } from '@trackunit/css-class-variance-utilities';
|
|
8
10
|
import { titleCase } from 'string-ts';
|
|
9
11
|
import { useCopyToClipboard } from 'usehooks-ts';
|
|
@@ -12,7 +14,6 @@ import ReactSelect, { components } from 'react-select';
|
|
|
12
14
|
export { default as ValueType } from 'react-select';
|
|
13
15
|
import ReactAsyncSelect from 'react-select/async';
|
|
14
16
|
import { isEqual, omit } from 'es-toolkit';
|
|
15
|
-
import { twMerge } from 'tailwind-merge';
|
|
16
17
|
import ReactAsyncCreatableSelect from 'react-select/async-creatable';
|
|
17
18
|
import ReactCreatableSelect from 'react-select/creatable';
|
|
18
19
|
import { uuidv4, nonNullable } from '@trackunit/shared-utils';
|
|
@@ -25,6 +26,10 @@ var defaultTranslations = {
|
|
|
25
26
|
"colorField.error.INVALID_HEX_CODE": "Please enter a valid color before continuing.",
|
|
26
27
|
"colorField.error.REQUIRED": "This field is required",
|
|
27
28
|
"colorField.tooltip": "Select color",
|
|
29
|
+
"dateField.actions.apply": "Apply",
|
|
30
|
+
"dateField.actions.cancel": "Cancel",
|
|
31
|
+
"dateField.actions.clear": "Clear",
|
|
32
|
+
"dateField.placeholder": "yyyy-mm-dd",
|
|
28
33
|
"dropzone.input.title": "Drag-and-drop file input",
|
|
29
34
|
"dropzone.label.default": "<clickable>Browse</clickable> or drag files here...",
|
|
30
35
|
"emailField.error.INVALID_EMAIL": "Please enter a valid email address",
|
|
@@ -101,6 +106,110 @@ const setupLibraryTranslations = () => {
|
|
|
101
106
|
registerTranslations(translations);
|
|
102
107
|
};
|
|
103
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Builds a synthetic change event for an input with a given value.
|
|
111
|
+
* Use when calling onChange from code (e.g. clear, apply, clamped value) so consumers
|
|
112
|
+
* that use `onChange={e => setValue(e.target.value)}` still receive the expected shape.
|
|
113
|
+
*/
|
|
114
|
+
function createInputChangeEvent(value, sourceInput) {
|
|
115
|
+
const target = document.createElement("input");
|
|
116
|
+
target.value = value;
|
|
117
|
+
if (sourceInput) {
|
|
118
|
+
target.name = sourceInput.name;
|
|
119
|
+
target.id = sourceInput.id;
|
|
120
|
+
}
|
|
121
|
+
const native = new Event("change", { bubbles: true });
|
|
122
|
+
return {
|
|
123
|
+
target,
|
|
124
|
+
currentTarget: target,
|
|
125
|
+
type: "change",
|
|
126
|
+
bubbles: native.bubbles,
|
|
127
|
+
cancelable: native.cancelable,
|
|
128
|
+
defaultPrevented: native.defaultPrevented,
|
|
129
|
+
eventPhase: native.eventPhase,
|
|
130
|
+
isTrusted: native.isTrusted,
|
|
131
|
+
nativeEvent: native,
|
|
132
|
+
timeStamp: native.timeStamp,
|
|
133
|
+
preventDefault: () => native.preventDefault(),
|
|
134
|
+
stopPropagation: () => native.stopPropagation(),
|
|
135
|
+
persist: () => {
|
|
136
|
+
return;
|
|
137
|
+
},
|
|
138
|
+
isDefaultPrevented: () => native.defaultPrevented,
|
|
139
|
+
isPropagationStopped: () => false,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const ISO_DATE_REGEX = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
144
|
+
function parseAndValidateYYYYMMDD(value) {
|
|
145
|
+
if (value === "")
|
|
146
|
+
return null;
|
|
147
|
+
const isoMatch = ISO_DATE_REGEX.exec(value);
|
|
148
|
+
if (!isoMatch)
|
|
149
|
+
return null;
|
|
150
|
+
const [, y, m, d] = isoMatch;
|
|
151
|
+
if (y === undefined || m === undefined || d === undefined)
|
|
152
|
+
return null;
|
|
153
|
+
const year = Number.parseInt(y, 10);
|
|
154
|
+
const month = Number.parseInt(m, 10) - 1;
|
|
155
|
+
const day = Number.parseInt(d, 10);
|
|
156
|
+
if (month < 0 || month > 11 || day < 1 || day > 31)
|
|
157
|
+
return null;
|
|
158
|
+
const local = new Date(year, month, day);
|
|
159
|
+
if (Number.isNaN(local.getTime()))
|
|
160
|
+
return null;
|
|
161
|
+
if (local.getFullYear() !== year || local.getMonth() !== month || local.getDate() !== day) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
return { year, month: month + 1, day };
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Parses the value from a DateField change event (YYYY-MM-DD string) to a Date at local midnight.
|
|
168
|
+
* Use this when you need a Date from event.target.value to avoid timezone shifts that occur
|
|
169
|
+
* with `new Date(value)` (which interprets the string as UTC).
|
|
170
|
+
*
|
|
171
|
+
* @param value - The string from event.target.value (e.g. "2025-03-07" or "")
|
|
172
|
+
* @returns {Date | null} - Date at local midnight for the given day, or null if value is empty or invalid
|
|
173
|
+
*/
|
|
174
|
+
function parseDateFieldValue(value) {
|
|
175
|
+
const parsed = parseAndValidateYYYYMMDD(value);
|
|
176
|
+
if (!parsed)
|
|
177
|
+
return null;
|
|
178
|
+
return new Date(parsed.year, parsed.month - 1, parsed.day);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Converts a YYYY-MM-DD string (e.g. from DateField) to an ISO string at UTC midnight for that calendar day.
|
|
182
|
+
* Use this when storing or sending date-only values so that the calendar day is preserved regardless of
|
|
183
|
+
* user timezone (unlike calling .toISOString() on a local-midnight Date, which shifts the day for UTC+).
|
|
184
|
+
*
|
|
185
|
+
* @param value - The string from event.target.value (e.g. "2025-03-07" or "")
|
|
186
|
+
* @returns {string | null} - "YYYY-MM-DDT00:00:00.000Z" or null if value is empty or invalid
|
|
187
|
+
*/
|
|
188
|
+
function toISODateStringUTC(value) {
|
|
189
|
+
const parsed = parseAndValidateYYYYMMDD(value);
|
|
190
|
+
if (!parsed)
|
|
191
|
+
return null;
|
|
192
|
+
const { year, month, day } = parsed;
|
|
193
|
+
const mm = String(month).padStart(2, "0");
|
|
194
|
+
const dd = String(day).padStart(2, "0");
|
|
195
|
+
return `${year}-${mm}-${dd}T00:00:00.000Z`;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Converts a Date (e.g. at local midnight) to an ISO string at UTC midnight for the same calendar day.
|
|
199
|
+
* Use when sending a date-only value to an API that expects UTC midnight (e.g. booking date).
|
|
200
|
+
*
|
|
201
|
+
* @param date - A Date instance (typically local midnight from a date picker)
|
|
202
|
+
* @returns {string} "YYYY-MM-DDT00:00:00.000Z" for that calendar day
|
|
203
|
+
*/
|
|
204
|
+
function dateToISODateUTC(date) {
|
|
205
|
+
const year = date.getFullYear();
|
|
206
|
+
const month = date.getMonth() + 1;
|
|
207
|
+
const day = date.getDate();
|
|
208
|
+
const mm = String(month).padStart(2, "0");
|
|
209
|
+
const dd = String(day).padStart(2, "0");
|
|
210
|
+
return `${year}-${mm}-${dd}T00:00:00.000Z`;
|
|
211
|
+
}
|
|
212
|
+
|
|
104
213
|
const cvaInputBase = cvaMerge([
|
|
105
214
|
"component-baseInput-shadow",
|
|
106
215
|
"component-baseInput-border",
|
|
@@ -486,26 +595,136 @@ const BaseInput = ({ className, isInvalid = false, "data-testid": dataTestId, pr
|
|
|
486
595
|
};
|
|
487
596
|
BaseInput.displayName = "BaseInput";
|
|
488
597
|
|
|
598
|
+
function parseToDate(v) {
|
|
599
|
+
if (v === undefined || v === "")
|
|
600
|
+
return undefined;
|
|
601
|
+
if (v instanceof Date)
|
|
602
|
+
return v;
|
|
603
|
+
if (typeof v === "number") {
|
|
604
|
+
const d = new Date(v);
|
|
605
|
+
return Number.isNaN(d.getTime()) ? undefined : d;
|
|
606
|
+
}
|
|
607
|
+
const str = String(v);
|
|
608
|
+
const fromField = parseDateFieldValue(str);
|
|
609
|
+
if (fromField !== null)
|
|
610
|
+
return fromField;
|
|
611
|
+
const fallback = new Date(str);
|
|
612
|
+
return Number.isNaN(fallback.getTime()) ? undefined : fallback;
|
|
613
|
+
}
|
|
614
|
+
function formatToInputString(d) {
|
|
615
|
+
if (!d)
|
|
616
|
+
return "";
|
|
617
|
+
return Temporal.PlainDateTime.from({
|
|
618
|
+
year: d.getFullYear(),
|
|
619
|
+
month: d.getMonth() + 1,
|
|
620
|
+
day: d.getDate(),
|
|
621
|
+
})
|
|
622
|
+
.toPlainDate()
|
|
623
|
+
.toString();
|
|
624
|
+
}
|
|
625
|
+
function startOfDayMs(d) {
|
|
626
|
+
return toDateUtil(startOfDayUtil(d)).getTime();
|
|
627
|
+
}
|
|
628
|
+
/** Clamp a date to [minDate, maxDate]; returns the same date if in range or no bounds. */
|
|
629
|
+
function clampToRange(date, minDate, maxDate) {
|
|
630
|
+
if (date === null || date === undefined)
|
|
631
|
+
return null;
|
|
632
|
+
const dayStart = startOfDayMs(date);
|
|
633
|
+
if (minDate !== undefined && dayStart < startOfDayMs(minDate))
|
|
634
|
+
return minDate;
|
|
635
|
+
if (maxDate !== undefined && dayStart > startOfDayMs(maxDate))
|
|
636
|
+
return maxDate;
|
|
637
|
+
return date;
|
|
638
|
+
}
|
|
489
639
|
/**
|
|
490
|
-
* A wrapper around BaseInput with a pop-up day picker.
|
|
640
|
+
* A wrapper around BaseInput with a pop-up day picker using the same calendar UI as DayPicker.
|
|
491
641
|
*
|
|
492
642
|
* The value is formatted to an ISO date string (YYYY-MM-DD)
|
|
493
643
|
*
|
|
494
644
|
* NOTE: If shown with a label, please use the `DateField` component instead.
|
|
495
645
|
*/
|
|
496
|
-
const DateBaseInput = ({ min, max, defaultValue, value, ref, ...rest }) => {
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
646
|
+
const DateBaseInput = ({ min, max, defaultValue, value, ref, onChange, suffix: suffixProp, "data-testid": dataTestId, ...rest }) => {
|
|
647
|
+
const isControlled = value !== undefined;
|
|
648
|
+
const [internalValue, setInternalValue] = useState(() => formatToInputString(parseToDate(defaultValue)));
|
|
649
|
+
const [pendingDate, setPendingDate] = useState(null);
|
|
650
|
+
// For controlled string value, only normalize when it's strict YYYY-MM-DD so partial input (e.g. "2025-03") is preserved.
|
|
651
|
+
const resolvedValue = isControlled
|
|
652
|
+
? typeof value === "string"
|
|
653
|
+
? (() => {
|
|
654
|
+
const strict = parseDateFieldValue(value);
|
|
655
|
+
return strict !== null ? formatToInputString(strict) : value;
|
|
656
|
+
})()
|
|
657
|
+
: formatToInputString(parseToDate(value))
|
|
658
|
+
: internalValue;
|
|
659
|
+
const selectedDate = isControlled && typeof value === "string"
|
|
660
|
+
? (parseDateFieldValue(value) ?? undefined)
|
|
661
|
+
: parseToDate(isControlled ? value : internalValue);
|
|
662
|
+
const inputRef = useRef(null);
|
|
663
|
+
const [t] = useTranslation();
|
|
664
|
+
useImperativeHandle(ref, () => inputRef.current ?? document.createElement("input"), []);
|
|
665
|
+
const syncPendingFromValue = useCallback(() => {
|
|
666
|
+
setPendingDate(selectedDate ?? null);
|
|
667
|
+
}, [selectedDate]);
|
|
668
|
+
const tileDisabled = useCallback(({ date, view }) => {
|
|
669
|
+
if (view !== "month")
|
|
670
|
+
return false;
|
|
671
|
+
const minDate = parseToDate(min);
|
|
672
|
+
const maxDate = parseToDate(max);
|
|
673
|
+
const dayStart = startOfDayMs(date);
|
|
674
|
+
if (minDate !== undefined && dayStart < startOfDayMs(minDate))
|
|
675
|
+
return true;
|
|
676
|
+
if (maxDate !== undefined && dayStart > startOfDayMs(maxDate))
|
|
677
|
+
return true;
|
|
678
|
+
return false;
|
|
679
|
+
}, [min, max]);
|
|
680
|
+
const handleCalendarChange = useCallback((next) => {
|
|
681
|
+
setPendingDate(next);
|
|
682
|
+
}, []);
|
|
683
|
+
const handleClear = useCallback((closePopover) => {
|
|
684
|
+
setPendingDate(null);
|
|
685
|
+
if (!isControlled)
|
|
686
|
+
setInternalValue("");
|
|
687
|
+
onChange?.(createInputChangeEvent("", inputRef.current));
|
|
688
|
+
closePopover();
|
|
689
|
+
}, [isControlled, onChange]);
|
|
690
|
+
const handleCancel = useCallback((closePopover) => {
|
|
691
|
+
closePopover();
|
|
692
|
+
}, []);
|
|
693
|
+
const handleApply = useCallback((closePopover) => {
|
|
694
|
+
const minDate = parseToDate(min);
|
|
695
|
+
const maxDate = parseToDate(max);
|
|
696
|
+
const clamped = clampToRange(pendingDate, minDate, maxDate);
|
|
697
|
+
const str = clamped ? formatToInputString(clamped) : "";
|
|
698
|
+
if (!isControlled)
|
|
699
|
+
setInternalValue(str);
|
|
700
|
+
onChange?.(createInputChangeEvent(str, inputRef.current));
|
|
701
|
+
closePopover();
|
|
702
|
+
}, [isControlled, min, max, onChange, pendingDate]);
|
|
703
|
+
const handleInputChange = useCallback((e) => {
|
|
704
|
+
const raw = e.target.value;
|
|
705
|
+
const parsed = parseDateFieldValue(raw);
|
|
706
|
+
const minDate = parseToDate(min);
|
|
707
|
+
const maxDate = parseToDate(max);
|
|
708
|
+
if (parsed !== null) {
|
|
709
|
+
const clamped = clampToRange(parsed, minDate, maxDate);
|
|
710
|
+
const str = clamped ? formatToInputString(clamped) : "";
|
|
711
|
+
if (!isControlled)
|
|
712
|
+
setInternalValue(str);
|
|
713
|
+
onChange?.(createInputChangeEvent(str, inputRef.current));
|
|
714
|
+
}
|
|
715
|
+
else {
|
|
716
|
+
if (!isControlled)
|
|
717
|
+
setInternalValue(raw);
|
|
718
|
+
onChange?.(e);
|
|
719
|
+
}
|
|
720
|
+
}, [isControlled, min, max, onChange]);
|
|
721
|
+
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 => {
|
|
722
|
+
const displayDate = pendingDate ?? selectedDate ?? null;
|
|
723
|
+
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", onChange: val => {
|
|
724
|
+
const next = val instanceof Date ? val : Array.isArray(val) ? (val[0] instanceof Date ? val[0] : null) : null;
|
|
725
|
+
handleCalendarChange(next);
|
|
726
|
+
}, selectRange: false, tileDisabled: tileDisabled, value: displayDate }), jsx("hr", {}), jsxs("div", { className: "flex w-full justify-between gap-2 px-4 py-3", children: [jsx(Button, { className: "mr-auto", "data-testid": dataTestId ? `${dataTestId}-clear-button` : undefined, onClick: () => handleClear(closePopover), size: "small", variant: "secondary", children: t("dateField.actions.clear") }), jsxs("div", { className: "flex gap-2", children: [jsx(Button, { "data-testid": dataTestId ? `${dataTestId}-cancel-button` : undefined, onClick: () => handleCancel(closePopover), size: "small", variant: "ghost-neutral", children: t("dateField.actions.cancel") }), jsx(Button, { "data-testid": dataTestId ? `${dataTestId}-apply-button` : undefined, onClick: () => handleApply(closePopover), size: "small", children: t("dateField.actions.apply") })] })] })] }));
|
|
727
|
+
} })] }));
|
|
509
728
|
};
|
|
510
729
|
|
|
511
730
|
/**
|
|
@@ -2283,7 +2502,7 @@ const ColorField = ({ label, id, tip, helpText, errorMessage, helpAddon, classNa
|
|
|
2283
2502
|
ColorField.displayName = "ColorField";
|
|
2284
2503
|
|
|
2285
2504
|
/**
|
|
2286
|
-
* The date field component is used for entering date values with a
|
|
2505
|
+
* The date field component is used for entering date values with a calendar picker (same UI as DayPicker).
|
|
2287
2506
|
*
|
|
2288
2507
|
* ### When to use
|
|
2289
2508
|
* Use DateField for selecting calendar dates such as birthdates, deadlines, or scheduling.
|
|
@@ -4697,4 +4916,4 @@ const useZodValidators = () => {
|
|
|
4697
4916
|
*/
|
|
4698
4917
|
setupLibraryTranslations();
|
|
4699
4918
|
|
|
4700
|
-
export { ActionButton, BaseInput, BaseSelect, Checkbox, CheckboxField, ColorField, CreatableSelect, CreatableSelectField, DEFAULT_TIME, DateBaseInput, DateField, DropZone, DropZoneDefaultLabel, EMAIL_REGEX, EmailField, FormFieldSelectAdapter, FormGroup, Label, MultiSelectField, NumberBaseInput, NumberField, OptionCard, PasswordBaseInput, PasswordField, PhoneBaseInput, PhoneField, PhoneFieldWithController, RadioGroup, RadioItem, Schedule, ScheduleVariant, Search, SelectField, TextAreaBaseInput, TextAreaField, TextBaseInput, TextField, TimeRange, TimeRangeField, ToggleSwitch, ToggleSwitchOption, UploadField, UploadInput, UrlField, checkIfPhoneNumberHasPlus, countryCodeToFlagEmoji, cvaAccessoriesContainer, cvaActionButton, cvaActionContainer, cvaInput$1 as cvaInput, cvaInputAddon, cvaInputBase, cvaInputBaseDisabled, cvaInputBaseInvalid, cvaInputBaseReadOnly, cvaInputBaseSize, cvaInputElement, cvaInputGroup, cvaInputItemPlacementManager, cvaInputPrefix, cvaInputSuffix, cvaLabel, cvaRadioItem, cvaSelectClearIndicator, cvaSelectContainer, cvaSelectControl, cvaSelectDropdownIconContainer, cvaSelectDropdownIndicator, cvaSelectIndicatorsContainer, cvaSelectLoadingMessage, cvaSelectMenu, cvaSelectMenuList, cvaSelectMultiValue, cvaSelectNoOptionsMessage, cvaSelectPlaceholder, cvaSelectPrefixSuffix, cvaSelectSingleValue, cvaSelectValueContainer, getCountryAbbreviation, getPhoneNumberWithPlus, isInvalidCountryCode, isInvalidPhoneNumber, isValidHEXColor, parseSchedule, phoneErrorMessage, serializeSchedule, useCreatableSelect, useCustomComponents, useGetPhoneValidationRules, usePhoneInput, useSelect, useZodValidators, validateEmailAddress, validatePhoneNumber, weekDay };
|
|
4919
|
+
export { ActionButton, BaseInput, BaseSelect, Checkbox, CheckboxField, ColorField, CreatableSelect, CreatableSelectField, DEFAULT_TIME, DateBaseInput, DateField, DropZone, DropZoneDefaultLabel, EMAIL_REGEX, EmailField, FormFieldSelectAdapter, FormGroup, Label, MultiSelectField, NumberBaseInput, NumberField, OptionCard, PasswordBaseInput, PasswordField, PhoneBaseInput, PhoneField, PhoneFieldWithController, RadioGroup, RadioItem, Schedule, ScheduleVariant, Search, SelectField, TextAreaBaseInput, TextAreaField, TextBaseInput, TextField, TimeRange, TimeRangeField, ToggleSwitch, ToggleSwitchOption, UploadField, UploadInput, UrlField, checkIfPhoneNumberHasPlus, countryCodeToFlagEmoji, cvaAccessoriesContainer, cvaActionButton, cvaActionContainer, cvaInput$1 as cvaInput, cvaInputAddon, cvaInputBase, cvaInputBaseDisabled, cvaInputBaseInvalid, cvaInputBaseReadOnly, cvaInputBaseSize, cvaInputElement, cvaInputGroup, cvaInputItemPlacementManager, cvaInputPrefix, cvaInputSuffix, cvaLabel, cvaRadioItem, cvaSelectClearIndicator, cvaSelectContainer, cvaSelectControl, cvaSelectDropdownIconContainer, cvaSelectDropdownIndicator, cvaSelectIndicatorsContainer, cvaSelectLoadingMessage, cvaSelectMenu, cvaSelectMenuList, cvaSelectMultiValue, cvaSelectNoOptionsMessage, cvaSelectPlaceholder, cvaSelectPrefixSuffix, cvaSelectSingleValue, cvaSelectValueContainer, dateToISODateUTC, getCountryAbbreviation, getPhoneNumberWithPlus, isInvalidCountryCode, isInvalidPhoneNumber, isValidHEXColor, parseDateFieldValue, parseSchedule, phoneErrorMessage, serializeSchedule, toISODateStringUTC, useCreatableSelect, useCustomComponents, useGetPhoneValidationRules, usePhoneInput, useSelect, useZodValidators, validateEmailAddress, validatePhoneNumber, weekDay };
|
package/package.json
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/react-form-components",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.17",
|
|
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
|
+
"react-calendar": "^6.0.0",
|
|
10
11
|
"react-select": "^5.10.2",
|
|
11
|
-
"@trackunit/date-and-time-utils": "1.11.
|
|
12
|
+
"@trackunit/date-and-time-utils": "1.11.87",
|
|
12
13
|
"usehooks-ts": "^3.1.0",
|
|
13
14
|
"libphonenumber-js": "^1.12.22",
|
|
14
15
|
"zod": "^3.23.8",
|
|
15
16
|
"tailwind-merge": "^2.0.0",
|
|
16
|
-
"@trackunit/css-class-variance-utilities": "1.11.
|
|
17
|
-
"@trackunit/react-components": "1.20.
|
|
18
|
-
"@trackunit/ui-icons": "1.11.
|
|
19
|
-
"@trackunit/shared-utils": "1.13.
|
|
20
|
-
"@trackunit/ui-design-tokens": "1.11.
|
|
21
|
-
"@trackunit/i18n-library-translation": "1.15.
|
|
17
|
+
"@trackunit/css-class-variance-utilities": "1.11.85",
|
|
18
|
+
"@trackunit/react-components": "1.20.14",
|
|
19
|
+
"@trackunit/ui-icons": "1.11.81",
|
|
20
|
+
"@trackunit/shared-utils": "1.13.85",
|
|
21
|
+
"@trackunit/ui-design-tokens": "1.11.82",
|
|
22
|
+
"@trackunit/i18n-library-translation": "1.15.15",
|
|
22
23
|
"string-ts": "^2.0.0",
|
|
23
24
|
"es-toolkit": "^1.39.10"
|
|
24
25
|
},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ReactElement } from "react";
|
|
1
2
|
import { BaseInputProps } from "../../BaseInput/BaseInput";
|
|
2
3
|
type BaseInputExposedProps = Omit<BaseInputProps, "defaultValue" | "min" | "max" | "value">;
|
|
3
4
|
export interface DateBaseInputProps extends BaseInputExposedProps {
|
|
@@ -19,11 +20,11 @@ export interface DateBaseInputProps extends BaseInputExposedProps {
|
|
|
19
20
|
value?: Date | string | number;
|
|
20
21
|
}
|
|
21
22
|
/**
|
|
22
|
-
* A wrapper around BaseInput with a pop-up day picker.
|
|
23
|
+
* A wrapper around BaseInput with a pop-up day picker using the same calendar UI as DayPicker.
|
|
23
24
|
*
|
|
24
25
|
* The value is formatted to an ISO date string (YYYY-MM-DD)
|
|
25
26
|
*
|
|
26
27
|
* NOTE: If shown with a label, please use the `DateField` component instead.
|
|
27
28
|
*/
|
|
28
|
-
export declare const DateBaseInput: ({ min, max, defaultValue, value, ref, ...rest }: DateBaseInputProps) =>
|
|
29
|
+
export declare const DateBaseInput: ({ min, max, defaultValue, value, ref, onChange, suffix: suffixProp, "data-testid": dataTestId, ...rest }: DateBaseInputProps) => ReactElement;
|
|
29
30
|
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ReactElement } from "react";
|
|
1
2
|
import { FormGroupProps } from "../FormGroup/FormGroup";
|
|
2
3
|
import { DateBaseInputProps } from "./DateBaseInput/DateBaseInput";
|
|
3
4
|
type FormGroupExposedProps = Pick<FormGroupProps, "label" | "tip" | "helpText" | "helpAddon">;
|
|
@@ -8,7 +9,7 @@ export interface DateFieldProps extends DateBaseInputProps, FormGroupExposedProp
|
|
|
8
9
|
errorMessage?: string;
|
|
9
10
|
}
|
|
10
11
|
/**
|
|
11
|
-
* The date field component is used for entering date values with a
|
|
12
|
+
* The date field component is used for entering date values with a calendar picker (same UI as DayPicker).
|
|
12
13
|
*
|
|
13
14
|
* ### When to use
|
|
14
15
|
* Use DateField for selecting calendar dates such as birthdates, deadlines, or scheduling.
|
|
@@ -56,7 +57,7 @@ export interface DateFieldProps extends DateBaseInputProps, FormGroupExposedProp
|
|
|
56
57
|
* ```
|
|
57
58
|
*/
|
|
58
59
|
export declare const DateField: {
|
|
59
|
-
({ label, id, tip, helpText, errorMessage, helpAddon, isInvalid, className, defaultValue, "data-testid": dataTestId, ref, required, ...rest }: DateFieldProps):
|
|
60
|
+
({ label, id, tip, helpText, errorMessage, helpAddon, isInvalid, className, defaultValue, "data-testid": dataTestId, ref, required, ...rest }: DateFieldProps): ReactElement;
|
|
60
61
|
displayName: string;
|
|
61
62
|
};
|
|
62
63
|
export {};
|
package/src/index.d.ts
CHANGED
|
@@ -52,6 +52,7 @@ export * from "./components/UploadField/UploadField";
|
|
|
52
52
|
export * from "./components/UploadInput/UploadInput";
|
|
53
53
|
export * from "./components/UrlField/UrlField";
|
|
54
54
|
export * from "./utilities/emailUtils";
|
|
55
|
+
export * from "./utilities/parseDateFieldValue";
|
|
55
56
|
export * from "./utilities/useGetPhoneValidationRules";
|
|
56
57
|
export * from "./utilities/usePhoneInput";
|
|
57
58
|
export * from "./utilities/useZodValidators";
|
package/src/translation.d.ts
CHANGED
|
@@ -14,8 +14,8 @@ export declare const translations: TranslationResource<TranslationKeys>;
|
|
|
14
14
|
/**
|
|
15
15
|
* Local useTranslation for this specific library
|
|
16
16
|
*/
|
|
17
|
-
export declare const useTranslation: () => [TransForLibs<"baseInput.copyAction.toolTip" | "clearIndicator.icon.tooltip.clearAll" | "colorField.error.INVALID_HEX_CODE" | "colorField.error.REQUIRED" | "colorField.tooltip" | "dropzone.input.title" | "dropzone.label.default" | "emailField.error.INVALID_EMAIL" | "emailField.error.REQUIRED" | "field.notEditable.tooltip" | "field.required.asterisk.tooltip" | "numberField.error.GREATER_THAN" | "numberField.error.INVALID_NUMBER" | "numberField.error.LESS_THAN" | "numberField.error.NOT_IN_BETWEEN" | "numberField.error.REQUIRED" | "phoneField.error.INVALID_COUNTRY" | "phoneField.error.INVALID_LENGTH" | "phoneField.error.INVALID_NUMBER" | "phoneField.error.NOT_A_NUMBER" | "phoneField.error.REQUIRED" | "phoneField.error.REQUIRED_COUNTRY" | "phoneField.error.TOO_LONG" | "phoneField.error.TOO_SHORT" | "phoneField.error.undefined" | "schedule.label.active" | "schedule.label.allDay" | "schedule.label.day" | "search.placeholder" | "select.loadingMessage" | "select.noOptionsMessage" | "urlField.error.INVALID_URL" | "urlField.error.REQUIRED">, import("i18next").i18n, boolean] & {
|
|
18
|
-
t: TransForLibs<"baseInput.copyAction.toolTip" | "clearIndicator.icon.tooltip.clearAll" | "colorField.error.INVALID_HEX_CODE" | "colorField.error.REQUIRED" | "colorField.tooltip" | "dropzone.input.title" | "dropzone.label.default" | "emailField.error.INVALID_EMAIL" | "emailField.error.REQUIRED" | "field.notEditable.tooltip" | "field.required.asterisk.tooltip" | "numberField.error.GREATER_THAN" | "numberField.error.INVALID_NUMBER" | "numberField.error.LESS_THAN" | "numberField.error.NOT_IN_BETWEEN" | "numberField.error.REQUIRED" | "phoneField.error.INVALID_COUNTRY" | "phoneField.error.INVALID_LENGTH" | "phoneField.error.INVALID_NUMBER" | "phoneField.error.NOT_A_NUMBER" | "phoneField.error.REQUIRED" | "phoneField.error.REQUIRED_COUNTRY" | "phoneField.error.TOO_LONG" | "phoneField.error.TOO_SHORT" | "phoneField.error.undefined" | "schedule.label.active" | "schedule.label.allDay" | "schedule.label.day" | "search.placeholder" | "select.loadingMessage" | "select.noOptionsMessage" | "urlField.error.INVALID_URL" | "urlField.error.REQUIRED">;
|
|
17
|
+
export declare const useTranslation: () => [TransForLibs<"baseInput.copyAction.toolTip" | "clearIndicator.icon.tooltip.clearAll" | "colorField.error.INVALID_HEX_CODE" | "colorField.error.REQUIRED" | "colorField.tooltip" | "dateField.actions.apply" | "dateField.actions.cancel" | "dateField.actions.clear" | "dateField.placeholder" | "dropzone.input.title" | "dropzone.label.default" | "emailField.error.INVALID_EMAIL" | "emailField.error.REQUIRED" | "field.notEditable.tooltip" | "field.required.asterisk.tooltip" | "numberField.error.GREATER_THAN" | "numberField.error.INVALID_NUMBER" | "numberField.error.LESS_THAN" | "numberField.error.NOT_IN_BETWEEN" | "numberField.error.REQUIRED" | "phoneField.error.INVALID_COUNTRY" | "phoneField.error.INVALID_LENGTH" | "phoneField.error.INVALID_NUMBER" | "phoneField.error.NOT_A_NUMBER" | "phoneField.error.REQUIRED" | "phoneField.error.REQUIRED_COUNTRY" | "phoneField.error.TOO_LONG" | "phoneField.error.TOO_SHORT" | "phoneField.error.undefined" | "schedule.label.active" | "schedule.label.allDay" | "schedule.label.day" | "search.placeholder" | "select.loadingMessage" | "select.noOptionsMessage" | "urlField.error.INVALID_URL" | "urlField.error.REQUIRED">, import("i18next").i18n, boolean] & {
|
|
18
|
+
t: TransForLibs<"baseInput.copyAction.toolTip" | "clearIndicator.icon.tooltip.clearAll" | "colorField.error.INVALID_HEX_CODE" | "colorField.error.REQUIRED" | "colorField.tooltip" | "dateField.actions.apply" | "dateField.actions.cancel" | "dateField.actions.clear" | "dateField.placeholder" | "dropzone.input.title" | "dropzone.label.default" | "emailField.error.INVALID_EMAIL" | "emailField.error.REQUIRED" | "field.notEditable.tooltip" | "field.required.asterisk.tooltip" | "numberField.error.GREATER_THAN" | "numberField.error.INVALID_NUMBER" | "numberField.error.LESS_THAN" | "numberField.error.NOT_IN_BETWEEN" | "numberField.error.REQUIRED" | "phoneField.error.INVALID_COUNTRY" | "phoneField.error.INVALID_LENGTH" | "phoneField.error.INVALID_NUMBER" | "phoneField.error.NOT_A_NUMBER" | "phoneField.error.REQUIRED" | "phoneField.error.REQUIRED_COUNTRY" | "phoneField.error.TOO_LONG" | "phoneField.error.TOO_SHORT" | "phoneField.error.undefined" | "schedule.label.active" | "schedule.label.allDay" | "schedule.label.day" | "search.placeholder" | "select.loadingMessage" | "select.noOptionsMessage" | "urlField.error.INVALID_URL" | "urlField.error.REQUIRED">;
|
|
19
19
|
i18n: import("i18next").i18n;
|
|
20
20
|
ready: boolean;
|
|
21
21
|
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ChangeEvent } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Builds a synthetic change event for an input with a given value.
|
|
4
|
+
* Use when calling onChange from code (e.g. clear, apply, clamped value) so consumers
|
|
5
|
+
* that use `onChange={e => setValue(e.target.value)}` still receive the expected shape.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createInputChangeEvent(value: string, sourceInput: HTMLInputElement | null): ChangeEvent<HTMLInputElement>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses the value from a DateField change event (YYYY-MM-DD string) to a Date at local midnight.
|
|
3
|
+
* Use this when you need a Date from event.target.value to avoid timezone shifts that occur
|
|
4
|
+
* with `new Date(value)` (which interprets the string as UTC).
|
|
5
|
+
*
|
|
6
|
+
* @param value - The string from event.target.value (e.g. "2025-03-07" or "")
|
|
7
|
+
* @returns {Date | null} - Date at local midnight for the given day, or null if value is empty or invalid
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseDateFieldValue(value: string): Date | null;
|
|
10
|
+
/**
|
|
11
|
+
* Converts a YYYY-MM-DD string (e.g. from DateField) to an ISO string at UTC midnight for that calendar day.
|
|
12
|
+
* Use this when storing or sending date-only values so that the calendar day is preserved regardless of
|
|
13
|
+
* user timezone (unlike calling .toISOString() on a local-midnight Date, which shifts the day for UTC+).
|
|
14
|
+
*
|
|
15
|
+
* @param value - The string from event.target.value (e.g. "2025-03-07" or "")
|
|
16
|
+
* @returns {string | null} - "YYYY-MM-DDT00:00:00.000Z" or null if value is empty or invalid
|
|
17
|
+
*/
|
|
18
|
+
export declare function toISODateStringUTC(value: string): string | null;
|
|
19
|
+
/**
|
|
20
|
+
* Converts a Date (e.g. at local midnight) to an ISO string at UTC midnight for the same calendar day.
|
|
21
|
+
* Use when sending a date-only value to an API that expects UTC midnight (e.g. booking date).
|
|
22
|
+
*
|
|
23
|
+
* @param date - A Date instance (typically local midnight from a date picker)
|
|
24
|
+
* @returns {string} "YYYY-MM-DDT00:00:00.000Z" for that calendar day
|
|
25
|
+
*/
|
|
26
|
+
export declare function dateToISODateUTC(date: Date): string;
|