@octanejs/base-ui 0.1.1

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 (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +33 -0
  3. package/package.json +47 -0
  4. package/src/avatar.ts +308 -0
  5. package/src/checkbox-group.ts +183 -0
  6. package/src/checkbox.ts +578 -0
  7. package/src/field.ts +676 -0
  8. package/src/fieldset.ts +150 -0
  9. package/src/form.ts +183 -0
  10. package/src/index.ts +23 -0
  11. package/src/input.ts +9 -0
  12. package/src/internal.ts +42 -0
  13. package/src/merge-props.ts +2 -0
  14. package/src/meter.ts +204 -0
  15. package/src/number-field.ts +978 -0
  16. package/src/progress.ts +260 -0
  17. package/src/radio-group.ts +285 -0
  18. package/src/radio.ts +401 -0
  19. package/src/separator.ts +43 -0
  20. package/src/slider.ts +1617 -0
  21. package/src/switch.ts +374 -0
  22. package/src/toggle-group.ts +136 -0
  23. package/src/toggle.ts +145 -0
  24. package/src/use-render.ts +40 -0
  25. package/src/utils/CheckboxGroupContext.ts +30 -0
  26. package/src/utils/CompositeRootContext.ts +27 -0
  27. package/src/utils/DirectionContext.ts +15 -0
  28. package/src/utils/RadioGroupContext.ts +27 -0
  29. package/src/utils/ToggleGroupContext.ts +33 -0
  30. package/src/utils/addEventListener.ts +13 -0
  31. package/src/utils/areArraysEqual.ts +11 -0
  32. package/src/utils/clamp.ts +8 -0
  33. package/src/utils/composeRefs.ts +53 -0
  34. package/src/utils/composite/CompositeItem.ts +56 -0
  35. package/src/utils/composite/CompositeList.ts +190 -0
  36. package/src/utils/composite/CompositeListContext.ts +23 -0
  37. package/src/utils/composite/CompositeRoot.ts +132 -0
  38. package/src/utils/composite/keys.ts +179 -0
  39. package/src/utils/composite/list-utils.ts +78 -0
  40. package/src/utils/composite/useCompositeItem.ts +58 -0
  41. package/src/utils/composite/useCompositeListItem.ts +108 -0
  42. package/src/utils/composite/useCompositeRoot.ts +346 -0
  43. package/src/utils/contains.ts +22 -0
  44. package/src/utils/createChangeEventDetails.ts +81 -0
  45. package/src/utils/field/FieldItemContext.ts +13 -0
  46. package/src/utils/field/FieldRootContext.ts +98 -0
  47. package/src/utils/field/FormContext.ts +37 -0
  48. package/src/utils/field/LabelableContext.ts +32 -0
  49. package/src/utils/field/LabelableProvider.ts +108 -0
  50. package/src/utils/field/constants.ts +80 -0
  51. package/src/utils/field/getCombinedFieldValidityData.ts +16 -0
  52. package/src/utils/field/useAriaLabelledBy.ts +86 -0
  53. package/src/utils/field/useFieldControlRegistration.ts +179 -0
  54. package/src/utils/field/useFieldValidation.ts +311 -0
  55. package/src/utils/field/useLabel.ts +95 -0
  56. package/src/utils/field/useLabelableId.ts +99 -0
  57. package/src/utils/field/useRegisterFieldControl.ts +41 -0
  58. package/src/utils/formatNumber.ts +44 -0
  59. package/src/utils/getDefaultFormSubmitter.ts +19 -0
  60. package/src/utils/getElementRef.ts +13 -0
  61. package/src/utils/getStateAttributesProps.ts +34 -0
  62. package/src/utils/matchesFocusVisible.ts +16 -0
  63. package/src/utils/mergeObjects.ts +18 -0
  64. package/src/utils/mergeProps.ts +206 -0
  65. package/src/utils/noop.ts +4 -0
  66. package/src/utils/number/constants.ts +4 -0
  67. package/src/utils/number/parse.ts +227 -0
  68. package/src/utils/number/types.ts +25 -0
  69. package/src/utils/number/validate.ts +129 -0
  70. package/src/utils/owner.ts +9 -0
  71. package/src/utils/platform.ts +15 -0
  72. package/src/utils/resolveAriaLabelledBy.ts +11 -0
  73. package/src/utils/resolveClassName.ts +8 -0
  74. package/src/utils/resolveRef.ts +10 -0
  75. package/src/utils/resolveStyle.ts +10 -0
  76. package/src/utils/serializeValue.ts +15 -0
  77. package/src/utils/slider/asc.ts +4 -0
  78. package/src/utils/slider/getMidpoint.ts +14 -0
  79. package/src/utils/slider/getPushedThumbValues.ts +73 -0
  80. package/src/utils/slider/getSliderValue.ts +27 -0
  81. package/src/utils/slider/replaceArrayItemAtIndex.ts +8 -0
  82. package/src/utils/slider/resolveThumbCollision.ts +178 -0
  83. package/src/utils/slider/roundValueToStep.ts +22 -0
  84. package/src/utils/slider/validateMinimumDistance.ts +22 -0
  85. package/src/utils/slider/valueArrayToPercentages.ts +11 -0
  86. package/src/utils/stringifyLocale.ts +10 -0
  87. package/src/utils/useAnimationFrame.ts +59 -0
  88. package/src/utils/useAnimationsFinished.ts +100 -0
  89. package/src/utils/useBaseUiId.ts +19 -0
  90. package/src/utils/useButton.ts +217 -0
  91. package/src/utils/useCheckboxGroupParent.ts +113 -0
  92. package/src/utils/useControlled.ts +42 -0
  93. package/src/utils/useFocusableWhenDisabled.ts +84 -0
  94. package/src/utils/useForcedRerendering.ts +11 -0
  95. package/src/utils/useIsHydrating.ts +9 -0
  96. package/src/utils/useOpenChangeComplete.ts +46 -0
  97. package/src/utils/usePressAndHold.ts +35 -0
  98. package/src/utils/useRefWithInit.ts +23 -0
  99. package/src/utils/useRegisteredLabelId.ts +32 -0
  100. package/src/utils/useRenderElement.ts +165 -0
  101. package/src/utils/useStableCallback.ts +26 -0
  102. package/src/utils/useTimeout.ts +51 -0
  103. package/src/utils/useTransitionStatus.ts +115 -0
  104. package/src/utils/useValueAsRef.ts +25 -0
  105. package/src/utils/useValueChanged.ts +37 -0
  106. package/src/utils/valueToPercent.ts +4 -0
  107. package/src/utils/visuallyHidden.ts +24 -0
package/src/field.ts ADDED
@@ -0,0 +1,676 @@
1
+ // Ported from .base-ui/packages/react/src/field/ (v1.6.0): root/FieldRoot,
2
+ // control/FieldControl, label/FieldLabel, description/FieldDescription, error/FieldError,
3
+ // validity/FieldValidity, item/FieldItem — plus its `index.parts` (the `Field` namespace).
4
+ //
5
+ // Field groups a control with its label/description/error and runs validation. The default
6
+ // (inert) field contexts are overridden here by the real providers. octane adaptations:
7
+ // native events; forwardRef → ref-as-prop; the text-input value adaptation (initial value →
8
+ // the `value` ATTRIBUTE; controlled value driven via the property).
9
+ import {
10
+ createContext,
11
+ createElement,
12
+ useContext,
13
+ useCallback,
14
+ useImperativeHandle,
15
+ useLayoutEffect,
16
+ useMemo,
17
+ useRef,
18
+ useState,
19
+ } from 'octane';
20
+
21
+ import { S, subSlot } from './internal';
22
+ import { useRenderElement, type RenderProp } from './utils/useRenderElement';
23
+ import { ownerDocument, ownerWindow } from './utils/owner';
24
+ import { createChangeEventDetails, REASONS } from './utils/createChangeEventDetails';
25
+ import { useControlled } from './utils/useControlled';
26
+ import { useStableCallback } from './utils/useStableCallback';
27
+ import {
28
+ DEFAULT_VALIDITY_STATE,
29
+ fieldValidityMapping,
30
+ type FieldRootState,
31
+ type FieldValidityData,
32
+ type FormValidationMode,
33
+ } from './utils/field/constants';
34
+ import { FieldRootContext, type FieldRootContextValue } from './utils/field/FieldRootContext';
35
+ import { FieldItemContext } from './utils/field/FieldItemContext';
36
+ import { useFieldRootContext } from './utils/field/FieldRootContext';
37
+ import { useFieldItemContext } from './utils/field/FieldItemContext';
38
+ import { useFormContext } from './utils/field/FormContext';
39
+ import { useLabelableContext } from './utils/field/LabelableContext';
40
+ import { LabelableProvider } from './utils/field/LabelableProvider';
41
+ import { useLabelableId } from './utils/field/useLabelableId';
42
+ import { useLabel } from './utils/field/useLabel';
43
+ import { useRegisterFieldControl } from './utils/field/useRegisterFieldControl';
44
+ import { useFieldValidation } from './utils/field/useFieldValidation';
45
+ import { useFieldControlRegistration } from './utils/field/useFieldControlRegistration';
46
+ import { getCombinedFieldValidityData } from './utils/field/getCombinedFieldValidityData';
47
+ import { useFieldsetRootContext } from './fieldset';
48
+ import { transitionStatusMapping, useTransitionStatus } from './utils/useTransitionStatus';
49
+ import { useOpenChangeComplete } from './utils/useOpenChangeComplete';
50
+ import { useBaseUiId } from './utils/useBaseUiId';
51
+ import type { StateAttributesMapping } from './utils/getStateAttributesProps';
52
+
53
+ function activeElement(doc: Document): Element | null {
54
+ return doc.activeElement;
55
+ }
56
+
57
+ // --- Root --------------------------------------------------------------------
58
+
59
+ function FieldRootInner(props: any): any {
60
+ const slot = S('FieldRootInner');
61
+ const { errors, validationMode: formValidationMode, submitAttemptedRef } = useFormContext();
62
+
63
+ const {
64
+ render,
65
+ className,
66
+ validate: validateProp,
67
+ validationDebounceTime = 0,
68
+ validationMode = formValidationMode,
69
+ name,
70
+ disabled: disabledProp = false,
71
+ invalid: invalidProp,
72
+ dirty: dirtyProp,
73
+ touched: touchedProp,
74
+ actionsRef,
75
+ style,
76
+ ref,
77
+ ...elementProps
78
+ } = props;
79
+
80
+ const disabledFieldset = useFieldsetRootContext(true)?.disabled;
81
+ const validate = useStableCallback(validateProp || (() => null), subSlot(slot, 'validate'));
82
+ const disabled = disabledFieldset || disabledProp;
83
+
84
+ const [touchedState, setTouchedUnwrapped] = useState(false, subSlot(slot, 'touched'));
85
+ const [dirtyState, setDirtyUnwrapped] = useState(false, subSlot(slot, 'dirty'));
86
+ const [filled, setFilled] = useState(false, subSlot(slot, 'filled'));
87
+ const [focused, setFocused] = useState(false, subSlot(slot, 'focused'));
88
+
89
+ const dirty = dirtyProp ?? dirtyState;
90
+ const touched = touchedProp ?? touchedState;
91
+
92
+ const markedDirtyRef = useRef(dirty, subSlot(slot, 'markedDirty'));
93
+ const registeredFieldIdRef = useRef<string | undefined>(undefined, subSlot(slot, 'regId'));
94
+ const [registeredFieldName, setRegisteredFieldName] = useState<string | undefined>(
95
+ undefined,
96
+ subSlot(slot, 'regName'),
97
+ );
98
+ const effectiveName = name ?? registeredFieldName;
99
+
100
+ useLayoutEffect(
101
+ () => {
102
+ if (dirtyProp !== undefined) {
103
+ markedDirtyRef.current = dirtyProp;
104
+ }
105
+ },
106
+ [dirtyProp],
107
+ subSlot(slot, 'e:markedDirty'),
108
+ );
109
+
110
+ const getRegisteredFieldId = useCallback(
111
+ () => registeredFieldIdRef.current,
112
+ [],
113
+ subSlot(slot, 'getRegId'),
114
+ );
115
+ const setRegisteredFieldId = useCallback(
116
+ (id: string | undefined) => {
117
+ registeredFieldIdRef.current = id;
118
+ },
119
+ [],
120
+ subSlot(slot, 'setRegId'),
121
+ );
122
+
123
+ const setDirty = useStableCallback(
124
+ (value: any) => {
125
+ if (dirtyProp !== undefined) {
126
+ return;
127
+ }
128
+ if (value) {
129
+ markedDirtyRef.current = true;
130
+ }
131
+ setDirtyUnwrapped(value);
132
+ },
133
+ subSlot(slot, 'setDirty'),
134
+ );
135
+
136
+ const setTouched = useStableCallback(
137
+ (value: any) => {
138
+ if (touchedProp !== undefined) {
139
+ return;
140
+ }
141
+ setTouchedUnwrapped(value);
142
+ },
143
+ subSlot(slot, 'setTouched'),
144
+ );
145
+
146
+ const shouldValidateOnChange = useStableCallback(
147
+ () =>
148
+ validationMode === 'onChange' ||
149
+ (validationMode === 'onSubmit' && submitAttemptedRef.current),
150
+ subSlot(slot, 'shouldValidate'),
151
+ );
152
+
153
+ const formError =
154
+ effectiveName && Object.hasOwn(errors, effectiveName) ? errors[effectiveName] : null;
155
+ const hasFormError = !!(Array.isArray(formError) ? formError.length : formError);
156
+ const invalid = invalidProp === true || hasFormError;
157
+
158
+ const [validityData, setValidityData] = useState<FieldValidityData>(
159
+ { state: DEFAULT_VALIDITY_STATE, error: '', errors: [], value: null, initialValue: null },
160
+ subSlot(slot, 'validityData'),
161
+ );
162
+
163
+ const valid = disabled ? null : !invalid && validityData.state.valid;
164
+
165
+ const state: FieldRootState = useMemo(
166
+ () => ({ disabled, touched, dirty, valid, filled, focused }),
167
+ [disabled, touched, dirty, valid, filled, focused],
168
+ subSlot(slot, 'state'),
169
+ );
170
+
171
+ const validation = useFieldValidation(
172
+ {
173
+ setValidityData,
174
+ validate,
175
+ validityData,
176
+ validationDebounceTime,
177
+ invalid,
178
+ markedDirtyRef,
179
+ state,
180
+ shouldValidateOnChange,
181
+ getRegisteredFieldId,
182
+ },
183
+ subSlot(slot, 'validation'),
184
+ );
185
+
186
+ const [validateFieldControl, registerFieldControl] = useFieldControlRegistration(
187
+ {
188
+ commit: validation.commit,
189
+ invalid,
190
+ markedDirtyRef,
191
+ name,
192
+ setRegisteredFieldName,
193
+ setRegisteredFieldId,
194
+ setValidityData,
195
+ validityData,
196
+ },
197
+ subSlot(slot, 'controlReg'),
198
+ );
199
+
200
+ useImperativeHandle(
201
+ actionsRef,
202
+ () => ({ validate: validateFieldControl }),
203
+ [validateFieldControl],
204
+ subSlot(slot, 'imperative'),
205
+ );
206
+
207
+ const contextValue: FieldRootContextValue = useMemo(
208
+ () => ({
209
+ invalid,
210
+ name: effectiveName,
211
+ validityData,
212
+ setValidityData,
213
+ disabled,
214
+ touched,
215
+ setTouched,
216
+ dirty,
217
+ setDirty,
218
+ filled,
219
+ setFilled,
220
+ focused,
221
+ setFocused,
222
+ validate,
223
+ validationMode,
224
+ validationDebounceTime,
225
+ shouldValidateOnChange,
226
+ state,
227
+ markedDirtyRef,
228
+ registerFieldControl,
229
+ validation,
230
+ }),
231
+ [
232
+ invalid,
233
+ effectiveName,
234
+ validityData,
235
+ disabled,
236
+ touched,
237
+ setTouched,
238
+ dirty,
239
+ setDirty,
240
+ filled,
241
+ setFilled,
242
+ focused,
243
+ setFocused,
244
+ validate,
245
+ validationMode,
246
+ validationDebounceTime,
247
+ shouldValidateOnChange,
248
+ state,
249
+ registerFieldControl,
250
+ validation,
251
+ ],
252
+ subSlot(slot, 'ctx'),
253
+ );
254
+
255
+ const element = useRenderElement(
256
+ 'div',
257
+ { render, className, style },
258
+ { ref, state, props: elementProps, stateAttributesMapping: fieldValidityMapping },
259
+ subSlot(slot, 're'),
260
+ );
261
+
262
+ return createElement(FieldRootContext.Provider, { value: contextValue, children: element });
263
+ }
264
+
265
+ function FieldRoot(props: any): any {
266
+ return createElement(LabelableProvider, {
267
+ children: createElement(FieldRootInner, props),
268
+ });
269
+ }
270
+
271
+ // --- Control -----------------------------------------------------------------
272
+
273
+ function FieldControl(props: any): any {
274
+ const slot = S('FieldControl');
275
+ const {
276
+ render,
277
+ className,
278
+ id: idProp,
279
+ name: nameProp,
280
+ value: valueProp,
281
+ disabled: disabledProp = false,
282
+ onValueChange,
283
+ defaultValue,
284
+ autoFocus = false,
285
+ style,
286
+ ref,
287
+ ...elementProps
288
+ } = props;
289
+
290
+ const {
291
+ state: fieldState,
292
+ name: fieldName,
293
+ disabled: fieldDisabled,
294
+ setTouched,
295
+ setDirty,
296
+ validityData,
297
+ setFocused,
298
+ setFilled,
299
+ validationMode,
300
+ validation,
301
+ } = useFieldRootContext();
302
+ const { clearErrors } = useFormContext();
303
+
304
+ const disabled = fieldDisabled || disabledProp;
305
+ const name = fieldName ?? nameProp;
306
+ const state = { ...fieldState, disabled };
307
+
308
+ const { labelId } = useLabelableContext();
309
+ const id = useLabelableId({ id: idProp }, subSlot(slot, 'id'));
310
+
311
+ const inputRef = useRef<HTMLElement | null>(null, subSlot(slot, 'inputRef'));
312
+
313
+ useLayoutEffect(
314
+ () => {
315
+ const hasExternalValue = valueProp != null;
316
+ if (validation.inputRef.current?.value || (hasExternalValue && valueProp !== '')) {
317
+ setFilled(true);
318
+ } else if (hasExternalValue && valueProp === '') {
319
+ setFilled(false);
320
+ }
321
+ },
322
+ [validation.inputRef, setFilled, valueProp],
323
+ subSlot(slot, 'e:filled'),
324
+ );
325
+
326
+ useLayoutEffect(
327
+ () => {
328
+ if (autoFocus && inputRef.current === activeElement(ownerDocument(inputRef.current))) {
329
+ setFocused(true);
330
+ }
331
+ },
332
+ [autoFocus, setFocused],
333
+ subSlot(slot, 'e:autofocus'),
334
+ );
335
+
336
+ const [valueUnwrapped] = useControlled(
337
+ { controlled: valueProp, default: defaultValue, name: 'FieldControl', state: 'value' },
338
+ subSlot(slot, 'value'),
339
+ );
340
+
341
+ const isControlled = valueProp !== undefined;
342
+ const value = isControlled ? valueUnwrapped : undefined;
343
+
344
+ // octane text-input adaptation: the INITIAL value is the `value` ATTRIBUTE; a controlled
345
+ // value is driven imperatively via the property.
346
+ const initialValueRef = useRef(
347
+ isControlled ? value : defaultValue,
348
+ subSlot(slot, 'initialValue'),
349
+ );
350
+
351
+ useLayoutEffect(
352
+ () => {
353
+ if (!isControlled) {
354
+ return;
355
+ }
356
+ const input = validation.inputRef.current;
357
+ if (!input) {
358
+ return;
359
+ }
360
+ const setNativeValue = Object.getOwnPropertyDescriptor(
361
+ ownerWindow(input).HTMLInputElement.prototype,
362
+ 'value',
363
+ )?.set;
364
+ setNativeValue?.call(input, value ?? '');
365
+ },
366
+ [isControlled, value],
367
+ subSlot(slot, 'e:syncValue'),
368
+ );
369
+
370
+ const getValueFromInput = useStableCallback(
371
+ () => validation.inputRef.current?.value,
372
+ subSlot(slot, 'getValue'),
373
+ );
374
+
375
+ useRegisterFieldControl(
376
+ validation.inputRef,
377
+ id,
378
+ value,
379
+ getValueFromInput,
380
+ !disabled,
381
+ nameProp,
382
+ subSlot(slot, 'register'),
383
+ );
384
+
385
+ return useRenderElement(
386
+ 'input',
387
+ { render, className, style },
388
+ {
389
+ ref: [ref, inputRef],
390
+ state,
391
+ props: [
392
+ {
393
+ id,
394
+ disabled,
395
+ name,
396
+ ref: validation.inputRef,
397
+ 'aria-labelledby': labelId,
398
+ autoFocus: autoFocus || undefined,
399
+ value: initialValueRef.current,
400
+ onChange(event: any) {
401
+ const inputValue = event.currentTarget.value;
402
+ onValueChange?.(inputValue, createChangeEventDetails(REASONS.none, event));
403
+ setDirty(inputValue !== validityData.initialValue);
404
+ setFilled(inputValue !== '');
405
+ if (!event.defaultPrevented) {
406
+ clearErrors(name);
407
+ validation.change(inputValue);
408
+ }
409
+ },
410
+ onFocus() {
411
+ setFocused(true);
412
+ },
413
+ onBlur(event: any) {
414
+ setTouched(true);
415
+ setFocused(false);
416
+ if (validationMode === 'onBlur') {
417
+ validation.commit(event.currentTarget.value);
418
+ }
419
+ },
420
+ onKeyDown(event: any) {
421
+ if (event.currentTarget.tagName === 'INPUT' && event.key === 'Enter') {
422
+ setTouched(true);
423
+ validation.commit(event.currentTarget.value);
424
+ }
425
+ },
426
+ },
427
+ elementProps,
428
+ (p: any) => validation.getValidationProps(disabled, p),
429
+ ],
430
+ stateAttributesMapping: fieldValidityMapping,
431
+ },
432
+ subSlot(slot, 're'),
433
+ );
434
+ }
435
+
436
+ // --- Label -------------------------------------------------------------------
437
+
438
+ function FieldLabel(props: any): any {
439
+ const slot = S('FieldLabel');
440
+ const { render, className, style, id: idProp, nativeLabel = true, ref, ...elementProps } = props;
441
+
442
+ const fieldRootContext = useFieldRootContext(false);
443
+ const fieldItemContext = useFieldItemContext();
444
+ const { labelId } = useLabelableContext();
445
+
446
+ const state = {
447
+ ...fieldRootContext.state,
448
+ disabled: fieldRootContext.disabled || fieldItemContext.disabled,
449
+ };
450
+
451
+ const labelRef = useRef<HTMLElement | null>(null, subSlot(slot, 'labelRef'));
452
+ const labelProps = useLabel(
453
+ { id: labelId ?? idProp, native: nativeLabel },
454
+ subSlot(slot, 'label'),
455
+ );
456
+
457
+ return useRenderElement(
458
+ 'label',
459
+ { render, className, style },
460
+ {
461
+ ref: [ref, labelRef],
462
+ state,
463
+ props: [labelProps, elementProps],
464
+ stateAttributesMapping: fieldValidityMapping,
465
+ },
466
+ subSlot(slot, 're'),
467
+ );
468
+ }
469
+
470
+ // --- Description --------------------------------------------------------------
471
+
472
+ function FieldDescription(props: any): any {
473
+ const slot = S('FieldDescription');
474
+ const { render, id: idProp, className, style, ref, ...elementProps } = props;
475
+
476
+ const id = useBaseUiId(idProp, subSlot(slot, 'id'));
477
+ const fieldRootContext = useFieldRootContext(false);
478
+ const fieldItemContext = useFieldItemContext();
479
+ const { setMessageIds } = useLabelableContext();
480
+
481
+ const state = {
482
+ ...fieldRootContext.state,
483
+ disabled: fieldRootContext.disabled || fieldItemContext.disabled,
484
+ };
485
+
486
+ useLayoutEffect(
487
+ () => {
488
+ if (!id) {
489
+ return undefined;
490
+ }
491
+ setMessageIds((v: string[]) => v.concat(id));
492
+ return () => {
493
+ setMessageIds((v: string[]) => v.filter((item) => item !== id));
494
+ };
495
+ },
496
+ [id, setMessageIds],
497
+ subSlot(slot, 'e:register'),
498
+ );
499
+
500
+ return useRenderElement(
501
+ 'p',
502
+ { render, className, style },
503
+ { ref, state, props: [{ id }, elementProps], stateAttributesMapping: fieldValidityMapping },
504
+ subSlot(slot, 're'),
505
+ );
506
+ }
507
+
508
+ // --- Error -------------------------------------------------------------------
509
+
510
+ const errorStateAttributesMapping: StateAttributesMapping<any> = {
511
+ ...(fieldValidityMapping as StateAttributesMapping<any>),
512
+ ...(transitionStatusMapping as StateAttributesMapping<any>),
513
+ };
514
+
515
+ function FieldError(props: any): any {
516
+ const slot = S('FieldError');
517
+ const { render, id: idProp, className, match, style, children, ref, ...elementProps } = props;
518
+
519
+ const id = useBaseUiId(idProp, subSlot(slot, 'id'));
520
+ const { validityData, state: fieldState, name } = useFieldRootContext(false);
521
+ const { setMessageIds } = useLabelableContext();
522
+ const { errors } = useFormContext();
523
+
524
+ const formError = name && Object.hasOwn(errors, name) ? errors[name] : null;
525
+ const hasFormError = !!(Array.isArray(formError) ? formError.length : formError);
526
+ const hasSpecificMatch = typeof match === 'string';
527
+
528
+ let rendered = false;
529
+ if (match === true) {
530
+ rendered = true;
531
+ } else if (fieldState.disabled) {
532
+ rendered = false;
533
+ } else if (hasSpecificMatch) {
534
+ rendered = Boolean((validityData.state as any)[match]);
535
+ } else {
536
+ rendered = hasFormError || validityData.state.valid === false;
537
+ }
538
+
539
+ const { mounted, transitionStatus, setMounted } = useTransitionStatus(
540
+ rendered,
541
+ subSlot(slot, 'ts'),
542
+ );
543
+
544
+ useLayoutEffect(
545
+ () => {
546
+ if (!rendered || !id) {
547
+ return undefined;
548
+ }
549
+ setMessageIds((v: string[]) => v.concat(id));
550
+ return () => {
551
+ setMessageIds((v: string[]) => v.filter((item) => item !== id));
552
+ };
553
+ },
554
+ [rendered, id, setMessageIds],
555
+ subSlot(slot, 'e:register'),
556
+ );
557
+
558
+ const errorRef = useRef<HTMLDivElement | null>(null, subSlot(slot, 'errorRef'));
559
+
560
+ let error: string | string[] | null | undefined = validityData.error;
561
+ if (!hasSpecificMatch && hasFormError) {
562
+ error = formError;
563
+ } else if (validityData.errors.length > 1) {
564
+ error = validityData.errors;
565
+ }
566
+
567
+ let errorMessage: any = error ?? '';
568
+ if (Array.isArray(error)) {
569
+ errorMessage =
570
+ error.length > 1
571
+ ? createElement('ul', {
572
+ children: error.map((message) =>
573
+ createElement('li', { key: message, children: message }),
574
+ ),
575
+ })
576
+ : error[0];
577
+ }
578
+
579
+ const state = { ...fieldState, transitionStatus };
580
+
581
+ useOpenChangeComplete(
582
+ {
583
+ open: rendered,
584
+ ref: errorRef,
585
+ onComplete() {
586
+ if (!rendered) {
587
+ setMounted(false);
588
+ }
589
+ },
590
+ },
591
+ subSlot(slot, 'occ'),
592
+ );
593
+
594
+ const element = useRenderElement(
595
+ 'div',
596
+ { render, className, style },
597
+ {
598
+ ref: [ref, errorRef],
599
+ state,
600
+ props: [{ id, children: children ?? errorMessage }, elementProps],
601
+ stateAttributesMapping: errorStateAttributesMapping,
602
+ },
603
+ subSlot(slot, 're'),
604
+ );
605
+
606
+ if (!mounted) {
607
+ return null;
608
+ }
609
+ return element;
610
+ }
611
+
612
+ // --- Validity (render-prop) --------------------------------------------------
613
+
614
+ function FieldValidity(props: any): any {
615
+ const slot = S('FieldValidity');
616
+ const { children } = props;
617
+ const { validityData, invalid } = useFieldRootContext(false);
618
+
619
+ const combined = useMemo(
620
+ () => getCombinedFieldValidityData(validityData, invalid),
621
+ [validityData, invalid],
622
+ subSlot(slot, 'combined'),
623
+ );
624
+ const isInvalid = combined.state.valid === false;
625
+ const { transitionStatus } = useTransitionStatus(isInvalid, subSlot(slot, 'ts'));
626
+
627
+ const fieldValidityState = useMemo(
628
+ () => ({ ...combined, validity: combined.state, transitionStatus }),
629
+ [combined, transitionStatus],
630
+ subSlot(slot, 'state'),
631
+ );
632
+
633
+ return children(fieldValidityState);
634
+ }
635
+
636
+ // --- Item --------------------------------------------------------------------
637
+
638
+ function FieldItem(props: any): any {
639
+ const slot = S('FieldItem');
640
+ const { render, className, style, disabled: disabledProp = false, ref, ...elementProps } = props;
641
+
642
+ const { state: fieldState, disabled: rootDisabled } = useFieldRootContext(false);
643
+ const disabled = rootDisabled || disabledProp;
644
+ const state = { ...fieldState, disabled };
645
+
646
+ const fieldItemContextValue = useMemo(() => ({ disabled }), [disabled], subSlot(slot, 'ctx'));
647
+
648
+ const element = useRenderElement(
649
+ 'div',
650
+ { render, className, style },
651
+ { ref, state, props: elementProps, stateAttributesMapping: fieldValidityMapping },
652
+ subSlot(slot, 're'),
653
+ );
654
+
655
+ return createElement(LabelableProvider, {
656
+ children: createElement(FieldItemContext.Provider, {
657
+ value: fieldItemContextValue,
658
+ children: element,
659
+ }),
660
+ });
661
+ }
662
+
663
+ // --- Namespace (mirrors `export * as Field`) ---------------------------------
664
+
665
+ export const Field = {
666
+ Root: FieldRoot,
667
+ Control: FieldControl,
668
+ Label: FieldLabel,
669
+ Description: FieldDescription,
670
+ Error: FieldError,
671
+ Validity: FieldValidity,
672
+ Item: FieldItem,
673
+ };
674
+
675
+ export type { FieldRootState, FieldValidityData, FormValidationMode };
676
+ export { useFieldRootContext };