@jobber/components 9.14.5 → 9.15.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.
@@ -29,6 +29,7 @@ require('../Content-cjs.js');
29
29
  require('../HelperText-cjs.js');
30
30
  require('../Text-cjs.js');
31
31
  require('../FieldDescription-cjs.js');
32
+ require('../useFieldValidation-cjs.js');
32
33
 
33
34
 
34
35
 
@@ -27,3 +27,4 @@ import '../Content-es.js';
27
27
  import '../HelperText-es.js';
28
28
  import '../Text-es.js';
29
29
  import '../FieldDescription-es.js';
30
+ import '../useFieldValidation-es.js';
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { bU as getGridNavigatedIndex, aO as isIndexOutOfListBounds, a7 as useDirection, v as useControlled, bV as Store, d as useStableCallback, U as useStore, w as useValueAsRef, s as useTransitionStatus, R as useOpenInteractionType, G as createGenericEventDetails, z as none, M as inputClear, O as inputChange, aE as getOverflowAncestors, i as isHTMLElement, a0 as contains, bu as escapeKey, h as createChangeEventDetails, Y as focusOut, Z as outsidePress, e as getTarget, ad as itemPress, l as useOpenChangeComplete, c as useIsoLayoutEffect, L as useValueChanged, V as useClick, bW as inputPress, W as useDismiss, F as FOCUSABLE_POPUP_PROPS, B as visuallyHiddenInput, C as visuallyHidden, P as keyboard, bX as pointer, _ as pressableTriggerOpenStateMapping, q as useTimeout, f as useButton, o as ownerDocument, aR as stopEvent, aS as listNavigation, a1 as cancelOpen, j as closePress, u as useBaseUiId, bg as android, bY as gecko, au as isElement, bv as isInteractiveElement, bZ as clearPress, t as transitionStatusMapping, be as triggerOpenStateMapping, x as ios, a2 as FloatingPortal, a6 as popupStateMapping, a3 as InternalBackdrop, a4 as inertValue, a5 as DROPDOWN_COLLISION_AVOIDANCE, n as FloatingFocusManager, S as SafeReact, r as error, b_ as chipRemovePress } from './useScrollLock-es.js';
2
+ import { bU as getGridNavigatedIndex, aO as isIndexOutOfListBounds, a7 as useDirection, v as useControlled, bV as Store, d as useStableCallback, U as useStore, w as useValueAsRef, m as useTransitionStatus, R as useOpenInteractionType, G as createGenericEventDetails, z as none, M as inputClear, O as inputChange, aE as getOverflowAncestors, i as isHTMLElement, a0 as contains, bu as escapeKey, j as createChangeEventDetails, Y as focusOut, Z as outsidePress, e as getTarget, ad as itemPress, n as useOpenChangeComplete, c as useIsoLayoutEffect, L as useValueChanged, V as useClick, bW as inputPress, W as useDismiss, F as FOCUSABLE_POPUP_PROPS, B as visuallyHiddenInput, C as visuallyHidden, P as keyboard, bX as pointer, _ as pressableTriggerOpenStateMapping, f as useTimeout, h as useButton, o as ownerDocument, aR as stopEvent, aS as listNavigation, a1 as cancelOpen, k as closePress, u as useBaseUiId, bg as android, bY as gecko, au as isElement, bv as isInteractiveElement, bZ as clearPress, t as transitionStatusMapping, be as triggerOpenStateMapping, x as ios, a2 as FloatingPortal, a6 as popupStateMapping, a3 as InternalBackdrop, a4 as inertValue, a5 as DROPDOWN_COLLISION_AVOIDANCE, s as FloatingFocusManager, S as SafeReact, l as error, b_ as chipRemovePress } from './useScrollLock-es.js';
3
3
  import { f as formatErrorMessage, c as EMPTY_ARRAY, a as useRefWithInit, E as EMPTY_OBJECT, N as NOOP, m as mergeProps, b as useMergedRefs, u as useRenderElement } from './useRenderElement-es.js';
4
4
  import { h as hasNullItemLabel, e as compareItemEquality, n as stringifyAsLabel, a as useFormContext, b as useFieldRootContext, c as useLabelableId, E as isGroupedItems, G as findSelectionIndex, s as stringifyAsValue, d as useRegisterFieldControl, H as selectedValueIncludes, B as removeItem, i as useFloatingRootContext, j as useListNavigation, l as useOnFirstRender, m as defaultItemEquality, o as areArraysEqual, f as fieldValidityMapping, u as useLabelableContext, k as useTypeahead, r as resolveAriaLabelledBy, p as isMouseWithinBounds, F as FieldRootContext, I as DEFAULT_FIELD_ROOT_CONTEXT, J as DEFAULT_FIELD_STATE_ATTRIBUTES, C as CompositeList, w as useAnchorPositioning, v as useAnchoredPopupScrollLock, x as usePositioner, z as getDisabledMountTransitionStyles, A as useCompositeListItem, g as findItemIndex, K as getDefaultLabelId, q as resolveMultipleLabels, t as resolveSelectedLabel } from './useCompositeListItem-es.js';
