@jobber/components 8.26.2 → 8.27.0

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.
Files changed (56) hide show
  1. package/Select.d.ts +1 -0
  2. package/Select.js +17 -0
  3. package/dist/ComboboxChipRemove-cjs.js +2 -1
  4. package/dist/ComboboxChipRemove-es.js +2 -1
  5. package/dist/FieldDescription-cjs.js +880 -0
  6. package/dist/FieldDescription-es.js +856 -0
  7. package/dist/InputNumberExperimental-cjs.js +5 -875
  8. package/dist/InputNumberExperimental-es.js +2 -854
  9. package/dist/MenuSubmenuTrigger-es.js +1 -1
  10. package/dist/NumberFieldInput-cjs.js +2 -2
  11. package/dist/NumberFieldInput-es.js +1 -1
  12. package/dist/Select/Select.d.ts +31 -0
  13. package/dist/Select/Select.types.d.ts +145 -0
  14. package/dist/Select/SelectBottomSheet.d.ts +4 -0
  15. package/dist/Select/SelectDropdown.d.ts +4 -0
  16. package/dist/Select/index.cjs +41 -0
  17. package/dist/Select/index.d.ts +2 -0
  18. package/dist/Select/index.mjs +35 -0
  19. package/dist/Select-cjs.js +231 -0
  20. package/dist/Select-es.js +229 -0
  21. package/dist/SelectPrimitive-cjs.js +20 -3
  22. package/dist/SelectPrimitive-es.js +20 -3
  23. package/dist/docs/Dialog/Dialog.md +74 -21
  24. package/dist/docs/Icon/Icon.md +60 -53
  25. package/dist/docs/Select/Select.md +428 -0
  26. package/dist/docs/SelectPrimitive/SelectPrimitive.md +14 -1
  27. package/dist/docs/index.md +1 -0
  28. package/dist/floating-ui.react-dom-es.js +1 -1
  29. package/dist/floating-ui.react-es.js +1 -1
  30. package/dist/floating-ui.utils.dom-es.js +1 -1
  31. package/dist/index.cjs +9 -0
  32. package/dist/index.d.mts +1 -0
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.mjs +8 -0
  35. package/dist/primitives/ComboboxPrimitive/index.cjs +1 -0
  36. package/dist/primitives/ComboboxPrimitive/index.mjs +1 -0
  37. package/dist/primitives/HelperText/HelperText.d.ts +2 -1
  38. package/dist/primitives/InputNumberExperimental/index.cjs +3 -1
  39. package/dist/primitives/InputNumberExperimental/index.mjs +3 -1
  40. package/dist/primitives/SelectPrimitive/SelectPrimitive.d.ts +11 -0
  41. package/dist/primitives/SelectPrimitive/index.d.ts +1 -1
  42. package/dist/primitives/SelectPrimitive/types.d.ts +15 -0
  43. package/dist/primitives/index.cjs +2 -0
  44. package/dist/primitives/index.mjs +2 -0
  45. package/dist/stringifyLocale-cjs.js +13 -0
  46. package/dist/stringifyLocale-es.js +11 -0
  47. package/dist/styles.css +234 -0
  48. package/dist/unstyledPrimitives/index.cjs +1 -0
  49. package/dist/unstyledPrimitives/index.mjs +2 -1
  50. package/dist/useButton-es.js +1 -1
  51. package/dist/useCompositeListItem-es.js +1 -1
  52. package/dist/useLabel-cjs.js +0 -11
  53. package/dist/useLabel-es.js +2 -12
  54. package/dist/useScrollLock-es.js +1 -1
  55. package/dist/utils/meta/meta.json +5 -0
  56. package/package.json +8 -3
