@trackunit/react-form-components 1.25.1 → 1.25.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.cjs.js CHANGED
@@ -2,15 +2,15 @@
2
2
 
3
3
  var jsxRuntime = require('react/jsx-runtime');
4
4
  var i18nLibraryTranslation = require('@trackunit/i18n-library-translation');
5
- var dateAndTimeUtils = require('@trackunit/date-and-time-utils');
6
5
  var reactComponents = require('@trackunit/react-components');
7
6
  var react = require('react');
8
- var ReactCalendar = require('react-calendar');
9
7
  var tailwindMerge = require('tailwind-merge');
10
- var uiDesignTokens = require('@trackunit/ui-design-tokens');
11
8
  var cssClassVarianceUtilities = require('@trackunit/css-class-variance-utilities');
9
+ var uiDesignTokens = require('@trackunit/ui-design-tokens');
12
10
  var stringTs = require('string-ts');
13
11
  var usehooksTs = require('usehooks-ts');
12
+ var ReactCalendar = require('react-calendar');
13
+ var dateAndTimeUtils = require('@trackunit/date-and-time-utils');
14
14
  var parsePhoneNumberFromString = require('libphonenumber-js');
15
15
  var ReactSelect = require('react-select');
16
16
  var ReactAsyncSelect = require('react-select/async');
@@ -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": "yyyy-mm-dd",
33
+ "dateField.openPicker.ariaLabel": "Open date picker",
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",
@@ -108,6 +108,45 @@ const setupLibraryTranslations = () => {
108
108
  };
109
109
 
110
110
  const ISO_DATE_REGEX = /^(\d{4})-(\d{2})-(\d{2})$/;