5
5
  import { jsx, jsxs } from 'react/jsx-runtime';
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import { u as useDialogRootContext, k as useDialogPortalContext, l as dialogStateAttributesMapping, m as useRenderDialogRoot, n as createNullDialogStore } from './ScrollAreaViewport-es.js';
3
3
  import { f as formatErrorMessage, u as useRenderElement } from './useRenderElement-es.js';
4
- import { A as AnimationFrame, h as createChangeEventDetails, k as imperativeAction, p as popupTransitionStateMapping, u as useBaseUiId, l as useOpenChangeComplete, m as createDefaultInitialFocus, F as FOCUSABLE_POPUP_PROPS, n as FloatingFocusManager } from './useScrollLock-es.js';
4
+ import { A as AnimationFrame, j as createChangeEventDetails, p as imperativeAction, q as popupTransitionStateMapping, u as useBaseUiId, n as useOpenChangeComplete, r as createDefaultInitialFocus, F as FOCUSABLE_POPUP_PROPS, s as FloatingFocusManager } from './useScrollLock-es.js';
5
5
  import { C as COMPOSITE_KEYS } from './scrollEdges-es.js';
6
6
  import { jsx } from 'react/jsx-runtime';
7
7
 
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import { u as useDialogRootContext } from './ScrollAreaViewport-es.js';
3
3
  import { u as useRenderElement } from './useRenderElement-es.js';
4
- import { f as useButton, h as createChangeEventDetails, j as closePress, u as useBaseUiId } from './useScrollLock-es.js';
4
+ import { h as useButton, j as createChangeEventDetails, k as closePress, u as useBaseUiId } from './useScrollLock-es.js';
5
5
 
