@tamagui/field 0.0.0-bootstrap.0 → 3.0.0-beta.643.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 (79) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cjs/Field.cjs +857 -0
  3. package/dist/cjs/Field.native.cjs +1018 -0
  4. package/dist/cjs/Field.native.js +1020 -0
  5. package/dist/cjs/Field.native.js.map +1 -0
  6. package/dist/cjs/FieldContext.cjs +86 -0
  7. package/dist/cjs/FieldContext.native.cjs +90 -0
  8. package/dist/cjs/FieldContext.native.js +92 -0
  9. package/dist/cjs/FieldContext.native.js.map +1 -0
  10. package/dist/cjs/index.cjs +21 -0
  11. package/dist/cjs/index.native.cjs +23 -0
  12. package/dist/cjs/index.native.js +25 -0
  13. package/dist/cjs/index.native.js.map +1 -0
  14. package/dist/cjs/types.cjs +17 -0
  15. package/dist/cjs/types.native.cjs +19 -0
  16. package/dist/cjs/types.native.js +21 -0
  17. package/dist/cjs/types.native.js.map +1 -0
  18. package/dist/cjs/validation.cjs +114 -0
  19. package/dist/cjs/validation.native.cjs +126 -0
  20. package/dist/cjs/validation.native.js +128 -0
  21. package/dist/cjs/validation.native.js.map +1 -0
  22. package/dist/esm/Field.mjs +824 -0
  23. package/dist/esm/Field.mjs.map +1 -0
  24. package/dist/esm/Field.native.js +983 -0
  25. package/dist/esm/Field.native.js.map +1 -0
  26. package/dist/esm/FieldContext.mjs +51 -0
  27. package/dist/esm/FieldContext.mjs.map +1 -0
  28. package/dist/esm/FieldContext.native.js +53 -0
  29. package/dist/esm/FieldContext.native.js.map +1 -0
  30. package/dist/esm/index.js +5 -0
  31. package/dist/esm/index.mjs +5 -0
  32. package/dist/esm/index.native.js +5 -0
  33. package/dist/esm/types.mjs +0 -0
  34. package/dist/esm/types.native.js +0 -0
  35. package/dist/esm/validation.mjs +86 -0
  36. package/dist/esm/validation.mjs.map +1 -0
  37. package/dist/esm/validation.native.js +96 -0
  38. package/dist/esm/validation.native.js.map +1 -0
  39. package/dist/jsx/Field.mjs +824 -0
  40. package/dist/jsx/Field.mjs.map +1 -0
  41. package/dist/jsx/Field.native.cjs +1018 -0
  42. package/dist/jsx/Field.native.js +1020 -0
  43. package/dist/jsx/Field.native.js.map +1 -0
  44. package/dist/jsx/FieldContext.mjs +51 -0
  45. package/dist/jsx/FieldContext.mjs.map +1 -0
  46. package/dist/jsx/FieldContext.native.cjs +90 -0
  47. package/dist/jsx/FieldContext.native.js +92 -0
  48. package/dist/jsx/FieldContext.native.js.map +1 -0
  49. package/dist/jsx/index.js +5 -0
  50. package/dist/jsx/index.mjs +5 -0
  51. package/dist/jsx/index.native.cjs +23 -0
  52. package/dist/jsx/index.native.js +25 -0
  53. package/dist/jsx/index.native.js.map +1 -0
  54. package/dist/jsx/types.mjs +0 -0
  55. package/dist/jsx/types.native.cjs +19 -0
  56. package/dist/jsx/types.native.js +21 -0
  57. package/dist/jsx/types.native.js.map +1 -0
  58. package/dist/jsx/validation.mjs +86 -0
  59. package/dist/jsx/validation.mjs.map +1 -0
  60. package/dist/jsx/validation.native.cjs +126 -0
  61. package/dist/jsx/validation.native.js +128 -0
  62. package/dist/jsx/validation.native.js.map +1 -0
  63. package/package.json +50 -5
  64. package/src/Field.tsx +1129 -0
  65. package/src/FieldContext.tsx +61 -0
  66. package/src/index.ts +3 -0
  67. package/src/types.ts +125 -0
  68. package/src/validation.ts +136 -0
  69. package/types/Field.d.ts +401 -0
  70. package/types/Field.d.ts.map +1 -0
  71. package/types/FieldContext.d.ts +11 -0
  72. package/types/FieldContext.d.ts.map +1 -0
  73. package/types/index.d.ts +4 -0
  74. package/types/index.d.ts.map +1 -0
  75. package/types/types.d.ts +112 -0
  76. package/types/types.d.ts.map +1 -0
  77. package/types/validation.d.ts +24 -0
  78. package/types/validation.d.ts.map +1 -0
  79. package/README.md +0 -1