111
+ const DATE_PART_SEQUENCE = ["day", "month", "year"];
112
+ const PLACEHOLDER_BY_PART = {
113
+ day: "dd",
114
+ month: "mm",
115
+ year: "yyyy",
116
+ };
117
+ const ISO_PART_ORDER = ["year", "month", "day"];
118
+ // i18next's built-in key-debug mode. The "Developer" entry in `LanguageSelection` writes this
119
+ // value to `localStorage["i18nextLng"]`, so this is the actual token we have to filter out
120
+ // before it reaches `Intl.DateTimeFormat`.
121
+ const KEY_DEBUG_LANGUAGE = "cimode";
122
+ const isDatePartName = (value) => DATE_PART_SEQUENCE.some(part => part === value);
123
+ const getBrowserLanguage = () => typeof navigator !== "undefined" && navigator.language ? navigator.language : "en";
124
+ function parseAndValidateDateParts(parts) {
125
+ const { year: yearValue, month: monthValue, day: dayValue } = parts;
126
+ if (yearValue.length !== 4 ||
127
+ monthValue.length < 1 ||
128
+ monthValue.length > 2 ||
129
+ dayValue.length < 1 ||
130
+ dayValue.length > 2) {
131
+ return null;
132
+ }
133
+ const year = Number.parseInt(yearValue, 10);
134
+ const month = Number.parseInt(monthValue, 10) - 1;
135
+ const day = Number.parseInt(dayValue, 10);
136
+ if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) {
137
+ return null;
138
+ }
139
+ if (month < 0 || month > 11 || day < 1 || day > 31) {
140
+ return null;
141
+ }
142
+ const local = new Date(year, month, day);
143
+ if (Number.isNaN(local.getTime()))
144
+ return null;
145
+ if (local.getFullYear() !== year || local.getMonth() !== month || local.getDate() !== day) {
146
+ return null;
147
+ }
148
+ return { year, month: month + 1, day };
149
+ }
111
150
  function parseAndValidateYYYYMMDD(value) {
112
151
  if (value === "")
113
152
  return null;
@@ -117,18 +156,76 @@ function parseAndValidateYYYYMMDD(value) {
117
156
  const [, y, m, d] = isoMatch;
118
157
  if (y === undefined || m === undefined || d === undefined)
119
158
  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)
159
+ return parseAndValidateDateParts({ year: y, month: m, day: d });
160
+ }
161
+ function buildDatePartValues(partOrder, getPartAt) {
162
+ const values = { day: "", month: "", year: "" };
163
+ for (const [index, part] of partOrder.entries()) {
164
+ const value = getPartAt(part, index);
165
+ if (value === undefined) {
166
+ return null;
167
+ }
168
+ values[part] = value;
169
+ }
170
+ return values;
171
+ }
172
+ function parseDateByPartOrder(value, partOrder) {
173
+ const parts = value
174
+ .split(/\D+/)
175
+ .map(part => part.trim())
176
+ .filter(Boolean);
177
+ if (parts.length !== DATE_PART_SEQUENCE.length) {
178
+ return null;
179
+ }
180
+ const partValues = buildDatePartValues(partOrder, (_part, index) => parts[index]);
181
+ if (!partValues)
124
182
  return null;
125
- const local = new Date(year, month, day);
126
- if (Number.isNaN(local.getTime()))
183
+ const parsed = parseAndValidateDateParts(partValues);
184
+ if (!parsed)
127
185
  return null;
128
- if (local.getFullYear() !== year || local.getMonth() !== month || local.getDate() !== day) {
186
+ return new Date(parsed.year, parsed.month - 1, parsed.day);
187
+ }
188
+ function parseQuickTypedDateByPartOrder(digits, partOrder) {
189
+ if (!/^\d{8}$/.test(digits)) {
129
190
  return null;
130
191
  }
131
- return { year, month: month + 1, day };
192
+ let cursor = 0;
193
+ const partValues = buildDatePartValues(partOrder, part => {
194
+ const length = part === "year" ? 4 : 2;
195
+ const slice = digits.slice(cursor, cursor + length);
196
+ cursor += length;
197
+ return slice;
198
+ });
199
+ if (!partValues || cursor !== digits.length) {
200
+ return null;
201
+ }
202
+ const parsed = parseAndValidateDateParts(partValues);
203
+ if (!parsed)
204
+ return null;
205
+ return new Date(parsed.year, parsed.month - 1, parsed.day);
206
+ }
207
+ const datePartOrderByLocale = new Map();
208
+ const datePlaceholderByLocale = new Map();
209
+ function getDateFieldFormatParts(locale) {
210
+ return new Intl.DateTimeFormat(locale, {
211
+ day: "2-digit",
212
+ month: "2-digit",
213
+ year: "numeric",
214
+ }).formatToParts(new Date(2006, 0, 22));
215
+ }
216
+ function getDateFieldPartOrder(locale) {
217
+ const cached = datePartOrderByLocale.get(locale);
218
+ if (cached !== undefined)
219
+ return cached;
220
+ const orderedParts = [];
221
+ for (const part of getDateFieldFormatParts(locale)) {
222
+ if (isDatePartName(part.type)) {
223
+ orderedParts.push(part.type);
224
+ }
225
+ }
226
+ const resolved = orderedParts.length === DATE_PART_SEQUENCE.length ? orderedParts : ISO_PART_ORDER;
227
+ datePartOrderByLocale.set(locale, resolved);
228
+ return resolved;
132
229
  }
133
230
  /**
134
231
  * Parses the value from a DateField change event (YYYY-MM-DD string) to a Date at local midnight.
@@ -144,6 +241,88 @@ function parseDateFieldValue(value) {
144
241
  return null;
145
242
  return new Date(parsed.year, parsed.month - 1, parsed.day);
146
243
  }
244
+ /**
245
+ * Normalises a locale tag for use with `Intl.DateTimeFormat`.
246
+ *
247
+ * Filters out tags that are not real Intl locales (the empty string and i18next's `cimode`
248
+ * key-debug pseudo-language, surfaced as "Developer" in `LanguageSelection`) and replaces them
249
+ * with the browser locale. Region-qualified and bare-language Intl tags are returned unchanged
250
+ * so that the caller (`useDateFieldLocale`, callers passing a manual `locale` prop, etc.) keeps
251
+ * full control of the policy decision.
252
+ *
253
+ * Note: prior versions of this function additionally rewrote bare `"en"` to the browser locale
254
+ * so that UK browsers would not see US-style dates when the Manager UI language was English.
255
+ * That policy now lives in `useDateFieldLocale` (browser-first), so this function no longer
256
+ * special-cases `"en"`.
257
+ */
258
+ function resolveDateFieldLocale(locale) {
259
+ if (!locale || locale === KEY_DEBUG_LANGUAGE) {
260
+ return getBrowserLanguage();
261
+ }
262
+ return locale;
263
+ }
264
+ /**
265
+ * Returns the locale-aligned placeholder shown in the DateField. Result is memoized per locale —
266
+ * see `datePlaceholderByLocale` above.
267
+ */
268
+ function getDateFieldPlaceholder(locale) {
269
+ const cached = datePlaceholderByLocale.get(locale);
270
+ if (cached !== undefined)
271
+ return cached;
272
+ const placeholder = getDateFieldFormatParts(locale)
273
+ .map(part => {
274
+ if (part.type === "literal") {
275
+ return part.value;
276
+ }
277
+ if (isDatePartName(part.type)) {
278
+ return PLACEHOLDER_BY_PART[part.type];
279
+ }
280
+ return "";
281
+ })
282
+ .join("");
283
+ datePlaceholderByLocale.set(locale, placeholder);
284
+ return placeholder;
285
+ }
286
+ /**
287
+ * Formats a Date for display in the DateField while keeping event payloads canonical.
288
+ */
289
+ function formatDateFieldValueForLocale(value, locale) {
290
+ return new Intl.DateTimeFormat(locale, {
291
+ day: "2-digit",
292
+ month: "2-digit",
293
+ year: "numeric",
294
+ }).format(value);
295
+ }
296
+ /**
297
+ * Parses localized or quick-typed DateField input into a Date.
298
+ * Supports canonical YYYY-MM-DD, locale-ordered quick typing (e.g. DDMMYYYY), and YYYYMMDD.
299
+ */
300
+ function parseDateFieldDisplayValue(value, locale) {
301
+ const trimmedValue = value.trim();
302
+ if (trimmedValue === "") {
303
+ return null;
304
+ }
305
+ const strictIsoDate = parseDateFieldValue(trimmedValue);
306
+ if (strictIsoDate) {
307
+ return strictIsoDate;
308
+ }
309
+ const digitsOnly = trimmedValue.replace(/\D/g, "");
310
+ if (digitsOnly.length === 8) {
311
+ const quickTypedIsoDate = parseQuickTypedDateByPartOrder(digitsOnly, ISO_PART_ORDER);
312
+ if (quickTypedIsoDate) {
313
+ return quickTypedIsoDate;
314
+ }
315
+ const quickTypedLocalizedDate = parseQuickTypedDateByPartOrder(digitsOnly, getDateFieldPartOrder(locale));
316
+ if (quickTypedLocalizedDate) {
317
+ return quickTypedLocalizedDate;
318
+ }
319
+ }
320
+ const localizedDate = parseDateByPartOrder(trimmedValue, getDateFieldPartOrder(locale));
321
+ if (localizedDate) {
322
+ return localizedDate;
323
+ }
324
+ return parseDateByPartOrder(trimmedValue, ISO_PART_ORDER);
325
+ }
147
326
  /**
148
327
  * Converts a YYYY-MM-DD string (e.g. from DateField) to an ISO string at UTC midnight for that calendar day.
149
328
  * Use this when storing or sending date-only values so that the calendar day is preserved regardless of
@@ -177,44 +356,68 @@ function dateToISODateUTC(date) {
177
356
  return `${year}-${mm}-${dd}T00:00:00.000Z`;
178
357
  }
179
358
 
180
- const createSyntheticInputChangeEvent = (value, sourceInput) => {
181
- const target = document.createElement("input");
182
- target.value = value;
183
- if (sourceInput) {
184
- target.name = sourceInput.name;
185
- target.id = sourceInput.id;
186
- }
187
- const native = new Event("change", { bubbles: true });
188
- return {
189
- target,
190
- currentTarget: target,
191
- type: "change",
192
- bubbles: native.bubbles,
193
- cancelable: native.cancelable,
194
- defaultPrevented: native.defaultPrevented,
195
- eventPhase: native.eventPhase,
196
- isTrusted: native.isTrusted,
197
- nativeEvent: native,
198
- timeStamp: native.timeStamp,
199
- preventDefault: () => native.preventDefault(),
200
- stopPropagation: () => native.stopPropagation(),
201
- persist: () => {
202
- return;
203
- },
204
- isDefaultPrevented: () => native.defaultPrevented,
205
- isPropagationStopped: () => false,
206
- };
207
- };
359
+ const LANG_STORAGE_KEY = "i18nextLng";
208
360
  /**
209
- * Returns a stable function that builds a synthetic change event for an input with a given value.
210
- * Use when calling onChange from code (e.g. clear, apply, clamped value) so consumers
211
- * that use `onChange={e => setValue(e.target.value)}` still receive the expected shape.
212
- *
213
- * @example
214
- * const createInputChangeEvent = useCreateInputChangeEvent();
215
- * onChange?.(createInputChangeEvent(str, inputRef.current));
361
+ * Resolves the locale used by `DateField` for placeholder, parsing, and display formatting.
362
+ *
363
+ * Date format is a *regional* preference (e.g. `dd/mm/yyyy` vs `mm/dd/yyyy`), independent of the
364
+ * UI translation language. We therefore prefer `navigator.language` — a region-qualified browser
365
+ * locale (`en-GB`, `en-US`, `fr-CA`, `da-DK`, …) is the user's explicit regional preference and
366
+ * wins over the user-selected Manager UI language. The selected language is only used as a
367
+ * fallback when the browser locale is missing or has no region.
368
+ *
369
+ * Examples (selected | browser → resolved):
370
+ * - `en` | `en-GB` → `en-GB` (UK customers see `dd/mm/yyyy`)
371
+ * - `en` | `en-US` → `en-US` (US customers see `mm/dd/yyyy`)
372
+ * - `da` | `en-US` → `en-US` (browser region wins for date format)
373
+ * - `da` | `da-DK` → `da-DK` (`dd.mm.yyyy`, dotted separator is the native Danish format)
374
+ * - `en` | `en` (no region) → falls through to `resolveDateFieldLocale`, which keeps `en`
375
+ * - `cimode` | `en-US` → `en-US` (browser wins; i18next's key-debug pseudo-language is non-Intl)
376
+ * - `cimode` | `en` → `en` (`resolveDateFieldLocale` filters `cimode` out and returns the browser locale)
377
+ *
378
+ * Note: this hook reads `localStorage` and `navigator.language` synchronously and does not
379
+ * subscribe to changes. Re-renders on same-tab language change rely on the consuming component
380
+ * also using `useTranslation` (which re-renders on `i18next.languageChanged`). Cross-tab sync
381
+ * is not supported.
216
382
  */
217
- const useCreateInputChangeEvent = () => react.useCallback((value, sourceInput) => createSyntheticInputChangeEvent(value, sourceInput), []);
383
+ const useDateFieldLocale = () => {
384
+ const browserLocale = typeof navigator !== "undefined" ? navigator.language : "";
385
+ const selectedLanguage = typeof localStorage !== "undefined" ? localStorage.getItem(LANG_STORAGE_KEY) : null;
386
+ return react.useMemo(() => {
387
+ if (browserLocale.includes("-")) {
388
+ return browserLocale;
389
+ }
390
+ return resolveDateFieldLocale(selectedLanguage || browserLocale || "en");
391
+ }, [browserLocale, selectedLanguage]);
392
+ };
393
+
394
+ const cvaActionButton = cssClassVarianceUtilities.cvaMerge(["drop-shadow-none", "rounded-md"], {
395
+ variants: {
396
+ size: {
397
+ small: ["w-6", "h-6", "min-h-0"],
398
+ medium: ["w-6", "h-6", "min-h-0"],
399
+ large: ["w-8", "h-8"],
400
+ },
401
+ },
402
+ defaultVariants: {
403
+ size: "medium",
404
+ },
405
+ });
406
+ const cvaActionContainer = cssClassVarianceUtilities.cvaMerge(["flex", "items-center"], {
407
+ variants: {
408
+ size: {
409
+ //I just measured manually the top/bottom spacing
410
+ //when using the action button inside an input
411
+ //might need tweaking in the future
412
+ small: ["m-[1px]"],
413
+ medium: ["m-[3px]"],
414
+ large: ["m-[7px]"],
415
+ },
416
+ },
417
+ defaultVariants: {
418
+ size: "medium",
419
+ },
420
+ });
218
421
 
219
422
  const cvaInputBase = cssClassVarianceUtilities.cvaMerge([
220
423
  "component-baseInput-shadow",
@@ -399,41 +602,13 @@ const AddonRenderer = ({ addon, "data-testid": dataTestId, className, fieldSize,
399
602
  return (jsxRuntime.jsx("div", { className: cvaInputAddon({ size: fieldSize, position, className }), "data-testid": dataTestId ? `${dataTestId}-addon${stringTs.titleCase(position)}` : null, children: addon }));
400
603
  };
401
604
 
402
- const cvaActionButton = cssClassVarianceUtilities.cvaMerge(["drop-shadow-none", "rounded-md"], {
403
- variants: {
404
- size: {
405
- small: ["w-6", "h-6", "min-h-0"],
406
- medium: ["w-6", "h-6", "min-h-0"],
407
- large: ["w-8", "h-8"],
408
- },
409
- },
410
- defaultVariants: {
411
- size: "medium",
412
- },
413
- });
414
- const cvaActionContainer = cssClassVarianceUtilities.cvaMerge(["flex", "items-center"], {
415
- variants: {
416
- size: {
417
- //I just measured manually the top/bottom spacing
418
- //when using the action button inside an input
419
- //might need tweaking in the future
420
- small: ["m-[1px]"],
421
- medium: ["m-[3px]"],
422
- large: ["m-[7px]"],
423
- },
424
- },
425
- defaultVariants: {
426
- size: "medium",
427
- },
428
- });
429
-
430
605
  /**
431
606
  * The ActionButton component is a wrapper over IconButton to perform an action when the onClick event is triggered.
432
607
  *
433
608
  * @param {ActionButtonProps} props - The props for the ActionButton component
434
609
  * @returns {ReactElement} ActionButton component
435
610
  */
436
- const ActionButton = ({ type, value, "data-testid": dataTestId, size = "medium", disabled, className, onClick, ref, }) => {
611
+ const ActionButton = ({ type, value, "data-testid": dataTestId, size = "medium", disabled, className, onClick, style, ref, }) => {
437
612
  const [, copyToClipboard] = usehooksTs.useCopyToClipboard();
438
613
  const getIconName = () => {
439
614
  switch (type) {
@@ -472,7 +647,7 @@ const ActionButton = ({ type, value, "data-testid": dataTestId, size = "medium",
472
647
  }
473
648
  };
474
649
  const adjustedIconSize = size === "large" ? "medium" : size;
475
- return (jsxRuntime.jsx("div", { className: cvaActionContainer({ className, size }), ref: ref, children: jsxRuntime.jsx(reactComponents.IconButton, { className: cvaActionButton({ size: adjustedIconSize }), "data-testid": dataTestId || "testIconButtonId", disabled: disabled, icon: jsxRuntime.jsx(reactComponents.Icon, { name: getIconName(), size: adjustedIconSize }), onClick: buttonAction, size: "small", variant: "ghost-neutral" }) }));
650
+ return (jsxRuntime.jsx("div", { className: cvaActionContainer({ className, size }), ref: ref, style: style, children: jsxRuntime.jsx(reactComponents.IconButton, { className: cvaActionButton({ size: adjustedIconSize }), "data-testid": dataTestId || "testIconButtonId", disabled: disabled, icon: jsxRuntime.jsx(reactComponents.Icon, { name: getIconName(), size: adjustedIconSize }), onClick: buttonAction, size: "small", variant: "ghost-neutral" }) }));
476
651
  };
477
652
 
478
653
  const GenericActionsRenderer = ({ genericAction = undefined, disabled, fieldSize = undefined, innerRef, tooltipLabel, }) => {
@@ -601,7 +776,11 @@ const BaseInput = ({ className, isInvalid = false, "data-testid": dataTestId, pr
601
776
  };
602
777
  BaseInput.displayName = "BaseInput";
603
778
 
604
- function parseToDate(v) {
779
+ /**
780
+ * Parses a heterogeneous date input (Date, ISO string, locale-formatted string, or epoch number)
781
+ * into a `Date`. Returns `undefined` when the input is empty or unparseable.
782
+ */
783
+ function parseToDate(v, locale) {
605
784
  if (v === undefined || v === "")
606
785
  return undefined;
607
786
  if (v instanceof Date)
@@ -611,12 +790,13 @@ function parseToDate(v) {
611
790
  return Number.isNaN(d.getTime()) ? undefined : d;
612
791
  }
613
792
  const str = String(v);
614
- const fromField = parseDateFieldValue(str);
793
+ const fromField = parseDateFieldDisplayValue(str, locale);
615
794
  if (fromField !== null)
616
795
  return fromField;
617
796
  const fallback = new Date(str);
618
797
  return Number.isNaN(fallback.getTime()) ? undefined : fallback;
619
798
  }
799
+ /** Formats a `Date` as a canonical `YYYY-MM-DD` ISO date string, or `""` when undefined. */
620
800
  function formatToInputString(d) {
621
801
  if (!d)
622
802
  return "";
@@ -628,10 +808,11 @@ function formatToInputString(d) {
628
808
  .toPlainDate()
629
809
  .toString();
630
810
  }
811
+ /** Returns the epoch millis at the start of `d`'s local calendar day. */
631
812
  function startOfDayMs(d) {
632
813
  return dateAndTimeUtils.toDateUtil(dateAndTimeUtils.startOfDayUtil(d)).getTime();
633
814
  }
634
- /** Clamp a date to [minDate, maxDate]; returns the same date if in range or no bounds. */
815
+ /** Clamp a date to `[minDate, maxDate]`; returns the same date if in range or no bounds. */
635
816
  function clampToRange(date, minDate, maxDate) {
636
817
  if (date === null || date === undefined)
637
818
  return null;
@@ -642,96 +823,532 @@ function clampToRange(date, minDate, maxDate) {
642
823
  return maxDate;
643
824
  return date;
644
825
  }
826
+
827
+ const CALENDAR_TILE_CLASS = "react-calendar__tile";
828
+ const DAY_TILE_CLASS = "react-calendar__month-view__days__day";
829
+ const CALENDAR_TILE_DATE_ATTRIBUTE = "data-date-field-picker-date";
830
+ const CALENDAR_TILE_DATE_SELECTOR = `[${CALENDAR_TILE_DATE_ATTRIBUTE}]`;
645
831
  /**
646
- * A wrapper around BaseInput with a pop-up day picker using the same calendar UI as DayPicker.
832
+ * Renders the calendar + Clear/Cancel/Apply buttons inside the DateBaseInput popover.
647
833
  *
648
- * The value is formatted to an ISO date string (YYYY-MM-DD)
834
+ * Action callbacks are responsible for parent state changes only — this component handles
835
+ * closing the popover after each action completes.
836
+ */
837
+ const DateBaseInputPickerContent = ({ pendingDate, selectedDate, tileDisabled, closePopover, onCalendarChange, onClear, onCancel, onApply, applyDate, "data-testid": dataTestId, }) => {
838
+ const [t] = useTranslation();
839
+ const displayDate = pendingDate ?? selectedDate ?? null;
840
+ const [activeStartDate, setActiveStartDate] = react.useState(displayDate ?? undefined);
841
+ const [focusedDate, setFocusedDate] = react.useState(displayDate);
842
+ const [calendarView, setCalendarView] = react.useState("month");
843
+ // Set when Enter is pressed on a day tile so the resulting calendar `onChange` (fired
844
+ // by the browser-synthesised click) can apply the date in a single keystroke instead
845
+ // of just staging it. Without this, keyboard users have to also Tab to Apply.
846
+ const commitOnNextChangeRef = react.useRef(false);
847
+ const calendarWrapperRef = react.useRef(null);
848
+ const pendingFocusValueRef = react.useRef(null);
849
+ const allowNextOutsideFocusRef = react.useRef(false);
850
+ const getCalendarTileButtons = react.useCallback(() => {
851
+ return Array.from(calendarWrapperRef.current?.querySelectorAll(`.${CALENDAR_TILE_CLASS}`) ?? []);
852
+ }, []);
853
+ const getButtonDateValue = react.useCallback((button) => {
854
+ return button.querySelector(CALENDAR_TILE_DATE_SELECTOR)?.getAttribute(CALENDAR_TILE_DATE_ATTRIBUTE) ?? null;
855
+ }, []);
856
+ const getTabbablePickerButtons = react.useCallback(() => {
857
+ return Array.from(calendarWrapperRef.current?.querySelectorAll("button:not(:disabled)") ?? []).filter(button => button.tabIndex >= 0);
858
+ }, []);
859
+ const getFocusableDateForView = react.useCallback((date, view) => {
860
+ switch (view) {
861
+ case "month":
862
+ return date;
863
+ case "year":
864
+ return new Date(date.getFullYear(), date.getMonth(), 1);
865
+ case "decade":
866
+ return new Date(date.getFullYear(), 0, 1);
867
+ case "century":
868
+ return new Date(Math.floor((date.getFullYear() - 1) / 10) * 10 + 1, 0, 1);
869
+ default: {
870
+ const exhaustiveCheck = view;
871
+ throw new Error(`Unknown calendar view: ${exhaustiveCheck}`);
872
+ }
873
+ }
874
+ }, []);
875
+ const getCalendarTileColumnCount = react.useCallback((view) => {
876
+ switch (view) {
877
+ case "month":
878
+ return 7;
879
+ case "year":
880
+ case "decade":
881
+ case "century":
882
+ return 3;
883
+ default: {
884
+ const exhaustiveCheck = view;
885
+ throw new Error(`Unknown calendar view: ${exhaustiveCheck}`);
886
+ }
887
+ }
888
+ }, []);
889
+ react.useEffect(() => {
890
+ setFocusedDate(displayDate);
891
+ if (displayDate !== null) {
892
+ setActiveStartDate(new Date(displayDate.getFullYear(), displayDate.getMonth(), 1));
893
+ }
894
+ }, [displayDate]);
895
+ react.useEffect(() => {
896
+ const buttons = getCalendarTileButtons();
897
+ if (buttons.length === 0)
898
+ return;
899
+ const focusedDateValue = focusedDate ? formatToInputString(getFocusableDateForView(focusedDate, calendarView)) : "";
900
+ const fallbackButton = buttons.find(button => !button.disabled);
901
+ const focusedButton = focusedDateValue
902
+ ? buttons.find(button => getButtonDateValue(button) === focusedDateValue && !button.disabled)
903
+ : undefined;
904
+ const rovingButton = focusedButton ?? fallbackButton;
905
+ buttons.forEach(button => {
906
+ button.tabIndex = button === rovingButton ? 0 : -1;
907
+ });
908
+ if (pendingFocusValueRef.current === null)
909
+ return;
910
+ const buttonToFocus = buttons.find(button => getButtonDateValue(button) === pendingFocusValueRef.current && !button.disabled);
911
+ if (buttonToFocus === undefined)
912
+ return;
913
+ pendingFocusValueRef.current = null;
914
+ buttonToFocus.focus({ preventScroll: true });
915
+ }, [activeStartDate, calendarView, focusedDate, getButtonDateValue, getCalendarTileButtons, getFocusableDateForView]);
916
+ react.useEffect(() => {
917
+ const handlePointerDown = (event) => {
918
+ const target = event.target;
919
+ allowNextOutsideFocusRef.current =
920
+ target instanceof Node && calendarWrapperRef.current !== null && !calendarWrapperRef.current.contains(target);
921
+ };
922
+ const handleFocusIn = (event) => {
923
+ if (allowNextOutsideFocusRef.current) {
924
+ allowNextOutsideFocusRef.current = false;
925
+ return;
926
+ }
927
+ const target = event.target;
928
+ if (!(target instanceof Node) ||
929
+ calendarWrapperRef.current === null ||
930
+ calendarWrapperRef.current.contains(target)) {
931
+ return;
932
+ }
933
+ getTabbablePickerButtons()[0]?.focus({ preventScroll: true });
934
+ };
935
+ document.addEventListener("pointerdown", handlePointerDown, true);
936
+ document.addEventListener("focusin", handleFocusIn);
937
+ return () => {
938
+ document.removeEventListener("pointerdown", handlePointerDown, true);
939
+ document.removeEventListener("focusin", handleFocusIn);
940
+ };
941
+ }, [getTabbablePickerButtons]);
942
+ const handleClear = () => {
943
+ onClear();
944
+ closePopover();
945
+ };
946
+ const handleCancel = () => {
947
+ onCancel();
948
+ closePopover();
949
+ };
950
+ const handleApply = () => {
951
+ onApply();
952
+ closePopover();
953
+ };
954
+ const moveFocusBy = react.useCallback((target, steps) => {
955
+ const dateValue = getButtonDateValue(target);
956
+ if (dateValue === null)
957
+ return;
958
+ const currentDate = parseDateFieldValue(dateValue);
959
+ if (currentDate === null)
960
+ return;
961
+ const nextDate = new Date(currentDate);
962
+ switch (calendarView) {
963
+ case "month":
964
+ nextDate.setDate(currentDate.getDate() + steps);
965
+ break;
966
+ case "year":
967
+ nextDate.setMonth(currentDate.getMonth() + steps);
968
+ break;
969
+ case "decade":
970
+ nextDate.setFullYear(currentDate.getFullYear() + steps);
971
+ break;
972
+ case "century":
973
+ nextDate.setFullYear(currentDate.getFullYear() + steps * 10);
974
+ break;
975
+ default: {
976
+ const exhaustiveCheck = calendarView;
977
+ throw new Error(`Unknown calendar view: ${exhaustiveCheck}`);
978
+ }
979
+ }
980
+ const focusableDate = getFocusableDateForView(nextDate, calendarView);
981
+ pendingFocusValueRef.current = formatToInputString(focusableDate);
982
+ setFocusedDate(focusableDate);
983
+ setActiveStartDate(new Date(focusableDate.getFullYear(), focusableDate.getMonth(), 1));
984
+ }, [calendarView, getButtonDateValue, getFocusableDateForView]);
985
+ const handleKeyDownCapture = (e) => {
986
+ const target = e.target;
987
+ if (!(target instanceof HTMLButtonElement) || !target.classList.contains(CALENDAR_TILE_CLASS))
988
+ return;
989
+ if (e.key === "Enter" && target.classList.contains(DAY_TILE_CLASS)) {
990
+ commitOnNextChangeRef.current = true;
991
+ return;
992
+ }
993
+ if (e.key === "ArrowLeft") {
994
+ e.preventDefault();
995
+ moveFocusBy(target, -1);
996
+ return;
997
+ }
998
+ if (e.key === "ArrowRight") {
999
+ e.preventDefault();
1000
+ moveFocusBy(target, 1);
1001
+ return;
1002
+ }
1003
+ if (e.key === "ArrowUp") {
1004
+ e.preventDefault();
1005
+ moveFocusBy(target, -getCalendarTileColumnCount(calendarView));
1006
+ return;
1007
+ }
1008
+ if (e.key === "ArrowDown") {
1009
+ e.preventDefault();
1010
+ moveFocusBy(target, getCalendarTileColumnCount(calendarView));
1011
+ }
1012
+ };
1013
+ return (jsxRuntime.jsxs("div", { className: tailwindMerge.twMerge("flex w-min flex-col overflow-hidden rounded-md border border-neutral-300 bg-white p-0"), onKeyDownCapture: handleKeyDownCapture, ref: calendarWrapperRef, children: [jsxRuntime.jsx(ReactCalendar, { activeStartDate: activeStartDate, allowPartialRange: true, className: tailwindMerge.twMerge("custom-day-picker", "range-picker", "p-0"), onActiveStartDateChange: ({ activeStartDate: nextActiveStartDate }) => {
1014
+ setActiveStartDate(nextActiveStartDate ?? undefined);
1015
+ }, onChange: val => {
1016
+ const next = val instanceof Date ? val : Array.isArray(val) ? (val[0] instanceof Date ? val[0] : null) : null;
1017
+ if (commitOnNextChangeRef.current) {
1018
+ commitOnNextChangeRef.current = false;
1019
+ applyDate(next);
1020
+ closePopover();
1021
+ return;
1022
+ }
1023
+ onCalendarChange(next);
1024
+ }, onViewChange: ({ action, activeStartDate: nextActiveStartDate, view }) => {
1025
+ setCalendarView(view);
1026
+ const nextFocusedDate = getFocusableDateForView((action === "drillDown" ? nextActiveStartDate : focusedDate) ?? nextActiveStartDate ?? new Date(), view);
1027
+ pendingFocusValueRef.current = formatToInputString(nextFocusedDate);
1028
+ setFocusedDate(nextFocusedDate);
1029
+ setActiveStartDate(nextActiveStartDate ?? undefined);
1030
+ }, selectRange: false, tileContent: ({ date }) => jsxRuntime.jsx("span", { "aria-hidden": true, "data-date-field-picker-date": formatToInputString(date) }), tileDisabled: tileDisabled, value: displayDate, view: calendarView }), 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, 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, size: "small", variant: "ghost-neutral", children: t("dateField.actions.cancel") }), jsxRuntime.jsx(reactComponents.Button, { "data-testid": dataTestId ? `${dataTestId}-apply-button` : undefined, onClick: handleApply, size: "small", children: t("dateField.actions.apply") })] })] })] }));
1031
+ };
1032
+
1033
+ function buildSyntheticInputEvent(type, value, sourceInput) {
1034
+ const target = document.createElement("input");
1035
+ target.value = value;
1036
+ if (sourceInput) {
1037
+ target.name = sourceInput.name;
1038
+ target.id = sourceInput.id;
1039
+ }
1040
+ const native = new Event(type, { bubbles: true });
1041
+ return {
1042
+ target,
1043
+ currentTarget: target,
1044
+ type,
1045
+ bubbles: native.bubbles,
1046
+ cancelable: native.cancelable,
1047
+ defaultPrevented: native.defaultPrevented,
1048
+ eventPhase: native.eventPhase,
1049
+ isTrusted: native.isTrusted,
1050
+ nativeEvent: native,
1051
+ timeStamp: native.timeStamp,
1052
+ preventDefault: () => native.preventDefault(),
1053
+ stopPropagation: () => native.stopPropagation(),
1054
+ persist: () => undefined,
1055
+ isDefaultPrevented: () => native.defaultPrevented,
1056
+ isPropagationStopped: () => false,
1057
+ relatedTarget: null,
1058
+ };
1059
+ }
1060
+ /**
1061
+ * Returns a stable function that builds a synthetic change event for an input with a given value.
1062
+ * Use when calling onChange from code (e.g. clear, apply, clamped value) so consumers
1063
+ * that use `onChange={e => setValue(e.target.value)}` still receive the expected shape.
649
1064
  *
650
- * NOTE: If shown with a label, please use the `DateField` component instead.
1065
+ * @example
1066
+ * const createInputChangeEvent = useCreateInputChangeEvent();
1067
+ * onChange?.(createInputChangeEvent(str, inputRef.current));
651
1068
  */
652
- const DateBaseInput = ({ min, max, defaultValue, value, ref, onChange, suffix: suffixProp, "data-testid": dataTestId, ...rest }) => {
653
- const isControlled = value !== undefined;
654
- const [internalValue, setInternalValue] = react.useState(() => formatToInputString(parseToDate(defaultValue)));
655
- const [pendingDate, setPendingDate] = react.useState(null);
656
- // For controlled string value, only normalize when it's strict YYYY-MM-DD so partial input (e.g. "2025-03") is preserved.
657
- const resolvedValue = isControlled
658
- ? typeof value === "string"
659
- ? (() => {
660
- const strict = parseDateFieldValue(value);
661
- return strict !== null ? formatToInputString(strict) : value;
662
- })()
663
- : formatToInputString(parseToDate(value))
664
- : internalValue;
665
- const selectedDate = isControlled && typeof value === "string"
666
- ? (parseDateFieldValue(value) ?? undefined)
667
- : parseToDate(isControlled ? value : internalValue);
1069
+ const useCreateInputChangeEvent = () => react.useCallback((value, sourceInput) => buildSyntheticInputEvent("change", value, sourceInput), []);
1070
+ /**
1071
+ * Returns a stable function that builds a synthetic blur (FocusEvent) for an input with a given value.
1072
+ * Use when calling onBlur from code (e.g. when a typed-in value is normalized on blur) so consumers
1073
+ * that use `onBlur={e => doSomething(e.target.value)}` still receive the expected shape.
1074
+ *
1075
+ * @example
1076
+ * const createInputBlurEvent = useCreateInputBlurEvent();
1077
+ * onBlur?.(createInputBlurEvent(canonicalValue, inputRef.current));
1078
+ */
1079
+ const useCreateInputBlurEvent = () => react.useCallback((value, sourceInput) => buildSyntheticInputEvent("blur", value, sourceInput), []);
1080
+
1081
+ /**
1082
+ * Owns the underlying `<input>` ref, ref forwarding, the DOM-value sync layout effect, and the
1083
+ * synthetic change/blur emitters used to surface the canonical (ISO) value to consumers while
1084
+ * the displayed input value remains in the locale-specific format.
1085
+ *
1086
+ * The dual format is the reason this hook exists at all: the rendered `value` is locale-formatted
1087
+ * (e.g. `15/03/2025`) but `e.target.value` on `onChange`/`onBlur` should be canonical ISO
1088
+ * (`2025-03-15`). The synthetic events carry the canonical value, and the layout effect ensures
1089
+ * the DOM value is reconciled back to the display string on the next paint.
1090
+ */
1091
+ const useCanonicalInputEmitter = ({ ref, onChange, onBlur, resolvedValue, }) => {
668
1092
  const inputRef = react.useRef(null);
669
1093
  const createInputChangeEvent = useCreateInputChangeEvent();
670
- const [t] = useTranslation();
671
- react.useImperativeHandle(ref, () => inputRef.current ?? document.createElement("input"), []);
672
- const syncPendingFromValue = react.useCallback(() => {
673
- setPendingDate(selectedDate ?? null);
674
- }, [selectedDate]);
1094
+ const createInputBlurEvent = useCreateInputBlurEvent();
1095
+ const setInputRef = react.useCallback((node) => {
1096
+ inputRef.current = node;
1097
+ if (typeof ref === "function") {
1098
+ ref(node);
1099
+ }
1100
+ else if (ref !== null && ref !== undefined) {
1101
+ ref.current = node;
1102
+ }
1103
+ }, [ref]);
1104
+ react.useLayoutEffect(() => {
1105
+ const input = inputRef.current;
1106
+ if (input && input.value !== resolvedValue) {
1107
+ input.value = resolvedValue;
1108
+ }
1109
+ });
1110
+ const emitCanonicalValueChange = react.useCallback((canonicalValue) => {
1111
+ if (inputRef.current) {
1112
+ inputRef.current.value = canonicalValue;
1113
+ }
1114
+ onChange?.(createInputChangeEvent(canonicalValue, inputRef.current));
1115
+ }, [createInputChangeEvent, onChange]);
1116
+ const emitCanonicalValueBlur = react.useCallback((canonicalValue) => {
1117
+ if (inputRef.current) {
1118
+ inputRef.current.value = canonicalValue;
1119
+ }
1120
+ onBlur?.(createInputBlurEvent(canonicalValue, inputRef.current));
1121
+ if (inputRef.current && inputRef.current.value !== resolvedValue) {
1122
+ inputRef.current.value = resolvedValue;
1123
+ }
1124
+ }, [createInputBlurEvent, onBlur, resolvedValue]);
1125
+ return react.useMemo(() => ({
1126
+ inputRef,
1127
+ setInputRef,
1128
+ createInputChangeEvent,
1129
+ createInputBlurEvent,
1130
+ emitCanonicalValueChange,
1131
+ emitCanonicalValueBlur,
1132
+ }), [setInputRef, createInputChangeEvent, createInputBlurEvent, emitCanonicalValueChange, emitCanonicalValueBlur]);
1133
+ };
1134
+
1135
+ /**
1136
+ * Picker state machine for `DateBaseInput`. Owns the staged date, the popover's
1137
+ * "was-open" tracking, the tile-disabled predicate, and all four user-driven action handlers
1138
+ * plus the auto-apply behaviour for outside dismissals.
1139
+ */
1140
+ const useDatePickerController = ({ selectedDate, minDate, maxDate, commitValue, notifyPickerClose, }) => {
1141
+ const [pendingDate, setPendingDate] = react.useState(null);
1142
+ const wasOpenRef = react.useRef(false);
675
1143
  const tileDisabled = react.useCallback(({ date, view }) => {
676
1144
  if (view !== "month")
677
1145
  return false;
678
- const minDate = parseToDate(min);
679
- const maxDate = parseToDate(max);
680
1146
  const dayStart = startOfDayMs(date);
681
1147
  if (minDate !== undefined && dayStart < startOfDayMs(minDate))
682
1148
  return true;
683
1149
  if (maxDate !== undefined && dayStart > startOfDayMs(maxDate))
684
1150
  return true;
685
1151
  return false;
686
- }, [min, max]);
687
- const handleCalendarChange = react.useCallback((next) => {
1152
+ }, [minDate, maxDate]);
1153
+ const onCalendarChange = react.useCallback((next) => {
688
1154
  setPendingDate(next);
689
1155
  }, []);
690
- const handleClear = react.useCallback((closePopover) => {
1156
+ const onClear = react.useCallback(() => {
691
1157
  setPendingDate(null);
692
- if (!isControlled)
693
- setInternalValue("");
694
- onChange?.(createInputChangeEvent("", inputRef.current));
695
- closePopover();
696
- }, [createInputChangeEvent, isControlled, onChange]);
697
- const handleCancel = react.useCallback((closePopover) => {
698
- closePopover();
699
- }, []);
700
- const handleApply = react.useCallback((closePopover) => {
701
- const minDate = parseToDate(min);
702
- const maxDate = parseToDate(max);
1158
+ commitValue("");
1159
+ notifyPickerClose("", "clear");
1160
+ }, [commitValue, notifyPickerClose]);
1161
+ const onCancel = react.useCallback(() => {
1162
+ notifyPickerClose(formatToInputString(selectedDate), "cancel");
1163
+ }, [notifyPickerClose, selectedDate]);
1164
+ const commitDate = react.useCallback((date) => {
1165
+ const clamped = clampToRange(date, minDate, maxDate);
1166
+ const canonicalStr = clamped ? formatToInputString(clamped) : "";
1167
+ commitValue(canonicalStr);
1168
+ notifyPickerClose(canonicalStr, "apply");
1169
+ }, [commitValue, maxDate, minDate, notifyPickerClose]);
1170
+ const onApply = react.useCallback(() => commitDate(pendingDate), [commitDate, pendingDate]);
1171
+ const applyDate = react.useCallback((next) => {
1172
+ setPendingDate(next);
1173
+ commitDate(next);
1174
+ }, [commitDate]);
1175
+ const onPopoverOpenStateChange = react.useCallback((open) => {
1176
+ if (open) {
1177
+ wasOpenRef.current = true;
1178
+ setPendingDate(selectedDate ?? null);
1179
+ return;
1180
+ }
1181
+ if (!wasOpenRef.current)
1182
+ return;
1183
+ wasOpenRef.current = false;
703
1184
  const clamped = clampToRange(pendingDate, minDate, maxDate);
704
- const str = clamped ? formatToInputString(clamped) : "";
1185
+ const canonicalStr = clamped ? formatToInputString(clamped) : "";
1186
+ const currentCanonical = formatToInputString(selectedDate);
1187
+ if (canonicalStr !== currentCanonical) {
1188
+ commitValue(canonicalStr);
1189
+ }
1190
+ notifyPickerClose(canonicalStr, "outside");
1191
+ }, [commitValue, maxDate, minDate, notifyPickerClose, pendingDate, selectedDate]);
1192
+ return react.useMemo(() => ({
1193
+ pendingDate,
1194
+ tileDisabled,
1195
+ onCalendarChange,
1196
+ onClear,
1197
+ onCancel,
1198
+ onApply,
1199
+ applyDate,
1200
+ onPopoverOpenStateChange,
1201
+ }), [pendingDate, tileDisabled, onCalendarChange, onClear, onCancel, onApply, applyDate, onPopoverOpenStateChange]);
1202
+ };
1203
+
1204
+ /**
1205
+ * A wrapper around BaseInput with a pop-up day picker using the same calendar UI as DayPicker.
1206
+ *
1207
+ * The value is formatted to an ISO date string (YYYY-MM-DD)
1208
+ *
1209
+ * NOTE: If shown with a label, please use the `DateField` component instead.
1210
+ */
1211
+ const DateBaseInput = ({ min, max, defaultValue, value, ref, onChange, onBlur, onPickerClose, openOnFocus = false, locale: localeProp, suffix: suffixProp, "data-testid": dataTestId, ...rest }) => {
1212
+ const [t] = useTranslation();
1213
+ const autoLocale = useDateFieldLocale();
1214
+ const locale = localeProp ? resolveDateFieldLocale(localeProp) : autoLocale;
1215
+ const isControlled = value !== undefined;
1216
+ const [internalValue, setInternalValue] = react.useState(() => formatToInputString(parseToDate(defaultValue, locale)));
1217
+ const rawValue = isControlled
1218
+ ? typeof value === "string"
1219
+ ? value
1220
+ : formatToInputString(parseToDate(value, locale))
1221
+ : internalValue;
1222
+ const resolvedValue = react.useMemo(() => {
1223
+ const parsed = parseDateFieldValue(rawValue) ?? parseDateFieldDisplayValue(rawValue, locale);
1224
+ return parsed ? formatDateFieldValueForLocale(parsed, locale) : rawValue;
1225
+ }, [rawValue, locale]);
1226
+ const selectedDate = react.useMemo(() => isControlled && typeof value === "string"
1227
+ ? (parseDateFieldDisplayValue(value, locale) ?? undefined)
1228
+ : parseToDate(isControlled ? value : internalValue, locale), [isControlled, value, internalValue, locale]);
1229
+ const minDate = react.useMemo(() => parseToDate(min, locale), [min, locale]);
1230
+ const maxDate = react.useMemo(() => parseToDate(max, locale), [max, locale]);
1231
+ const { inputRef, setInputRef, createInputChangeEvent, createInputBlurEvent, emitCanonicalValueChange, emitCanonicalValueBlur, } = useCanonicalInputEmitter({ ref, onChange, onBlur, resolvedValue });
1232
+ const commitValue = react.useCallback((canonicalValue) => {
705
1233
  if (!isControlled)
706
- setInternalValue(str);
707
- onChange?.(createInputChangeEvent(str, inputRef.current));
708
- closePopover();
709
- }, [createInputChangeEvent, isControlled, min, max, onChange, pendingDate]);
1234
+ setInternalValue(canonicalValue);
1235
+ emitCanonicalValueChange(canonicalValue);
1236
+ }, [emitCanonicalValueChange, isControlled]);
1237
+ // Guards against `openOnFocus` re-opening the picker as soon as Floating UI returns focus
1238
+ // to the input after a close (Cancel/Apply/Escape/outside-press). The flag is set when the
1239
+ // picker closes and consumed by the very next focus event on the input; a setTimeout
1240
+ // fallback clears it so a later, user-initiated focus can still re-open the picker.
1241
+ const skipNextFocusOpenRef = react.useRef(false);
1242
+ const notifyPickerClose = react.useCallback((canonicalValue, reason) => {
1243
+ onPickerClose?.(createInputBlurEvent(canonicalValue, inputRef.current), reason);
1244
+ if (reason === "outside") {
1245
+ // The user dismissed the popover without an explicit action — they're done with
1246
+ // this field. Emit a synthetic blur so form libraries (e.g. react-hook-form) can
1247
+ // run validation; the underlying `<input>` may never have lost focus naturally
1248
+ // during the popover's lifecycle, depending on the browser and click target.
1249
+ emitCanonicalValueBlur(canonicalValue);
1250
+ }
1251
+ skipNextFocusOpenRef.current = true;
1252
+ setTimeout(() => {
1253
+ skipNextFocusOpenRef.current = false;
1254
+ }, 0);
1255
+ // Floating UI's `FloatingFocusManager` suppresses `returnFocus` for outside-press
1256
+ // dismissals, so the trigger never gets focus back. Restore focus to the input
1257
+ // ourselves, but only if focus was lost (active element is the body) — never steal
1258
+ // focus from a focusable element the user just clicked. The microtask defers this
1259
+ // past Floating UI's own focus handling and React's commit phase.
1260
+ queueMicrotask(() => {
1261
+ if (document.activeElement === document.body) {
1262
+ inputRef.current?.focus({ preventScroll: true });
1263
+ }
1264
+ });
1265
+ }, [createInputBlurEvent, emitCanonicalValueBlur, inputRef, onPickerClose]);
1266
+ const { pendingDate, tileDisabled, onCalendarChange, onClear, onCancel, onApply, applyDate, onPopoverOpenStateChange, } = useDatePickerController({ selectedDate, minDate, maxDate, commitValue, notifyPickerClose });
710
1267
  const handleInputChange = react.useCallback((e) => {
711
1268
  const raw = e.target.value;
712
- const parsed = parseDateFieldValue(raw);
713
- const minDate = parseToDate(min);
714
- const maxDate = parseToDate(max);
1269
+ const parsed = parseDateFieldDisplayValue(raw, locale);
715
1270
  if (parsed !== null) {
716
1271
  const clamped = clampToRange(parsed, minDate, maxDate);
717
- const str = clamped ? formatToInputString(clamped) : "";
718
- if (!isControlled)
719
- setInternalValue(str);
720
- onChange?.(createInputChangeEvent(str, inputRef.current));
1272
+ commitValue(clamped ? formatToInputString(clamped) : "");
721
1273
  }
722
1274
  else {
723
1275
  if (!isControlled)
724
1276
  setInternalValue(raw);
725
1277
  onChange?.(e);
726
1278
  }
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 => {
729
- const displayDate = pendingDate ?? selectedDate ?? null;
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 => {
731
- const next = val instanceof Date ? val : Array.isArray(val) ? (val[0] instanceof Date ? val[0] : null) : null;
732
- handleCalendarChange(next);
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") })] })] })] }));
734
- } })] }));
1279
+ }, [commitValue, isControlled, locale, minDate, maxDate, onChange]);
1280
+ const handleInputBlur = react.useCallback((e) => {
1281
+ const raw = e.target.value;
1282
+ if (raw === "") {
1283
+ if (!isControlled)
1284
+ setInternalValue("");
1285
+ emitCanonicalValueBlur("");
1286
+ return;
1287
+ }
1288
+ const parsed = parseDateFieldDisplayValue(raw, locale);
1289
+ if (parsed !== null) {
1290
+ const clamped = clampToRange(parsed, minDate, maxDate);
1291
+ const canonicalStr = clamped ? formatToInputString(clamped) : "";
1292
+ if (!isControlled)
1293
+ setInternalValue(canonicalStr);
1294
+ emitCanonicalValueBlur(canonicalStr);
1295
+ return;
1296
+ }
1297
+ if (!isControlled)
1298
+ setInternalValue("");
1299
+ onChange?.(createInputChangeEvent("", inputRef.current));
1300
+ onBlur?.(createInputBlurEvent("", inputRef.current));
1301
+ }, [
1302
+ createInputBlurEvent,
1303
+ createInputChangeEvent,
1304
+ emitCanonicalValueBlur,
1305
+ inputRef,
1306
+ isControlled,
1307
+ locale,
1308
+ maxDate,
1309
+ minDate,
1310
+ onBlur,
1311
+ onChange,
1312
+ ]);
1313
+ const isPickerDisabled = Boolean(rest.disabled) || Boolean(rest.readOnly);
1314
+ return (jsxRuntime.jsx(reactComponents.Popover, { activation: { click: false, hover: false, keyboardHandlers: false }, isModal: true, onOpenStateChange: onPopoverOpenStateChange, placement: "bottom-start", children: ({ isOpen, setIsOpen }) => {
1315
+ // Click activation is disabled on the popover itself so that focusing or clicking
1316
+ // the input does not open the picker — only the calendar IconButton does. Toggling
1317
+ // via the IconButton bypasses Floating UI's `onOpenChange`, so we forward the state
1318
+ // change to the controller manually to keep `notifyPickerClose` and pending-date
1319
+ // staging in sync with the rest of the lifecycle.
1320
+ const togglePopover = () => {
1321
+ const next = !isOpen;
1322
+ setIsOpen(next);
1323
+ onPopoverOpenStateChange(next);
1324
+ };
1325
+ // Restores the previous keyboard affordance: pressing Space while the input is focused
1326
+ // opens (or toggles) the picker. Enter is intentionally not handled here so that form
1327
+ // submission semantics on a text input remain intact.
1328
+ const handleInputKeyDown = (event) => {
1329
+ if (event.key === " " && !isPickerDisabled) {
1330
+ event.preventDefault();
1331
+ togglePopover();
1332
+ return;
1333
+ }
1334
+ rest.onKeyDown?.(event);
1335
+ };
1336
+ // Opt-in: opens the picker the moment the input gains focus. The `skipNextFocusOpenRef`
1337
+ // guard prevents the synthetic focus event fired by FloatingFocusManager's returnFocus
1338
+ // (after Cancel/Apply/Escape/outside-press) from immediately reopening the picker.
1339
+ const handleInputFocus = (event) => {
1340
+ rest.onFocus?.(event);
1341
+ if (skipNextFocusOpenRef.current) {
1342
+ skipNextFocusOpenRef.current = false;
1343
+ return;
1344
+ }
1345
+ if (openOnFocus && !isOpen && !isPickerDisabled) {
1346
+ setIsOpen(true);
1347
+ onPopoverOpenStateChange(true);
1348
+ }
1349
+ };
1350
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(reactComponents.PopoverTrigger, { children: jsxRuntime.jsx("div", { className: "flex w-full min-w-0 items-center", 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: handleInputBlur, onChange: handleInputChange, onFocus: handleInputFocus, onKeyDown: handleInputKeyDown, placeholder: rest.placeholder ?? getDateFieldPlaceholder(locale), ref: setInputRef, suffix: suffixProp ?? (jsxRuntime.jsx("div", { className: cvaActionContainer({ size: "medium" }), children: jsxRuntime.jsx(reactComponents.IconButton, { "aria-expanded": isOpen, "aria-label": t("dateField.openPicker.ariaLabel"), className: cvaActionButton({ size: "small" }), "data-testid": dataTestId ? `${dataTestId}-calendar` : "calendar", disabled: isPickerDisabled, icon: jsxRuntime.jsx(reactComponents.Icon, { "aria-label": undefined, name: "Calendar", size: "small", type: "solid" }), onClick: togglePopover, size: "small", tabIndex: isOpen ? -1 : undefined, variant: "ghost-neutral" }) })), tabIndex: isOpen ? -1 : rest.tabIndex, type: "text", value: resolvedValue }) }) }), jsxRuntime.jsx(reactComponents.PopoverContent, { initialFocus: 1, children: closePopover => (jsxRuntime.jsx(DateBaseInputPickerContent, { applyDate: applyDate, closePopover: closePopover, "data-testid": dataTestId, onApply: onApply, onCalendarChange: onCalendarChange, onCancel: onCancel, onClear: onClear, pendingDate: pendingDate, selectedDate: selectedDate, tileDisabled: tileDisabled })) })] }));
1351
+ } }));
735
1352
  };
