@trackunit/react-form-components 1.23.2 → 1.23.4
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 +128 -191
- package/index.esm.js +127 -193
- package/package.json +8 -8
- package/src/components/DateField/DateBaseInput/DateBaseInput.d.ts +2 -3
- package/src/components/DateField/DateField.d.ts +1 -6
- package/src/index.d.ts +1 -0
- package/src/utilities/parseDateFieldValue.d.ts +26 -0
- package/src/components/DateField/DateBaseInput/dateToCalendarDayString.d.ts +0 -25
- package/src/components/DateField/dateFieldInputUtils.d.ts +0 -23
package/index.cjs.js
CHANGED
|
@@ -30,7 +30,7 @@ var defaultTranslations = {
|
|
|
30
30
|
"dateField.actions.apply": "Apply",
|
|
31
31
|
"dateField.actions.cancel": "Cancel",
|
|
32
32
|
"dateField.actions.clear": "Clear",
|
|
33
|
-
"dateField.placeholder": "mm
|
|
33
|
+
"dateField.placeholder": "yyyy-mm-dd",
|
|
34
34
|
"dropzone.input.title": "Drag-and-drop file input",
|
|
35
35
|
"dropzone.label.default": "<clickable>Browse</clickable> or drag files here...",
|
|
36
36
|
"emailField.error.INVALID_EMAIL": "Please enter a valid email address",
|
|
@@ -107,6 +107,76 @@ const setupLibraryTranslations = () => {
|
|
|
107
107
|
i18nLibraryTranslation.registerTranslations(translations);
|
|
108
108
|
};
|
|
109
109
|
|
|
110
|
+
const ISO_DATE_REGEX = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
111
|
+
function parseAndValidateYYYYMMDD(value) {
|
|
112
|
+
if (value === "")
|
|
113
|
+
return null;
|
|
114
|
+
const isoMatch = ISO_DATE_REGEX.exec(value);
|
|
115
|
+
if (!isoMatch)
|
|
116
|
+
return null;
|
|
117
|
+
const [, y, m, d] = isoMatch;
|
|
118
|
+
if (y === undefined || m === undefined || d === undefined)
|
|
119
|
+
return null;
|
|
120
|
+
const year = Number.parseInt(y, 10);
|
|
121
|
+
const month = Number.parseInt(m, 10) - 1;
|
|
122
|
+
const day = Number.parseInt(d, 10);
|
|
123
|
+
if (month < 0 || month > 11 || day < 1 || day > 31)
|
|
124
|
+
return null;
|
|
125
|
+
const local = new Date(year, month, day);
|
|
126
|
+
if (Number.isNaN(local.getTime()))
|
|
127
|
+
return null;
|
|
128
|
+
if (local.getFullYear() !== year || local.getMonth() !== month || local.getDate() !== day) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
return { year, month: month + 1, day };
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Parses the value from a DateField change event (YYYY-MM-DD string) to a Date at local midnight.
|
|
135
|
+
* Use this when you need a Date from event.target.value to avoid timezone shifts that occur
|
|
136
|
+
* with `new Date(value)` (which interprets the string as UTC).
|
|
137
|
+
*
|
|
138
|
+
* @param value - The string from event.target.value (e.g. "2025-03-07" or "")
|
|
139
|
+
* @returns {Date | null} - Date at local midnight for the given day, or null if value is empty or invalid
|
|
140
|
+
*/
|
|
141
|
+
function parseDateFieldValue(value) {
|
|
142
|
+
const parsed = parseAndValidateYYYYMMDD(value);
|
|
143
|
+
if (!parsed)
|
|
144
|
+
return null;
|
|
145
|
+
return new Date(parsed.year, parsed.month - 1, parsed.day);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Converts a YYYY-MM-DD string (e.g. from DateField) to an ISO string at UTC midnight for that calendar day.
|
|
149
|
+
* Use this when storing or sending date-only values so that the calendar day is preserved regardless of
|
|
150
|
+
* user timezone (unlike calling .toISOString() on a local-midnight Date, which shifts the day for UTC+).
|
|
151
|
+
*
|
|
152
|
+
* @param value - The string from event.target.value (e.g. "2025-03-07" or "")
|
|
153
|
+
* @returns {string | null} - "YYYY-MM-DDT00:00:00.000Z" or null if value is empty or invalid
|
|
154
|
+
*/
|
|
155
|
+
function toISODateStringUTC(value) {
|
|
156
|
+
const parsed = parseAndValidateYYYYMMDD(value);
|
|
157
|
+
if (!parsed)
|
|
158
|
+
return null;
|
|
159
|
+
const { year, month, day } = parsed;
|
|
160
|
+
const mm = String(month).padStart(2, "0");
|
|
161
|
+
const dd = String(day).padStart(2, "0");
|
|
162
|
+
return `${year}-${mm}-${dd}T00:00:00.000Z`;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Converts a Date (e.g. at local midnight) to an ISO string at UTC midnight for the same calendar day.
|
|
166
|
+
* Use when sending a date-only value to an API that expects UTC midnight (e.g. booking date).
|
|
167
|
+
*
|
|
168
|
+
* @param date - A Date instance (typically local midnight from a date picker)
|
|
169
|
+
* @returns {string} "YYYY-MM-DDT00:00:00.000Z" for that calendar day
|
|
170
|
+
*/
|
|
171
|
+
function dateToISODateUTC(date) {
|
|
172
|
+
const year = date.getFullYear();
|
|
173
|
+
const month = date.getMonth() + 1;
|
|
174
|
+
const day = date.getDate();
|
|
175
|
+
const mm = String(month).padStart(2, "0");
|
|
176
|
+
const dd = String(day).padStart(2, "0");
|
|
177
|
+
return `${year}-${mm}-${dd}T00:00:00.000Z`;
|
|
178
|
+
}
|
|
179
|
+
|
|
110
180
|
const createSyntheticInputChangeEvent = (value, sourceInput) => {
|
|
111
181
|
const target = document.createElement("input");
|
|
112
182
|
target.value = value;
|
|
@@ -531,147 +601,6 @@ const BaseInput = ({ className, isInvalid = false, "data-testid": dataTestId, pr
|
|
|
531
601
|
};
|
|
532
602
|
BaseInput.displayName = "BaseInput";
|
|
533
603
|
|
|
534
|
-
const normalizeDisplayCompare = (value) => value
|
|
535
|
-
.normalize("NFKC")
|
|
536
|
-
.replace(/[\u202f\u00a0]/g, " ")
|
|
537
|
-
.replace(/\s+/g, " ")
|
|
538
|
-
.trim();
|
|
539
|
-
const COMPACT_DATE_REGEX = /^\d{8}$/;
|
|
540
|
-
const localePartOrderCache = new Map();
|
|
541
|
-
/**
|
|
542
|
-
* Returns the date-part display order (month/day/year) for the given locale by inspecting
|
|
543
|
-
* `Intl.DateTimeFormat.formatToParts`. Results are cached after the first call per locale.
|
|
544
|
-
* Falls back to MDY (English/US convention) on any error.
|
|
545
|
-
*/
|
|
546
|
-
function getLocaleDatePartOrder(locale) {
|
|
547
|
-
const cached = localePartOrderCache.get(locale);
|
|
548
|
-
if (cached !== undefined)
|
|
549
|
-
return cached;
|
|
550
|
-
try {
|
|
551
|
-
const parts = new Intl.DateTimeFormat(locale, { year: "numeric", month: "2-digit", day: "2-digit" })
|
|
552
|
-
.formatToParts(new Date(2000, 0, 15))
|
|
553
|
-
.filter(p => p.type === "month" || p.type === "day" || p.type === "year")
|
|
554
|
-
.map(p => p.type.charAt(0).toUpperCase())
|
|
555
|
-
.join("");
|
|
556
|
-
const order = parts === "DMY" ? "DMY" : parts === "YMD" ? "YMD" : "MDY";
|
|
557
|
-
localePartOrderCache.set(locale, order);
|
|
558
|
-
return order;
|
|
559
|
-
}
|
|
560
|
-
catch {
|
|
561
|
-
return "MDY";
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
/**
|
|
565
|
-
* Converts a locale-ordered 8-digit compact string to `YYYY-MM-DD`.
|
|
566
|
-
*
|
|
567
|
-
* The split follows the locale's field order:
|
|
568
|
-
* - MDY (e.g. `en`): `MMDDYYYY` → `YYYY-MM-DD`
|
|
569
|
-
* - DMY (e.g. `de`): `DDMMYYYY` → `YYYY-MM-DD`
|
|
570
|
-
* - YMD (e.g. `ja`): `YYYYMMDD` → `YYYY-MM-DD`
|
|
571
|
-
*
|
|
572
|
-
* Returns the original value unchanged for non-8-digit inputs.
|
|
573
|
-
*/
|
|
574
|
-
const normalizeCompactDateInput = (value, locale) => {
|
|
575
|
-
if (!COMPACT_DATE_REGEX.test(value))
|
|
576
|
-
return value;
|
|
577
|
-
const order = getLocaleDatePartOrder(locale ?? "en");
|
|
578
|
-
if (order === "MDY")
|
|
579
|
-
return `${value.slice(4, 8)}-${value.slice(0, 2)}-${value.slice(2, 4)}`;
|
|
580
|
-
if (order === "DMY")
|
|
581
|
-
return `${value.slice(4, 8)}-${value.slice(2, 4)}-${value.slice(0, 2)}`;
|
|
582
|
-
return `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)}`;
|
|
583
|
-
};
|
|
584
|
-
/**
|
|
585
|
-
* Parses date-field keyboard input to a local-midnight {@link Date}.
|
|
586
|
-
*
|
|
587
|
-
* Strict `YYYY-MM-DD` is accepted first; a locale-ordered compact 8-digit string (e.g. `04132026`
|
|
588
|
-
* in English for April 13 2026) is normalised to `YYYY-MM-DD` before that check. Otherwise the
|
|
589
|
-
* string must round-trip with {@link formatShortDateUtil} for the given `locale`. Arbitrary
|
|
590
|
-
* `new Date(string)`-parseable inputs that don't match the locale's format return `null` to avoid
|
|
591
|
-
* accepting confusable formats like `03/07/2025`.
|
|
592
|
-
*
|
|
593
|
-
* @param value - Raw text from the input.
|
|
594
|
-
* @param locale - Locale to compare against when the input is not `YYYY-MM-DD`.
|
|
595
|
-
*/
|
|
596
|
-
const parseDateFieldInputForChange = (value, locale) => {
|
|
597
|
-
const fromIso = dateAndTimeUtils.parseYYYYMMDDUtil(normalizeCompactDateInput(value, locale));
|
|
598
|
-
if (fromIso !== null)
|
|
599
|
-
return fromIso;
|
|
600
|
-
const trimmed = value.trim();
|
|
601
|
-
if (trimmed === "")
|
|
602
|
-
return null;
|
|
603
|
-
const parsed = dateAndTimeUtils.parseValidDate(trimmed);
|
|
604
|
-
if (parsed === null)
|
|
605
|
-
return null;
|
|
606
|
-
const localMidnight = dateAndTimeUtils.toDateUtil(dateAndTimeUtils.startOfDayUtil(parsed));
|
|
607
|
-
const formatted = dateAndTimeUtils.formatShortDateUtil(localMidnight, locale);
|
|
608
|
-
if (normalizeDisplayCompare(formatted) === normalizeDisplayCompare(trimmed)) {
|
|
609
|
-
return localMidnight;
|
|
610
|
-
}
|
|
611
|
-
return null;
|
|
612
|
-
};
|
|
613
|
-
/**
|
|
614
|
-
* Renders the stored date-field value for display:
|
|
615
|
-
* - empty string → empty
|
|
616
|
-
* - full canonical `YYYY-MM-DD` → locale-aware short date
|
|
617
|
-
* - partial input (e.g. `"2025-03"`) → returned as-is so the user can keep typing
|
|
618
|
-
*
|
|
619
|
-
* @param stored - The current canonical value (typically from `event.target.value`).
|
|
620
|
-
* @param locale - Locale for the display format.
|
|
621
|
-
*/
|
|
622
|
-
const dateFieldStoredValueToDisplayString = (stored, locale) => {
|
|
623
|
-
if (stored === "")
|
|
624
|
-
return "";
|
|
625
|
-
const parsed = dateAndTimeUtils.parseYYYYMMDDUtil(stored);
|
|
626
|
-
if (parsed !== null)
|
|
627
|
-
return dateAndTimeUtils.formatShortDateUtil(parsed, locale);
|
|
628
|
-
return stored;
|
|
629
|
-
};
|
|
630
|
-
|
|
631
|
-
/**
|
|
632
|
-
* Converts a Date instant to the canonical YYYY-MM-DD for a specific IANA time zone.
|
|
633
|
-
* This preserves the calendar day the user selected in their local zone.
|
|
634
|
-
*/
|
|
635
|
-
const dateToCalendarDayString = ({ date, timeZoneId }) => {
|
|
636
|
-
if (!date || Number.isNaN(date.getTime()))
|
|
637
|
-
return "";
|
|
638
|
-
const resolvedTimeZoneId = timeZoneId ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
639
|
-
return dateAndTimeUtils.toZonedDateTimeUtil(date, resolvedTimeZoneId).toPlainDate().toString();
|
|
640
|
-
};
|
|
641
|
-
/**
|
|
642
|
-
* Clamps a date to [minDate, maxDate] by comparing calendar days in a specific timezone.
|
|
643
|
-
* Returns the original `date` when it's in range, or the relevant boundary date when out of range.
|
|
644
|
-
*/
|
|
645
|
-
const clampDateToCalendarDayRange = ({ date, minDate, maxDate, timeZoneId, }) => {
|
|
646
|
-
if (date === null || date === undefined)
|
|
647
|
-
return null;
|
|
648
|
-
const day = dateToCalendarDayString({ date, timeZoneId });
|
|
649
|
-
if (day === "")
|
|
650
|
-
return null;
|
|
651
|
-
if (minDate !== undefined) {
|
|
652
|
-
const minDay = dateToCalendarDayString({ date: minDate, timeZoneId });
|
|
653
|
-
if (minDay !== "" && day < minDay)
|
|
654
|
-
return minDate;
|
|
655
|
-
}
|
|
656
|
-
if (maxDate !== undefined) {
|
|
657
|
-
const maxDay = dateToCalendarDayString({ date: maxDate, timeZoneId });
|
|
658
|
-
if (maxDay !== "" && day > maxDay)
|
|
659
|
-
return maxDate;
|
|
660
|
-
}
|
|
661
|
-
return date;
|
|
662
|
-
};
|
|
663
|
-
/**
|
|
664
|
-
* Returns true when a date falls outside [minDate, maxDate] by calendar day in a specific timezone.
|
|
665
|
-
*/
|
|
666
|
-
const isCalendarDayOutOfRange = ({ date, minDate, maxDate, timeZoneId, }) => {
|
|
667
|
-
if (!date)
|
|
668
|
-
return false;
|
|
669
|
-
const clamped = clampDateToCalendarDayRange({ date, minDate, maxDate, timeZoneId });
|
|
670
|
-
if (!clamped)
|
|
671
|
-
return false;
|
|
672
|
-
return clamped.getTime() !== date.getTime();
|
|
673
|
-
};
|
|
674
|
-
|
|
675
604
|
function parseToDate(v) {
|
|
676
605
|
if (v === undefined || v === "")
|
|
677
606
|
return undefined;
|
|
@@ -682,38 +611,63 @@ function parseToDate(v) {
|
|
|
682
611
|
return Number.isNaN(d.getTime()) ? undefined : d;
|
|
683
612
|
}
|
|
684
613
|
const str = String(v);
|
|
685
|
-
const fromField =
|
|
614
|
+
const fromField = parseDateFieldValue(str);
|
|
686
615
|
if (fromField !== null)
|
|
687
616
|
return fromField;
|
|
688
617
|
const fallback = new Date(str);
|
|
689
618
|
return Number.isNaN(fallback.getTime()) ? undefined : fallback;
|
|
690
619
|
}
|
|
620
|
+
function formatToInputString(d) {
|
|
621
|
+
if (!d)
|
|
622
|
+
return "";
|
|
623
|
+
return dateAndTimeUtils.Temporal.PlainDateTime.from({
|
|
624
|
+
year: d.getFullYear(),
|
|
625
|
+
month: d.getMonth() + 1,
|
|
626
|
+
day: d.getDate(),
|
|
627
|
+
})
|
|
628
|
+
.toPlainDate()
|
|
629
|
+
.toString();
|
|
630
|
+
}
|
|
631
|
+
function startOfDayMs(d) {
|
|
632
|
+
return dateAndTimeUtils.toDateUtil(dateAndTimeUtils.startOfDayUtil(d)).getTime();
|
|
633
|
+
}
|
|
634
|
+
/** Clamp a date to [minDate, maxDate]; returns the same date if in range or no bounds. */
|
|
635
|
+
function clampToRange(date, minDate, maxDate) {
|
|
636
|
+
if (date === null || date === undefined)
|
|
637
|
+
return null;
|
|
638
|
+
const dayStart = startOfDayMs(date);
|
|
639
|
+
if (minDate !== undefined && dayStart < startOfDayMs(minDate))
|
|
640
|
+
return minDate;
|
|
641
|
+
if (maxDate !== undefined && dayStart > startOfDayMs(maxDate))
|
|
642
|
+
return maxDate;
|
|
643
|
+
return date;
|
|
644
|
+
}
|
|
691
645
|
/**
|
|
692
646
|
* A wrapper around BaseInput with a pop-up day picker using the same calendar UI as DayPicker.
|
|
693
647
|
*
|
|
694
|
-
* The
|
|
695
|
-
* `onChange` still emits the canonical YYYY-MM-DD string for form parsing.
|
|
648
|
+
* The value is formatted to an ISO date string (YYYY-MM-DD)
|
|
696
649
|
*
|
|
697
650
|
* NOTE: If shown with a label, please use the `DateField` component instead.
|
|
698
651
|
*/
|
|
699
|
-
const DateBaseInput = ({ min, max, defaultValue, value, ref, onChange, suffix: suffixProp, "data-testid": dataTestId,
|
|
700
|
-
const timeZoneId = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
652
|
+
const DateBaseInput = ({ min, max, defaultValue, value, ref, onChange, suffix: suffixProp, "data-testid": dataTestId, ...rest }) => {
|
|
701
653
|
const isControlled = value !== undefined;
|
|
702
|
-
const [internalValue, setInternalValue] = react.useState(() =>
|
|
654
|
+
const [internalValue, setInternalValue] = react.useState(() => formatToInputString(parseToDate(defaultValue)));
|
|
703
655
|
const [pendingDate, setPendingDate] = react.useState(null);
|
|
704
|
-
const [t, i18n] = useTranslation();
|
|
705
|
-
const locale = i18n.resolvedLanguage ?? i18n.language;
|
|
706
656
|
// For controlled string value, only normalize when it's strict YYYY-MM-DD so partial input (e.g. "2025-03") is preserved.
|
|
707
657
|
const resolvedValue = isControlled
|
|
708
658
|
? typeof value === "string"
|
|
709
|
-
?
|
|
710
|
-
|
|
711
|
-
|
|
659
|
+
? (() => {
|
|
660
|
+
const strict = parseDateFieldValue(value);
|
|
661
|
+
return strict !== null ? formatToInputString(strict) : value;
|
|
662
|
+
})()
|
|
663
|
+
: formatToInputString(parseToDate(value))
|
|
664
|
+
: internalValue;
|
|
712
665
|
const selectedDate = isControlled && typeof value === "string"
|
|
713
|
-
? (
|
|
666
|
+
? (parseDateFieldValue(value) ?? undefined)
|
|
714
667
|
: parseToDate(isControlled ? value : internalValue);
|
|
715
668
|
const inputRef = react.useRef(null);
|
|
716
669
|
const createInputChangeEvent = useCreateInputChangeEvent();
|
|
670
|
+
const [t] = useTranslation();
|
|
717
671
|
react.useImperativeHandle(ref, () => inputRef.current ?? document.createElement("input"), []);
|
|
718
672
|
const syncPendingFromValue = react.useCallback(() => {
|
|
719
673
|
setPendingDate(selectedDate ?? null);
|
|
@@ -723,8 +677,13 @@ const DateBaseInput = ({ min, max, defaultValue, value, ref, onChange, suffix: s
|
|
|
723
677
|
return false;
|
|
724
678
|
const minDate = parseToDate(min);
|
|
725
679
|
const maxDate = parseToDate(max);
|
|
726
|
-
|
|
727
|
-
|
|
680
|
+
const dayStart = startOfDayMs(date);
|
|
681
|
+
if (minDate !== undefined && dayStart < startOfDayMs(minDate))
|
|
682
|
+
return true;
|
|
683
|
+
if (maxDate !== undefined && dayStart > startOfDayMs(maxDate))
|
|
684
|
+
return true;
|
|
685
|
+
return false;
|
|
686
|
+
}, [min, max]);
|
|
728
687
|
const handleCalendarChange = react.useCallback((next) => {
|
|
729
688
|
setPendingDate(next);
|
|
730
689
|
}, []);
|
|
@@ -741,21 +700,21 @@ const DateBaseInput = ({ min, max, defaultValue, value, ref, onChange, suffix: s
|
|
|
741
700
|
const handleApply = react.useCallback((closePopover) => {
|
|
742
701
|
const minDate = parseToDate(min);
|
|
743
702
|
const maxDate = parseToDate(max);
|
|
744
|
-
const clamped =
|
|
745
|
-
const str = clamped ?
|
|
703
|
+
const clamped = clampToRange(pendingDate, minDate, maxDate);
|
|
704
|
+
const str = clamped ? formatToInputString(clamped) : "";
|
|
746
705
|
if (!isControlled)
|
|
747
706
|
setInternalValue(str);
|
|
748
707
|
onChange?.(createInputChangeEvent(str, inputRef.current));
|
|
749
708
|
closePopover();
|
|
750
|
-
}, [createInputChangeEvent, isControlled, min, max, onChange, pendingDate
|
|
709
|
+
}, [createInputChangeEvent, isControlled, min, max, onChange, pendingDate]);
|
|
751
710
|
const handleInputChange = react.useCallback((e) => {
|
|
752
711
|
const raw = e.target.value;
|
|
753
|
-
const parsed =
|
|
712
|
+
const parsed = parseDateFieldValue(raw);
|
|
754
713
|
const minDate = parseToDate(min);
|
|
755
714
|
const maxDate = parseToDate(max);
|
|
756
715
|
if (parsed !== null) {
|
|
757
|
-
const clamped =
|
|
758
|
-
const str = clamped ?
|
|
716
|
+
const clamped = clampToRange(parsed, minDate, maxDate);
|
|
717
|
+
const str = clamped ? formatToInputString(clamped) : "";
|
|
759
718
|
if (!isControlled)
|
|
760
719
|
setInternalValue(str);
|
|
761
720
|
onChange?.(createInputChangeEvent(str, inputRef.current));
|
|
@@ -765,30 +724,10 @@ const DateBaseInput = ({ min, max, defaultValue, value, ref, onChange, suffix: s
|
|
|
765
724
|
setInternalValue(raw);
|
|
766
725
|
onChange?.(e);
|
|
767
726
|
}
|
|
768
|
-
}, [createInputChangeEvent, isControlled,
|
|
769
|
-
|
|
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 => {
|
|
727
|
+
}, [createInputChangeEvent, isControlled, min, max, onChange]);
|
|
728
|
+
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 => {
|
|
790
729
|
const displayDate = pendingDate ?? selectedDate ?? null;
|
|
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",
|
|
730
|
+
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 => {
|
|
792
731
|
const next = val instanceof Date ? val : Array.isArray(val) ? (val[0] instanceof Date ? val[0] : null) : null;
|
|
793
732
|
handleCalendarChange(next);
|
|
794
733
|
}, 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") })] })] })] }));
|
|
@@ -2571,9 +2510,6 @@ ColorField.displayName = "ColorField";
|
|
|
2571
2510
|
|
|
2572
2511
|
/**
|
|
2573
2512
|
* The date field component is used for entering date values with a calendar picker (same UI as DayPicker).
|
|
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`).
|
|
2577
2513
|
*
|
|
2578
2514
|
* ### When to use
|
|
2579
2515
|
* Use DateField for selecting calendar dates such as birthdates, deadlines, or scheduling.
|
|
@@ -2601,13 +2537,11 @@ ColorField.displayName = "ColorField";
|
|
|
2601
2537
|
* @example Date field with constraints
|
|
2602
2538
|
* ```tsx
|
|
2603
2539
|
* import { DateField } from "@trackunit/react-form-components";
|
|
2604
|
-
* import { dateToYYYYMMDDUtil } from "@trackunit/date-and-time-utils";
|
|
2605
2540
|
* import { useState } from "react";
|
|
2606
2541
|
*
|
|
2607
2542
|
* const BookingForm = () => {
|
|
2608
2543
|
* const [checkIn, setCheckIn] = useState("");
|
|
2609
|
-
*
|
|
2610
|
-
* const today = dateToYYYYMMDDUtil(new Date());
|
|
2544
|
+
* const today = new Date().toISOString().split("T")[0];
|
|
2611
2545
|
*
|
|
2612
2546
|
* return (
|
|
2613
2547
|
* <DateField
|
|
@@ -5082,14 +5016,17 @@ exports.cvaSelectPlaceholder = cvaSelectPlaceholder;
|
|
|
5082
5016
|
exports.cvaSelectPrefixSuffix = cvaSelectPrefixSuffix;
|
|
5083
5017
|
exports.cvaSelectSingleValue = cvaSelectSingleValue;
|
|
5084
5018
|
exports.cvaSelectValueContainer = cvaSelectValueContainer;
|
|
5019
|
+
exports.dateToISODateUTC = dateToISODateUTC;
|
|
5085
5020
|
exports.getCountryAbbreviation = getCountryAbbreviation;
|
|
5086
5021
|
exports.getPhoneNumberWithPlus = getPhoneNumberWithPlus;
|
|
5087
5022
|
exports.isInvalidCountryCode = isInvalidCountryCode;
|
|
5088
5023
|
exports.isInvalidPhoneNumber = isInvalidPhoneNumber;
|
|
5089
5024
|
exports.isValidHEXColor = isValidHEXColor;
|
|
5025
|
+
exports.parseDateFieldValue = parseDateFieldValue;
|
|
5090
5026
|
exports.parseSchedule = parseSchedule;
|
|
5091
5027
|
exports.phoneErrorMessage = phoneErrorMessage;
|
|
5092
5028
|
exports.serializeSchedule = serializeSchedule;
|
|
5029
|
+
exports.toISODateStringUTC = toISODateStringUTC;
|
|
5093
5030
|
exports.useCreatableSelect = useCreatableSelect;
|
|
5094
5031
|
exports.useCreateInputChangeEvent = useCreateInputChangeEvent;
|
|
5095
5032
|
exports.useCustomComponents = useCustomComponents;
|
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 {
|
|
3
|
+
import { Temporal, toDateUtil, startOfDayUtil } 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 { useCallback, useRef, useEffect, useImperativeHandle, useState, cloneElement, isValidElement, useLayoutEffect, useMemo, useReducer, createContext, useContext, useId } from 'react';
|
|
6
6
|
import ReactCalendar from 'react-calendar';
|
|
@@ -29,7 +29,7 @@ var defaultTranslations = {
|
|
|
29
29
|
"dateField.actions.apply": "Apply",
|
|
30
30
|
"dateField.actions.cancel": "Cancel",
|
|
31
31
|
"dateField.actions.clear": "Clear",
|
|
32
|
-
"dateField.placeholder": "mm
|
|
32
|
+
"dateField.placeholder": "yyyy-mm-dd",
|
|
33
33
|
"dropzone.input.title": "Drag-and-drop file input",
|
|
34
34
|
"dropzone.label.default": "<clickable>Browse</clickable> or drag files here...",
|
|
35
35
|
"emailField.error.INVALID_EMAIL": "Please enter a valid email address",
|
|
@@ -106,6 +106,76 @@ const setupLibraryTranslations = () => {
|
|
|
106
106
|
registerTranslations(translations);
|
|
107
107
|
};
|
|
108
108
|
|
|
109
|
+
const ISO_DATE_REGEX = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
110
|
+
function parseAndValidateYYYYMMDD(value) {
|
|
111
|
+
if (value === "")
|
|
112
|
+
return null;
|
|
113
|
+
const isoMatch = ISO_DATE_REGEX.exec(value);
|
|
114
|
+
if (!isoMatch)
|
|
115
|
+
return null;
|
|
116
|
+
const [, y, m, d] = isoMatch;
|
|
117
|
+
if (y === undefined || m === undefined || d === undefined)
|
|
118
|
+
return null;
|
|
119
|
+
const year = Number.parseInt(y, 10);
|
|
120
|
+
const month = Number.parseInt(m, 10) - 1;
|
|
121
|
+
const day = Number.parseInt(d, 10);
|
|
122
|
+
if (month < 0 || month > 11 || day < 1 || day > 31)
|
|
123
|
+
return null;
|
|
124
|
+
const local = new Date(year, month, day);
|
|
125
|
+
if (Number.isNaN(local.getTime()))
|
|
126
|
+
return null;
|
|
127
|
+
if (local.getFullYear() !== year || local.getMonth() !== month || local.getDate() !== day) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
return { year, month: month + 1, day };
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Parses the value from a DateField change event (YYYY-MM-DD string) to a Date at local midnight.
|
|
134
|
+
* Use this when you need a Date from event.target.value to avoid timezone shifts that occur
|
|
135
|
+
* with `new Date(value)` (which interprets the string as UTC).
|
|
136
|
+
*
|
|
137
|
+
* @param value - The string from event.target.value (e.g. "2025-03-07" or "")
|
|
138
|
+
* @returns {Date | null} - Date at local midnight for the given day, or null if value is empty or invalid
|
|
139
|
+
*/
|
|
140
|
+
function parseDateFieldValue(value) {
|
|
141
|
+
const parsed = parseAndValidateYYYYMMDD(value);
|
|
142
|
+
if (!parsed)
|
|
143
|
+
return null;
|
|
144
|
+
return new Date(parsed.year, parsed.month - 1, parsed.day);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Converts a YYYY-MM-DD string (e.g. from DateField) to an ISO string at UTC midnight for that calendar day.
|
|
148
|
+
* Use this when storing or sending date-only values so that the calendar day is preserved regardless of
|
|
149
|
+
* user timezone (unlike calling .toISOString() on a local-midnight Date, which shifts the day for UTC+).
|
|
150
|
+
*
|
|
151
|
+
* @param value - The string from event.target.value (e.g. "2025-03-07" or "")
|
|
152
|
+
* @returns {string | null} - "YYYY-MM-DDT00:00:00.000Z" or null if value is empty or invalid
|
|
153
|
+
*/
|
|
154
|
+
function toISODateStringUTC(value) {
|
|
155
|
+
const parsed = parseAndValidateYYYYMMDD(value);
|
|
156
|
+
if (!parsed)
|
|
157
|
+
return null;
|
|
158
|
+
const { year, month, day } = parsed;
|
|
159
|
+
const mm = String(month).padStart(2, "0");
|
|
160
|
+
const dd = String(day).padStart(2, "0");
|
|
161
|
+
return `${year}-${mm}-${dd}T00:00:00.000Z`;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Converts a Date (e.g. at local midnight) to an ISO string at UTC midnight for the same calendar day.
|
|
165
|
+
* Use when sending a date-only value to an API that expects UTC midnight (e.g. booking date).
|
|
166
|
+
*
|
|
167
|
+
* @param date - A Date instance (typically local midnight from a date picker)
|
|
168
|
+
* @returns {string} "YYYY-MM-DDT00:00:00.000Z" for that calendar day
|
|
169
|
+
*/
|
|
170
|
+
function dateToISODateUTC(date) {
|
|
171
|
+
const year = date.getFullYear();
|
|
172
|
+
const month = date.getMonth() + 1;
|
|
173
|
+
const day = date.getDate();
|
|
174
|
+
const mm = String(month).padStart(2, "0");
|
|
175
|
+
const dd = String(day).padStart(2, "0");
|
|
176
|
+
return `${year}-${mm}-${dd}T00:00:00.000Z`;
|
|
177
|
+
}
|
|
178
|
+
|
|
109
179
|
const createSyntheticInputChangeEvent = (value, sourceInput) => {
|
|
110
180
|
const target = document.createElement("input");
|
|
111
181
|
target.value = value;
|
|
@@ -530,147 +600,6 @@ const BaseInput = ({ className, isInvalid = false, "data-testid": dataTestId, pr
|
|
|
530
600
|
};
|
|
531
601
|
BaseInput.displayName = "BaseInput";
|
|
532
602
|
|
|
533
|
-
const normalizeDisplayCompare = (value) => value
|
|
534
|
-
.normalize("NFKC")
|
|
535
|
-
.replace(/[\u202f\u00a0]/g, " ")
|
|
536
|
-
.replace(/\s+/g, " ")
|
|
537
|
-
.trim();
|
|
538
|
-
const COMPACT_DATE_REGEX = /^\d{8}$/;
|
|
539
|
-
const localePartOrderCache = new Map();
|
|
540
|
-
/**
|
|
541
|
-
* Returns the date-part display order (month/day/year) for the given locale by inspecting
|
|
542
|
-
* `Intl.DateTimeFormat.formatToParts`. Results are cached after the first call per locale.
|
|
543
|
-
* Falls back to MDY (English/US convention) on any error.
|
|
544
|
-
*/
|
|
545
|
-
function getLocaleDatePartOrder(locale) {
|
|
546
|
-
const cached = localePartOrderCache.get(locale);
|
|
547
|
-
if (cached !== undefined)
|
|
548
|
-
return cached;
|
|
549
|
-
try {
|
|
550
|
-
const parts = new Intl.DateTimeFormat(locale, { year: "numeric", month: "2-digit", day: "2-digit" })
|
|
551
|
-
.formatToParts(new Date(2000, 0, 15))
|
|
552
|
-
.filter(p => p.type === "month" || p.type === "day" || p.type === "year")
|
|
553
|
-
.map(p => p.type.charAt(0).toUpperCase())
|
|
554
|
-
.join("");
|
|
555
|
-
const order = parts === "DMY" ? "DMY" : parts === "YMD" ? "YMD" : "MDY";
|
|
556
|
-
localePartOrderCache.set(locale, order);
|
|
557
|
-
return order;
|
|
558
|
-
}
|
|
559
|
-
catch {
|
|
560
|
-
return "MDY";
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
/**
|
|
564
|
-
* Converts a locale-ordered 8-digit compact string to `YYYY-MM-DD`.
|
|
565
|
-
*
|
|
566
|
-
* The split follows the locale's field order:
|
|
567
|
-
* - MDY (e.g. `en`): `MMDDYYYY` → `YYYY-MM-DD`
|
|
568
|
-
* - DMY (e.g. `de`): `DDMMYYYY` → `YYYY-MM-DD`
|
|
569
|
-
* - YMD (e.g. `ja`): `YYYYMMDD` → `YYYY-MM-DD`
|
|
570
|
-
*
|
|
571
|
-
* Returns the original value unchanged for non-8-digit inputs.
|
|
572
|
-
*/
|
|
573
|
-
const normalizeCompactDateInput = (value, locale) => {
|
|
574
|
-
if (!COMPACT_DATE_REGEX.test(value))
|
|
575
|
-
return value;
|
|
576
|
-
const order = getLocaleDatePartOrder(locale ?? "en");
|
|
577
|
-
if (order === "MDY")
|
|
578
|
-
return `${value.slice(4, 8)}-${value.slice(0, 2)}-${value.slice(2, 4)}`;
|
|
579
|
-
if (order === "DMY")
|
|
580
|
-
return `${value.slice(4, 8)}-${value.slice(2, 4)}-${value.slice(0, 2)}`;
|
|
581
|
-
return `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)}`;
|
|
582
|
-
};
|
|
583
|
-
/**
|
|
584
|
-
* Parses date-field keyboard input to a local-midnight {@link Date}.
|
|
585
|
-
*
|
|
586
|
-
* Strict `YYYY-MM-DD` is accepted first; a locale-ordered compact 8-digit string (e.g. `04132026`
|
|
587
|
-
* in English for April 13 2026) is normalised to `YYYY-MM-DD` before that check. Otherwise the
|
|
588
|
-
* string must round-trip with {@link formatShortDateUtil} for the given `locale`. Arbitrary
|
|
589
|
-
* `new Date(string)`-parseable inputs that don't match the locale's format return `null` to avoid
|
|
590
|
-
* accepting confusable formats like `03/07/2025`.
|
|
591
|
-
*
|
|
592
|
-
* @param value - Raw text from the input.
|
|
593
|
-
* @param locale - Locale to compare against when the input is not `YYYY-MM-DD`.
|
|
594
|
-
*/
|
|
595
|
-
const parseDateFieldInputForChange = (value, locale) => {
|
|
596
|
-
const fromIso = parseYYYYMMDDUtil(normalizeCompactDateInput(value, locale));
|
|
597
|
-
if (fromIso !== null)
|
|
598
|
-
return fromIso;
|
|
599
|
-
const trimmed = value.trim();
|
|
600
|
-
if (trimmed === "")
|
|
601
|
-
return null;
|
|
602
|
-
const parsed = parseValidDate(trimmed);
|
|
603
|
-
if (parsed === null)
|
|
604
|
-
return null;
|
|
605
|
-
const localMidnight = toDateUtil(startOfDayUtil(parsed));
|
|
606
|
-
const formatted = formatShortDateUtil(localMidnight, locale);
|
|
607
|
-
if (normalizeDisplayCompare(formatted) === normalizeDisplayCompare(trimmed)) {
|
|
608
|
-
return localMidnight;
|
|
609
|
-
}
|
|
610
|
-
return null;
|
|
611
|
-
};
|
|
612
|
-
/**
|
|
613
|
-
* Renders the stored date-field value for display:
|
|
614
|
-
* - empty string → empty
|
|
615
|
-
* - full canonical `YYYY-MM-DD` → locale-aware short date
|
|
616
|
-
* - partial input (e.g. `"2025-03"`) → returned as-is so the user can keep typing
|
|
617
|
-
*
|
|
618
|
-
* @param stored - The current canonical value (typically from `event.target.value`).
|
|
619
|
-
* @param locale - Locale for the display format.
|
|
620
|
-
*/
|
|
621
|
-
const dateFieldStoredValueToDisplayString = (stored, locale) => {
|
|
622
|
-
if (stored === "")
|
|
623
|
-
return "";
|
|
624
|
-
const parsed = parseYYYYMMDDUtil(stored);
|
|
625
|
-
if (parsed !== null)
|
|
626
|
-
return formatShortDateUtil(parsed, locale);
|
|
627
|
-
return stored;
|
|
628
|
-
};
|
|
629
|
-
|
|
630
|
-
/**
|
|
631
|
-
* Converts a Date instant to the canonical YYYY-MM-DD for a specific IANA time zone.
|
|
632
|
-
* This preserves the calendar day the user selected in their local zone.
|
|
633
|
-
*/
|
|
634
|
-
const dateToCalendarDayString = ({ date, timeZoneId }) => {
|
|
635
|
-
if (!date || Number.isNaN(date.getTime()))
|
|
636
|
-
return "";
|
|
637
|
-
const resolvedTimeZoneId = timeZoneId ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
638
|
-
return toZonedDateTimeUtil(date, resolvedTimeZoneId).toPlainDate().toString();
|
|
639
|
-
};
|
|
640
|
-
/**
|
|
641
|
-
* Clamps a date to [minDate, maxDate] by comparing calendar days in a specific timezone.
|
|
642
|
-
* Returns the original `date` when it's in range, or the relevant boundary date when out of range.
|
|
643
|
-
*/
|
|
644
|
-
const clampDateToCalendarDayRange = ({ date, minDate, maxDate, timeZoneId, }) => {
|
|
645
|
-
if (date === null || date === undefined)
|
|
646
|
-
return null;
|
|
647
|
-
const day = dateToCalendarDayString({ date, timeZoneId });
|
|
648
|
-
if (day === "")
|
|
649
|
-
return null;
|
|
650
|
-
if (minDate !== undefined) {
|
|
651
|
-
const minDay = dateToCalendarDayString({ date: minDate, timeZoneId });
|
|
652
|
-
if (minDay !== "" && day < minDay)
|
|
653
|
-
return minDate;
|
|
654
|
-
}
|
|
655
|
-
if (maxDate !== undefined) {
|
|
656
|
-
const maxDay = dateToCalendarDayString({ date: maxDate, timeZoneId });
|
|
657
|
-
if (maxDay !== "" && day > maxDay)
|
|
658
|
-
return maxDate;
|
|
659
|
-
}
|
|
660
|
-
return date;
|
|
661
|
-
};
|
|
662
|
-
/**
|
|
663
|
-
* Returns true when a date falls outside [minDate, maxDate] by calendar day in a specific timezone.
|
|
664
|
-
*/
|
|
665
|
-
const isCalendarDayOutOfRange = ({ date, minDate, maxDate, timeZoneId, }) => {
|
|
666
|
-
if (!date)
|
|
667
|
-
return false;
|
|
668
|
-
const clamped = clampDateToCalendarDayRange({ date, minDate, maxDate, timeZoneId });
|
|
669
|
-
if (!clamped)
|
|
670
|
-
return false;
|
|
671
|
-
return clamped.getTime() !== date.getTime();
|
|
672
|
-
};
|
|
673
|
-
|
|
674
603
|
function parseToDate(v) {
|
|
675
604
|
if (v === undefined || v === "")
|
|
676
605
|
return undefined;
|
|
@@ -681,38 +610,63 @@ function parseToDate(v) {
|
|
|
681
610
|
return Number.isNaN(d.getTime()) ? undefined : d;
|
|
682
611
|
}
|
|
683
612
|
const str = String(v);
|
|
684
|
-
const fromField =
|
|
613
|
+
const fromField = parseDateFieldValue(str);
|
|
685
614
|
if (fromField !== null)
|
|
686
615
|
return fromField;
|
|
687
616
|
const fallback = new Date(str);
|
|
688
617
|
return Number.isNaN(fallback.getTime()) ? undefined : fallback;
|
|
689
618
|
}
|
|
619
|
+
function formatToInputString(d) {
|
|
620
|
+
if (!d)
|
|
621
|
+
return "";
|
|
622
|
+
return Temporal.PlainDateTime.from({
|
|
623
|
+
year: d.getFullYear(),
|
|
624
|
+
month: d.getMonth() + 1,
|
|
625
|
+
day: d.getDate(),
|
|
626
|
+
})
|
|
627
|
+
.toPlainDate()
|
|
628
|
+
.toString();
|
|
629
|
+
}
|
|
630
|
+
function startOfDayMs(d) {
|
|
631
|
+
return toDateUtil(startOfDayUtil(d)).getTime();
|
|
632
|
+
}
|
|
633
|
+
/** Clamp a date to [minDate, maxDate]; returns the same date if in range or no bounds. */
|
|
634
|
+
function clampToRange(date, minDate, maxDate) {
|
|
635
|
+
if (date === null || date === undefined)
|
|
636
|
+
return null;
|
|
637
|
+
const dayStart = startOfDayMs(date);
|
|
638
|
+
if (minDate !== undefined && dayStart < startOfDayMs(minDate))
|
|
639
|
+
return minDate;
|
|
640
|
+
if (maxDate !== undefined && dayStart > startOfDayMs(maxDate))
|
|
641
|
+
return maxDate;
|
|
642
|
+
return date;
|
|
643
|
+
}
|
|
690
644
|
/**
|
|
691
645
|
* A wrapper around BaseInput with a pop-up day picker using the same calendar UI as DayPicker.
|
|
692
646
|
*
|
|
693
|
-
* The
|
|
694
|
-
* `onChange` still emits the canonical YYYY-MM-DD string for form parsing.
|
|
647
|
+
* The value is formatted to an ISO date string (YYYY-MM-DD)
|
|
695
648
|
*
|
|
696
649
|
* NOTE: If shown with a label, please use the `DateField` component instead.
|
|
697
650
|
*/
|
|
698
|
-
const DateBaseInput = ({ min, max, defaultValue, value, ref, onChange, suffix: suffixProp, "data-testid": dataTestId,
|
|
699
|
-
const timeZoneId = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
651
|
+
const DateBaseInput = ({ min, max, defaultValue, value, ref, onChange, suffix: suffixProp, "data-testid": dataTestId, ...rest }) => {
|
|
700
652
|
const isControlled = value !== undefined;
|
|
701
|
-
const [internalValue, setInternalValue] = useState(() =>
|
|
653
|
+
const [internalValue, setInternalValue] = useState(() => formatToInputString(parseToDate(defaultValue)));
|
|
702
654
|
const [pendingDate, setPendingDate] = useState(null);
|
|
703
|
-
const [t, i18n] = useTranslation();
|
|
704
|
-
const locale = i18n.resolvedLanguage ?? i18n.language;
|
|
705
655
|
// For controlled string value, only normalize when it's strict YYYY-MM-DD so partial input (e.g. "2025-03") is preserved.
|
|
706
656
|
const resolvedValue = isControlled
|
|
707
657
|
? typeof value === "string"
|
|
708
|
-
?
|
|
709
|
-
|
|
710
|
-
|
|
658
|
+
? (() => {
|
|
659
|
+
const strict = parseDateFieldValue(value);
|
|
660
|
+
return strict !== null ? formatToInputString(strict) : value;
|
|
661
|
+
})()
|
|
662
|
+
: formatToInputString(parseToDate(value))
|
|
663
|
+
: internalValue;
|
|
711
664
|
const selectedDate = isControlled && typeof value === "string"
|
|
712
|
-
? (
|
|
665
|
+
? (parseDateFieldValue(value) ?? undefined)
|
|
713
666
|
: parseToDate(isControlled ? value : internalValue);
|
|
714
667
|
const inputRef = useRef(null);
|
|
715
668
|
const createInputChangeEvent = useCreateInputChangeEvent();
|
|
669
|
+
const [t] = useTranslation();
|
|
716
670
|
useImperativeHandle(ref, () => inputRef.current ?? document.createElement("input"), []);
|
|
717
671
|
const syncPendingFromValue = useCallback(() => {
|
|
718
672
|
setPendingDate(selectedDate ?? null);
|
|
@@ -722,8 +676,13 @@ const DateBaseInput = ({ min, max, defaultValue, value, ref, onChange, suffix: s
|
|
|
722
676
|
return false;
|
|
723
677
|
const minDate = parseToDate(min);
|
|
724
678
|
const maxDate = parseToDate(max);
|
|
725
|
-
|
|
726
|
-
|
|
679
|
+
const dayStart = startOfDayMs(date);
|
|
680
|
+
if (minDate !== undefined && dayStart < startOfDayMs(minDate))
|
|
681
|
+
return true;
|
|
682
|
+
if (maxDate !== undefined && dayStart > startOfDayMs(maxDate))
|
|
683
|
+
return true;
|
|
684
|
+
return false;
|
|
685
|
+
}, [min, max]);
|
|
727
686
|
const handleCalendarChange = useCallback((next) => {
|
|
728
687
|
setPendingDate(next);
|
|
729
688
|
}, []);
|
|
@@ -740,21 +699,21 @@ const DateBaseInput = ({ min, max, defaultValue, value, ref, onChange, suffix: s
|
|
|
740
699
|
const handleApply = useCallback((closePopover) => {
|
|
741
700
|
const minDate = parseToDate(min);
|
|
742
701
|
const maxDate = parseToDate(max);
|
|
743
|
-
const clamped =
|
|
744
|
-
const str = clamped ?
|
|
702
|
+
const clamped = clampToRange(pendingDate, minDate, maxDate);
|
|
703
|
+
const str = clamped ? formatToInputString(clamped) : "";
|
|
745
704
|
if (!isControlled)
|
|
746
705
|
setInternalValue(str);
|
|
747
706
|
onChange?.(createInputChangeEvent(str, inputRef.current));
|
|
748
707
|
closePopover();
|
|
749
|
-
}, [createInputChangeEvent, isControlled, min, max, onChange, pendingDate
|
|
708
|
+
}, [createInputChangeEvent, isControlled, min, max, onChange, pendingDate]);
|
|
750
709
|
const handleInputChange = useCallback((e) => {
|
|
751
710
|
const raw = e.target.value;
|
|
752
|
-
const parsed =
|
|
711
|
+
const parsed = parseDateFieldValue(raw);
|
|
753
712
|
const minDate = parseToDate(min);
|
|
754
713
|
const maxDate = parseToDate(max);
|
|
755
714
|
if (parsed !== null) {
|
|
756
|
-
const clamped =
|
|
757
|
-
const str = clamped ?
|
|
715
|
+
const clamped = clampToRange(parsed, minDate, maxDate);
|
|
716
|
+
const str = clamped ? formatToInputString(clamped) : "";
|
|
758
717
|
if (!isControlled)
|
|
759
718
|
setInternalValue(str);
|
|
760
719
|
onChange?.(createInputChangeEvent(str, inputRef.current));
|
|
@@ -764,30 +723,10 @@ const DateBaseInput = ({ min, max, defaultValue, value, ref, onChange, suffix: s
|
|
|
764
723
|
setInternalValue(raw);
|
|
765
724
|
onChange?.(e);
|
|
766
725
|
}
|
|
767
|
-
}, [createInputChangeEvent, isControlled,
|
|
768
|
-
|
|
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 => {
|
|
726
|
+
}, [createInputChangeEvent, isControlled, min, max, onChange]);
|
|
727
|
+
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 => {
|
|
789
728
|
const displayDate = pendingDate ?? selectedDate ?? null;
|
|
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",
|
|
729
|
+
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 => {
|
|
791
730
|
const next = val instanceof Date ? val : Array.isArray(val) ? (val[0] instanceof Date ? val[0] : null) : null;
|
|
792
731
|
handleCalendarChange(next);
|
|
793
732
|
}, 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") })] })] })] }));
|
|
@@ -2570,9 +2509,6 @@ ColorField.displayName = "ColorField";
|
|
|
2570
2509
|
|
|
2571
2510
|
/**
|
|
2572
2511
|
* The date field component is used for entering date values with a calendar picker (same UI as DayPicker).
|
|
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`).
|
|
2576
2512
|
*
|
|
2577
2513
|
* ### When to use
|
|
2578
2514
|
* Use DateField for selecting calendar dates such as birthdates, deadlines, or scheduling.
|
|
@@ -2600,13 +2536,11 @@ ColorField.displayName = "ColorField";
|
|
|
2600
2536
|
* @example Date field with constraints
|
|
2601
2537
|
* ```tsx
|
|
2602
2538
|
* import { DateField } from "@trackunit/react-form-components";
|
|
2603
|
-
* import { dateToYYYYMMDDUtil } from "@trackunit/date-and-time-utils";
|
|
2604
2539
|
* import { useState } from "react";
|
|
2605
2540
|
*
|
|
2606
2541
|
* const BookingForm = () => {
|
|
2607
2542
|
* const [checkIn, setCheckIn] = useState("");
|
|
2608
|
-
*
|
|
2609
|
-
* const today = dateToYYYYMMDDUtil(new Date());
|
|
2543
|
+
* const today = new Date().toISOString().split("T")[0];
|
|
2610
2544
|
*
|
|
2611
2545
|
* return (
|
|
2612
2546
|
* <DateField
|
|
@@ -5002,4 +4936,4 @@ const useZodValidators = () => {
|
|
|
5002
4936
|
*/
|
|
5003
4937
|
setupLibraryTranslations();
|
|
5004
4938
|
|
|
5005
|
-
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, RadioGroupContext, 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, useCreateInputChangeEvent, useCustomComponents, useGetPhoneValidationRules, usePhoneInput, useRadioItemChecked, useSelect, useZodValidators, validateEmailAddress, validatePhoneNumber, weekDay };
|
|
4939
|
+
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, RadioGroupContext, 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, useCreateInputChangeEvent, useCustomComponents, useGetPhoneValidationRules, usePhoneInput, useRadioItemChecked, useSelect, useZodValidators, validateEmailAddress, validatePhoneNumber, weekDay };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/react-form-components",
|
|
3
|
-
"version": "1.23.
|
|
3
|
+
"version": "1.23.4",
|
|
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.
|
|
12
|
+
"@trackunit/date-and-time-utils": "1.11.121",
|
|
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.
|
|
18
|
-
"@trackunit/react-components": "1.22.
|
|
19
|
-
"@trackunit/ui-icons": "1.11.
|
|
20
|
-
"@trackunit/shared-utils": "1.13.
|
|
21
|
-
"@trackunit/ui-design-tokens": "1.11.
|
|
22
|
-
"@trackunit/i18n-library-translation": "1.19.
|
|
17
|
+
"@trackunit/css-class-variance-utilities": "1.11.118",
|
|
18
|
+
"@trackunit/react-components": "1.22.27",
|
|
19
|
+
"@trackunit/ui-icons": "1.11.114",
|
|
20
|
+
"@trackunit/shared-utils": "1.13.118",
|
|
21
|
+
"@trackunit/ui-design-tokens": "1.11.115",
|
|
22
|
+
"@trackunit/i18n-library-translation": "1.19.4",
|
|
23
23
|
"string-ts": "^2.0.0",
|
|
24
24
|
"es-toolkit": "^1.39.10"
|
|
25
25
|
},
|
|
@@ -22,10 +22,9 @@ export interface DateBaseInputProps extends BaseInputExposedProps {
|
|
|
22
22
|
/**
|
|
23
23
|
* A wrapper around BaseInput with a pop-up day picker using the same calendar UI as DayPicker.
|
|
24
24
|
*
|
|
25
|
-
* The
|
|
26
|
-
* `onChange` still emits the canonical YYYY-MM-DD string for form parsing.
|
|
25
|
+
* The value is formatted to an ISO date string (YYYY-MM-DD)
|
|
27
26
|
*
|
|
28
27
|
* NOTE: If shown with a label, please use the `DateField` component instead.
|
|
29
28
|
*/
|
|
30
|
-
export declare const DateBaseInput: ({ min, max, defaultValue, value, ref, onChange, suffix: suffixProp, "data-testid": dataTestId,
|
|
29
|
+
export declare const DateBaseInput: ({ min, max, defaultValue, value, ref, onChange, suffix: suffixProp, "data-testid": dataTestId, ...rest }: DateBaseInputProps) => ReactElement;
|
|
31
30
|
export {};
|
|
@@ -10,9 +10,6 @@ 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, 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`).
|
|
16
13
|
*
|
|
17
14
|
* ### When to use
|
|
18
15
|
* Use DateField for selecting calendar dates such as birthdates, deadlines, or scheduling.
|
|
@@ -40,13 +37,11 @@ export interface DateFieldProps extends DateBaseInputProps, FormGroupExposedProp
|
|
|
40
37
|
* @example Date field with constraints
|
|
41
38
|
* ```tsx
|
|
42
39
|
* import { DateField } from "@trackunit/react-form-components";
|
|
43
|
-
* import { dateToYYYYMMDDUtil } from "@trackunit/date-and-time-utils";
|
|
44
40
|
* import { useState } from "react";
|
|
45
41
|
*
|
|
46
42
|
* const BookingForm = () => {
|
|
47
43
|
* const [checkIn, setCheckIn] = useState("");
|
|
48
|
-
*
|
|
49
|
-
* const today = dateToYYYYMMDDUtil(new Date());
|
|
44
|
+
* const today = new Date().toISOString().split("T")[0];
|
|
50
45
|
*
|
|
51
46
|
* return (
|
|
52
47
|
* <DateField
|
package/src/index.d.ts
CHANGED
|
@@ -54,6 +54,7 @@ export * from "./components/UploadField/UploadField";
|
|
|
54
54
|
export * from "./components/UploadInput/UploadInput";
|
|
55
55
|
export * from "./components/UrlField/UrlField";
|
|
56
56
|
export * from "./utilities/emailUtils";
|
|
57
|
+
export * from "./utilities/parseDateFieldValue";
|
|
57
58
|
export * from "./utilities/useCreateInputChangeEvent";
|
|
58
59
|
export * from "./utilities/useGetPhoneValidationRules";
|
|
59
60
|
export * from "./utilities/usePhoneInput";
|
|
@@ -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;
|
|
@@ -1,25 +0,0 @@
|
|
|
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 {};
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Parses date-field keyboard input to a local-midnight {@link Date}.
|
|
3
|
-
*
|
|
4
|
-
* Strict `YYYY-MM-DD` is accepted first; a locale-ordered compact 8-digit string (e.g. `04132026`
|
|
5
|
-
* in English for April 13 2026) is normalised to `YYYY-MM-DD` before that check. Otherwise the
|
|
6
|
-
* string must round-trip with {@link formatShortDateUtil} for the given `locale`. Arbitrary
|
|
7
|
-
* `new Date(string)`-parseable inputs that don't match the locale's format return `null` to avoid
|
|
8
|
-
* accepting confusable formats like `03/07/2025`.
|
|
9
|
-
*
|
|
10
|
-
* @param value - Raw text from the input.
|
|
11
|
-
* @param locale - Locale to compare against when the input is not `YYYY-MM-DD`.
|
|
12
|
-
*/
|
|
13
|
-
export declare const parseDateFieldInputForChange: (value: string, locale: string | undefined) => Date | null;
|
|
14
|
-
/**
|
|
15
|
-
* Renders the stored date-field value for display:
|
|
16
|
-
* - empty string → empty
|
|
17
|
-
* - full canonical `YYYY-MM-DD` → locale-aware short date
|
|
18
|
-
* - partial input (e.g. `"2025-03"`) → returned as-is so the user can keep typing
|
|
19
|
-
*
|
|
20
|
-
* @param stored - The current canonical value (typically from `event.target.value`).
|
|
21
|
-
* @param locale - Locale for the display format.
|
|
22
|
-
*/
|
|
23
|
-
export declare const dateFieldStoredValueToDisplayString: (stored: string, locale: string | undefined) => string;
|