@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.
- package/LICENSE +21 -0
- package/dist/cjs/Field.cjs +857 -0
- package/dist/cjs/Field.native.cjs +1018 -0
- package/dist/cjs/Field.native.js +1020 -0
- package/dist/cjs/Field.native.js.map +1 -0
- package/dist/cjs/FieldContext.cjs +86 -0
- package/dist/cjs/FieldContext.native.cjs +90 -0
- package/dist/cjs/FieldContext.native.js +92 -0
- package/dist/cjs/FieldContext.native.js.map +1 -0
- package/dist/cjs/index.cjs +21 -0
- package/dist/cjs/index.native.cjs +23 -0
- package/dist/cjs/index.native.js +25 -0
- package/dist/cjs/index.native.js.map +1 -0
- package/dist/cjs/types.cjs +17 -0
- package/dist/cjs/types.native.cjs +19 -0
- package/dist/cjs/types.native.js +21 -0
- package/dist/cjs/types.native.js.map +1 -0
- package/dist/cjs/validation.cjs +114 -0
- package/dist/cjs/validation.native.cjs +126 -0
- package/dist/cjs/validation.native.js +128 -0
- package/dist/cjs/validation.native.js.map +1 -0
- package/dist/esm/Field.mjs +824 -0
- package/dist/esm/Field.mjs.map +1 -0
- package/dist/esm/Field.native.js +983 -0
- package/dist/esm/Field.native.js.map +1 -0
- package/dist/esm/FieldContext.mjs +51 -0
- package/dist/esm/FieldContext.mjs.map +1 -0
- package/dist/esm/FieldContext.native.js +53 -0
- package/dist/esm/FieldContext.native.js.map +1 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.mjs +5 -0
- package/dist/esm/index.native.js +5 -0
- package/dist/esm/types.mjs +0 -0
- package/dist/esm/types.native.js +0 -0
- package/dist/esm/validation.mjs +86 -0
- package/dist/esm/validation.mjs.map +1 -0
- package/dist/esm/validation.native.js +96 -0
- package/dist/esm/validation.native.js.map +1 -0
- package/dist/jsx/Field.mjs +824 -0
- package/dist/jsx/Field.mjs.map +1 -0
- package/dist/jsx/Field.native.cjs +1018 -0
- package/dist/jsx/Field.native.js +1020 -0
- package/dist/jsx/Field.native.js.map +1 -0
- package/dist/jsx/FieldContext.mjs +51 -0
- package/dist/jsx/FieldContext.mjs.map +1 -0
- package/dist/jsx/FieldContext.native.cjs +90 -0
- package/dist/jsx/FieldContext.native.js +92 -0
- package/dist/jsx/FieldContext.native.js.map +1 -0
- package/dist/jsx/index.js +5 -0
- package/dist/jsx/index.mjs +5 -0
- package/dist/jsx/index.native.cjs +23 -0
- package/dist/jsx/index.native.js +25 -0
- package/dist/jsx/index.native.js.map +1 -0
- package/dist/jsx/types.mjs +0 -0
- package/dist/jsx/types.native.cjs +19 -0
- package/dist/jsx/types.native.js +21 -0
- package/dist/jsx/types.native.js.map +1 -0
- package/dist/jsx/validation.mjs +86 -0
- package/dist/jsx/validation.mjs.map +1 -0
- package/dist/jsx/validation.native.cjs +126 -0
- package/dist/jsx/validation.native.js +128 -0
- package/dist/jsx/validation.native.js.map +1 -0
- package/package.json +50 -5
- package/src/Field.tsx +1129 -0
- package/src/FieldContext.tsx +61 -0
- package/src/index.ts +3 -0
- package/src/types.ts +125 -0
- package/src/validation.ts +136 -0
- package/types/Field.d.ts +401 -0
- package/types/Field.d.ts.map +1 -0
- package/types/FieldContext.d.ts +11 -0
- package/types/FieldContext.d.ts.map +1 -0
- package/types/index.d.ts +4 -0
- package/types/index.d.ts.map +1 -0
- package/types/types.d.ts +112 -0
- package/types/types.d.ts.map +1 -0
- package/types/validation.d.ts +24 -0
- package/types/validation.d.ts.map +1 -0
- 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
|
+
}
|