736
1353
 
737
1354
  /**
@@ -2262,8 +2879,8 @@ const CreatableSelect = (props) => {
2262
2879
  * @param {LabelProps} props - The props for the Label component
2263
2880
  * @returns {ReactElement} Label component
2264
2881
  */
2265
- const Label = ({ id, htmlFor, children, className, "data-testid": dataTestId, disabled = false, isInvalid = false, ref, }) => {
2266
- return (jsxRuntime.jsx("label", { className: cvaLabel({ invalid: isInvalid, disabled, className }), "data-testid": dataTestId, htmlFor: htmlFor || "", id: id || "", ref: ref, children: children }));
2882
+ const Label = ({ id, htmlFor, children, className, "data-testid": dataTestId, disabled = false, isInvalid = false, style, ref, }) => {
2883
+ return (jsxRuntime.jsx("label", { className: cvaLabel({ invalid: isInvalid, disabled, className }), "data-testid": dataTestId, htmlFor: htmlFor || "", id: id || "", ref: ref, style: style, children: children }));
2267
2884
  };
2268
2885
 
2269
2886
  const cvaFormGroup = cssClassVarianceUtilities.cvaMerge(["component-formGroup-gap", "group", "form-group"]);
@@ -2314,13 +2931,13 @@ const cvaHelpAddon = cssClassVarianceUtilities.cvaMerge(["ml-auto"]);
2314
2931
  * @param {FormGroupProps} props - The props for the FormGroup component
2315
2932
  * @returns {ReactElement} FormGroup component
2316
2933
  */
2317
- const FormGroup = ({ isInvalid = false, isWarning = false, helpText, helpAddon, tip, className, "data-testid": dataTestId, label, htmlFor, children, required = false, ref, }) => {
2934
+ const FormGroup = ({ isInvalid = false, isWarning = false, helpText, helpAddon, tip, className, "data-testid": dataTestId, label, htmlFor, children, required = false, style, ref, }) => {
2318
2935
  const [t] = useTranslation();
2319
2936
  const validationStateIcon = react.useMemo(() => {
2320
2937
  const color = isInvalid ? "danger" : isWarning ? "warning" : null;
2321
2938
  return color ? jsxRuntime.jsx(reactComponents.Icon, { color: color, name: "ExclamationTriangle", size: "small" }) : null;
2322
2939
  }, [isInvalid, isWarning]);
2323
- return (jsxRuntime.jsxs("div", { className: cvaFormGroup({ className }), "data-testid": dataTestId, ref: ref, children: [label !== undefined && label !== null ? (jsxRuntime.jsxs("div", { className: cvaFormGroupContainerBefore(), children: [jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Label, { className: "component-formGroup-font", "data-testid": dataTestId ? `${dataTestId}-label` : undefined, htmlFor: htmlFor, id: htmlFor + "-label", children: label }), required ? (jsxRuntime.jsx(reactComponents.Tooltip, { "data-testid": "required-asterisk", label: t("field.required.asterisk.tooltip"), children: jsxRuntime.jsx("span", { children: "*" }) })) : null] }), tip !== undefined && tip !== null ? (jsxRuntime.jsx("span", { className: "ml-1", children: jsxRuntime.jsx(reactComponents.Tooltip, { "data-testid": dataTestId ? `${dataTestId}-tooltip` : undefined, label: tip, placement: "bottom" }) })) : null] })) : null, children, ((helpText !== undefined && helpText !== null) || (helpAddon !== undefined && helpAddon !== null)) ? (jsxRuntime.jsxs("div", { className: cvaFormGroupContainerAfter({ invalid: isInvalid, isWarning: isWarning }), children: [helpText ? (jsxRuntime.jsxs("div", { className: "flex gap-1", children: [validationStateIcon, jsxRuntime.jsx("span", { "data-testid": dataTestId ? `${dataTestId}-helpText` : undefined, children: helpText })] })) : undefined, helpAddon !== undefined && helpAddon !== null ? (jsxRuntime.jsx("span", { className: cvaHelpAddon(), "data-testid": dataTestId ? `${dataTestId}-helpAddon` : null, children: helpAddon })) : null] })) : null] }));
2940
+ return (jsxRuntime.jsxs("div", { className: cvaFormGroup({ className }), "data-testid": dataTestId, ref: ref, style: style, children: [label !== undefined && label !== null ? (jsxRuntime.jsxs("div", { className: cvaFormGroupContainerBefore(), children: [jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Label, { className: "component-formGroup-font", "data-testid": dataTestId ? `${dataTestId}-label` : undefined, htmlFor: htmlFor, id: htmlFor + "-label", children: label }), required ? (jsxRuntime.jsx(reactComponents.Tooltip, { "data-testid": "required-asterisk", label: t("field.required.asterisk.tooltip"), children: jsxRuntime.jsx("span", { children: "*" }) })) : null] }), tip !== undefined && tip !== null ? (jsxRuntime.jsx("span", { className: "ml-1", children: jsxRuntime.jsx(reactComponents.Tooltip, { "data-testid": dataTestId ? `${dataTestId}-tooltip` : undefined, label: tip, placement: "bottom" }) })) : null] })) : null, children, (helpText !== undefined && helpText !== null) || (helpAddon !== undefined && helpAddon !== null) ? (jsxRuntime.jsxs("div", { className: cvaFormGroupContainerAfter({ invalid: isInvalid, isWarning: isWarning }), children: [helpText ? (jsxRuntime.jsxs("div", { className: "flex gap-1", children: [validationStateIcon, jsxRuntime.jsx("span", { "data-testid": dataTestId ? `${dataTestId}-helpText` : undefined, children: helpText })] })) : undefined, helpAddon !== undefined && helpAddon !== null ? (jsxRuntime.jsx("span", { className: cvaHelpAddon(), "data-testid": dataTestId ? `${dataTestId}-helpAddon` : null, children: helpAddon })) : null] })) : null] }));
2324
2941
  };
