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