@jobber/components 9.5.3 → 9.6.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 (178) hide show
  1. package/Combobox.d.ts +1 -0
  2. package/Combobox.js +17 -0
  3. package/dist/Autocomplete/index.cjs +0 -2
  4. package/dist/Autocomplete/index.mjs +0 -2
  5. package/dist/Autocomplete-cjs.js +4 -5
  6. package/dist/Autocomplete-es.js +1 -2
  7. package/dist/Card/index.cjs +2 -2
  8. package/dist/Card/index.mjs +2 -2
  9. package/dist/Chip/index.cjs +0 -2
  10. package/dist/Chip/index.mjs +0 -2
  11. package/dist/Chips/InternalChipDismissible/hooks/index.cjs +0 -2
  12. package/dist/Chips/InternalChipDismissible/hooks/index.mjs +0 -2
  13. package/dist/Chips/InternalChipDismissible/index.cjs +0 -2
  14. package/dist/Chips/InternalChipDismissible/index.mjs +0 -2
  15. package/dist/Chips/index.cjs +0 -2
  16. package/dist/Chips/index.mjs +0 -2
  17. package/dist/Combobox/Combobox.contentSlots.d.ts +5 -0
  18. package/dist/Combobox/Combobox.context.d.ts +34 -0
  19. package/dist/Combobox/Combobox.d.ts +69 -0
  20. package/dist/Combobox/Combobox.footer.d.ts +7 -0
  21. package/dist/Combobox/Combobox.items.d.ts +25 -0
  22. package/dist/Combobox/Combobox.responsive.d.ts +54 -0
  23. package/dist/Combobox/Combobox.selectAll.d.ts +4 -0
  24. package/dist/Combobox/Combobox.slotFactory.d.ts +7 -0
  25. package/dist/Combobox/Combobox.testUtils.d.ts +12 -0
  26. package/dist/Combobox/Combobox.types.d.ts +166 -0
  27. package/dist/Combobox/index.cjs +35 -0
  28. package/dist/Combobox/index.d.ts +2 -0
  29. package/dist/Combobox/index.mjs +29 -0
  30. package/dist/Combobox-cjs.js +675 -0
  31. package/dist/Combobox-es.js +673 -0
  32. package/dist/ComboboxPrimitive-cjs.js +88 -47
  33. package/dist/ComboboxPrimitive-es.js +39 -23
  34. package/dist/{ComboboxChipRemove-cjs.js → ComboboxSeparator-cjs.js} +501 -455
  35. package/dist/{ComboboxChipRemove-es.js → ComboboxSeparator-es.js} +500 -456
  36. package/dist/ConfirmationModal/index.cjs +0 -2
  37. package/dist/ConfirmationModal/index.mjs +0 -2
  38. package/dist/ContentBlock/index.cjs +1 -0
  39. package/dist/ContentBlock/index.mjs +1 -0
  40. package/dist/ContentBlock-cjs.js +2 -18
  41. package/dist/ContentBlock-es.js +1 -17
  42. package/dist/DataDump/index.cjs +2 -2
  43. package/dist/DataDump/index.mjs +2 -2
  44. package/dist/DataList/components/DataListActions/index.cjs +0 -2
  45. package/dist/DataList/components/DataListActions/index.mjs +0 -2
  46. package/dist/DataList/components/DataListBulkActions/index.cjs +0 -2
  47. package/dist/DataList/components/DataListBulkActions/index.mjs +0 -2
  48. package/dist/DataList/components/DataListFilters/components/DataListSort/index.cjs +0 -2
  49. package/dist/DataList/components/DataListFilters/components/DataListSort/index.mjs +0 -2
  50. package/dist/DataList/components/DataListFilters/index.cjs +0 -2
  51. package/dist/DataList/components/DataListFilters/index.mjs +0 -2
  52. package/dist/DataList/components/DataListHeader/index.cjs +0 -2
  53. package/dist/DataList/components/DataListHeader/index.mjs +0 -2
  54. package/dist/DataList/components/DataListItem/index.cjs +0 -2
  55. package/dist/DataList/components/DataListItem/index.mjs +0 -2
  56. package/dist/DataList/components/DataListItemActions/index.cjs +0 -2
  57. package/dist/DataList/components/DataListItemActions/index.mjs +0 -2
  58. package/dist/DataList/components/DataListItemActionsOverflow/index.cjs +0 -2
  59. package/dist/DataList/components/DataListItemActionsOverflow/index.mjs +0 -2
  60. package/dist/DataList/components/DataListItems/index.cjs +0 -2
  61. package/dist/DataList/components/DataListItems/index.mjs +0 -2
  62. package/dist/DataList/components/DataListLayout/index.cjs +0 -2
  63. package/dist/DataList/components/DataListLayout/index.mjs +0 -2
  64. package/dist/DataList/components/DataListLayoutActions/index.cjs +0 -2
  65. package/dist/DataList/components/DataListLayoutActions/index.mjs +0 -2
  66. package/dist/DataList/index.cjs +0 -2
  67. package/dist/DataList/index.mjs +0 -2
  68. package/dist/DatePicker/index.cjs +0 -2
  69. package/dist/DatePicker/index.mjs +0 -2
  70. package/dist/DatePicker-cjs.js +4 -5
  71. package/dist/DatePicker-es.js +1 -2
  72. package/dist/Dialog/index.cjs +2 -2
  73. package/dist/Dialog/index.mjs +2 -2
  74. package/dist/DrawerDescription-cjs.js +268 -99
  75. package/dist/DrawerDescription-es.js +270 -103
  76. package/dist/DrawerTitle-cjs.js +2 -6
  77. package/dist/DrawerTitle-es.js +3 -7
  78. package/dist/FieldDescription-cjs.js +184 -181
  79. package/dist/FieldDescription-es.js +187 -184
  80. package/dist/FilterPicker/components/FilterPickerActivator/index.cjs +0 -2
  81. package/dist/FilterPicker/components/FilterPickerActivator/index.mjs +0 -2
  82. package/dist/FilterPicker/components/FilterPickerContent/index.cjs +0 -2
  83. package/dist/FilterPicker/components/FilterPickerContent/index.mjs +0 -2
  84. package/dist/FilterPicker/components/FilterPickerTrigger/index.cjs +0 -2
  85. package/dist/FilterPicker/components/FilterPickerTrigger/index.mjs +0 -2
  86. package/dist/FilterPicker/index.cjs +0 -2
  87. package/dist/FilterPicker/index.mjs +0 -2
  88. package/dist/FilterPickerContent-cjs.js +4 -5
  89. package/dist/FilterPickerContent-es.js +1 -2
  90. package/dist/FormatFile/index.cjs +0 -2
  91. package/dist/FormatFile/index.mjs +0 -2
  92. package/dist/Gallery/index.cjs +0 -2
  93. package/dist/Gallery/index.mjs +0 -2
  94. package/dist/InputDate/index.cjs +0 -2
  95. package/dist/InputDate/index.mjs +0 -2
  96. package/dist/InputNumber/index.cjs +2 -2
  97. package/dist/InputNumber/index.mjs +2 -2
  98. package/dist/InputNumber/types.d.ts +5 -0
  99. package/dist/InputNumber-cjs.js +2 -2
  100. package/dist/InputNumber-es.js +2 -2
  101. package/dist/LightBox/index.cjs +0 -2
  102. package/dist/LightBox/index.mjs +0 -2
  103. package/dist/Menu/index.cjs +2 -2
  104. package/dist/Menu/index.mjs +2 -2
  105. package/dist/Menu-cjs.js +4 -5
  106. package/dist/Menu-es.js +1 -2
  107. package/dist/MenuSubmenuTrigger-cjs.js +206 -164
  108. package/dist/MenuSubmenuTrigger-es.js +194 -152
  109. package/dist/Modal/index.cjs +0 -2
  110. package/dist/Modal/index.mjs +1 -3
  111. package/dist/NumberFieldInput-cjs.js +187 -308
  112. package/dist/NumberFieldInput-es.js +187 -308
  113. package/dist/Page/index.cjs +2 -2
  114. package/dist/Page/index.mjs +2 -2
  115. package/dist/Popover/index.cjs +0 -2
  116. package/dist/Popover/index.mjs +0 -2
  117. package/dist/Popover-cjs.js +7 -8
  118. package/dist/Popover-es.js +1 -2
  119. package/dist/ScrollAreaViewport-cjs.js +747 -1093
  120. package/dist/ScrollAreaViewport-es.js +734 -1081
  121. package/dist/Select/index.cjs +2 -2
  122. package/dist/Select/index.mjs +2 -2
  123. package/dist/SelectGroupLabel-cjs.js +207 -306
  124. package/dist/SelectGroupLabel-es.js +203 -302
  125. package/dist/SelectPrimitive-cjs.js +11 -17
  126. package/dist/SelectPrimitive-es.js +1 -7
  127. package/dist/Tooltip/index.cjs +0 -2
  128. package/dist/Tooltip/index.mjs +0 -2
  129. package/dist/Tooltip-cjs.js +6 -7
  130. package/dist/Tooltip-es.js +1 -2
  131. package/dist/buttonRenderAdapter-cjs.js +2 -1
  132. package/dist/buttonRenderAdapter-es.js +2 -1
  133. package/dist/dialogReturnFocus-cjs.js +1 -1
  134. package/dist/dialogReturnFocus-es.js +2 -2
  135. package/dist/docs/InputNumber/InputNumber.md +2 -2
  136. package/dist/docs/SelectPrimitive/SelectPrimitive.md +1 -1
  137. package/dist/floating-ui.react-cjs.js +2557 -185
  138. package/dist/floating-ui.react-es.js +2512 -149
  139. package/dist/getMappedBreakpointWidth-cjs.js +21 -0
  140. package/dist/getMappedBreakpointWidth-es.js +18 -0
  141. package/dist/index.cjs +15 -10
  142. package/dist/index.d.mts +1 -0
  143. package/dist/index.d.ts +1 -0
  144. package/dist/index.mjs +14 -10
  145. package/dist/mergeClassName-cjs.js +12 -0
  146. package/dist/mergeClassName-es.js +10 -0
  147. package/dist/primitives/BottomSheet/index.cjs +1 -1
  148. package/dist/primitives/BottomSheet/index.mjs +1 -1
  149. package/dist/primitives/ComboboxPrimitive/ComboboxPrimitive.d.ts +2 -2
  150. package/dist/primitives/ComboboxPrimitive/index.cjs +4 -5
  151. package/dist/primitives/ComboboxPrimitive/index.mjs +4 -5
  152. package/dist/primitives/SelectPrimitive/index.cjs +1 -2
  153. package/dist/primitives/SelectPrimitive/index.mjs +1 -2
  154. package/dist/primitives/index.cjs +3 -3
  155. package/dist/primitives/index.mjs +3 -3
  156. package/dist/scrollEdges-cjs.js +1 -11
  157. package/dist/scrollEdges-es.js +1 -11
  158. package/dist/scrollable-cjs.js +58 -0
  159. package/dist/scrollable-es.js +54 -0
  160. package/dist/styles.css +759 -42
  161. package/dist/unstyledPrimitives/index.cjs +720 -478
  162. package/dist/unstyledPrimitives/index.mjs +663 -421
  163. package/dist/useCompositeListItem-cjs.js +2108 -361
  164. package/dist/useCompositeListItem-es.js +2076 -331
  165. package/dist/useLabel-cjs.js +2 -3
  166. package/dist/useLabel-es.js +2 -3
  167. package/dist/useRenderElement-cjs.js +9 -7
  168. package/dist/useRenderElement-es.js +9 -7
  169. package/dist/useScrollLock-cjs.js +819 -344
  170. package/dist/useScrollLock-es.js +732 -296
  171. package/dist/useScrollToActive-cjs.js +4 -5
  172. package/dist/useScrollToActive-es.js +1 -2
  173. package/dist/utils/meta/meta.json +43 -1
  174. package/package.json +8 -3
  175. package/dist/floating-ui.react-dom-cjs.js +0 -2103
  176. package/dist/floating-ui.react-dom-es.js +0 -2072
  177. package/dist/floating-ui.utils-cjs.js +0 -342
  178. package/dist/floating-ui.utils-es.js +0 -300