package/src/Field.tsx ADDED
@@ -0,0 +1,1129 @@
1
+ import type { GetProps } from '@tamagui/core'
2
+ import {
3
+ createStyledHOC,
4
+ isWeb,
5
+ styled,
6
+ Text,
7
+ useIsomorphicLayoutEffect,
8
+ View,
9
+ withStaticProperties,
10
+ } from '@tamagui/core'
11
+ import type { FormFieldRegistration } from '@tamagui/form'
12
+ import { useFormRegistryContext } from '@tamagui/form'
13
+ import { focusFocusable } from '@tamagui/focusable'
14
+ import * as React from 'react'
15
+ import {
16
+ FieldControlContext,
17
+ FieldStateContext,
18
+ FieldStyledContext,
19
+ useFieldControl,
20
+ useFieldState,
21
+ } from './FieldContext'
22
+ import type {
23
+ FieldControlContextValue,
24
+ FieldControlRegistration,
25
+ FieldDataProps,
26
+ FieldState,
27
+ FieldStyleState,
28
+ FieldValidationMode,
29
+ FieldValidator,
30
+ FieldValidityData,
31
+ FieldValidityState,
32
+ } from './types'
33
+ import {
34
+ createDefaultValidityState,
35
+ createValidationCommitter,
36
+ getValidationResult,
37
+ isFilled,
38
+ normalizeNativeValidity,
39
+ shouldValidateOnChange,
40
+ } from './validation'
41
+
42
+ const fieldStateVariants = {
43
+ valid: { true: {} },
44
+ invalid: { true: {} },
45
+ touched: { true: {} },
46
+ dirty: { true: {} },
47
+ filled: { true: {} },
48
+ focused: { true: {} },
49
+ disabled: { true: {} },
50
+ } as const
51
+
52
+ export const FieldFrame = styled(View, {
53
+ displayName: 'Field',
54
+ context: FieldStyledContext,
55
+ variants: fieldStateVariants,
56
+ })
57
+
58
+ export const FieldLabelFrame = styled(Text, {
59
+ displayName: 'FieldLabel',
60
+ render: 'label',
61
+ context: FieldStyledContext,
62
+ variants: fieldStateVariants,
63
+ })
64
+
65
+ export const FieldDescriptionFrame = styled(Text, {
66
+ displayName: 'FieldDescription',
67
+ render: 'p',
68
+ context: FieldStyledContext,
69
+ variants: fieldStateVariants,
70
+ })
71
+
72
+ export const FieldErrorFrame = styled(Text, {
73
+ displayName: 'FieldError',
74
+ render: 'span',
75
+ context: FieldStyledContext,
76
+ variants: fieldStateVariants,
77
+ })
78
+
79
+ export const FieldItemFrame = styled(View, {
80
+ displayName: 'FieldItem',
81
+ context: FieldStyledContext,
82
+ variants: fieldStateVariants,
83
+ })
84
+
85
+ const validityKeys: Array<Exclude<keyof FieldValidityState, 'valid'>> = [
86
+ 'badInput',
87
+ 'customError',
88
+ 'patternMismatch',
89
+ 'rangeOverflow',
90
+ 'rangeUnderflow',
91
+ 'stepMismatch',
92
+ 'tooLong',
93
+ 'tooShort',
94
+ 'typeMismatch',
95
+ 'valueMissing',
96
+ ]
97
+
98
+ type AssociationContextValue = {
99
+ controlId: string
100
+ labelId?: string
101
+ labelText?: string
102
+ messageIds: string[]
103
+ messageTexts: string[]
104
+ registerControlId: (id: string) => () => void
105
+ registerLabel: (id: string, text?: string) => () => void
106
+ registerMessage: (id: string, text?: string) => () => void
107
+ }
108
+
109
+ const AssociationContext = React.createContext<AssociationContextValue | null>(null)
110
+ const FieldRootContext = React.createContext<FieldRootContextValue | null>(null)
111
+ const FieldItemContext = React.createContext(false)
112
+
113
+ type FieldRootContextValue = {
114
+ state: FieldState
115
+ validityData: FieldValidityData
116
+ name?: string
117
+ rootDisabled: boolean
118
+ registerControl: (
119
+ registration: FieldControlRegistration,
120
+ reportDisabled?: boolean
121
+ ) => () => void
122
+ onFocus: () => void
123
+ onBlur: (value?: unknown) => void
124
+ onChange: (value: unknown) => void
125
+ onDisabledChange: (disabled: boolean) => void
126
+ }
127
+
128
+ function getText(children: React.ReactNode): string | undefined {
129
+ const text: string[] = []
130
+
131
+ React.Children.forEach(children, (child) => {
132
+ if (typeof child === 'string' || typeof child === 'number') {
133
+ text.push(String(child))
134
+ return
135
+ }
136
+ if (React.isValidElement<{ children?: React.ReactNode }>(child)) {
137
+ const nested = getText(child.props.children)
138
+ if (nested) {
139
+ text.push(nested)
140
+ }
141
+ }
142
+ })
143
+
144
+ return text.join(' ').trim() || undefined
145
+ }
146
+
147
+ function useAssociation(prefix: string, parent?: AssociationContextValue | null) {
148
+ const defaultControlId = `${prefix}-control`
149
+ const controlIdsRef = React.useRef(new Map<symbol, string>())
150
+ const labelsRef = React.useRef(new Map<symbol, { id: string; text?: string }>())
151
+ const messagesRef = React.useRef(new Map<symbol, { id: string; text?: string }>())
152
+ const [, update] = React.useReducer((value) => value + 1, 0)
153
+
154
+ const registerControlId = React.useCallback((id: string) => {
155
+ const source = Symbol()
156
+ controlIdsRef.current.set(source, id)
157
+ update()
158
+ return () => {
159
+ controlIdsRef.current.delete(source)
160
+ update()
161
+ }
162
+ }, [])
163
+
164
+ const registerLabel = React.useCallback((id: string, text?: string) => {
165
+ const source = Symbol()
166
+ labelsRef.current.set(source, { id, text })
167
+ update()
168
+ return () => {
169
+ labelsRef.current.delete(source)
170
+ update()
171
+ }
172
+ }, [])
173
+
174
+ const registerMessage = React.useCallback((id: string, text?: string) => {
175
+ const source = Symbol()
176
+ messagesRef.current.set(source, { id, text })
177
+ update()
178
+ return () => {
179
+ messagesRef.current.delete(source)
180
+ update()
181
+ }
182
+ }, [])
183
+
184
+ const controlId = Array.from(controlIdsRef.current.values()).at(-1) ?? defaultControlId
185
+ const label = Array.from(labelsRef.current.values()).at(-1)
186
+ const messages = Array.from(messagesRef.current.values())
187
+
188
+ return React.useMemo<AssociationContextValue>(
189
+ () => ({
190
+ controlId,
191
+ labelId: [parent?.labelId, label?.id].filter(Boolean).join(' ') || undefined,
192
+ labelText: [parent?.labelText, label?.text].filter(Boolean).join(' ') || undefined,
193
+ messageIds: Array.from(
194
+ new Set([...(parent?.messageIds ?? []), ...messages.map((message) => message.id)])
195
+ ),
196
+ messageTexts: [
197
+ ...(parent?.messageTexts ?? []),
198
+ ...messages.flatMap((message) => (message.text ? [message.text] : [])),
199
+ ],
200
+ registerControlId,
201
+ registerLabel,
202
+ registerMessage,
203
+ }),
204
+ [
205
+ controlId,
206
+ label?.id,
207
+ label?.text,
208
+ messages,
209
+ parent?.labelId,
210
+ parent?.labelText,
211
+ parent?.messageIds,
212
+ parent?.messageTexts,
213
+ registerControlId,
214
+ registerLabel,
215
+ registerMessage,
216
+ ]
217
+ )
218
+ }
219
+
220
+ function getStyleState(state: FieldState, disabled = state.disabled): FieldStyleState {
221
+ return {
222
+ valid: state.valid === true,
223
+ invalid: state.valid === false,
224
+ touched: state.touched,
225
+ dirty: state.dirty,
226
+ filled: state.filled,
227
+ focused: state.focused,
228
+ disabled,
229
+ }
230
+ }
231
+
232
+ function getDataProps(state: FieldState, disabled = state.disabled): FieldDataProps {
233
+ if (!isWeb) {
234
+ return {}
235
+ }
236
+
237
+ return {
238
+ 'data-valid': state.valid === true ? '' : undefined,
239
+ 'data-invalid': state.valid === false ? '' : undefined,
240
+ 'data-touched': state.touched ? '' : undefined,
241
+ 'data-dirty': state.dirty ? '' : undefined,
242
+ 'data-filled': state.filled ? '' : undefined,
243
+ 'data-focused': state.focused ? '' : undefined,
244
+ 'data-disabled': disabled ? '' : undefined,
245
+ }
246
+ }
247
+
248
+ function useControlContext(
249
+ root: FieldRootContextValue,
250
+ association: AssociationContextValue,
251
+ itemDisabled = false,
252
+ isItem = false
253
+ ) {
254
+ const disabled = root.state.disabled || itemDisabled
255
+ const controlIdRef = React.useRef(association.controlId)
256
+ const registrationDisabledRef = React.useRef(root.rootDisabled || itemDisabled)
257
+ controlIdRef.current = association.controlId
258
+ registrationDisabledRef.current = root.rootDisabled || itemDisabled
259
+ const registerControl = React.useCallback(
260
+ (registration: FieldControlRegistration) => {
261
+ const id = registration.id ?? controlIdRef.current
262
+ const unregisterId = association.registerControlId(id)
263
+ const nextRegistration = {
264
+ ...registration,
265
+ id,
266
+ get disabled() {
267
+ return registrationDisabledRef.current || registration.disabled
268
+ },
269
+ }
270
+ const unregisterControl = root.registerControl(nextRegistration, !isItem)
271
+
272
+ return () => {
273
+ unregisterControl()
274
+ unregisterId()
275
+ }
276
+ },
277
+ [association.registerControlId, isItem, root.registerControl]
278
+ )
279
+
280
+ return React.useMemo<FieldControlContextValue>(
281
+ () => ({
282
+ name: root.name,
283
+ disabled,
284
+ ariaProps: {
285
+ id: association.controlId,
286
+ ...(isWeb
287
+ ? {
288
+ 'aria-labelledby': association.labelId,
289
+ 'aria-describedby': association.messageIds.join(' ') || undefined,
290
+ 'aria-invalid': root.state.valid === false ? true : undefined,
291
+ }
292
+ : {
293
+ accessibilityLabel: association.labelText,
294
+ accessibilityHint: association.messageTexts.join(' ') || undefined,
295
+ accessibilityState: disabled ? { disabled: true } : undefined,
296
+ }),
297
+ },
298
+ dataProps: getDataProps(root.state, disabled),
299
+ onFocus: root.onFocus,
300
+ onBlur: root.onBlur,
301
+ onChange: root.onChange,
302
+ onDisabledChange: isItem ? () => {} : root.onDisabledChange,
303
+ registerControl,
304
+ }),
305
+ [
306
+ association.controlId,
307
+ association.labelId,
308
+ association.labelText,
309
+ association.messageIds,
310
+ association.messageTexts,
311
+ disabled,
312
+ isItem,
313
+ registerControl,
314
+ root.name,
315
+ root.onBlur,
316
+ root.onChange,
317
+ root.onDisabledChange,
318
+ root.onFocus,
319
+ root.state,
320
+ ]
321
+ )
322
+ }
323
+
324
+ function getRegistrationValue(registration?: FieldControlRegistration) {
325
+ return registration?.getValue ? registration.getValue() : registration?.value
326
+ }
327
+
328
+ function getInput(registration: FieldControlRegistration) {
329
+ return registration.inputRef?.current
330
+ }
331
+
332
+ function canValidateInput(input: any): input is {
333
+ disabled?: boolean
334
+ validity: ValidityState
335
+ validationMessage: string
336
+ setCustomValidity: (message: string) => void
337
+ } {
338
+ return Boolean(
339
+ input &&
340
+ input.validity &&
341
+ typeof input.setCustomValidity === 'function' &&
342
+ !input.disabled
343
+ )
344
+ }
345
+
346
+ function isEligibleInput(input: any, formElement: HTMLFormElement | null) {
347
+ if (input.matches?.(':disabled')) {
348
+ return false
349
+ }
350
+ if (!formElement || input.form === formElement) {
351
+ return true
352
+ }
353
+ return input.form === null && !input.hasAttribute?.('form')
354
+ }
355
+
356
+ function isEligibleRegistration(
357
+ registration: FieldControlRegistration,
358
+ formElement: HTMLFormElement | null
359
+ ) {
360
+ if (registration.disabled) {
361
+ return false
362
+ }
363
+ const input = getInput(registration)
364
+ return !canValidateInput(input) || isEligibleInput(input, formElement)
365
+ }
366
+
367
+ function getNativeValidity(input: any, showValueMissing: boolean) {
368
+ const state = createDefaultValidityState(true)
369
+
370
+ for (const key of validityKeys) {
371
+ state[key] = Boolean(input.validity[key])
372
+ }
373
+ state.valid = Boolean(input.validity.valid)
374
+
375
+ return normalizeNativeValidity(state, showValueMissing)
376
+ }
377
+
378
+ function hasValidityError(state: FieldValidityState) {
379
+ return state.valid === false
380
+ }
381
+
382
+ function hasError(errors: string | string[] | undefined) {
383
+ return Array.isArray(errors) ? errors.length > 0 : Boolean(errors)
384
+ }
385
+
386
+ type FieldExtraProps = {
387
+ name?: string
388
+ disabled?: boolean
389
+ invalid?: boolean
390
+ validate?: FieldValidator
391
+ validationMode?: FieldValidationMode
392
+ validationDebounceTime?: number
393
+ }
394
+
395
+ export type FieldProps = Omit<GetProps<typeof FieldFrame>, keyof FieldExtraProps> &
396
+ FieldExtraProps
397
+
398
+ const FieldComponent = createStyledHOC(
399
+ FieldFrame,
400
+ function Field(
401
+ {
402
+ name: nameProp,
403
+ disabled: disabledProp = false,
404
+ invalid = false,
405
+ validate,
406
+ validationMode: validationModeProp,
407
+ validationDebounceTime = 0,
408
+ children,
409
+ ...fieldProps
410
+ }: FieldProps,
411
+ forwardedRef
412
+ ) {
413
+ const form = useFormRegistryContext()
414
+ const {
415
+ clearErrors: clearFormErrors,
416
+ errors: formErrors,
417
+ formElementRef,
418
+ getValues: getFormValues,
419
+ registerField: registerFormField,
420
+ submitAttemptedRef,
421
+ validationMode: formValidationMode,
422
+ } = form
423
+ const reactId = React.useId().replace(/:/g, '')
424
+ const association = useAssociation(`field-${reactId}`)
425
+ const fieldId = `field-${reactId}`
426
+ const controlsRef = React.useRef(new Map<symbol, FieldControlRegistration>())
427
+ const reportsDisabledRef = React.useRef(
428
+ new WeakMap<FieldControlRegistration, boolean>()
429
+ )
430
+ const activeControlRef = React.useRef<FieldControlRegistration | undefined>(undefined)
431
+ const activeValidationControlRef = React.useRef<FieldControlRegistration | undefined>(
432
+ undefined
433
+ )
434
+ const formControlRef = React.useRef<any>(null)
435
+ const formValidityDataRef = React.useRef({
436
+ state: {
437
+ valid: null as boolean | null,
438
+ },
439
+ })
440
+ const initialValueRef = React.useRef<unknown>(undefined)
441
+ const hasInitialValueRef = React.useRef(false)
442
+ const valueRef = React.useRef<unknown>(undefined)
443
+ const dirtyRef = React.useRef(false)
444
+ const debounceRef = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
445
+ const [registeredName, setRegisteredName] = React.useState<string>()
446
+ const [controlDisabled, setControlDisabled] = React.useState(false)
447
+ const [touched, setTouched] = React.useState(false)
448
+ const [dirty, setDirty] = React.useState(false)
449
+ const [filled, setFilled] = React.useState(false)
450
+ const [focused, setFocused] = React.useState(false)
451
+ const [currentValue, setCurrentValue] = React.useState<unknown>(undefined)
452
+ const [validityData, setValidityData] = React.useState<FieldValidityData>({
453
+ state: createDefaultValidityState(),
454
+ error: '',
455
+ errors: [],
456
+ value: undefined,
457
+ initialValue: undefined,
458
+ })
459
+ const validityDataRef = React.useRef(validityData)
460
+ validityDataRef.current = validityData
461
+ const effectiveName = nameProp ?? registeredName
462
+ const effectiveNameRef = React.useRef(effectiveName)
463
+ effectiveNameRef.current = effectiveName
464
+ const disabled = disabledProp || controlDisabled
465
+ const disabledRef = React.useRef(disabled)
466
+ disabledRef.current = disabled
467
+ const validationMode = validationModeProp ?? formValidationMode
468
+ const externalError =
469
+ effectiveName && Object.hasOwn(formErrors, effectiveName)
470
+ ? formErrors[effectiveName]
471
+ : undefined
472
+ const externalErrors = Array.isArray(externalError)
473
+ ? externalError
474
+ : externalError
475
+ ? [externalError]
476
+ : []
477
+ const externallyInvalid = invalid || hasError(externalError)
478
+ const externallyInvalidRef = React.useRef(externallyInvalid)
479
+ externallyInvalidRef.current = externallyInvalid
480
+ const valid = externallyInvalid ? false : disabled ? null : validityData.state.valid
481
+ formValidityDataRef.current.state.valid = disabled
482
+ ? null
483
+ : externallyInvalid
484
+ ? false
485
+ : validityData.state.valid
486
+ const errors = externalErrors.length ? externalErrors : validityData.errors
487
+ const error = errors[0] ?? ''
488
+
489
+ const state = React.useMemo<FieldState>(
490
+ () => ({
491
+ name: effectiveName,
492
+ value: currentValue,
493
+ error,
494
+ errors,
495
+ validity: validityData.state,
496
+ valid,
497
+ touched,
498
+ dirty,
499
+ filled,
500
+ focused,
501
+ disabled,
502
+ }),
503
+ [
504
+ dirty,
505
+ disabled,
506
+ currentValue,
507
+ effectiveName,
508
+ error,
509
+ errors,
510
+ filled,
511
+ focused,
512
+ touched,
513
+ valid,
514
+ validityData.state,
515
+ ]
516
+ )
517
+ const stateRef = React.useRef(state)
518
+ stateRef.current = state
519
+
520
+ const publishValidityRef = React.useRef<(data: FieldValidityData) => void>(() => {})
521
+ const validationCommitterRef = React.useRef<
522
+ ReturnType<typeof createValidationCommitter<FieldValidityData>> | undefined
523
+ >(undefined)
524
+ if (!validationCommitterRef.current) {
525
+ validationCommitterRef.current = createValidationCommitter((data) => {
526
+ publishValidityRef.current(data)
527
+ })
528
+ }
529
+
530
+ const getActiveControl = React.useCallback(() => {
531
+ let active: FieldControlRegistration | undefined
532
+ for (const registration of controlsRef.current.values()) {
533
+ if (!isEligibleRegistration(registration, formElementRef.current)) {
534
+ continue
535
+ }
536
+ active = registration
537
+ }
538
+ return active
539
+ }, [formElementRef])
540
+
541
+ const syncControlDisabled = React.useCallback(() => {
542
+ let nextDisabled = false
543
+ for (const registration of controlsRef.current.values()) {
544
+ if (reportsDisabledRef.current.get(registration)) {
545
+ nextDisabled = Boolean(registration.disabled)
546
+ }
547
+ }
548
+ setControlDisabled(nextDisabled)
549
+ }, [])
550
+
551
+ const getRepresentativeControl = React.useCallback(() => {
552
+ let fallback: FieldControlRegistration | undefined
553
+
554
+ for (const registration of controlsRef.current.values()) {
555
+ const input = getInput(registration)
556
+ if (!isEligibleRegistration(registration, formElementRef.current)) {
557
+ continue
558
+ }
559
+ fallback ??= registration
560
+ if (canValidateInput(input) && !input.validity.valid) {
561
+ return registration
562
+ }
563
+ }
564
+
565
+ return fallback ?? getActiveControl()
566
+ }, [formElementRef, getActiveControl])
567
+
568
+ const clearCustomValidity = React.useCallback(() => {
569
+ for (const registration of controlsRef.current.values()) {
570
+ const input = getInput(registration)
571
+ if (canValidateInput(input)) {
572
+ input.setCustomValidity('')
573
+ }
574
+ }
575
+ }, [])
576
+
577
+ const setCustomValidity = React.useCallback(
578
+ (message: string) => {
579
+ for (const registration of controlsRef.current.values()) {
580
+ if (!isEligibleRegistration(registration, formElementRef.current)) {
581
+ continue
582
+ }
583
+ const input = getInput(registration)
584
+ if (canValidateInput(input)) {
585
+ input.setCustomValidity(message)
586
+ }
587
+ }
588
+ },
589
+ [formElementRef]
590
+ )
591
+
592
+ const publishValidity = React.useCallback(
593
+ (data: FieldValidityData) => {
594
+ if (data.state.customError) {
595
+ setCustomValidity(data.errors.join('\n'))
596
+ } else {
597
+ clearCustomValidity()
598
+ }
599
+ formValidityDataRef.current.state.valid = disabledRef.current
600
+ ? null
601
+ : externallyInvalidRef.current
602
+ ? false
603
+ : data.state.valid
604
+ validityDataRef.current = data
605
+ setCurrentValue(data.value)
606
+ setValidityData(data)
607
+ },
608
+ [clearCustomValidity, setCustomValidity]
609
+ )
610
+ publishValidityRef.current = publishValidity
611
+
612
+ const validateValue = React.useCallback(
613
+ (value: unknown, submit = false, fromChange = false) => {
614
+ if (debounceRef.current) {
615
+ clearTimeout(debounceRef.current)
616
+ debounceRef.current = undefined
617
+ }
618
+
619
+ const showValueMissing = dirtyRef.current || submit || submitAttemptedRef.current
620
+
621
+ if (fromChange) {
622
+ clearCustomValidity()
623
+ }
624
+
625
+ const representative = getRepresentativeControl()
626
+ activeValidationControlRef.current = representative
627
+ formControlRef.current = representative?.controlRef.current ?? null
628
+ const input = representative && getInput(representative)
629
+ let nativeState = createDefaultValidityState(true)
630
+ let nativeMessage = ''
631
+
632
+ if (canValidateInput(input)) {
633
+ nativeState = getNativeValidity(input, showValueMissing)
634
+ nativeMessage = nativeState.valid === false ? input.validationMessage : ''
635
+ } else {
636
+ const required = Array.from(controlsRef.current.values()).some(
637
+ (registration) =>
638
+ registration.required &&
639
+ isEligibleRegistration(registration, formElementRef.current)
640
+ )
641
+ if (required && !isFilled(value) && showValueMissing) {
642
+ nativeState = {
643
+ ...createDefaultValidityState(false),
644
+ valueMissing: true,
645
+ }
646
+ nativeMessage = 'Please fill out this field.'
647
+ }
648
+ }
649
+
650
+ const initialValue = initialValueRef.current
651
+ const makeValidityData = (customErrors: string[]): FieldValidityData => {
652
+ if (customErrors.length) {
653
+ return {
654
+ state: {
655
+ ...nativeState,
656
+ valid: false,
657
+ customError: true,
658
+ },
659
+ error: customErrors[0] ?? '',
660
+ errors: customErrors,
661
+ value,
662
+ initialValue,
663
+ }
664
+ }
665
+
666
+ if (hasValidityError(nativeState)) {
667
+ return {
668
+ state: nativeState,
669
+ error: nativeMessage,
670
+ errors: nativeMessage ? [nativeMessage] : [],
671
+ value,
672
+ initialValue,
673
+ }
674
+ }
675
+
676
+ return {
677
+ state: createDefaultValidityState(true),
678
+ error: '',
679
+ errors: [],
680
+ value,
681
+ initialValue,
682
+ }
683
+ }
684
+
685
+ if (nativeMessage && !fromChange) {
686
+ validationCommitterRef.current!.run(makeValidityData([]))
687
+ return
688
+ }
689
+
690
+ const result = validate
691
+ ? getValidationResult(validate, value, getFormValues())
692
+ : []
693
+
694
+ if (result instanceof Promise) {
695
+ void validationCommitterRef.current!.run(
696
+ result.then((customErrors) => makeValidityData(customErrors))
697
+ )
698
+ } else {
699
+ validationCommitterRef.current!.run(makeValidityData(result))
700
+ }
701
+ },
702
+ [
703
+ clearCustomValidity,
704
+ formElementRef,
705
+ getFormValues,
706
+ getRepresentativeControl,
707
+ submitAttemptedRef,
708
+ validate,
709
+ ]
710
+ )
711
+
712
+ const registerControl = React.useCallback(
713
+ (registration: FieldControlRegistration, reportDisabled = true) => {
714
+ const source = Symbol()
715
+ controlsRef.current.set(source, registration)
716
+ reportsDisabledRef.current.set(registration, reportDisabled)
717
+ activeControlRef.current = getActiveControl()
718
+ formControlRef.current = activeControlRef.current?.controlRef.current ?? null
719
+ const value = getRegistrationValue(registration)
720
+ valueRef.current = value
721
+ setCurrentValue(value)
722
+
723
+ if (!hasInitialValueRef.current) {
724
+ hasInitialValueRef.current = true
725
+ initialValueRef.current = value
726
+ validityDataRef.current = {
727
+ ...validityDataRef.current,
728
+ initialValue: value,
729
+ }
730
+ setValidityData(validityDataRef.current)
731
+ }
732
+
733
+ if (!nameProp && registration.name) {
734
+ setRegisteredName(registration.name)
735
+ }
736
+ setFilled(isFilled(value))
737
+ syncControlDisabled()
738
+
739
+ return () => {
740
+ controlsRef.current.delete(source)
741
+ activeControlRef.current = getActiveControl()
742
+ activeValidationControlRef.current = undefined
743
+ formControlRef.current = activeControlRef.current?.controlRef.current ?? null
744
+ const next = activeControlRef.current
745
+ const nextValue = getRegistrationValue(next)
746
+ valueRef.current = nextValue
747
+ setCurrentValue(nextValue)
748
+ setFilled(isFilled(nextValue))
749
+ if (!nameProp) {
750
+ setRegisteredName(next?.name)
751
+ }
752
+ syncControlDisabled()
753
+ }
754
+ },
755
+ [getActiveControl, nameProp, syncControlDisabled]
756
+ )
757
+
758
+ const onFocus = React.useCallback(() => {
759
+ setFocused(true)
760
+ }, [])
761
+
762
+ const onBlur = React.useCallback(
763
+ (value = valueRef.current) => {
764
+ valueRef.current = value
765
+ setCurrentValue(value)
766
+ setTouched(true)
767
+ setFocused(false)
768
+ if (validationMode === 'onBlur') {
769
+ validateValue(value)
770
+ }
771
+ },
772
+ [validateValue, validationMode]
773
+ )
774
+
775
+ const onChange = React.useCallback(
776
+ (value: unknown) => {
777
+ valueRef.current = value
778
+ setCurrentValue(value)
779
+ validationCommitterRef.current!.invalidate()
780
+ if (debounceRef.current) {
781
+ clearTimeout(debounceRef.current)
782
+ debounceRef.current = undefined
783
+ }
784
+ const nextDirty = !Object.is(value, initialValueRef.current)
785
+ dirtyRef.current = nextDirty
786
+ setDirty(nextDirty)
787
+ setFilled(isFilled(value))
788
+ clearFormErrors(effectiveNameRef.current)
789
+
790
+ const validateOnChange = shouldValidateOnChange(
791
+ validationMode,
792
+ submitAttemptedRef.current
793
+ )
794
+
795
+ if (validateOnChange) {
796
+ if (
797
+ validationMode === 'onChange' &&
798
+ validationDebounceTime > 0 &&
799
+ value !== ''
800
+ ) {
801
+ debounceRef.current = setTimeout(() => {
802
+ validateValue(value, false, true)
803
+ }, validationDebounceTime)
804
+ } else {
805
+ validateValue(value, false, true)
806
+ }
807
+ return
808
+ }
809
+
810
+ if (stateRef.current.valid === false) {
811
+ const required = Array.from(controlsRef.current.values()).some(
812
+ (registration) =>
813
+ registration.required &&
814
+ isEligibleRegistration(registration, formElementRef.current)
815
+ )
816
+ if (required && !isFilled(value) && dirtyRef.current) {
817
+ validateValue(value)
818
+ return
819
+ }
820
+
821
+ clearCustomValidity()
822
+ publishValidity({
823
+ state: createDefaultValidityState(true),
824
+ error: '',
825
+ errors: [],
826
+ value,
827
+ initialValue: initialValueRef.current,
828
+ })
829
+ }
830
+ },
831
+ [
832
+ clearFormErrors,
833
+ clearCustomValidity,
834
+ formElementRef,
835
+ publishValidity,
836
+ submitAttemptedRef,
837
+ validateValue,
838
+ validationDebounceTime,
839
+ validationMode,
840
+ ]
841
+ )
842
+
843
+ const onDisabledChange = React.useCallback((nextDisabled: boolean) => {
844
+ setControlDisabled(nextDisabled)
845
+ }, [])
846
+
847
+ React.useEffect(() => {
848
+ return () => {
849
+ validationCommitterRef.current?.invalidate()
850
+ if (debounceRef.current) {
851
+ clearTimeout(debounceRef.current)
852
+ }
853
+ }
854
+ }, [])
855
+
856
+ const formRegistration = React.useMemo<FormFieldRegistration>(
857
+ () => ({
858
+ controlRef: formControlRef,
859
+ validityData: formValidityDataRef.current,
860
+ get name() {
861
+ return stateRef.current.disabled ? undefined : effectiveNameRef.current
862
+ },
863
+ get controlId() {
864
+ return (
865
+ activeValidationControlRef.current?.id ??
866
+ getRepresentativeControl()?.id ??
867
+ association.controlId
868
+ )
869
+ },
870
+ getValue() {
871
+ return getRegistrationValue(getActiveControl()) ?? valueRef.current
872
+ },
873
+ validate() {
874
+ validateValue(
875
+ getRegistrationValue(getActiveControl()) ?? valueRef.current,
876
+ true
877
+ )
878
+ },
879
+ }),
880
+ [association.controlId, getActiveControl, getRepresentativeControl, validateValue]
881
+ )
882
+
883
+ React.useEffect(() => {
884
+ return registerFormField(fieldId, formRegistration)
885
+ }, [fieldId, formRegistration, registerFormField])
886
+
887
+ const rootContext = React.useMemo<FieldRootContextValue>(
888
+ () => ({
889
+ state,
890
+ validityData,
891
+ name: effectiveName,
892
+ rootDisabled: disabledProp,
893
+ registerControl,
894
+ onFocus,
895
+ onBlur,
896
+ onChange,
897
+ onDisabledChange,
898
+ }),
899
+ [
900
+ disabledProp,
901
+ effectiveName,
902
+ onBlur,
903
+ onChange,
904
+ onDisabledChange,
905
+ onFocus,
906
+ registerControl,
907
+ state,
908
+ validityData,
909
+ ]
910
+ )
911
+ const controlContext = useControlContext(rootContext, association)
912
+ const styleState = getStyleState(state)
913
+
914
+ return (
915
+ <FieldStateContext.Provider value={state}>
916
+ <FieldRootContext.Provider value={rootContext}>
917
+ <AssociationContext.Provider value={association}>
918
+ <FieldControlContext.Provider value={controlContext}>
919
+ <FieldStyledContext.Provider {...styleState}>
920
+ <FieldFrame
921
+ {...fieldProps}
922
+ {...getDataProps(state)}
923
+ disabled={disabled}
924
+ ref={forwardedRef}
925
+ >
926
+ {children}
927
+ </FieldFrame>
928
+ </FieldStyledContext.Provider>
929
+ </FieldControlContext.Provider>
930
+ </AssociationContext.Provider>
931
+ </FieldRootContext.Provider>
932
+ </FieldStateContext.Provider>
933
+ )
934
+ }
935
+ )
936
+
937
+ function usePartContexts() {
938
+ const root = React.useContext(FieldRootContext)
939
+ const association = React.useContext(AssociationContext)
940
+ const itemDisabled = React.useContext(FieldItemContext)
941
+
942
+ if (!root || !association) {
943
+ throw new Error('Field parts must be rendered inside <Field>.')
944
+ }
945
+
946
+ return { root, association, itemDisabled }
947
+ }
948
+
949
+ export type FieldLabelProps = GetProps<typeof FieldLabelFrame>
950
+
951
+ const FieldLabel = createStyledHOC(
952
+ FieldLabelFrame,
953
+ function FieldLabel(
954
+ { id: idProp, children, onPress, onMouseDown, ...labelProps }: FieldLabelProps,
955
+ forwardedRef
956
+ ) {
957
+ const { root, association, itemDisabled } = usePartContexts()
958
+ const generatedId = React.useId().replace(/:/g, '')
959
+ const id = idProp ?? `field-label-${generatedId}`
960
+ const text = getText(children)
961
+ const disabled = root.state.disabled || itemDisabled
962
+
963
+ useIsomorphicLayoutEffect(() => {
964
+ return association.registerLabel(id, text)
965
+ }, [association.registerLabel, id, text])
966
+
967
+ return (
968
+ <FieldLabelFrame
969
+ {...labelProps}
970
+ {...getDataProps(root.state, disabled)}
971
+ id={id}
972
+ ref={forwardedRef}
973
+ {...(isWeb ? { htmlFor: association.controlId } : undefined)}
974
+ {...(!isWeb && disabled ? { accessibilityState: { disabled: true } } : undefined)}
975
+ onMouseDown={(event: any) => {
976
+ onMouseDown?.(event)
977
+ if (!event.defaultPrevented && event.detail > 1) {
978
+ event.preventDefault()
979
+ }
980
+ }}
981
+ onPress={(event: any) => {
982
+ onPress?.(event)
983
+ if (!isWeb && !event?.defaultPrevented) {
984
+ focusFocusable(association.controlId)
985
+ }
986
+ }}
987
+ >
988
+ {children}
989
+ </FieldLabelFrame>
990
+ )
991
+ }
992
+ )
993
+
994
+ export type FieldDescriptionProps = GetProps<typeof FieldDescriptionFrame>
995
+
996
+ const FieldDescription = createStyledHOC(
997
+ FieldDescriptionFrame,
998
+ function FieldDescription(
999
+ { id: idProp, children, ...descriptionProps }: FieldDescriptionProps,
1000
+ forwardedRef
1001
+ ) {
1002
+ const { root, association, itemDisabled } = usePartContexts()
1003
+ const generatedId = React.useId().replace(/:/g, '')
1004
+ const id = idProp ?? `field-description-${generatedId}`
1005
+ const text = getText(children)
1006
+ const disabled = root.state.disabled || itemDisabled
1007
+
1008
+ useIsomorphicLayoutEffect(() => {
1009
+ return association.registerMessage(id, text)
1010
+ }, [association.registerMessage, id, text])
1011
+
1012
+ return (
1013
+ <FieldDescriptionFrame
1014
+ {...descriptionProps}
1015
+ {...getDataProps(root.state, disabled)}
1016
+ id={id}
1017
+ ref={forwardedRef}
1018
+ >
1019
+ {children}
1020
+ </FieldDescriptionFrame>
1021
+ )
1022
+ }
1023
+ )
1024
+
1025
+ export type FieldErrorProps = GetProps<typeof FieldErrorFrame> & {
1026
+ match?: boolean | keyof FieldValidityState
1027
+ }
1028
+
1029
+ const FieldError = createStyledHOC(
1030
+ FieldErrorFrame,
1031
+ function FieldError(
1032
+ { id: idProp, children, match, ...errorProps }: FieldErrorProps,
1033
+ forwardedRef
1034
+ ) {
1035
+ const { root, association, itemDisabled } = usePartContexts()
1036
+ const generatedId = React.useId().replace(/:/g, '')
1037
+ const id = idProp ?? `field-error-${generatedId}`
1038
+ const disabled = root.state.disabled || itemDisabled
1039
+ const rendered =
1040
+ match === true ||
1041
+ (!disabled &&
1042
+ (typeof match === 'string'
1043
+ ? Boolean(root.validityData.state[match])
1044
+ : root.state.valid === false))
1045
+ const message = children ?? root.state.errors.join('\n')
1046
+ const text = getText(message)
1047
+
1048
+ useIsomorphicLayoutEffect(() => {
1049
+ if (!rendered) {
1050
+ return
1051
+ }
1052
+ return association.registerMessage(id, text)
1053
+ }, [association.registerMessage, id, rendered, text])
1054
+
1055
+ if (!rendered) {
1056
+ return null
1057
+ }
1058
+
1059
+ return (
1060
+ <FieldErrorFrame
1061
+ {...errorProps}
1062
+ {...getDataProps(root.state, disabled)}
1063
+ id={id}
1064
+ ref={forwardedRef}
1065
+ >
1066
+ {message}
1067
+ </FieldErrorFrame>
1068
+ )
1069
+ }
1070
+ )
1071
+
1072
+ export type FieldItemProps = GetProps<typeof FieldItemFrame> & {
1073
+ disabled?: boolean
1074
+ }
1075
+
1076
+ const FieldItem = createStyledHOC(
1077
+ FieldItemFrame,
1078
+ function FieldItem(
1079
+ { disabled: disabledProp = false, children, ...itemProps }: FieldItemProps,
1080
+ forwardedRef
1081
+ ) {
1082
+ const root = React.useContext(FieldRootContext)
1083
+ const parentAssociation = React.useContext(AssociationContext)
1084
+ const reactId = React.useId().replace(/:/g, '')
1085
+ const association = useAssociation(`field-item-${reactId}`, parentAssociation)
1086
+
1087
+ if (!root || !parentAssociation) {
1088
+ throw new Error('Field.Item must be rendered inside <Field>.')
1089
+ }
1090
+
1091
+ const disabled = root.state.disabled || disabledProp
1092
+ const controlContext = useControlContext(root, association, disabledProp, true)
1093
+ const styleState = getStyleState(root.state, disabled)
1094
+
1095
+ return (
1096
+ <FieldItemContext.Provider value={disabledProp}>
1097
+ <AssociationContext.Provider value={association}>
1098
+ <FieldControlContext.Provider value={controlContext}>
1099
+ <FieldStyledContext.Provider {...styleState}>
1100
+ <FieldItemFrame
1101
+ {...itemProps}
1102
+ {...getDataProps(root.state, disabled)}
1103
+ disabled={disabled}
1104
+ ref={forwardedRef}
1105
+ >
1106
+ {children}
1107
+ </FieldItemFrame>
1108
+ </FieldStyledContext.Provider>
1109
+ </FieldControlContext.Provider>
1110
+ </AssociationContext.Provider>
1111
+ </FieldItemContext.Provider>
1112
+ )
1113
+ }
1114
+ )
1115
+
1116
+ export const Field = withStaticProperties(FieldComponent, {
1117
+ Label: FieldLabel,
1118
+ Description: FieldDescription,
1119
+ Error: FieldError,
1120
+ Item: FieldItem,
1121
+ useFieldState,
1122
+ useFieldControl,
1123
+ })
1124
+
1125
+ export namespace Field {
1126
+ export type Props = FieldProps
1127
+ export type State = FieldState
1128
+ export type ValidityState = FieldValidityState
1129
+ }