@tamagui/field 0.0.0-bootstrap.0 → 3.0.0-beta.1093.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 +856 -0
  3. package/dist/cjs/Field.native.cjs +1010 -0
  4. package/dist/cjs/Field.native.js +1012 -0
  5. package/dist/cjs/Field.native.js.map +1 -0
  6. package/dist/cjs/FieldContext.cjs +85 -0
  7. package/dist/cjs/FieldContext.native.cjs +89 -0
  8. package/dist/cjs/FieldContext.native.js +91 -0
  9. package/dist/cjs/FieldContext.native.js.map +1 -0
  10. package/dist/cjs/index.cjs +20 -0
  11. package/dist/cjs/index.native.cjs +22 -0
  12. package/dist/cjs/index.native.js +24 -0
  13. package/dist/cjs/index.native.js.map +1 -0
  14. package/dist/cjs/types.cjs +16 -0
  15. package/dist/cjs/types.native.cjs +18 -0
  16. package/dist/cjs/types.native.js +20 -0
  17. package/dist/cjs/types.native.js.map +1 -0
  18. package/dist/cjs/validation.cjs +113 -0
  19. package/dist/cjs/validation.native.cjs +121 -0
  20. package/dist/cjs/validation.native.js +123 -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 +976 -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 +92 -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 +1010 -0
  42. package/dist/jsx/Field.native.js +1012 -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 +89 -0
  47. package/dist/jsx/FieldContext.native.js +91 -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 +22 -0
  52. package/dist/jsx/index.native.js +24 -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 +18 -0
  56. package/dist/jsx/types.native.js +20 -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 +121 -0
  61. package/dist/jsx/validation.native.js +123 -0
  62. package/dist/jsx/validation.native.js.map +1 -0
  63. package/package.json +51 -4
  64. package/src/Field.tsx +1127 -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,1127 @@
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 && effectiveName in formErrors ? formErrors[effectiveName] : undefined
470
+ const externalErrors = Array.isArray(externalError)
471
+ ? externalError
472
+ : externalError
473
+ ? [externalError]
474
+ : []
475
+ const externallyInvalid = invalid || hasError(externalError)
476
+ const externallyInvalidRef = React.useRef(externallyInvalid)
477
+ externallyInvalidRef.current = externallyInvalid
478
+ const valid = externallyInvalid ? false : disabled ? null : validityData.state.valid
479
+ formValidityDataRef.current.state.valid = disabled
480
+ ? null
481
+ : externallyInvalid
482
+ ? false
483
+ : validityData.state.valid
484
+ const errors = externalErrors.length ? externalErrors : validityData.errors
485
+ const error = errors[0] ?? ''
486
+
487
+ const state = React.useMemo<FieldState>(
488
+ () => ({
489
+ name: effectiveName,
490
+ value: currentValue,
491
+ error,
492
+ errors,
493
+ validity: validityData.state,
494
+ valid,
495
+ touched,
496
+ dirty,
497
+ filled,
498
+ focused,
499
+ disabled,
500
+ }),
501
+ [
502
+ dirty,
503
+ disabled,
504
+ currentValue,
505
+ effectiveName,
506
+ error,
507
+ errors,
508
+ filled,
509
+ focused,
510
+ touched,
511
+ valid,
512
+ validityData.state,
513
+ ]
514
+ )
515
+ const stateRef = React.useRef(state)
516
+ stateRef.current = state
517
+
518
+ const publishValidityRef = React.useRef<(data: FieldValidityData) => void>(() => {})
519
+ const validationCommitterRef = React.useRef<
520
+ ReturnType<typeof createValidationCommitter<FieldValidityData>> | undefined
521
+ >(undefined)
522
+ if (!validationCommitterRef.current) {
523
+ validationCommitterRef.current = createValidationCommitter((data) => {
524
+ publishValidityRef.current(data)
525
+ })
526
+ }
527
+
528
+ const getActiveControl = React.useCallback(() => {
529
+ let active: FieldControlRegistration | undefined
530
+ for (const registration of controlsRef.current.values()) {
531
+ if (!isEligibleRegistration(registration, formElementRef.current)) {
532
+ continue
533
+ }
534
+ active = registration
535
+ }
536
+ return active
537
+ }, [formElementRef])
538
+
539
+ const syncControlDisabled = React.useCallback(() => {
540
+ let nextDisabled = false
541
+ for (const registration of controlsRef.current.values()) {
542
+ if (reportsDisabledRef.current.get(registration)) {
543
+ nextDisabled = Boolean(registration.disabled)
544
+ }
545
+ }
546
+ setControlDisabled(nextDisabled)
547
+ }, [])
548
+
549
+ const getRepresentativeControl = React.useCallback(() => {
550
+ let fallback: FieldControlRegistration | undefined
551
+
552
+ for (const registration of controlsRef.current.values()) {
553
+ const input = getInput(registration)
554
+ if (!isEligibleRegistration(registration, formElementRef.current)) {
555
+ continue
556
+ }
557
+ fallback ??= registration
558
+ if (canValidateInput(input) && !input.validity.valid) {
559
+ return registration
560
+ }
561
+ }
562
+
563
+ return fallback ?? getActiveControl()
564
+ }, [formElementRef, getActiveControl])
565
+
566
+ const clearCustomValidity = React.useCallback(() => {
567
+ for (const registration of controlsRef.current.values()) {
568
+ const input = getInput(registration)
569
+ if (canValidateInput(input)) {
570
+ input.setCustomValidity('')
571
+ }
572
+ }
573
+ }, [])
574
+
575
+ const setCustomValidity = React.useCallback(
576
+ (message: string) => {
577
+ for (const registration of controlsRef.current.values()) {
578
+ if (!isEligibleRegistration(registration, formElementRef.current)) {
579
+ continue
580
+ }
581
+ const input = getInput(registration)
582
+ if (canValidateInput(input)) {
583
+ input.setCustomValidity(message)
584
+ }
585
+ }
586
+ },
587
+ [formElementRef]
588
+ )
589
+
590
+ const publishValidity = React.useCallback(
591
+ (data: FieldValidityData) => {
592
+ if (data.state.customError) {
593
+ setCustomValidity(data.errors.join('\n'))
594
+ } else {
595
+ clearCustomValidity()
596
+ }
597
+ formValidityDataRef.current.state.valid = disabledRef.current
598
+ ? null
599
+ : externallyInvalidRef.current
600
+ ? false
601
+ : data.state.valid
602
+ validityDataRef.current = data
603
+ setCurrentValue(data.value)
604
+ setValidityData(data)
605
+ },
606
+ [clearCustomValidity, setCustomValidity]
607
+ )
608
+ publishValidityRef.current = publishValidity
609
+
610
+ const validateValue = React.useCallback(
611
+ (value: unknown, submit = false, fromChange = false) => {
612
+ if (debounceRef.current) {
613
+ clearTimeout(debounceRef.current)
614
+ debounceRef.current = undefined
615
+ }
616
+
617
+ const showValueMissing = dirtyRef.current || submit || submitAttemptedRef.current
618
+
619
+ if (fromChange) {
620
+ clearCustomValidity()
621
+ }
622
+
623
+ const representative = getRepresentativeControl()
624
+ activeValidationControlRef.current = representative
625
+ formControlRef.current = representative?.controlRef.current ?? null
626
+ const input = representative && getInput(representative)
627
+ let nativeState = createDefaultValidityState(true)
628
+ let nativeMessage = ''
629
+
630
+ if (canValidateInput(input)) {
631
+ nativeState = getNativeValidity(input, showValueMissing)
632
+ nativeMessage = nativeState.valid === false ? input.validationMessage : ''
633
+ } else {
634
+ const required = Array.from(controlsRef.current.values()).some(
635
+ (registration) =>
636
+ registration.required &&
637
+ isEligibleRegistration(registration, formElementRef.current)
638
+ )
639
+ if (required && !isFilled(value) && showValueMissing) {
640
+ nativeState = {
641
+ ...createDefaultValidityState(false),
642
+ valueMissing: true,
643
+ }
644
+ nativeMessage = 'Please fill out this field.'
645
+ }
646
+ }
647
+
648
+ const initialValue = initialValueRef.current
649
+ const makeValidityData = (customErrors: string[]): FieldValidityData => {
650
+ if (customErrors.length) {
651
+ return {
652
+ state: {
653
+ ...nativeState,
654
+ valid: false,
655
+ customError: true,
656
+ },
657
+ error: customErrors[0] ?? '',
658
+ errors: customErrors,
659
+ value,
660
+ initialValue,
661
+ }
662
+ }
663
+
664
+ if (hasValidityError(nativeState)) {
665
+ return {
666
+ state: nativeState,
667
+ error: nativeMessage,
668
+ errors: nativeMessage ? [nativeMessage] : [],
669
+ value,
670
+ initialValue,
671
+ }
672
+ }
673
+
674
+ return {
675
+ state: createDefaultValidityState(true),
676
+ error: '',
677
+ errors: [],
678
+ value,
679
+ initialValue,
680
+ }
681
+ }
682
+
683
+ if (nativeMessage && !fromChange) {
684
+ validationCommitterRef.current!.run(makeValidityData([]))
685
+ return
686
+ }
687
+
688
+ const result = validate
689
+ ? getValidationResult(validate, value, getFormValues())
690
+ : []
691
+
692
+ if (result instanceof Promise) {
693
+ void validationCommitterRef.current!.run(
694
+ result.then((customErrors) => makeValidityData(customErrors))
695
+ )
696
+ } else {
697
+ validationCommitterRef.current!.run(makeValidityData(result))
698
+ }
699
+ },
700
+ [
701
+ clearCustomValidity,
702
+ formElementRef,
703
+ getFormValues,
704
+ getRepresentativeControl,
705
+ submitAttemptedRef,
706
+ validate,
707
+ ]
708
+ )
709
+
710
+ const registerControl = React.useCallback(
711
+ (registration: FieldControlRegistration, reportDisabled = true) => {
712
+ const source = Symbol()
713
+ controlsRef.current.set(source, registration)
714
+ reportsDisabledRef.current.set(registration, reportDisabled)
715
+ activeControlRef.current = getActiveControl()
716
+ formControlRef.current = activeControlRef.current?.controlRef.current ?? null
717
+ const value = getRegistrationValue(registration)
718
+ valueRef.current = value
719
+ setCurrentValue(value)
720
+
721
+ if (!hasInitialValueRef.current) {
722
+ hasInitialValueRef.current = true
723
+ initialValueRef.current = value
724
+ validityDataRef.current = {
725
+ ...validityDataRef.current,
726
+ initialValue: value,
727
+ }
728
+ setValidityData(validityDataRef.current)
729
+ }
730
+
731
+ if (!nameProp && registration.name) {
732
+ setRegisteredName(registration.name)
733
+ }
734
+ setFilled(isFilled(value))
735
+ syncControlDisabled()
736
+
737
+ return () => {
738
+ controlsRef.current.delete(source)
739
+ activeControlRef.current = getActiveControl()
740
+ activeValidationControlRef.current = undefined
741
+ formControlRef.current = activeControlRef.current?.controlRef.current ?? null
742
+ const next = activeControlRef.current
743
+ const nextValue = getRegistrationValue(next)
744
+ valueRef.current = nextValue
745
+ setCurrentValue(nextValue)
746
+ setFilled(isFilled(nextValue))
747
+ if (!nameProp) {
748
+ setRegisteredName(next?.name)
749
+ }
750
+ syncControlDisabled()
751
+ }
752
+ },
753
+ [getActiveControl, nameProp, syncControlDisabled]
754
+ )
755
+
756
+ const onFocus = React.useCallback(() => {
757
+ setFocused(true)
758
+ }, [])
759
+
760
+ const onBlur = React.useCallback(
761
+ (value = valueRef.current) => {
762
+ valueRef.current = value
763
+ setCurrentValue(value)
764
+ setTouched(true)
765
+ setFocused(false)
766
+ if (validationMode === 'onBlur') {
767
+ validateValue(value)
768
+ }
769
+ },
770
+ [validateValue, validationMode]
771
+ )
772
+
773
+ const onChange = React.useCallback(
774
+ (value: unknown) => {
775
+ valueRef.current = value
776
+ setCurrentValue(value)
777
+ validationCommitterRef.current!.invalidate()
778
+ if (debounceRef.current) {
779
+ clearTimeout(debounceRef.current)
780
+ debounceRef.current = undefined
781
+ }
782
+ const nextDirty = !Object.is(value, initialValueRef.current)
783
+ dirtyRef.current = nextDirty
784
+ setDirty(nextDirty)
785
+ setFilled(isFilled(value))
786
+ clearFormErrors(effectiveNameRef.current)
787
+
788
+ const validateOnChange = shouldValidateOnChange(
789
+ validationMode,
790
+ submitAttemptedRef.current
791
+ )
792
+
793
+ if (validateOnChange) {
794
+ if (
795
+ validationMode === 'onChange' &&
796
+ validationDebounceTime > 0 &&
797
+ value !== ''
798
+ ) {
799
+ debounceRef.current = setTimeout(() => {
800
+ validateValue(value, false, true)
801
+ }, validationDebounceTime)
802
+ } else {
803
+ validateValue(value, false, true)
804
+ }
805
+ return
806
+ }
807
+
808
+ if (stateRef.current.valid === false) {
809
+ const required = Array.from(controlsRef.current.values()).some(
810
+ (registration) =>
811
+ registration.required &&
812
+ isEligibleRegistration(registration, formElementRef.current)
813
+ )
814
+ if (required && !isFilled(value) && dirtyRef.current) {
815
+ validateValue(value)
816
+ return
817
+ }
818
+
819
+ clearCustomValidity()
820
+ publishValidity({
821
+ state: createDefaultValidityState(true),
822
+ error: '',
823
+ errors: [],
824
+ value,
825
+ initialValue: initialValueRef.current,
826
+ })
827
+ }
828
+ },
829
+ [
830
+ clearFormErrors,
831
+ clearCustomValidity,
832
+ formElementRef,
833
+ publishValidity,
834
+ submitAttemptedRef,
835
+ validateValue,
836
+ validationDebounceTime,
837
+ validationMode,
838
+ ]
839
+ )
840
+
841
+ const onDisabledChange = React.useCallback((nextDisabled: boolean) => {
842
+ setControlDisabled(nextDisabled)
843
+ }, [])
844
+
845
+ React.useEffect(() => {
846
+ return () => {
847
+ validationCommitterRef.current?.invalidate()
848
+ if (debounceRef.current) {
849
+ clearTimeout(debounceRef.current)
850
+ }
851
+ }
852
+ }, [])
853
+
854
+ const formRegistration = React.useMemo<FormFieldRegistration>(
855
+ () => ({
856
+ controlRef: formControlRef,
857
+ validityData: formValidityDataRef.current,
858
+ get name() {
859
+ return stateRef.current.disabled ? undefined : effectiveNameRef.current
860
+ },
861
+ get controlId() {
862
+ return (
863
+ activeValidationControlRef.current?.id ??
864
+ getRepresentativeControl()?.id ??
865
+ association.controlId
866
+ )
867
+ },
868
+ getValue() {
869
+ return getRegistrationValue(getActiveControl()) ?? valueRef.current
870
+ },
871
+ validate() {
872
+ validateValue(
873
+ getRegistrationValue(getActiveControl()) ?? valueRef.current,
874
+ true
875
+ )
876
+ },
877
+ }),
878
+ [association.controlId, getActiveControl, getRepresentativeControl, validateValue]
879
+ )
880
+
881
+ React.useEffect(() => {
882
+ return registerFormField(fieldId, formRegistration)
883
+ }, [fieldId, formRegistration, registerFormField])
884
+
885
+ const rootContext = React.useMemo<FieldRootContextValue>(
886
+ () => ({
887
+ state,
888
+ validityData,
889
+ name: effectiveName,
890
+ rootDisabled: disabledProp,
891
+ registerControl,
892
+ onFocus,
893
+ onBlur,
894
+ onChange,
895
+ onDisabledChange,
896
+ }),
897
+ [
898
+ disabledProp,
899
+ effectiveName,
900
+ onBlur,
901
+ onChange,
902
+ onDisabledChange,
903
+ onFocus,
904
+ registerControl,
905
+ state,
906
+ validityData,
907
+ ]
908
+ )
909
+ const controlContext = useControlContext(rootContext, association)
910
+ const styleState = getStyleState(state)
911
+
912
+ return (
913
+ <FieldStateContext.Provider value={state}>
914
+ <FieldRootContext.Provider value={rootContext}>
915
+ <AssociationContext.Provider value={association}>
916
+ <FieldControlContext.Provider value={controlContext}>
917
+ <FieldStyledContext.Provider {...styleState}>
918
+ <FieldFrame
919
+ {...fieldProps}
920
+ {...getDataProps(state)}
921
+ disabled={disabled}
922
+ ref={forwardedRef}
923
+ >
924
+ {children}
925
+ </FieldFrame>
926
+ </FieldStyledContext.Provider>
927
+ </FieldControlContext.Provider>
928
+ </AssociationContext.Provider>
929
+ </FieldRootContext.Provider>
930
+ </FieldStateContext.Provider>
931
+ )
932
+ }
933
+ )
934
+
935
+ function usePartContexts() {
936
+ const root = React.useContext(FieldRootContext)
937
+ const association = React.useContext(AssociationContext)
938
+ const itemDisabled = React.useContext(FieldItemContext)
939
+
940
+ if (!root || !association) {
941
+ throw new Error('Field parts must be rendered inside <Field>.')
942
+ }
943
+
944
+ return { root, association, itemDisabled }
945
+ }
946
+
947
+ export type FieldLabelProps = GetProps<typeof FieldLabelFrame>
948
+
949
+ const FieldLabel = createStyledHOC(
950
+ FieldLabelFrame,
951
+ function FieldLabel(
952
+ { id: idProp, children, onPress, onMouseDown, ...labelProps }: FieldLabelProps,
953
+ forwardedRef
954
+ ) {
955
+ const { root, association, itemDisabled } = usePartContexts()
956
+ const generatedId = React.useId().replace(/:/g, '')
957
+ const id = idProp ?? `field-label-${generatedId}`
958
+ const text = getText(children)
959
+ const disabled = root.state.disabled || itemDisabled
960
+
961
+ useIsomorphicLayoutEffect(() => {
962
+ return association.registerLabel(id, text)
963
+ }, [association.registerLabel, id, text])
964
+
965
+ return (
966
+ <FieldLabelFrame
967
+ {...labelProps}
968
+ {...getDataProps(root.state, disabled)}
969
+ id={id}
970
+ ref={forwardedRef}
971
+ {...(isWeb ? { htmlFor: association.controlId } : undefined)}
972
+ {...(!isWeb && disabled ? { accessibilityState: { disabled: true } } : undefined)}
973
+ onMouseDown={(event: any) => {
974
+ onMouseDown?.(event)
975
+ if (!event.defaultPrevented && event.detail > 1) {
976
+ event.preventDefault()
977
+ }
978
+ }}
979
+ onPress={(event: any) => {
980
+ onPress?.(event)
981
+ if (!isWeb && !event?.defaultPrevented) {
982
+ focusFocusable(association.controlId)
983
+ }
984
+ }}
985
+ >
986
+ {children}
987
+ </FieldLabelFrame>
988
+ )
989
+ }
990
+ )
991
+
992
+ export type FieldDescriptionProps = GetProps<typeof FieldDescriptionFrame>
993
+
994
+ const FieldDescription = createStyledHOC(
995
+ FieldDescriptionFrame,
996
+ function FieldDescription(
997
+ { id: idProp, children, ...descriptionProps }: FieldDescriptionProps,
998
+ forwardedRef
999
+ ) {
1000
+ const { root, association, itemDisabled } = usePartContexts()
1001
+ const generatedId = React.useId().replace(/:/g, '')
1002
+ const id = idProp ?? `field-description-${generatedId}`
1003
+ const text = getText(children)
1004
+ const disabled = root.state.disabled || itemDisabled
1005
+
1006
+ useIsomorphicLayoutEffect(() => {
1007
+ return association.registerMessage(id, text)
1008
+ }, [association.registerMessage, id, text])
1009
+
1010
+ return (
1011
+ <FieldDescriptionFrame
1012
+ {...descriptionProps}
1013
+ {...getDataProps(root.state, disabled)}
1014
+ id={id}
1015
+ ref={forwardedRef}
1016
+ >
1017
+ {children}
1018
+ </FieldDescriptionFrame>
1019
+ )
1020
+ }
1021
+ )
1022
+
1023
+ export type FieldErrorProps = GetProps<typeof FieldErrorFrame> & {
1024
+ match?: boolean | keyof FieldValidityState
1025
+ }
1026
+
1027
+ const FieldError = createStyledHOC(
1028
+ FieldErrorFrame,
1029
+ function FieldError(
1030
+ { id: idProp, children, match, ...errorProps }: FieldErrorProps,
1031
+ forwardedRef
1032
+ ) {
1033
+ const { root, association, itemDisabled } = usePartContexts()
1034
+ const generatedId = React.useId().replace(/:/g, '')
1035
+ const id = idProp ?? `field-error-${generatedId}`
1036
+ const disabled = root.state.disabled || itemDisabled
1037
+ const rendered =
1038
+ match === true ||
1039
+ (!disabled &&
1040
+ (typeof match === 'string'
1041
+ ? Boolean(root.validityData.state[match])
1042
+ : root.state.valid === false))
1043
+ const message = children ?? root.state.errors.join('\n')
1044
+ const text = getText(message)
1045
+
1046
+ useIsomorphicLayoutEffect(() => {
1047
+ if (!rendered) {
1048
+ return
1049
+ }
1050
+ return association.registerMessage(id, text)
1051
+ }, [association.registerMessage, id, rendered, text])
1052
+
1053
+ if (!rendered) {
1054
+ return null
1055
+ }
1056
+
1057
+ return (
1058
+ <FieldErrorFrame
1059
+ {...errorProps}
1060
+ {...getDataProps(root.state, disabled)}
1061
+ id={id}
1062
+ ref={forwardedRef}
1063
+ >
1064
+ {message}
1065
+ </FieldErrorFrame>
1066
+ )
1067
+ }
1068
+ )
1069
+
1070
+ export type FieldItemProps = GetProps<typeof FieldItemFrame> & {
1071
+ disabled?: boolean
1072
+ }
1073
+
1074
+ const FieldItem = createStyledHOC(
1075
+ FieldItemFrame,
1076
+ function FieldItem(
1077
+ { disabled: disabledProp = false, children, ...itemProps }: FieldItemProps,
1078
+ forwardedRef
1079
+ ) {
1080
+ const root = React.useContext(FieldRootContext)
1081
+ const parentAssociation = React.useContext(AssociationContext)
1082
+ const reactId = React.useId().replace(/:/g, '')
1083
+ const association = useAssociation(`field-item-${reactId}`, parentAssociation)
1084
+
1085
+ if (!root || !parentAssociation) {
1086
+ throw new Error('Field.Item must be rendered inside <Field>.')
1087
+ }
1088
+
1089
+ const disabled = root.state.disabled || disabledProp
1090
+ const controlContext = useControlContext(root, association, disabledProp, true)
1091
+ const styleState = getStyleState(root.state, disabled)
1092
+
1093
+ return (
1094
+ <FieldItemContext.Provider value={disabledProp}>
1095
+ <AssociationContext.Provider value={association}>
1096
+ <FieldControlContext.Provider value={controlContext}>
1097
+ <FieldStyledContext.Provider {...styleState}>
1098
+ <FieldItemFrame
1099
+ {...itemProps}
1100
+ {...getDataProps(root.state, disabled)}
1101
+ disabled={disabled}
1102
+ ref={forwardedRef}
1103
+ >
1104
+ {children}
1105
+ </FieldItemFrame>
1106
+ </FieldStyledContext.Provider>
1107
+ </FieldControlContext.Provider>
1108
+ </AssociationContext.Provider>
1109
+ </FieldItemContext.Provider>
1110
+ )
1111
+ }
1112
+ )
1113
+
1114
+ export const Field = withStaticProperties(FieldComponent, {
1115
+ Label: FieldLabel,
1116
+ Description: FieldDescription,
1117
+ Error: FieldError,
1118
+ Item: FieldItem,
1119
+ useFieldState,
1120
+ useFieldControl,
1121
+ })
1122
+
1123
+ export namespace Field {
1124
+ export type Props = FieldProps
1125
+ export type State = FieldState
1126
+ export type ValidityState = FieldValidityState
1127
+ }