2325
2942
 
2326
2943
  /**
@@ -2568,11 +3185,34 @@ ColorField.displayName = "ColorField";
2568
3185
  * );
2569
3186
  * };
2570
3187
  * ```
3188
+ * @example Server-side validation when the calendar popover closes
3189
+ * ```tsx
3190
+ * import { DateField } from "@trackunit/react-form-components";
3191
+ *
3192
+ * const ValidatedField = () => (
3193
+ * <DateField
3194
+ * label="Start date"
3195
+ * onChange={(e) => setLocalValue(e.target.value)}
3196
+ * onPickerClose={(e, reason) => {
3197
+ * if (reason === "cancel") return;
3198
+ * validateOnServer(e.target.value);
3199
+ * }}
3200
+ * />
3201
+ * );
3202
+ * ```
3203
+ * @example Open the picker automatically when the field is focused (e.g. via Tab)
3204
+ * ```tsx
3205
+ * import { DateField } from "@trackunit/react-form-components";
3206
+ *
3207
+ * const QuickPickField = () => (
3208
+ * <DateField label="Start date" openOnFocus />
3209
+ * );
3210
+ * ```
2571
3211
  */
2572
3212
  const DateField = ({ label, id, tip, helpText, errorMessage, helpAddon, isInvalid = undefined, className, defaultValue, "data-testid": dataTestId, ref, required = false, ...rest }) => {
2573
3213
  const renderAsInvalid = isInvalid === undefined ? Boolean(errorMessage) : isInvalid;
2574
3214
  const htmlForId = id ? id : "dateField-" + sharedUtils.uuidv4();
2575
- return (jsxRuntime.jsx(FormGroup, { "data-testid": dataTestId ? `${dataTestId}-FormGroup` : undefined, helpAddon: helpAddon, helpText: (renderAsInvalid && errorMessage) || helpText, htmlFor: htmlForId, isInvalid: renderAsInvalid, label: label, required: required ? !(Boolean(rest.disabled) || Boolean(rest.readOnly)) : false, tip: tip, children: jsxRuntime.jsx(DateBaseInput, { "aria-labelledby": htmlForId + "-label", defaultValue: defaultValue, id: htmlForId, isInvalid: renderAsInvalid, ref: ref, required: required, ...rest, className: className, "data-testid": dataTestId }) }));
3215
+ return (jsxRuntime.jsx(FormGroup, { "data-testid": dataTestId ? `${dataTestId}-FormGroup` : undefined, helpAddon: helpAddon, helpText: (renderAsInvalid && errorMessage) || helpText, htmlFor: htmlForId, isInvalid: renderAsInvalid, label: label, required: required ? !(Boolean(rest.disabled) || Boolean(rest.readOnly)) : false, tip: tip, children: jsxRuntime.jsx(DateBaseInput, { ...rest, "aria-labelledby": htmlForId + "-label", className: className, "data-testid": dataTestId, defaultValue: defaultValue, id: htmlForId, isInvalid: renderAsInvalid, ref: ref, required: required }) }));
2576
3216
  };