@@ -35,77 +35,6 @@ function useFieldItemContext() {
35
35
  return context;
36
36
  }
37
37
 
38
- const FieldsetRootContext = /*#__PURE__*/React__namespace.createContext(undefined);
39
- if (process.env.NODE_ENV !== "production") FieldsetRootContext.displayName = "FieldsetRootContext";
40
- function useFieldsetRootContext(optional = false) {
41
- const context = React__namespace.useContext(FieldsetRootContext);
42
- if (!context && !optional) {
43
- throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: FieldsetRootContext is missing. Fieldset parts must be placed within <Fieldset.Root>.' : useRenderElement.formatErrorMessage(86));
44
- }
45
- return context;
46
- }
47
-
48
- const LabelableProvider = function LabelableProvider(props) {
49
- const defaultId = useScrollLock.useBaseUiId();
50
- const initialControlId = props.controlId === undefined ? defaultId : props.controlId;
51
- const [controlId, setControlIdState] = React__namespace.useState(initialControlId);
52
- const [labelId, setLabelId] = React__namespace.useState(props.labelId);
53
- const [messageIds, setMessageIds] = React__namespace.useState([]);
54
- const registrationsRef = useRenderElement.useRefWithInit(() => new Map());
55
- const {
56
- messageIds: parentMessageIds
57
- } = useCompositeListItem.useLabelableContext();
58
- const registerControlId = useScrollLock.useStableCallback((source, nextId) => {
59
- const registrations = registrationsRef.current;
60
- if (nextId === undefined) {
61
- registrations.delete(source);
62
- return;
63
- }
64
- registrations.set(source, nextId);
65
-
66
- // Only flush when registering, not when unregistering.
67
- // This prevents loops during rapid unmount/remount cycles (e.g. React Activity).
68
- // The next registration will pick up the correct state.
69
- setControlIdState(prev => {
70
- if (registrations.size === 0) {
71
- return undefined;
72
- }
73
- let nextControlId;
74
- for (const id of registrations.values()) {
75
- if (prev !== undefined && id === prev) {
76
- return prev;
77
- }
78
- if (nextControlId === undefined) {
79
- nextControlId = id;
80
- }
81
- }
82
- return nextControlId;
83
- });
84
- });
85
- const getDescriptionProps = React__namespace.useCallback(externalProps => {
86
- const ids = externalProps['aria-describedby'] ? externalProps['aria-describedby'].split(' ') : [];
87
- ids.push(...parentMessageIds, ...messageIds);
88
- return {
89
- ...externalProps,
90
- 'aria-describedby': Array.from(new Set(ids)).join(' ') || undefined
91
- };
92
- }, [parentMessageIds, messageIds]);
93
- const contextValue = React__namespace.useMemo(() => ({
94
- controlId,
95
- registerControlId,
96
- labelId,
97
- setLabelId,
98
- messageIds,
99
- setMessageIds,
100
- getDescriptionProps
101
- }), [controlId, registerControlId, labelId, setLabelId, messageIds, setMessageIds, getDescriptionProps]);
102
- return /*#__PURE__*/jsxRuntime.jsx(useCompositeListItem.LabelableContext.Provider, {
103
- value: contextValue,
104
- children: props.children
105
- });
106
- };
107
- if (process.env.NODE_ENV !== "production") LabelableProvider.displayName = "LabelableProvider";
108
-
109
38
  /**
110
39
  * Combines the field's client-side, stateful validity data with the external invalid state to
111
40
  * determine the field's true validity.
@@ -121,34 +50,35 @@ function getCombinedFieldValidityData(validityData, invalid) {
121
50
  }
122
51
 
123
52
  const validityKeys = Object.keys(useCompositeListItem.DEFAULT_VALIDITY_STATE);
124
- function isOnlyValueMissing(state) {
125
- if (!state || state.valid || !state.valueMissing) {
53
+ /**
54
+ * Whether an input participates in the surrounding Base UI Form. Inputs that are effectively
55
+ * disabled, or whose `form` attribute explicitly associates them with another form, are excluded.
56
+ * DOM position only matters when it associates the input with a different form. Otherwise, field
57
+ * registration is context-driven, so portaled inputs (for example inside a dialog) still belong to
58
+ * the form for both validation and values projected into `onFormSubmit`.
59
+ */
60
+ function isEligibleInput(input, formElement) {
61
+ if (input.matches(':disabled')) {
126
62
  return false;
127
63
  }
128
- let onlyValueMissing = false;
129
- for (const key of validityKeys) {
130
- if (key === 'valid') {
131
- continue;
132
- }
133
- if (key === 'valueMissing') {
134
- onlyValueMissing = state[key];
135
- } else if (state[key]) {
136
- onlyValueMissing = false;
137
- }
64
+ if (!formElement || input.form === formElement) {
65
+ return true;
138
66
  }
139
- return onlyValueMissing;
67
+
68
+ // React context crosses portal boundaries. An unassociated portaled input still participates in
69
+ // contextual validation, unless an explicit `form` attribute opts it out of the surrounding Form.
70
+ return input.form === null && !input.hasAttribute('form');
140
71
  }
141
72
 
142
73
  /**
143
74
  * Picks the input whose native validity should represent a field that owns several inputs (such as a
144
- * checkbox group). Prefers the first enabled currently-invalid input, where "first" follows Set
145
- * insertion order (mount order), and otherwise returns the first enabled input. Disabled inputs are
146
- * skipped because they don't participate in native constraint validation.
75
+ * checkbox or radio group). Prefers the first eligible currently-invalid input, where "first" follows
76
+ * registration order (mount order), and otherwise returns the first eligible input.
147
77
  */
148
- function findRepresentativeInput(inputs) {
78
+ function findRepresentativeInput(inputs, formElement) {
149
79
  let fallback = null;
150
- for (const input of inputs) {
151
- if (input.disabled) {
80
+ for (const input of inputs.keys()) {
81
+ if (!isEligibleInput(input, formElement)) {
152
82
  continue;
153
83
  }
154
84
  if (!input.validity.valid) {
@@ -159,17 +89,14 @@ function findRepresentativeInput(inputs) {
159
89
  return fallback;
160
90
  }
161
91
  function clearCustomValidity(element, inputs) {
162
- let didClearElement = false;
163
- for (const input of inputs) {
92
+ for (const input of inputs.keys()) {
164
93
  input.setCustomValidity('');
165
- didClearElement ||= input === element;
166
- }
167
- if (!didClearElement) {
168
- element.setCustomValidity('');
169
94
  }
95
+ element?.setCustomValidity('');
170
96
  }
171
97
  function useFieldValidation(params) {
172
98
  const {
99
+ elementRef,
173
100
  formRef
174
101
  } = useCompositeListItem.useFormContext();
175
102
  const {
@@ -181,7 +108,7 @@ function useFieldValidation(params) {
181
108
  markedDirtyRef,
182
109
  state,
183
110
  shouldValidateOnChange,
184
- getRegisteredFieldId
111
+ registeredFieldIdRef
185
112
  } = params;
186
113
  const {
187
114
  controlId,
@@ -189,32 +116,27 @@ function useFieldValidation(params) {
189
116
  } = useCompositeListItem.useLabelableContext();
190
117
  const timeout = useScrollLock.useTimeout();
191
118
  const inputRef = React__namespace.useRef(null);
192
- const registeredInputs = useRenderElement.useRefWithInit(() => new Set()).current;
119
+ const registeredInputs = useRenderElement.useRefWithInit(() => new Map()).current;
193
120
  const validationCommitIdRef = React__namespace.useRef(0);
194
121
 
195
- // Checkbox groups register several inputs against a single field. Track them so a `required`
196
- // checkbox can't be satisfied by another input in the group, matching native per-checkbox behavior.
197
- const registerInput = React__namespace.useCallback(element => {
198
- if (!element) {
199
- return undefined;
200
- }
201
- registeredInputs.add(element);
122
+ // Groups register several inputs against a single field so focus, validation, and form-value
123
+ // projection can use the same live controls. This also ensures a `required` checkbox can't be
124
+ // satisfied by another input in the group, matching native per-checkbox behavior.
125
+ const registerInput = React__namespace.useCallback((element, registration) => {
126
+ registeredInputs.set(element, registration);
202
127
  return () => {
203
128
  registeredInputs.delete(element);
204
129
  };
205
130
  }, [registeredInputs]);
131
+ const getInputControl = useScrollLock.useStableCallback(() => {
132
+ const element = findRepresentativeInput(registeredInputs, elementRef.current);
133
+ return element && registeredInputs.get(element)?.controlRef.current || null;
134
+ });
206
135
  const commit = useScrollLock.useStableCallback(async (value, revalidate = false) => {
207
- // A field can own several inputs (a checkbox group), but only the last-mounted one wins the shared
208
- // `inputRef`. Validate against the registry instead so every input counts; `inputRef` is the
209
- // fallback only when no registered input applies (none registered, or all of them disabled).
210
- const element = findRepresentativeInput(registeredInputs) ?? inputRef.current;
211
- if (!element) {
212
- return;
213
- }
214
136
  validationCommitIdRef.current += 1;
215
137
  const validationCommitId = validationCommitIdRef.current;
216
138
  function updateRegisteredFieldValidity(nextValidityData, externalInvalid = invalid) {
217
- const fieldId = getRegisteredFieldId() ?? controlId;
139
+ const fieldId = registeredFieldIdRef.current ?? controlId;
218
140
  if (fieldId == null) {
219
141
  return;
220
142
  }
@@ -228,8 +150,31 @@ function useFieldValidation(params) {
228
150
  validityData: validityDataWithFormErrors
229
151
  });
230
152
  }
153
+ function publishAllValid(input, externalInvalid) {
154
+ const nextValidityData = {
155
+ value,
156
+ state: {
157
+ ...useCompositeListItem.DEFAULT_VALIDITY_STATE,
158
+ valid: true
159
+ },
160
+ error: '',
161
+ errors: [],
162
+ initialValue: validityData.initialValue
163
+ };
164
+ clearCustomValidity(input, registeredInputs);
165
+ updateRegisteredFieldValidity(nextValidityData, externalInvalid);
166
+ setValidityData(nextValidityData);
167
+ }
168
+
169
+ // A field can own several inputs (such as a checkbox or radio group), but only the last-mounted
170
+ // one wins the shared `inputRef`. Validate against the registry instead so every input counts;
171
+ // `inputRef` is the fallback only when no inputs are registered.
172
+ const element = registeredInputs.size > 0 ? findRepresentativeInput(registeredInputs, elementRef.current) : inputRef.current;
173
+ // A field with no eligible input has no native constraint, but its custom validator still
174
+ // applies to the logical value at the configured validation boundary.
175
+
231
176
  if (revalidate) {
232
- if (state.valid !== false) {
177
+ if (state.valid !== false || !element) {
233
178
  return;
234
179
  }
235
180
  const currentNativeValidity = element.validity;
@@ -237,39 +182,20 @@ function useFieldValidation(params) {
237
182
  // The 'valueMissing' (required) condition has been resolved by the user typing.
238
183
  // Temporarily mark the field as valid for this onChange event.
239
184
  // Other native errors (e.g., typeMismatch) will be caught by full validation on blur or submit.
240
- const nextValidityData = {
241
- value,
242
- state: {
243
- ...useCompositeListItem.DEFAULT_VALIDITY_STATE,
244
- valid: true
245
- },
246
- error: '',
247
- errors: [],
248
- initialValue: validityData.initialValue
249
- };
250
- clearCustomValidity(element, registeredInputs);
251
-
252
185
  // The required value is now present; ignore stale external invalid state for this pass.
253
- updateRegisteredFieldValidity(nextValidityData, false);
254
- setValidityData(nextValidityData);
186
+ publishAllValid(element, false);
255
187
  return;
256
188
  }
257
189
 
258
- // Value is still missing, or other conditions apply.
259
- // Let's use a representation of current validity for isOnlyValueMissing.
260
- const currentNativeValidityObject = validityKeys.reduce((acc, key) => {
261
- acc[key] = currentNativeValidity[key];
262
- return acc;
263
- }, {});
264
-
265
- // If it's (still) natively invalid due to something other than just valueMissing,
266
- // then bail from this revalidation on change to avoid "scolding" for other errors.
267
- if (!currentNativeValidityObject.valid && !isOnlyValueMissing(currentNativeValidityObject)) {
268
- return;
190
+ // A stale custom error can coexist with valueMissing, but defer any other native errors.
191
+ for (const key of validityKeys) {
192
+ if (key !== 'valid' && key !== 'valueMissing' && key !== 'customError' && currentNativeValidity[key]) {
193
+ return;
194
+ }
269
195
  }
270
196
 
271
- // If valueMissing is still true AND it's the only issue, or if the field is now natively valid,
272
- // let it fall through to the main validation logic below.
197
+ // Value is still missing: publish the current native state so valueMissing and the changed
198
+ // value are observable immediately. Full custom validation still waits for its boundary.
273
199
  }
274
200
  function getState(el) {
275
201
  const computedState = validityKeys.reduce((acc, key) => {
@@ -299,10 +225,16 @@ function useFieldValidation(params) {
299
225
  timeout.clear();
300
226
  let result = null;
301
227
  let validationErrors = [];
302
- const nextState = getState(element);
228
+
229
+ // With no representative input the field carries no native constraint, so start from an
230
+ // all-valid native state and let the custom `validate` result below decide the outcome.
231
+ const nextState = element ? getState(element) : {
232
+ ...useCompositeListItem.DEFAULT_VALIDITY_STATE,
233
+ valid: true
234
+ };
303
235
  let defaultValidationMessage;
304
236
  const isValidatingOnChange = shouldValidateOnChange();
305
- if (element.validationMessage && !isValidatingOnChange) {
237
+ if (element && element.validationMessage && !isValidatingOnChange) {
306
238
  // not validating on change, if there is a `validationMessage` from
307
239
  // native validity, set errors and skip calling the custom validate fn
308
240
  defaultValidationMessage = element.validationMessage;
@@ -331,20 +263,20 @@ function useFieldValidation(params) {
331
263
  nextState.customError = true;
332
264
  if (Array.isArray(result)) {
333
265
  validationErrors = result;
334
- element.setCustomValidity(result.join('\n'));
266
+ element?.setCustomValidity(result.join('\n'));
335
267
  } else if (result) {
336
268
  validationErrors = [result];
337
- element.setCustomValidity(result);
269
+ element?.setCustomValidity(result);
338
270
  }
339
271
  } else if (isValidatingOnChange) {
340
272
  // validate function returned no errors, if validating on change
341
273
  // we need to clear the custom validity state
342
274
  clearCustomValidity(element, registeredInputs);
343
275
  nextState.customError = false;
344
- if (element.validationMessage) {
276
+ if (element && element.validationMessage) {
345
277
  defaultValidationMessage = element.validationMessage;
346
278
  validationErrors = [element.validationMessage];
347
- } else if (element.validity.valid && !nextState.valid) {
279
+ } else if ((!element || element.validity.valid) && !nextState.valid) {
348
280
  nextState.valid = true;
349
281
  }
350
282
  }
@@ -379,12 +311,85 @@ function useFieldValidation(params) {
379
311
  return React__namespace.useMemo(() => ({
380
312
  getValidationProps,
381
313
  inputRef,
314
+ registeredInputs,
382
315
  registerInput,
316
+ getInputControl,
383
317
  commit,
384
318
  change
385
- }), [getValidationProps, registerInput, commit, change]);
319
+ }), [getValidationProps, registeredInputs, registerInput, getInputControl, commit, change]);
386
320
  }
387
321
 
322
+ const FieldsetRootContext = /*#__PURE__*/React__namespace.createContext(undefined);
323
+ if (process.env.NODE_ENV !== "production") FieldsetRootContext.displayName = "FieldsetRootContext";
324
+ function useFieldsetRootContext(optional = false) {
325
+ const context = React__namespace.useContext(FieldsetRootContext);
326
+ if (!context && !optional) {
327
+ throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: FieldsetRootContext is missing. Fieldset parts must be placed within <Fieldset.Root>.' : useRenderElement.formatErrorMessage(86));
328
+ }
329
+ return context;
330
+ }
331
+
332
+ const LabelableProvider = function LabelableProvider(props) {
333
+ const defaultId = useScrollLock.useBaseUiId();
334
+ const initialControlId = props.controlId === undefined ? defaultId : props.controlId;
335
+ const [controlId, setControlIdState] = React__namespace.useState(initialControlId);
336
+ const [labelId, setLabelId] = React__namespace.useState(props.labelId);
337
+ const [messageIds, setMessageIds] = React__namespace.useState([]);
338
+ const registrationsRef = useRenderElement.useRefWithInit(() => new Map());
339
+ const {
340
+ messageIds: parentMessageIds
341
+ } = useCompositeListItem.useLabelableContext();
342
+ const registerControlId = useScrollLock.useStableCallback((source, nextId) => {
343
+ const registrations = registrationsRef.current;
344
+ if (nextId === undefined) {
345
+ registrations.delete(source);
346
+ return;
347
+ }
348
+ registrations.set(source, nextId);
349
+
350
+ // Only flush when registering, not when unregistering.
351
+ // This prevents loops during rapid unmount/remount cycles (e.g. React Activity).
352
+ // The next registration will pick up the correct state.
353
+ setControlIdState(prev => {
354
+ if (registrations.size === 0) {
355
+ return undefined;
356
+ }
357
+ let nextControlId;
358
+ for (const id of registrations.values()) {
359
+ if (prev !== undefined && id === prev) {
360
+ return prev;
361
+ }
362
+ if (nextControlId === undefined) {
363
+ nextControlId = id;
364
+ }
365
+ }
366
+ return nextControlId;
367
+ });
368
+ });
369
+ const getDescriptionProps = React__namespace.useCallback(externalProps => {
370
+ const ids = externalProps['aria-describedby'] ? externalProps['aria-describedby'].split(' ') : [];
371
+ ids.push(...parentMessageIds, ...messageIds);
372
+ return {
373
+ ...externalProps,
374
+ 'aria-describedby': Array.from(new Set(ids)).join(' ') || undefined
375
+ };
376
+ }, [parentMessageIds, messageIds]);
377
+ const contextValue = React__namespace.useMemo(() => ({
378
+ controlId,
379
+ registerControlId,
380
+ labelId,
381
+ setLabelId,
382
+ messageIds,
383
+ setMessageIds,
384
+ getDescriptionProps
385
+ }), [controlId, registerControlId, labelId, setLabelId, messageIds, setMessageIds, getDescriptionProps]);
386
+ return /*#__PURE__*/jsxRuntime.jsx(useCompositeListItem.LabelableContext.Provider, {
387
+ value: contextValue,
388
+ children: props.children
389
+ });
390
+ };
391
+ if (process.env.NODE_ENV !== "production") LabelableProvider.displayName = "LabelableProvider";
392
+
388
393
  function useFieldControlRegistration(params) {
389
394
  const {
390
395
  commit,
@@ -392,7 +397,7 @@ function useFieldControlRegistration(params) {
392
397
  markedDirtyRef,
393
398
  name,
394
399
  setRegisteredFieldName,
395
- setRegisteredFieldId,
400
+ registeredFieldIdRef,
396
401
  setValidityData,
397
402
  validityData
398
403
  } = params;
@@ -401,7 +406,7 @@ function useFieldControlRegistration(params) {
401
406
  } = useCompositeListItem.useFormContext();
402
407
  const activeFieldControlSourceRef = React__namespace.useRef(null);
403
408
  const registrationRef = React__namespace.useRef(null);
404
- const fallbackControlRef = React__namespace.useRef(null);
409
+ const initialValueCapturedRef = React__namespace.useRef(false);
405
410
  const getValueForForm = useScrollLock.useStableCallback(() => {
406
411
  const registration = registrationRef.current;
407
412
  if (!registration) {
@@ -432,7 +437,7 @@ function useFieldControlRegistration(params) {
432
437
  formRef.current.fields.set(registration.id, {
433
438
  getValue: getValueForForm,
434
439
  name: name ?? registration.name,
435
- controlRef: registration.controlRef ?? fallbackControlRef,
440
+ controlRef: registration.controlRef,
436
441
  validityData: getCombinedFieldValidityData(validityData, invalid),
437
442
  validate
438
443
  });
@@ -442,18 +447,23 @@ function useFieldControlRegistration(params) {
442
447
  formRef.current.fields.delete(id);
443
448
  }
444
449
  }
445
- function syncInitialValue() {
446
- const registration = registrationRef.current;
447
- if (!registration) {
450
+
451
+ // The baseline belongs to the field, not to a control instance: registration re-runs on every
452
+ // value change, and a control that unmounts and remounts (or is swapped for another one) comes
453
+ // back as a brand new registration. Capturing more than once would turn whichever value the
454
+ // control happens to hold at that point into the initial value, so a modified field would read
455
+ // pristine and its real initial value would read dirty. Consumers that want a fresh baseline
456
+ // remount or key `<Field.Root>` itself.
457
+ function captureInitialValue(registration) {
458
+ if (initialValueCapturedRef.current) {
448
459
  return;
449
460
  }
461
+ initialValueCapturedRef.current = true;
450
462
  const initialValue = getRegistrationValue(registration);
451
- if (validityData.initialValue === null && initialValue !== null) {
452
- setValidityData(prev => ({
453
- ...prev,
454
- initialValue
455
- }));
456
- }
463
+ setValidityData(prev => prev.initialValue === initialValue ? prev : {
464
+ ...prev,
465
+ initialValue
466
+ });
457
467
  }
458
468
  useScrollLock.useIsoLayoutEffect(() => {
459
469
  const registration = registrationRef.current;
@@ -464,7 +474,7 @@ function useFieldControlRegistration(params) {
464
474
  formRef.current.fields.set(registration.id, {
465
475
  getValue: getValueForForm,
466
476
  name: name ?? registration.name,
467
- controlRef: registration.controlRef ?? fallbackControlRef,
477
+ controlRef: registration.controlRef,
468
478
  validityData: getCombinedFieldValidityData(validityData, invalid),
469
479
  validate
470
480
  });
@@ -485,7 +495,7 @@ function useFieldControlRegistration(params) {
485
495
  deleteRegistration();
486
496
  registrationRef.current = null;
487
497
  setRegisteredFieldName(undefined);
488
- setRegisteredFieldId(undefined);
498
+ registeredFieldIdRef.current = undefined;
489
499
  }
490
500
  return;
491
501
  }
@@ -495,11 +505,11 @@ function useFieldControlRegistration(params) {
495
505
  if (!name) {
496
506
  setRegisteredFieldName(registration.name);
497
507
  }
498
- setRegisteredFieldId(registration.id);
508
+ registeredFieldIdRef.current = registration.id;
499
509
  if (previousId && previousId !== registration.id) {
500
510
  deleteRegistration(previousId);
501
511
  }
502
- syncInitialValue();
512
+ captureInitialValue(registration);
503
513
  refreshRegistration();
504
514
  });
505
515
  return [validate, register];
@@ -544,10 +554,6 @@ const FieldRootInner = /*#__PURE__*/React__namespace.forwardRef(function FieldRo
544
554
  markedDirtyRef.current = dirtyProp;
545
555
  }
546
556
  }, [dirtyProp]);
547
- const getRegisteredFieldId = React__namespace.useCallback(() => registeredFieldIdRef.current, []);
548
- const setRegisteredFieldId = React__namespace.useCallback(id => {
549
- registeredFieldIdRef.current = id;
550
- }, []);
551
557
  const setDirty = useScrollLock.useStableCallback(value => {
552
558
  if (dirtyProp !== undefined) {
553
559
  return;
@@ -574,7 +580,11 @@ const FieldRootInner = /*#__PURE__*/React__namespace.forwardRef(function FieldRo
574
580
  value: null,
575
581
  initialValue: null
576
582
  });
577
- const valid = disabled ? null : !invalid && validityData.state.valid;
583
+
584
+ // App-controlled invalidity (the `invalid` prop and `<Form>` errors) keeps the field marked
585
+ // invalid even while disabled. Only computed validity (native constraints and `validate`)
586
+ // is suppressed when disabled, matching `:disabled` not participating in constraint validation.
587
+ const valid = !invalid && (disabled ? null : validityData.state.valid);
578
588
  const state = React__namespace.useMemo(() => ({
579
589
  disabled,
580
590
  touched,
@@ -592,7 +602,7 @@ const FieldRootInner = /*#__PURE__*/React__namespace.forwardRef(function FieldRo
592
602
  markedDirtyRef,
593
603
  state,
594
604
  shouldValidateOnChange,
595
- getRegisteredFieldId
605
+ registeredFieldIdRef
596
606
  });
597
607
  const [validateFieldControl, registerFieldControl] = useFieldControlRegistration({
598
608
  commit: validation.commit,
@@ -600,7 +610,7 @@ const FieldRootInner = /*#__PURE__*/React__namespace.forwardRef(function FieldRo
600
610
  markedDirtyRef,
601
611
  name,
602
612
  setRegisteredFieldName,
603
- setRegisteredFieldId,
613
+ registeredFieldIdRef,
604
614
  setValidityData,
605
615
  validityData
606
616
  });
@@ -613,23 +623,16 @@ const FieldRootInner = /*#__PURE__*/React__namespace.forwardRef(function FieldRo
613
623
  validityData,
614
624
  setValidityData,
615
625
  disabled,
616
- touched,
617
626
  setTouched,
618
- dirty,
619
627
  setDirty,
620
- filled,
621
628
  setFilled,
622
- focused,
623
629
  setFocused,
624
- validate,
625
630
  validationMode,
626
- validationDebounceTime,
627
631
  shouldValidateOnChange,
628
632
  state,
629
- markedDirtyRef,
630
633
  registerFieldControl,
631
634
  validation
632
- }), [invalid, effectiveName, validityData, disabled, touched, setTouched, dirty, setDirty, filled, setFilled, focused, setFocused, validate, validationMode, validationDebounceTime, shouldValidateOnChange, state, registerFieldControl, validation]);
635
+ }), [invalid, effectiveName, validityData, disabled, setTouched, setDirty, setFilled, setFocused, validationMode, shouldValidateOnChange, state, registerFieldControl, validation]);
633
636
  const element = useRenderElement.useRenderElement('div', componentProps, {
634
637
  ref: forwardedRef,
635
638
  state,
@@ -786,13 +789,13 @@ const FieldError = /*#__PURE__*/React__namespace.forwardRef(function FieldError(
786
789
  } else if (validityData.errors.length > 1) {
787
790
  error = validityData.errors;
788
791
  }
789
- let errorMessage = error ?? '';
792
+ let errorMessage = error;
790
793
  if (Array.isArray(error)) {
791
794
  errorMessage = error.length > 1 ? /*#__PURE__*/jsxRuntime.jsx("ul", {
792
795
  children: error.map(message => /*#__PURE__*/jsxRuntime.jsx("li", {
793
796
  children: message
794
797
  }, message))
795
- }) : error[0] ?? '';
798
+ }) : error[0];
796
799
  }
797
800
  const errorKey = Array.isArray(error) ? JSON.stringify(error) : error;
798
801
  if (rendered && errorKey !== lastRenderedMessageKey) {