6
6
  /**
7
7
  * A button that closes the dialog.
@@ -4,6 +4,7 @@ var React = require('react');
4
4
  var useScrollLock = require('./useScrollLock-cjs.js');
5
5
  var useCompositeListItem = require('./useCompositeListItem-cjs.js');
6
6
  var useRenderElement = require('./useRenderElement-cjs.js');
7
+ var useFieldValidation = require('./useFieldValidation-cjs.js');
7
8
  var jsxRuntime = require('react/jsx-runtime');
8
9
  var useLabel = require('./useLabel-cjs.js');
9
10
 
@@ -26,299 +27,6 @@ function _interopNamespaceDefault(e) {
26
27
 
27
28
  var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
28
29
 
29
- const FieldItemContext = /*#__PURE__*/React__namespace.createContext({
30
- disabled: false
31
- });
32
- if (process.env.NODE_ENV !== "production") FieldItemContext.displayName = "FieldItemContext";
33
- function useFieldItemContext() {
34
- const context = React__namespace.useContext(FieldItemContext);
35
- return context;
36
- }
37
-
38
- /**
39
- * Combines the field's client-side, stateful validity data with the external invalid state to
40
- * determine the field's true validity.
41
- */
42
- function getCombinedFieldValidityData(validityData, invalid) {
43
- return {
44
- ...validityData,
45
- state: {
46
- ...validityData.state,
47
- valid: !invalid && validityData.state.valid
48
- }
49
- };
50
- }
51
-
52
- const validityKeys = Object.keys(useCompositeListItem.DEFAULT_VALIDITY_STATE);
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')) {
62
- return false;
63
- }
64
- if (!formElement || input.form === formElement) {
65
- return true;
66
- }
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');
71
- }
72
-
73
- /**
74
- * Picks the input whose native validity should represent a field that owns several inputs (such as a
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.
77
- */
78
- function findRepresentativeInput(inputs, formElement) {
79
- let fallback = null;
80
- for (const input of inputs.keys()) {
81
- if (!isEligibleInput(input, formElement)) {
82
- continue;
83
- }
84
- if (!input.validity.valid) {
85
- return input;
86
- }
87
- fallback ??= input;
88
- }
89
- return fallback;
90
- }
91
- function clearCustomValidity(element, inputs) {
92
- for (const input of inputs.keys()) {
93
- input.setCustomValidity('');
94
- }
95
- element?.setCustomValidity('');
96
- }
97
- function useFieldValidation(params) {
98
- const {
99
- elementRef,
100
- formRef
101
- } = useCompositeListItem.useFormContext();
102
- const {
103
- setValidityData,
104
- validate,
105
- validityData,
106
- validationDebounceTime,
107
- invalid,
108
- markedDirtyRef,
109
- state,
110
- shouldValidateOnChange,
111
- registeredFieldIdRef
112
- } = params;
113
- const {
114
- controlId,
115
- getDescriptionProps
116
- } = useCompositeListItem.useLabelableContext();
117
- const timeout = useScrollLock.useTimeout();
118
- const inputRef = React__namespace.useRef(null);
119
- const registeredInputs = useRenderElement.useRefWithInit(() => new Map()).current;
120
- const validationCommitIdRef = React__namespace.useRef(0);
121
-
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);
127
- return () => {
128
- registeredInputs.delete(element);
129
- };
130
- }, [registeredInputs]);
131
- const getInputControl = useScrollLock.useStableCallback(() => {
132
- const element = findRepresentativeInput(registeredInputs, elementRef.current);
133
- return element && registeredInputs.get(element)?.controlRef.current || null;
134
- });
135
- const commit = useScrollLock.useStableCallback(async (value, revalidate = false) => {
136
- validationCommitIdRef.current += 1;
137
- const validationCommitId = validationCommitIdRef.current;
138
- function updateRegisteredFieldValidity(nextValidityData, externalInvalid = invalid) {
139
- const fieldId = registeredFieldIdRef.current ?? controlId;
140
- if (fieldId == null) {
141
- return;
142
- }
143
- const currentFieldData = formRef.current.fields.get(fieldId);
144
- if (!currentFieldData) {
145
- return;
146
- }
147
- const validityDataWithFormErrors = getCombinedFieldValidityData(nextValidityData, externalInvalid);
148
- formRef.current.fields.set(fieldId, {
149
- ...currentFieldData,
150
- validityData: validityDataWithFormErrors
151
- });
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
-
176
- if (revalidate) {
177
- if (state.valid !== false || !element) {
178
- return;
179
- }
180
- const currentNativeValidity = element.validity;
181
- if (!currentNativeValidity.valueMissing) {
182
- // The 'valueMissing' (required) condition has been resolved by the user typing.
183
- // Temporarily mark the field as valid for this onChange event.
184
- // Other native errors (e.g., typeMismatch) will be caught by full validation on blur or submit.
185
- // The required value is now present; ignore stale external invalid state for this pass.
186
- publishAllValid(element, false);
187
- return;
188
- }
189
-
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
- }
195
- }
196
-
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.
199
- }
200
- function getState(el) {
201
- const computedState = validityKeys.reduce((acc, key) => {
202
- acc[key] = el.validity[key];
203
- return acc;
204
- }, {});
205
- let hasOnlyValueMissingError = false;
206
- for (const key of validityKeys) {
207
- if (key === 'valid') {
208
- continue;
209
- }
210
- if (key === 'valueMissing' && computedState[key]) {
211
- hasOnlyValueMissingError = true;
212
- } else if (computedState[key]) {
213
- return computedState;
214
- }
215
- }
216
-
217
- // Only make `valueMissing` mark the field invalid if it's been changed
218
- // to reduce error noise.
219
- if (hasOnlyValueMissingError && !markedDirtyRef.current) {
220
- computedState.valid = true;
221
- computedState.valueMissing = false;
222
- }
223
- return computedState;
224
- }
225
- timeout.clear();
226
- let result = null;
227
- let validationErrors = [];
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
- };
235
- let defaultValidationMessage;
236
- const isValidatingOnChange = shouldValidateOnChange();
237
- if (element && element.validationMessage && !isValidatingOnChange) {
238
- // not validating on change, if there is a `validationMessage` from
239
- // native validity, set errors and skip calling the custom validate fn
240
- defaultValidationMessage = element.validationMessage;
241
- validationErrors = [element.validationMessage];
242
- } else {
243
- // call the validate function because either
244
- // - validating on change, or
245
- // - native constraint validations passed, custom validity check is next
246
- const formValues = Array.from(formRef.current.fields.values()).reduce((acc, field) => {
247
- if (field.name) {
248
- acc[field.name] = field.getValue();
249
- }
250
- return acc;
251
- }, {});
252
- const resultOrPromise = validate(value, formValues);
253
- if (typeof resultOrPromise === 'object' && resultOrPromise !== null && 'then' in resultOrPromise) {
254
- result = await resultOrPromise;
255
- if (validationCommitId !== validationCommitIdRef.current) {
256
- return;
257
- }
258
- } else {
259
- result = resultOrPromise;
260
- }
261
- if (result !== null) {
262
- nextState.valid = false;
263
- nextState.customError = true;
264
- if (Array.isArray(result)) {
265
- validationErrors = result;
266
- element?.setCustomValidity(result.join('\n'));
267
- } else if (result) {
268
- validationErrors = [result];
269
- element?.setCustomValidity(result);
270
- }
271
- } else if (isValidatingOnChange) {
272
- // validate function returned no errors, if validating on change
273
- // we need to clear the custom validity state
274
- clearCustomValidity(element, registeredInputs);
275
- nextState.customError = false;
276
- if (element && element.validationMessage) {
277
- defaultValidationMessage = element.validationMessage;
278
- validationErrors = [element.validationMessage];
279
- } else if ((!element || element.validity.valid) && !nextState.valid) {
280
- nextState.valid = true;
281
- }
282
- }
283
- }
284
- const nextValidityData = {
285
- value,
286
- state: nextState,
287
- error: defaultValidationMessage ?? (Array.isArray(result) ? result[0] : result ?? ''),
288
- errors: validationErrors,
289
- initialValue: validityData.initialValue
290
- };
291
-
292
- // Keep Form-level errors part of overall field validity for submit blocking/focus logic.
293
- updateRegisteredFieldValidity(nextValidityData);
294
- setValidityData(nextValidityData);
295
- });
296
- const change = useScrollLock.useStableCallback(value => {
297
- timeout.clear();
298
- const validateOnChange = shouldValidateOnChange();
299
- if (validateOnChange && value !== '' && validationDebounceTime) {
300
- validationCommitIdRef.current += 1;
301
- timeout.start(validationDebounceTime, () => {
302
- commit(value);
303
- });
304
- } else {
305
- commit(value, !validateOnChange);
306
- }
307
- });
308
- const getValidationProps = React__namespace.useCallback((disabled, externalProps = {}) => useRenderElement.mergeProps(getDescriptionProps(externalProps), state.valid === false && !state.disabled && !disabled ? {
309
- 'aria-invalid': true
310
- } : useRenderElement.EMPTY_OBJECT), [getDescriptionProps, state.disabled, state.valid]);
311
- return React__namespace.useMemo(() => ({
312
- getValidationProps,
313
- inputRef,
314
- registeredInputs,
315
- registerInput,
316
- getInputControl,
317
- commit,
318
- change
319
- }), [getValidationProps, registeredInputs, registerInput, getInputControl, commit, change]);
320
- }
321
-
322
30
  const FieldsetRootContext = /*#__PURE__*/React__namespace.createContext(undefined);