2577
3217
  DateField.displayName = "DateField";
2578
3218
 
@@ -2893,7 +3533,7 @@ const IGNORED_RS_ACTIONS = new Set(["set-value", "input-blur", "menu-close"]);
2893
3533
  * @returns {ReactElement} FormFieldSelectAdapterMulti component
2894
3534
  */
2895
3535
  const FormFieldSelectAdapterMulti = (props) => {
2896
- const { className, "data-testid": dataTestId, helpText, helpAddon, tip, label, isInvalid = undefined, errorMessage, name, onBlur, options, value, defaultValue, id, htmlFor: htmlForProp, onChange, children, ref, onInputChange, ...selectProps } = props;
3536
+ const { className, "data-testid": dataTestId, helpText, helpAddon, tip, label, isInvalid = undefined, errorMessage, name, onBlur, options, value, defaultValue, id, htmlFor: htmlForProp, onChange, children, style, ref, onInputChange, ...selectProps } = props;
2897
3537
  const [inputValue, setInputValue] = react.useState("");
2898
3538
  // Hidden select for a stable DOM ref target (API parity with single adapter)
2899
3539
  const innerRef = react.useRef(null);
@@ -2950,7 +3590,7 @@ const FormFieldSelectAdapterMulti = (props) => {
2950
3590
  return (jsxRuntime.jsxs(FormGroup, { className: className, "data-testid": dataTestId ? `${dataTestId}-FormGroup` : undefined, helpAddon: helpAddon, helpText: (renderAsInvalid && errorMessage) || helpText, htmlFor: controlId, isInvalid: renderAsInvalid, label: label, required: "required" in selectProps && selectProps.required
2951
3591
  ? !(("disabled" in selectProps && Boolean(selectProps.disabled)) ||
2952
3592
  ("readOnly" in selectProps && Boolean(selectProps.readOnly)))
2953
- : false, tip: tip, children: [jsxRuntime.jsx("select", { "aria-hidden": "true", defaultValue: "", hidden: true, name: name, ref: innerRef }), typeof getOptionValue === "function" &&
3593
+ : false, style: style, tip: tip, children: [jsxRuntime.jsx("select", { "aria-hidden": "true", defaultValue: "", hidden: true, name: name, ref: innerRef }), typeof getOptionValue === "function" &&
2954
3594
  selectedOptions.map((opt, idx) => {
2955
3595
  const primitiveValue = getOptionValue(opt);
2956
3596
  return typeof primitiveValue === "string" ? (jsxRuntime.jsx("input", { name: name, type: "hidden", value: primitiveValue }, `${primitiveValue}-${idx}`)) : null;
@@ -3632,8 +4272,8 @@ const RadioGroupContext = react.createContext(null);
3632
4272
  * @param {RadioGroupProps} props - The props for the RadioGroup component
3633
4273
  * @returns {ReactElement} RadioGroup component
3634
4274
  */
3635
- const RadioGroup = ({ children, id, name, value, disabled, onChange, label, inline = false, className, "data-testid": dataTestId, isInvalid, ref, }) => {
3636
- return (jsxRuntime.jsx(FormGroup, { "data-testid": dataTestId ? `${dataTestId}-FormGroup` : undefined, label: label, children: jsxRuntime.jsx("div", { className: cvaInputGroup({ layout: inline ? "inline" : null, className }), "data-testid": dataTestId, ref: ref, role: "radiogroup", children: jsxRuntime.jsx(RadioGroupContext.Provider, { value: {
4275
+ const RadioGroup = ({ children, id, name, value, disabled, onChange, label, inline = false, className, "data-testid": dataTestId, isInvalid, style, ref, }) => {
4276
+ return (jsxRuntime.jsx(FormGroup, { "data-testid": dataTestId ? `${dataTestId}-FormGroup` : undefined, label: label, children: jsxRuntime.jsx("div", { className: cvaInputGroup({ layout: inline ? "inline" : null, className }), "data-testid": dataTestId, ref: ref, role: "radiogroup", style: style, children: jsxRuntime.jsx(RadioGroupContext.Provider, { value: {
3637
4277
  id,
3638
4278
  value,
3639
4279
  name: name || id,
@@ -3731,7 +4371,7 @@ const cvaTimeRange = cssClassVarianceUtilities.cvaMerge([
3731
4371
  * @param {TimeRangeProps} props - The props for the TimeRange component
3732
4372
  * @returns {ReactElement} TimeRange component
3733
4373
  */
3734
- const TimeRange = ({ id, className, "data-testid": dataTestId, children, range, onChange, disabled, isInvalid, ref, }) => {
4374
+ const TimeRange = ({ id, className, "data-testid": dataTestId, children, range, onChange, disabled, isInvalid, style, ref, }) => {
3735
4375
  const [timeRange, setTimeRange] = react.useState(range ?? {
3736
4376
  timeFrom: DEFAULT_TIME,
3737
4377
  timeTo: DEFAULT_TIME,
@@ -3743,7 +4383,7 @@ const TimeRange = ({ id, className, "data-testid": dataTestId, children, range,
3743
4383
  setTimeRange(prev => ({ ...prev, timeTo }));
3744
4384
  };
3745
4385
  const onRangeChange = () => onChange(timeRange);
3746
- return (jsxRuntime.jsxs("div", { className: cvaTimeRange({ className }), "data-testid": dataTestId, id: id, ref: ref, children: [jsxRuntime.jsx(BaseInput, { "data-testid": `${dataTestId}-from`, disabled: disabled, isInvalid: isInvalid, onBlur: onRangeChange, onChange: (time) => onChangeFrom(time.currentTarget.value), type: "time", value: timeRange.timeFrom === "" ? DEFAULT_TIME : timeRange.timeFrom }), children ?? jsxRuntime.jsx("div", { "data-testid": `${dataTestId}-separator`, children: "-" }), jsxRuntime.jsx(BaseInput, { "data-testid": `${dataTestId}-to`, disabled: disabled, isInvalid: isInvalid, onBlur: onRangeChange, onChange: (time) => onChangeTo(time.currentTarget.value), type: "time", value: timeRange.timeTo === "" ? DEFAULT_TIME : timeRange.timeTo })] }));
4386
+ return (jsxRuntime.jsxs("div", { className: cvaTimeRange({ className }), "data-testid": dataTestId, id: id, ref: ref, style: style, children: [jsxRuntime.jsx(BaseInput, { "data-testid": `${dataTestId}-from`, disabled: disabled, isInvalid: isInvalid, onBlur: onRangeChange, onChange: (time) => onChangeFrom(time.currentTarget.value), type: "time", value: timeRange.timeFrom === "" ? DEFAULT_TIME : timeRange.timeFrom }), children ?? jsxRuntime.jsx("div", { "data-testid": `${dataTestId}-separator`, children: "-" }), jsxRuntime.jsx(BaseInput, { "data-testid": `${dataTestId}-to`, disabled: disabled, isInvalid: isInvalid, onBlur: onRangeChange, onChange: (time) => onChangeTo(time.currentTarget.value), type: "time", value: timeRange.timeTo === "" ? DEFAULT_TIME : timeRange.timeTo })] }));
3747
4387
  };
3748
4388
  const DEFAULT_TIME = "12:00";
3749
4389
 
@@ -3785,7 +4425,7 @@ const cvaScheduleItemText = cssClassVarianceUtilities.cvaMerge(["flex", "font-bo
3785
4425
  * @param {ScheduleProps} props - The props for the Schedule component
3786
4426
  * @returns {ReactElement} Schedule component
3787
4427
  */
3788
- const Schedule = ({ className, "data-testid": dataTestId, schedule, onChange, invalidKeys = [], ref, }) => {
4428
+ const Schedule = ({ className, "data-testid": dataTestId, schedule, onChange, invalidKeys = [], style, ref, }) => {
3789
4429
  const [t] = useTranslation();
3790
4430
  const onRangeChange = (range, index) => {
3791
4431
  const newSchedule = schedule.map((day, dayIndex) => (index === dayIndex ? { ...day, range: { ...range } } : day));
@@ -3817,7 +4457,7 @@ const Schedule = ({ className, "data-testid": dataTestId, schedule, onChange, in
3817
4457
  : day);
3818
4458
  onChange(newSchedule);
3819
4459
  };
3820
- return (jsxRuntime.jsx("div", { className: className, "data-testid": dataTestId, ref: ref, children: schedule.map(({ label, range, isActive, key, checkboxLabel, isAllDay }, index) => {
4460
+ return (jsxRuntime.jsx("div", { className: className, "data-testid": dataTestId, ref: ref, style: style, children: schedule.map(({ label, range, isActive, key, checkboxLabel, isAllDay }, index) => {
3821
4461
  return (jsxRuntime.jsxs("div", { className: cvaScheduleItem(), children: [jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-4 sm:hidden", children: [jsxRuntime.jsx(reactComponents.Text, { className: "font-medium text-neutral-500", children: t("schedule.label.day") }), jsxRuntime.jsx(reactComponents.Text, { className: cvaScheduleItemText(), size: "medium", subtle: !isActive, children: label }), jsxRuntime.jsx(reactComponents.Text, { className: "font-medium text-neutral-500", children: t("schedule.label.active") }), jsxRuntime.jsx(Checkbox, { checked: isActive, label: checkboxLabel, onChange: (event) => onActiveChange(Boolean(event.currentTarget.checked), index) }), jsxRuntime.jsx(reactComponents.Text, { className: "font-medium text-neutral-500", children: t("schedule.label.allDay") }), jsxRuntime.jsx(Checkbox, { checked: isAllDay ? isActive : undefined, disabled: !isActive, onChange: (event) => onAllDayChange(Boolean(event.currentTarget.checked), index) }), jsxRuntime.jsx(TimeRange, { disabled: !isActive || isAllDay, isInvalid: !!invalidKeys.find((invalidKey) => invalidKey === key), onChange: (newRange) => onRangeChange(newRange, index), range: range })] }), jsxRuntime.jsxs("div", { className: "max-sm:hidden sm:grid sm:grid-cols-[100px_200px_60px_250px_250px] sm:gap-2", children: [jsxRuntime.jsx(Checkbox, { checked: isActive, "data-testid": `${dataTestId}-${key}-checkbox`, label: checkboxLabel, onChange: (event) => onActiveChange(Boolean(event.currentTarget.checked), index) }), jsxRuntime.jsx(reactComponents.Text, { className: cvaScheduleItemText(), size: "medium", subtle: !isActive, children: label }), jsxRuntime.jsx(Checkbox, { checked: isAllDay ? isActive : undefined, "data-testid": `${dataTestId}-${key}-allday-checkbox`, disabled: !isActive, onChange: (event) => onAllDayChange(Boolean(event.currentTarget.checked), index) }), jsxRuntime.jsx(TimeRange, { "data-testid": `${dataTestId}-${key}-range`, disabled: !isActive || isAllDay, isInvalid: !!invalidKeys.find((invalidKey) => invalidKey === key), onChange: (newRange) => onRangeChange(newRange, index), range: isAllDay ? undefined : range })] })] }, key + label));
3822
4462
  }) }));
3823
4463
  };
@@ -4014,7 +4654,7 @@ const Search = ({ className, placeholder, value, widenInputOnFocus = false, hide
4014
4654
  const { t } = useTranslation();
4015
4655
  const generatedId = react.useId();
4016
4656
  const inputId = id ?? `search-${generatedId}`;
4017
- return (jsxRuntime.jsx(TextBaseInput, { ...rest, autoComplete: autoComplete, className: cvaSearch({ className, border: hideBorderWhenNotInFocus, widenOnFocus: widenInputOnFocus }), "data-testid": dataTestId, disabled: disabled, fieldSize: fieldSize, id: inputId, inputClassName: inputClassName, name: name, onBlur: onBlur, onChange: onChange, onFocus: onFocus, onKeyUp: onKeyUp, placeholder: placeholder ?? t("search.placeholder"), prefix: loading ? (jsxRuntime.jsx(reactComponents.Spinner, { centering: "centered", containerClassName: "!p-0", size: fieldSize })) : (jsxRuntime.jsx(reactComponents.Icon, { name: iconName, size: fieldSize })), ref: ref, suffix:
4657
+ return (jsxRuntime.jsx(TextBaseInput, { ...rest, autoComplete: autoComplete, className: cvaSearch({ className, border: hideBorderWhenNotInFocus, widenOnFocus: widenInputOnFocus }), "data-testid": dataTestId, disabled: disabled, fieldSize: fieldSize, id: inputId, inputClassName: inputClassName, name: name, onBlur: onBlur, onChange: onChange, onFocus: onFocus, onKeyUp: onKeyUp, placeholder: placeholder ?? t("search.placeholder"), prefix: loading ? (jsxRuntime.jsx(reactComponents.Spinner, { centering: "centered", containerClassName: "!p-0", size: fieldSize })) : (jsxRuntime.jsx(reactComponents.Icon, { name: iconName, size: fieldSize })), ref: ref, style: style, suffix:
4018
4658
  //only show the clear button if there is a value and the onClear function is provided
4019
4659
  onClear && value ? (jsxRuntime.jsx("button", { className: "flex", "data-testid": dataTestId ? `${dataTestId}_suffix_component` : null, onClick: () => {
4020
4660
  onClear();
@@ -4025,7 +4665,7 @@ Search.displayName = "Search";
4025
4665
  /**
4026
4666
  *
4027
4667
  */
4028
- const FormFieldSelectAdapter = ({ className, "data-testid": dataTestId, helpText, helpAddon, tip, label, isInvalid = false, errorMessage, name, onBlur, options, value, defaultValue, id, htmlFor: htmlForProp, onChange, children, ref, required = false, ...rest }) => {
4668
+ const FormFieldSelectAdapter = ({ className, "data-testid": dataTestId, helpText, helpAddon, tip, label, isInvalid = false, errorMessage, name, onBlur, options, value, defaultValue, id, htmlFor: htmlForProp, onChange, children, style, ref, required = false, ...rest }) => {
4029
4669
  const isFirstRender = reactComponents.useIsFirstRender();
4030
4670
  const [innerValue, setInnerValue] = react.useState(value ?? defaultValue);
4031
4671
  const onChangeRef = react.useRef(onChange);
@@ -4079,7 +4719,8 @@ const FormFieldSelectAdapter = ({ className, "data-testid": dataTestId, helpText
4079
4719
  helpAddon,
4080
4720
  tip,
4081
4721
  label,
4082
- required: required ? !Boolean(rest.disabled ?? rest.readOnly) : false, children: [jsxRuntime.jsx("select", { onChange, ref: innerRef, name, value: innerValue, hidden: true, children: optionsWithCurrentSelectionBackupOption.map(option => {
4722
+ required: required ? !Boolean(rest.disabled ?? rest.readOnly) : false,
4723
+ style, children: [jsxRuntime.jsx("select", { onChange, ref: innerRef, name, value: innerValue, hidden: true, children: optionsWithCurrentSelectionBackupOption.map(option => {
4083
4724
  return (jsxRuntime.jsx("option", { value: option.value, children: option.label }, option.value));
4084
4725
  }) }), children({
4085
4726
  ...rest,
@@ -4379,10 +5020,10 @@ TextField.displayName = "TextField";
4379
5020
  * @param {TimeRangeFieldProps} props - The props for the TimeRangeField component
4380
5021
  * @returns {ReactElement} TimeRangeField component
4381
5022
  */
4382
- const TimeRangeField = ({ className, "data-testid": dataTestId, onChange, isInvalid = undefined, errorMessage, label, tip, children, helpText, id, ref, ...rest }) => {
5023
+ const TimeRangeField = ({ className, "data-testid": dataTestId, onChange, isInvalid = undefined, errorMessage, label, tip, children, helpText, id, style, ref, ...rest }) => {
4383
5024
  const renderAsInvalid = isInvalid === undefined ? Boolean(errorMessage) : isInvalid;
4384
5025
  const htmlFor = id ? id : "timeRangeField-" + sharedUtils.uuidv4();
4385
- return (jsxRuntime.jsx(FormGroup, { "data-testid": dataTestId ? `${dataTestId}-FormGroup` : undefined, helpText: (renderAsInvalid && errorMessage) || helpText, htmlFor: htmlFor, isInvalid: renderAsInvalid, label: label, ref: ref, tip: tip, children: jsxRuntime.jsx(TimeRange, { className: className, "data-testid": dataTestId, isInvalid: renderAsInvalid, onChange: onChange, ...rest, children: children }) }));
5026
+ return (jsxRuntime.jsx(FormGroup, { "data-testid": dataTestId ? `${dataTestId}-FormGroup` : undefined, helpText: (renderAsInvalid && errorMessage) || helpText, htmlFor: htmlFor, isInvalid: renderAsInvalid, label: label, ref: ref, style: style, tip: tip, children: jsxRuntime.jsx(TimeRange, { className: className, "data-testid": dataTestId, isInvalid: renderAsInvalid, onChange: onChange, ...rest, children: children }) }));
4386
5027
  };
4387
5028
 
4388
5029
  const cvaToggleSwitchWrapper = cssClassVarianceUtilities.cvaMerge(["grid", "grid-cols-[auto]", "items-center"]);
@@ -5041,6 +5682,7 @@ exports.phoneErrorMessage = phoneErrorMessage;
5041
5682
  exports.serializeSchedule = serializeSchedule;
5042
5683
  exports.toISODateStringUTC = toISODateStringUTC;
5043
5684
  exports.useCreatableSelect = useCreatableSelect;
5685
+ exports.useCreateInputBlurEvent = useCreateInputBlurEvent;
5044
5686
  exports.useCreateInputChangeEvent = useCreateInputChangeEvent;
5045
5687
  exports.useCustomComponents = useCustomComponents;
5046
5688
  exports.useGetPhoneValidationRules = useGetPhoneValidationRules;