@@ -0,0 +1,856 @@
1
+ import * as React from 'react';
2
+ import { u as useBaseUiId, b as useStableCallback, h as useTimeout, a as useIsoLayoutEffect, S as SafeReact, j as error, t as transitionStatusMapping, k as useTransitionStatus, f as useOpenChangeComplete } from './useButton-es.js';
3
+ import { u as useLabelableContext, L as LabelableContext, a as useFormContext, D as DEFAULT_VALIDITY_STATE, f as fieldValidityMapping, F as FieldRootContext, b as useFieldRootContext } from './useLabelableId-es.js';
4
+ import { f as formatErrorMessage, a as useRefWithInit, m as mergeProps, E as EMPTY_OBJECT, u as useRenderElement } from './useRenderElement-es.js';
5
+ import { jsx } from 'react/jsx-runtime';
6
+ import { u as useLabel } from './useLabel-es.js';
7
+
8
+ const FieldItemContext = /*#__PURE__*/React.createContext({
9
+ disabled: false
10
+ });
11
+ if (process.env.NODE_ENV !== "production") FieldItemContext.displayName = "FieldItemContext";
12
+ function useFieldItemContext() {
13
+ const context = React.useContext(FieldItemContext);
14
+ return context;
15
+ }
16
+
17
+ const FieldsetRootContext = /*#__PURE__*/React.createContext(undefined);
18
+ if (process.env.NODE_ENV !== "production") FieldsetRootContext.displayName = "FieldsetRootContext";
19
+ function useFieldsetRootContext(optional = false) {
20
+ const context = React.useContext(FieldsetRootContext);
21
+ if (!context && !optional) {
22
+ throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: FieldsetRootContext is missing. Fieldset parts must be placed within <Fieldset.Root>.' : formatErrorMessage(86));
23
+ }
24
+ return context;
25
+ }
26
+
27
+ const LabelableProvider = function LabelableProvider(props) {
28
+ const defaultId = useBaseUiId();
29
+ const initialControlId = props.controlId === undefined ? defaultId : props.controlId;
30
+ const [controlId, setControlIdState] = React.useState(initialControlId);
31
+ const [labelId, setLabelId] = React.useState(props.labelId);
32
+ const [messageIds, setMessageIds] = React.useState([]);
33
+ const registrationsRef = useRefWithInit(() => new Map());
34
+ const {
35
+ messageIds: parentMessageIds
36
+ } = useLabelableContext();
37
+ const registerControlId = useStableCallback((source, nextId) => {
38
+ const registrations = registrationsRef.current;
39
+ if (nextId === undefined) {
40
+ registrations.delete(source);
41
+ return;
42
+ }
43
+ registrations.set(source, nextId);
44
+
45
+ // Only flush when registering, not when unregistering.
46
+ // This prevents loops during rapid unmount/remount cycles (e.g. React Activity).
47
+ // The next registration will pick up the correct state.
48
+ setControlIdState(prev => {
49
+ if (registrations.size === 0) {
50
+ return undefined;
51
+ }
52
+ let nextControlId;
53
+ for (const id of registrations.values()) {
54
+ if (prev !== undefined && id === prev) {
55
+ return prev;
56
+ }
57
+ if (nextControlId === undefined) {
58
+ nextControlId = id;
59
+ }
60
+ }
61
+ return nextControlId;
62
+ });
63
+ });
64
+ const getDescriptionProps = React.useCallback(externalProps => {
65
+ const ids = externalProps['aria-describedby'] ? externalProps['aria-describedby'].split(' ') : [];
66
+ ids.push(...parentMessageIds, ...messageIds);
67
+ return {
68
+ ...externalProps,
69
+ 'aria-describedby': Array.from(new Set(ids)).join(' ') || undefined
70
+ };
71
+ }, [parentMessageIds, messageIds]);
72
+ const contextValue = React.useMemo(() => ({
73
+ controlId,
74
+ registerControlId,
75
+ labelId,
76
+ setLabelId,
77
+ messageIds,
78
+ setMessageIds,
79
+ getDescriptionProps
80
+ }), [controlId, registerControlId, labelId, setLabelId, messageIds, setMessageIds, getDescriptionProps]);
81
+ return /*#__PURE__*/jsx(LabelableContext.Provider, {
82
+ value: contextValue,
83
+ children: props.children
84
+ });
85
+ };
86
+ if (process.env.NODE_ENV !== "production") LabelableProvider.displayName = "LabelableProvider";
87
+
88
+ /**
89
+ * Combines the field's client-side, stateful validity data with the external invalid state to
90
+ * determine the field's true validity.
91
+ */
92
+ function getCombinedFieldValidityData(validityData, invalid) {
93
+ return {
94
+ ...validityData,
95
+ state: {
96
+ ...validityData.state,
97
+ valid: !invalid && validityData.state.valid
98
+ }
99
+ };
100
+ }
101
+
102
+ const validityKeys = Object.keys(DEFAULT_VALIDITY_STATE);
103
+ function isOnlyValueMissing(state) {
104
+ if (!state || state.valid || !state.valueMissing) {
105
+ return false;
106
+ }
107
+ let onlyValueMissing = false;
108
+ for (const key of validityKeys) {
109
+ if (key === 'valid') {
110
+ continue;
111
+ }
112
+ if (key === 'valueMissing') {
113
+ onlyValueMissing = state[key];
114
+ } else if (state[key]) {
115
+ onlyValueMissing = false;
116
+ }
117
+ }
118
+ return onlyValueMissing;
119
+ }
120
+
121
+ /**
122
+ * Picks the input whose native validity should represent a field that owns several inputs (such as a
123
+ * checkbox group). Prefers the first enabled currently-invalid input, where "first" follows Set
124
+ * insertion order (mount order), and otherwise returns the first enabled input. Disabled inputs are
125
+ * skipped because they don't participate in native constraint validation.
126
+ */
127
+ function findRepresentativeInput(inputs) {
128
+ let fallback = null;
129
+ for (const input of inputs) {
130
+ if (input.disabled) {
131
+ continue;
132
+ }
133
+ if (!input.validity.valid) {
134
+ return input;
135
+ }
136
+ fallback ??= input;
137
+ }
138
+ return fallback;
139
+ }
140
+ function clearCustomValidity(element, inputs) {
141
+ let didClearElement = false;
142
+ for (const input of inputs) {
143
+ input.setCustomValidity('');
144
+ didClearElement ||= input === element;
145
+ }
146
+ if (!didClearElement) {
147
+ element.setCustomValidity('');
148
+ }
149
+ }
150
+ function useFieldValidation(params) {
151
+ const {
152
+ formRef
153
+ } = useFormContext();
154
+ const {
155
+ setValidityData,
156
+ validate,
157
+ validityData,
158
+ validationDebounceTime,
159
+ invalid,
160
+ markedDirtyRef,
161
+ state,
162
+ shouldValidateOnChange,
163
+ getRegisteredFieldId
164
+ } = params;
165
+ const {
166
+ controlId,
167
+ getDescriptionProps
168
+ } = useLabelableContext();
169
+ const timeout = useTimeout();
170
+ const inputRef = React.useRef(null);
171
+ const registeredInputs = useRefWithInit(() => new Set()).current;
172
+ const validationCommitIdRef = React.useRef(0);
173
+
174
+ // Checkbox groups register several inputs against a single field. Track them so a `required`
175
+ // checkbox can't be satisfied by another input in the group, matching native per-checkbox behavior.
176
+ const registerInput = React.useCallback(element => {
177
+ if (!element) {
178
+ return undefined;
179
+ }
180
+ registeredInputs.add(element);
181
+ return () => {
182
+ registeredInputs.delete(element);
183
+ };
184
+ }, [registeredInputs]);
185
+ const commit = useStableCallback(async (value, revalidate = false) => {
186
+ // A field can own several inputs (a checkbox group), but only the last-mounted one wins the shared
187
+ // `inputRef`. Validate against the registry instead so every input counts; `inputRef` is the
188
+ // fallback only when no registered input applies (none registered, or all of them disabled).
189
+ const element = findRepresentativeInput(registeredInputs) ?? inputRef.current;
190
+ if (!element) {
191
+ return;
192
+ }
193
+ validationCommitIdRef.current += 1;
194
+ const validationCommitId = validationCommitIdRef.current;
195
+ function updateRegisteredFieldValidity(nextValidityData, externalInvalid = invalid) {
196
+ const fieldId = getRegisteredFieldId() ?? controlId;
197
+ if (fieldId == null) {
198
+ return;
199
+ }
200
+ const currentFieldData = formRef.current.fields.get(fieldId);
201
+ if (!currentFieldData) {
202
+ return;
203
+ }
204
+ const validityDataWithFormErrors = getCombinedFieldValidityData(nextValidityData, externalInvalid);
205
+ formRef.current.fields.set(fieldId, {
206
+ ...currentFieldData,
207
+ validityData: validityDataWithFormErrors
208
+ });
209
+ }
210
+ if (revalidate) {
211
+ if (state.valid !== false) {
212
+ return;
213
+ }
214
+ const currentNativeValidity = element.validity;
215
+ if (!currentNativeValidity.valueMissing) {
216
+ // The 'valueMissing' (required) condition has been resolved by the user typing.
217
+ // Temporarily mark the field as valid for this onChange event.
218
+ // Other native errors (e.g., typeMismatch) will be caught by full validation on blur or submit.
219
+ const nextValidityData = {
220
+ value,
221
+ state: {
222
+ ...DEFAULT_VALIDITY_STATE,
223
+ valid: true
224
+ },
225
+ error: '',
226
+ errors: [],
227
+ initialValue: validityData.initialValue
228
+ };
229
+ clearCustomValidity(element, registeredInputs);
230
+
231
+ // The required value is now present; ignore stale external invalid state for this pass.
232
+ updateRegisteredFieldValidity(nextValidityData, false);
233
+ setValidityData(nextValidityData);
234
+ return;
235
+ }
236
+
237
+ // Value is still missing, or other conditions apply.
238
+ // Let's use a representation of current validity for isOnlyValueMissing.
239
+ const currentNativeValidityObject = validityKeys.reduce((acc, key) => {
240
+ acc[key] = currentNativeValidity[key];
241
+ return acc;
242
+ }, {});
243
+
244
+ // If it's (still) natively invalid due to something other than just valueMissing,
245
+ // then bail from this revalidation on change to avoid "scolding" for other errors.
246
+ if (!currentNativeValidityObject.valid && !isOnlyValueMissing(currentNativeValidityObject)) {
247
+ return;
248
+ }
249
+
250
+ // If valueMissing is still true AND it's the only issue, or if the field is now natively valid,
251
+ // let it fall through to the main validation logic below.
252
+ }
253
+ function getState(el) {
254
+ const computedState = validityKeys.reduce((acc, key) => {
255
+ acc[key] = el.validity[key];
256
+ return acc;
257
+ }, {});
258
+ let hasOnlyValueMissingError = false;
259
+ for (const key of validityKeys) {
260
+ if (key === 'valid') {
261
+ continue;
262
+ }
263
+ if (key === 'valueMissing' && computedState[key]) {
264
+ hasOnlyValueMissingError = true;
265
+ } else if (computedState[key]) {
266
+ return computedState;
267
+ }
268
+ }
269
+
270
+ // Only make `valueMissing` mark the field invalid if it's been changed
271
+ // to reduce error noise.
272
+ if (hasOnlyValueMissingError && !markedDirtyRef.current) {
273
+ computedState.valid = true;
274
+ computedState.valueMissing = false;
275
+ }
276
+ return computedState;
277
+ }
278
+ timeout.clear();
279
+ let result = null;
280
+ let validationErrors = [];
281
+ const nextState = getState(element);
282
+ let defaultValidationMessage;
283
+ const isValidatingOnChange = shouldValidateOnChange();
284
+ if (element.validationMessage && !isValidatingOnChange) {
285
+ // not validating on change, if there is a `validationMessage` from
286
+ // native validity, set errors and skip calling the custom validate fn
287
+ defaultValidationMessage = element.validationMessage;
288
+ validationErrors = [element.validationMessage];
289
+ } else {
290
+ // call the validate function because either
291
+ // - validating on change, or
292
+ // - native constraint validations passed, custom validity check is next
293
+ const formValues = Array.from(formRef.current.fields.values()).reduce((acc, field) => {
294
+ if (field.name) {
295
+ acc[field.name] = field.getValue();
296
+ }
297
+ return acc;
298
+ }, {});
299
+ const resultOrPromise = validate(value, formValues);
300
+ if (typeof resultOrPromise === 'object' && resultOrPromise !== null && 'then' in resultOrPromise) {
301
+ result = await resultOrPromise;
302
+ if (validationCommitId !== validationCommitIdRef.current) {
303
+ return;
304
+ }
305
+ } else {
306
+ result = resultOrPromise;
307
+ }
308
+ if (result !== null) {
309
+ nextState.valid = false;
310
+ nextState.customError = true;
311
+ if (Array.isArray(result)) {
312
+ validationErrors = result;
313
+ element.setCustomValidity(result.join('\n'));
314
+ } else if (result) {
315
+ validationErrors = [result];
316
+ element.setCustomValidity(result);
317
+ }
318
+ } else if (isValidatingOnChange) {
319
+ // validate function returned no errors, if validating on change
320
+ // we need to clear the custom validity state
321
+ clearCustomValidity(element, registeredInputs);
322
+ nextState.customError = false;
323
+ if (element.validationMessage) {
324
+ defaultValidationMessage = element.validationMessage;
325
+ validationErrors = [element.validationMessage];
326
+ } else if (element.validity.valid && !nextState.valid) {
327
+ nextState.valid = true;
328
+ }
329
+ }
330
+ }
331
+ const nextValidityData = {
332
+ value,
333
+ state: nextState,
334
+ error: defaultValidationMessage ?? (Array.isArray(result) ? result[0] : result ?? ''),
335
+ errors: validationErrors,
336
+ initialValue: validityData.initialValue
337
+ };
338
+
339
+ // Keep Form-level errors part of overall field validity for submit blocking/focus logic.
340
+ updateRegisteredFieldValidity(nextValidityData);
341
+ setValidityData(nextValidityData);
342
+ });
343
+ const change = useStableCallback(value => {
344
+ timeout.clear();
345
+ const validateOnChange = shouldValidateOnChange();
346
+ if (validateOnChange && value !== '' && validationDebounceTime) {
347
+ validationCommitIdRef.current += 1;
348
+ timeout.start(validationDebounceTime, () => {
349
+ commit(value);
350
+ });
351
+ } else {
352
+ commit(value, !validateOnChange);
353
+ }
354
+ });
355
+ const getValidationProps = React.useCallback((disabled, externalProps = {}) => mergeProps(getDescriptionProps(externalProps), state.valid === false && !state.disabled && !disabled ? {
356
+ 'aria-invalid': true
357
+ } : EMPTY_OBJECT), [getDescriptionProps, state.disabled, state.valid]);
358
+ return React.useMemo(() => ({
359
+ getValidationProps,
360
+ inputRef,
361
+ registerInput,
362
+ commit,
363
+ change
364
+ }), [getValidationProps, registerInput, commit, change]);
365
+ }
366
+
367
+ function useFieldControlRegistration(params) {
368
+ const {
369
+ commit,
370
+ invalid,
371
+ markedDirtyRef,
372
+ name,
373
+ setRegisteredFieldName,
374
+ setRegisteredFieldId,
375
+ setValidityData,
376
+ validityData
377
+ } = params;
378
+ const {
379
+ formRef
380
+ } = useFormContext();
381
+ const activeFieldControlSourceRef = React.useRef(null);
382
+ const registrationRef = React.useRef(null);
383
+ const fallbackControlRef = React.useRef(null);
384
+ const getValueForForm = useStableCallback(() => {
385
+ const registration = registrationRef.current;
386
+ if (!registration) {
387
+ return undefined;
388
+ }
389
+ if (registration.getValue) {
390
+ return registration.getValue();
391
+ }
392
+ return registration.value;
393
+ });
394
+ function getRegistrationValue(registration) {
395
+ return registration.value === undefined ? getValueForForm() : registration.value;
396
+ }
397
+ const validate = useStableCallback(() => {
398
+ const registration = registrationRef.current;
399
+ markedDirtyRef.current = true;
400
+ if (!registration) {
401
+ commit(validityData.value);
402
+ return;
403
+ }
404
+ commit(getRegistrationValue(registration));
405
+ });
406
+ function refreshRegistration() {
407
+ const registration = registrationRef.current;
408
+ if (!registration || !registration.id) {
409
+ return;
410
+ }
411
+ formRef.current.fields.set(registration.id, {
412
+ getValue: getValueForForm,
413
+ name: name ?? registration.name,
414
+ controlRef: registration.controlRef ?? fallbackControlRef,
415
+ validityData: getCombinedFieldValidityData(validityData, invalid),
416
+ validate
417
+ });
418
+ }
419
+ function deleteRegistration(id = registrationRef.current?.id) {
420
+ if (id) {
421
+ formRef.current.fields.delete(id);
422
+ }
423
+ }
424
+ function syncInitialValue() {
425
+ const registration = registrationRef.current;
426
+ if (!registration) {
427
+ return;
428
+ }
429
+ const initialValue = getRegistrationValue(registration);
430
+ if (validityData.initialValue === null && initialValue !== null) {
431
+ setValidityData(prev => ({
432
+ ...prev,
433
+ initialValue
434
+ }));
435
+ }
436
+ }
437
+ useIsoLayoutEffect(() => {
438
+ const registration = registrationRef.current;
439
+ if (!registration || !registration.id) {
440
+ return;
441
+ }
442
+ setRegisteredFieldName(name ? undefined : registration.name);
443
+ formRef.current.fields.set(registration.id, {
444
+ getValue: getValueForForm,
445
+ name: name ?? registration.name,
446
+ controlRef: registration.controlRef ?? fallbackControlRef,
447
+ validityData: getCombinedFieldValidityData(validityData, invalid),
448
+ validate
449
+ });
450
+ }, [formRef, getValueForForm, invalid, name, setRegisteredFieldName, validate, validityData]);
451
+ useIsoLayoutEffect(() => {
452
+ const fields = formRef.current.fields;
453
+ return () => {
454
+ const id = registrationRef.current?.id;
455
+ if (id) {
456
+ fields.delete(id);
457
+ }
458
+ };
459
+ }, [formRef]);
460
+ const register = useStableCallback((source, registration) => {
461
+ if (!registration) {
462
+ if (activeFieldControlSourceRef.current === source) {
463
+ activeFieldControlSourceRef.current = null;
464
+ deleteRegistration();
465
+ registrationRef.current = null;
466
+ setRegisteredFieldName(undefined);
467
+ setRegisteredFieldId(undefined);
468
+ }
469
+ return;
470
+ }
471
+ const previousId = registrationRef.current?.id;
472
+ activeFieldControlSourceRef.current = source;
473
+ registrationRef.current = registration;
474
+ if (!name) {
475
+ setRegisteredFieldName(registration.name);
476
+ }
477
+ setRegisteredFieldId(registration.id);
478
+ if (previousId && previousId !== registration.id) {
479
+ deleteRegistration(previousId);
480
+ }
481
+ syncInitialValue();
482
+ refreshRegistration();
483
+ });
484
+ return [validate, register];
485
+ }
486
+
487
+ const FieldRootInner = /*#__PURE__*/React.forwardRef(function FieldRootInner(componentProps, forwardedRef) {
488
+ const {
489
+ errors,
490
+ validationMode: formValidationMode,
491
+ submitAttemptedRef
492
+ } = useFormContext();
493
+ const {
494
+ render,
495
+ className,
496
+ validate: validateProp,
497
+ validationDebounceTime = 0,
498
+ validationMode = formValidationMode,
499
+ name,
500
+ disabled: disabledProp = false,
501
+ invalid: invalidProp,
502
+ dirty: dirtyProp,
503
+ touched: touchedProp,
504
+ actionsRef,
505
+ style,
506
+ ...elementProps
507
+ } = componentProps;
508
+ const disabledFieldset = useFieldsetRootContext(true)?.disabled;
509
+ const validate = useStableCallback(validateProp || (() => null));
510
+ const disabled = disabledFieldset || disabledProp;
511
+ const [touchedState, setTouchedUnwrapped] = React.useState(false);
512
+ const [dirtyState, setDirtyUnwrapped] = React.useState(false);
513
+ const [filled, setFilled] = React.useState(false);
514
+ const [focused, setFocused] = React.useState(false);
515
+ const dirty = dirtyProp ?? dirtyState;
516
+ const touched = touchedProp ?? touchedState;
517
+ const markedDirtyRef = React.useRef(dirty);
518
+ const registeredFieldIdRef = React.useRef(undefined);
519
+ const [registeredFieldName, setRegisteredFieldName] = React.useState();
520
+ const effectiveName = name ?? registeredFieldName;
521
+ useIsoLayoutEffect(() => {
522
+ if (dirtyProp !== undefined) {
523
+ markedDirtyRef.current = dirtyProp;
524
+ }
525
+ }, [dirtyProp]);
526
+ const getRegisteredFieldId = React.useCallback(() => registeredFieldIdRef.current, []);
527
+ const setRegisteredFieldId = React.useCallback(id => {
528
+ registeredFieldIdRef.current = id;
529
+ }, []);
530
+ const setDirty = useStableCallback(value => {
531
+ if (dirtyProp !== undefined) {
532
+ return;
533
+ }
534
+ if (value) {
535
+ markedDirtyRef.current = true;
536
+ }
537
+ setDirtyUnwrapped(value);
538
+ });
539
+ const setTouched = useStableCallback(value => {
540
+ if (touchedProp !== undefined) {
541
+ return;
542
+ }
543
+ setTouchedUnwrapped(value);
544
+ });
545
+ const shouldValidateOnChange = useStableCallback(() => validationMode === 'onChange' || validationMode === 'onSubmit' && submitAttemptedRef.current);
546
+ const formError = effectiveName && Object.hasOwn(errors, effectiveName) ? errors[effectiveName] : null;
547
+ const hasFormError = !!(Array.isArray(formError) ? formError.length : formError);
548
+ const invalid = invalidProp === true || hasFormError;
549
+ const [validityData, setValidityData] = React.useState({
550
+ state: DEFAULT_VALIDITY_STATE,
551
+ error: '',
552
+ errors: [],
553
+ value: null,
554
+ initialValue: null
555
+ });
556
+ const valid = disabled ? null : !invalid && validityData.state.valid;
557
+ const state = React.useMemo(() => ({
558
+ disabled,
559
+ touched,
560
+ dirty,
561
+ valid,
562
+ filled,
563
+ focused
564
+ }), [disabled, touched, dirty, valid, filled, focused]);
565
+ const validation = useFieldValidation({
566
+ setValidityData,
567
+ validate,
568
+ validityData,
569
+ validationDebounceTime,
570
+ invalid,
571
+ markedDirtyRef,
572
+ state,
573
+ shouldValidateOnChange,
574
+ getRegisteredFieldId
575
+ });
576
+ const [validateFieldControl, registerFieldControl] = useFieldControlRegistration({
577
+ commit: validation.commit,
578
+ invalid,
579
+ markedDirtyRef,
580
+ name,
581
+ setRegisteredFieldName,
582
+ setRegisteredFieldId,
583
+ setValidityData,
584
+ validityData
585
+ });
586
+ React.useImperativeHandle(actionsRef, () => ({
587
+ validate: validateFieldControl
588
+ }), [validateFieldControl]);
589
+ const contextValue = React.useMemo(() => ({
590
+ invalid,
591
+ name: effectiveName,
592
+ validityData,
593
+ setValidityData,
594
+ disabled,
595
+ touched,
596
+ setTouched,
597
+ dirty,
598
+ setDirty,
599
+ filled,
600
+ setFilled,
601
+ focused,
602
+ setFocused,
603
+ validate,
604
+ validationMode,
605
+ validationDebounceTime,
606
+ shouldValidateOnChange,
607
+ state,
608
+ markedDirtyRef,
609
+ registerFieldControl,
610
+ validation
611
+ }), [invalid, effectiveName, validityData, disabled, touched, setTouched, dirty, setDirty, filled, setFilled, focused, setFocused, validate, validationMode, validationDebounceTime, shouldValidateOnChange, state, registerFieldControl, validation]);
612
+ const element = useRenderElement('div', componentProps, {
613
+ ref: forwardedRef,
614
+ state,
615
+ props: elementProps,
616
+ stateAttributesMapping: fieldValidityMapping
617
+ });
618
+ return /*#__PURE__*/jsx(FieldRootContext.Provider, {
619
+ value: contextValue,
620
+ children: element
621
+ });
622
+ });
623
+
624
+ /**
625
+ * Groups all parts of the field.
626
+ * Renders a `<div>` element.
627
+ *
628
+ * Documentation: [Base UI Field](https://base-ui.com/react/components/field)
629
+ */
630
+ if (process.env.NODE_ENV !== "production") FieldRootInner.displayName = "FieldRootInner";
631
+ const FieldRoot = /*#__PURE__*/React.forwardRef(function FieldRoot(componentProps, forwardedRef) {
632
+ return /*#__PURE__*/jsx(LabelableProvider, {
633
+ children: /*#__PURE__*/jsx(FieldRootInner, {
634
+ ...componentProps,
635
+ ref: forwardedRef
636
+ })
637
+ });
638
+ });
639
+ if (process.env.NODE_ENV !== "production") FieldRoot.displayName = "FieldRoot";
640
+
641
+ /**
642
+ * An accessible label that is automatically associated with the field control.
643
+ * Renders a `<label>` element.
644
+ *
645
+ * Documentation: [Base UI Field](https://base-ui.com/react/components/field)
646
+ */
647
+ const FieldLabel = /*#__PURE__*/React.forwardRef(function FieldLabel(componentProps, forwardedRef) {
648
+ const {
649
+ render,
650
+ className,
651
+ style,
652
+ id: idProp,
653
+ nativeLabel = true,
654
+ ...elementProps
655
+ } = componentProps;
656
+ const fieldRootContext = useFieldRootContext(false);
657
+ const fieldItemContext = useFieldItemContext();
658
+ const {
659
+ labelId
660
+ } = useLabelableContext();
661
+ const state = {
662
+ ...fieldRootContext.state,
663
+ disabled: fieldRootContext.disabled || fieldItemContext.disabled
664
+ };
665
+ const labelRef = React.useRef(null);
666
+ const labelProps = useLabel({
667
+ id: labelId ?? idProp,
668
+ native: nativeLabel
669
+ });
670
+ if (process.env.NODE_ENV !== 'production') {
671
+ // eslint-disable-next-line react-hooks/rules-of-hooks
672
+ React.useEffect(() => {
673
+ if (!labelRef.current) {
674
+ return;
675
+ }
676
+ const isLabelTag = labelRef.current.tagName === 'LABEL';
677
+ if (nativeLabel) {
678
+ if (!isLabelTag) {
679
+ const ownerStackMessage = SafeReact.captureOwnerStack?.() || '';
680
+ const message = '<Field.Label> expected a <label> element because the `nativeLabel` prop is true. ' + 'Rendering a non-<label> disables native label association, so `htmlFor` will not ' + 'work. Use a real <label> in the `render` prop, or set `nativeLabel` to `false`.';
681
+ error(`${message}${ownerStackMessage}`);
682
+ }
683
+ } else if (isLabelTag) {
684
+ const ownerStackMessage = SafeReact.captureOwnerStack?.() || '';
685
+ const message = '<Field.Label> expected a non-<label> element because the `nativeLabel` prop is false. ' + 'Rendering a <label> assumes native label behavior while Base UI treats it as ' + 'non-native, which can cause unexpected pointer behavior. Use a non-<label> in the ' + '`render` prop, or set `nativeLabel` to `true`.';
686
+ error(`${message}${ownerStackMessage}`);
687
+ }
688
+ }, [nativeLabel]);
689
+ }
690
+ const element = useRenderElement('label', componentProps, {
691
+ ref: [forwardedRef, labelRef],
692
+ state,
693
+ props: [labelProps, elementProps],
694
+ stateAttributesMapping: fieldValidityMapping
695
+ });
696
+ return element;
697
+ });
698
+ if (process.env.NODE_ENV !== "production") FieldLabel.displayName = "FieldLabel";
699
+
700
+ const stateAttributesMapping = {
701
+ ...fieldValidityMapping,
702
+ ...transitionStatusMapping
703
+ };
704
+
705
+ /**
706
+ * An error message displayed if the field control fails validation.
707
+ * Renders a `<div>` element.
708
+ *
709
+ * Documentation: [Base UI Field](https://base-ui.com/react/components/field)
710
+ */
711
+ const FieldError = /*#__PURE__*/React.forwardRef(function FieldError(componentProps, forwardedRef) {
712
+ const {
713
+ render,
714
+ id: idProp,
715
+ className,
716
+ match,
717
+ style,
718
+ ...elementProps
719
+ } = componentProps;
720
+ const id = useBaseUiId(idProp);
721
+ const {
722
+ validityData,
723
+ state: fieldState,
724
+ name
725
+ } = useFieldRootContext(false);
726
+ const {
727
+ setMessageIds
728
+ } = useLabelableContext();
729
+ const {
730
+ errors
731
+ } = useFormContext();
732
+ const formError = name && Object.hasOwn(errors, name) ? errors[name] : null;
733
+ const hasFormError = !!(Array.isArray(formError) ? formError.length : formError);
734
+ const hasSpecificMatch = typeof match === 'string';
735
+ let rendered = false;
736
+ if (match === true) {
737
+ rendered = true;
738
+ } else if (fieldState.disabled) {
739
+ rendered = false;
740
+ } else if (hasSpecificMatch) {
741
+ rendered = Boolean(validityData.state[match]);
742
+ } else {
743
+ rendered = hasFormError || validityData.state.valid === false;
744
+ }
745
+ const {
746
+ mounted,
747
+ transitionStatus,
748
+ setMounted
749
+ } = useTransitionStatus(rendered);
750
+ useIsoLayoutEffect(() => {
751
+ if (!rendered || !id) {
752
+ return undefined;
753
+ }
754
+ setMessageIds(v => v.concat(id));
755
+ return () => {
756
+ setMessageIds(v => v.filter(item => item !== id));
757
+ };
758
+ }, [rendered, id, setMessageIds]);
759
+ const errorRef = React.useRef(null);
760
+ const [lastRenderedMessage, setLastRenderedMessage] = React.useState(null);
761
+ const [lastRenderedMessageKey, setLastRenderedMessageKey] = React.useState(null);
762
+ let error = validityData.error;
763
+ if (!hasSpecificMatch && hasFormError) {
764
+ error = formError;
765
+ } else if (validityData.errors.length > 1) {
766
+ error = validityData.errors;
767
+ }
768
+ let errorMessage = error ?? '';
769
+ if (Array.isArray(error)) {
770
+ errorMessage = error.length > 1 ? /*#__PURE__*/jsx("ul", {
771
+ children: error.map(message => /*#__PURE__*/jsx("li", {
772
+ children: message
773
+ }, message))
774
+ }) : error[0] ?? '';
775
+ }
776
+ const errorKey = Array.isArray(error) ? JSON.stringify(error) : error;
777
+ if (rendered && errorKey !== lastRenderedMessageKey) {
778
+ setLastRenderedMessageKey(errorKey);
779
+ setLastRenderedMessage(errorMessage);
780
+ }
781
+ useOpenChangeComplete({
782
+ open: rendered,
783
+ ref: errorRef,
784
+ onComplete() {
785
+ if (!rendered) {
786
+ setMounted(false);
787
+ }
788
+ }
789
+ });
790
+ const state = {
791
+ ...fieldState,
792
+ transitionStatus
793
+ };
794
+ const element = useRenderElement('div', componentProps, {
795
+ ref: [forwardedRef, errorRef],
796
+ state,
797
+ props: [{
798
+ id,
799
+ children: rendered ? errorMessage : lastRenderedMessage
800
+ }, elementProps],
801
+ stateAttributesMapping,
802
+ enabled: mounted
803
+ });
804
+ if (!mounted) {
805
+ return null;
806
+ }
807
+ return element;
808
+ });
809
+ if (process.env.NODE_ENV !== "production") FieldError.displayName = "FieldError";
810
+
811
+ /**
812
+ * A paragraph with additional information about the field.
813
+ * Renders a `<p>` element.
814
+ *
815
+ * Documentation: [Base UI Field](https://base-ui.com/react/components/field)
816
+ */
817
+ const FieldDescription = /*#__PURE__*/React.forwardRef(function FieldDescription(componentProps, forwardedRef) {
818
+ const {
819
+ render,
820
+ id: idProp,
821
+ className,
822
+ style,
823
+ ...elementProps
824
+ } = componentProps;
825
+ const id = useBaseUiId(idProp);
826
+ const fieldRootContext = useFieldRootContext(false);
827
+ const fieldItemContext = useFieldItemContext();
828
+ const {
829
+ setMessageIds
830
+ } = useLabelableContext();
831
+ const state = {
832
+ ...fieldRootContext.state,
833
+ disabled: fieldRootContext.disabled || fieldItemContext.disabled
834
+ };
835
+ useIsoLayoutEffect(() => {
836
+ if (!id) {
837
+ return undefined;
838
+ }
839
+ setMessageIds(v => v.concat(id));
840
+ return () => {
841
+ setMessageIds(v => v.filter(item => item !== id));
842
+ };
843
+ }, [id, setMessageIds]);
844
+ const element = useRenderElement('p', componentProps, {
845
+ ref: forwardedRef,
846
+ state,
847
+ props: [{
848
+ id
849
+ }, elementProps],
850
+ stateAttributesMapping: fieldValidityMapping
851
+ });
852
+ return element;
853
+ });
854
+ if (process.env.NODE_ENV !== "production") FieldDescription.displayName = "FieldDescription";
855
+
856
+ export { FieldRoot as F, FieldLabel as a, FieldDescription as b, FieldError as c };