@trackunit/react-form-components 1.25.2 → 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 +781 -140
- package/index.esm.js +783 -143
- package/package.json +3 -3
- package/src/components/BaseInput/BaseInput.variants.d.ts +1 -1
- package/src/components/Checkbox/Checkbox.variants.d.ts +1 -1
- package/src/components/DateField/DateBaseInput/DateBaseInput.d.ts +40 -3
- package/src/components/DateField/DateBaseInput/DateBaseInputPickerContent.d.ts +34 -0
- package/src/components/DateField/DateBaseInput/utils/dateValueUtils.d.ts +11 -0
- package/src/components/DateField/DateBaseInput/utils/useCanonicalInputEmitter.d.ts +39 -0
- package/src/components/DateField/DateBaseInput/utils/useDatePickerController.d.ts +58 -0
- package/src/components/DateField/DateField.d.ts +23 -0
- package/src/index.d.ts +1 -1
- package/src/translation.d.ts +2 -2
- package/src/utilities/parseDateFieldValue.d.ts +29 -0
- package/src/utilities/useCreateInputChangeEvent.d.ts +11 -1
- package/src/utilities/useDateFieldLocale.d.ts +24 -0
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 {
|
|
4
|
-
import {
|
|
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.
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
125
|
-
if (
|
|
182
|
+
const parsed = parseAndValidateDateParts(partValues);
|
|
183
|
+
if (!parsed)
|
|
126
184
|
return null;
|
|
127
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
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
|
|
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,34 +601,6 @@ 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
|
*
|
|
@@ -600,7 +775,11 @@ const BaseInput = ({ className, isInvalid = false, "data-testid": dataTestId, pr
|
|
|
600
775
|
};
|
|
601
776
|
BaseInput.displayName = "BaseInput";
|
|
602
777
|
|
|
603
|
-
|
|
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 =
|
|
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]
|
|
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
|
-
*
|
|
831
|
+
* Renders the calendar + Clear/Cancel/Apply buttons inside the DateBaseInput popover.
|
|
646
832
|
*
|
|
647
|
-
*
|
|
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
|
-
*
|
|
1064
|
+
* @example
|
|
1065
|
+
* const createInputChangeEvent = useCreateInputChangeEvent();
|
|
1066
|
+
* onChange?.(createInputChangeEvent(str, inputRef.current));
|
|
650
1067
|
*/
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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
|
-
}, [
|
|
686
|
-
const
|
|
1151
|
+
}, [minDate, maxDate]);
|
|
1152
|
+
const onCalendarChange = useCallback((next) => {
|
|
687
1153
|
setPendingDate(next);
|
|
688
1154
|
}, []);
|
|
689
|
-
const
|
|
1155
|
+
const onClear = useCallback(() => {
|
|
690
1156
|
setPendingDate(null);
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
|
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(
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
}, [
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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
|
/**
|
|
@@ -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
|
|
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
|
|
|
@@ -4950,4 +5590,4 @@ const useZodValidators = () => {
|
|
|
4950
5590
|
*/
|
|
4951
5591
|
setupLibraryTranslations();
|
|
4952
5592
|
|
|
4953
|
-
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 };
|