323
31
  if (process.env.NODE_ENV !== "production") FieldsetRootContext.displayName = "FieldsetRootContext";
324
32
  function useFieldsetRootContext(optional = false) {
@@ -438,7 +146,7 @@ function useFieldControlRegistration(params) {
438
146
  getValue: getValueForForm,
439
147
  name: name ?? registration.name,
440
148
  controlRef: registration.controlRef,
441
- validityData: getCombinedFieldValidityData(validityData, invalid),
149
+ validityData: useFieldValidation.getCombinedFieldValidityData(validityData, invalid),
442
150
  validate
443
151
  });
444
152
  }
@@ -475,7 +183,7 @@ function useFieldControlRegistration(params) {
475
183
  getValue: getValueForForm,
476
184
  name: name ?? registration.name,
477
185
  controlRef: registration.controlRef,
478
- validityData: getCombinedFieldValidityData(validityData, invalid),
186
+ validityData: useFieldValidation.getCombinedFieldValidityData(validityData, invalid),
479
187
  validate
480
188
  });
481
189
  }, [formRef, getValueForForm, invalid, name, setRegisteredFieldName, validate, validityData]);
@@ -593,7 +301,7 @@ const FieldRootInner = /*#__PURE__*/React__namespace.forwardRef(function FieldRo
593
301
  filled,
594
302
  focused
595
303
  }), [disabled, touched, dirty, valid, filled, focused]);
596
- const validation = useFieldValidation({
304
+ const validation = useFieldValidation.useFieldValidation({
597
305
  setValidityData,
598
306
  validate,
599
307
  validityData,
@@ -678,7 +386,7 @@ const FieldLabel = /*#__PURE__*/React__namespace.forwardRef(function FieldLabel(
678
386
  ...elementProps
679
387
  } = componentProps;
680
388
  const fieldRootContext = useCompositeListItem.useFieldRootContext(false);
681
- const fieldItemContext = useFieldItemContext();
389
+ const fieldItemContext = useFieldValidation.useFieldItemContext();
682
390
  const {
683
391
  labelId
684
392
  } = useCompositeListItem.useLabelableContext();
@@ -848,7 +556,7 @@ const FieldDescription = /*#__PURE__*/React__namespace.forwardRef(function Field
848
556
  } = componentProps;
849
557
  const id = useScrollLock.useBaseUiId(idProp);
850
558
  const fieldRootContext = useCompositeListItem.useFieldRootContext(false);
851
- const fieldItemContext = useFieldItemContext();
559
+ const fieldItemContext = useFieldValidation.useFieldItemContext();
852
560
  const {
853
561
  setMessageIds
854
562
  } = useCompositeListItem.useLabelableContext();