jfs-components 0.0.73 → 0.0.77
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/CHANGELOG.md +115 -6
- package/lib/commonjs/components/AccountCard/AccountCard.js +247 -0
- package/lib/commonjs/components/ActionFooter/ActionFooter.js +147 -82
- package/lib/commonjs/components/AppBar/AppBar.js +17 -11
- package/lib/commonjs/components/Avatar/Avatar.js +20 -0
- package/lib/commonjs/components/Badge/Badge.js +23 -0
- package/lib/commonjs/components/Button/Button.js +37 -0
- package/lib/commonjs/components/CardBankAccount/CardBankAccount.js +18 -2
- package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +40 -25
- package/lib/commonjs/components/Dropdown/Dropdown.js +214 -0
- package/lib/commonjs/components/DropdownInput/DropdownInput.js +542 -0
- package/lib/commonjs/components/FormField/FormField.js +328 -178
- package/lib/commonjs/components/IconButton/IconButton.js +20 -0
- package/lib/commonjs/components/Image/Image.js +26 -1
- package/lib/commonjs/components/LottieIntroBlock/LottieIntroBlock.js +150 -0
- package/lib/commonjs/components/LottiePlayer/LottiePlayer.js +116 -0
- package/lib/commonjs/components/LottiePlayer/LottiePlayer.web.js +82 -0
- package/lib/commonjs/components/LottiePlayer/loadNativeLottieView.js +74 -0
- package/lib/commonjs/components/LottiePlayer/loadWebLottieView.js +50 -0
- package/lib/commonjs/components/PageHero/PageHero.js +189 -0
- package/lib/commonjs/components/PoweredByLabel/PoweredByLabel.js +135 -0
- package/lib/commonjs/components/PoweredByLabel/finvu.png +0 -0
- package/lib/commonjs/components/RechargeCard/RechargeCard.js +32 -17
- package/lib/commonjs/components/Text/Text.js +40 -3
- package/lib/commonjs/components/Tooltip/Tooltip.js +34 -27
- package/lib/commonjs/components/index.js +67 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/icons/Icon.js +16 -0
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/commonjs/index.js +12 -0
- package/lib/commonjs/skeleton/Skeleton.js +234 -0
- package/lib/commonjs/skeleton/SkeletonGroup.js +140 -0
- package/lib/commonjs/skeleton/index.js +58 -0
- package/lib/commonjs/skeleton/shimmer-tokens.js +189 -0
- package/lib/commonjs/skeleton/useReducedMotion.js +64 -0
- package/lib/module/components/AccountCard/AccountCard.js +241 -0
- package/lib/module/components/ActionFooter/ActionFooter.js +146 -82
- package/lib/module/components/AppBar/AppBar.js +17 -11
- package/lib/module/components/Avatar/Avatar.js +19 -0
- package/lib/module/components/Badge/Badge.js +23 -0
- package/lib/module/components/Button/Button.js +37 -0
- package/lib/module/components/CardBankAccount/CardBankAccount.js +17 -2
- package/lib/module/components/CheckboxItem/CheckboxItem.js +41 -26
- package/lib/module/components/Dropdown/Dropdown.js +206 -0
- package/lib/module/components/DropdownInput/DropdownInput.js +536 -0
- package/lib/module/components/FormField/FormField.js +330 -180
- package/lib/module/components/IconButton/IconButton.js +20 -0
- package/lib/module/components/Image/Image.js +25 -1
- package/lib/module/components/LottieIntroBlock/LottieIntroBlock.js +144 -0
- package/lib/module/components/LottiePlayer/LottiePlayer.js +111 -0
- package/lib/module/components/LottiePlayer/LottiePlayer.web.js +77 -0
- package/lib/module/components/LottiePlayer/loadNativeLottieView.js +69 -0
- package/lib/module/components/LottiePlayer/loadWebLottieView.js +45 -0
- package/lib/module/components/PageHero/PageHero.js +183 -0
- package/lib/module/components/PoweredByLabel/PoweredByLabel.js +130 -0
- package/lib/module/components/PoweredByLabel/finvu.png +0 -0
- package/lib/module/components/RechargeCard/RechargeCard.js +33 -17
- package/lib/module/components/Text/Text.js +40 -3
- package/lib/module/components/Tooltip/Tooltip.js +34 -27
- package/lib/module/components/index.js +8 -1
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/icons/Icon.js +16 -0
- package/lib/module/icons/registry.js +1 -1
- package/lib/module/index.js +2 -1
- package/lib/module/skeleton/Skeleton.js +229 -0
- package/lib/module/skeleton/SkeletonGroup.js +133 -0
- package/lib/module/skeleton/index.js +6 -0
- package/lib/module/skeleton/shimmer-tokens.js +181 -0
- package/lib/module/skeleton/useReducedMotion.js +61 -0
- package/lib/typescript/src/components/AccountCard/AccountCard.d.ts +81 -0
- package/lib/typescript/src/components/ActionFooter/ActionFooter.d.ts +26 -21
- package/lib/typescript/src/components/Avatar/Avatar.d.ts +7 -1
- package/lib/typescript/src/components/Badge/Badge.d.ts +7 -1
- package/lib/typescript/src/components/Button/Button.d.ts +8 -1
- package/lib/typescript/src/components/CardBankAccount/CardBankAccount.d.ts +9 -2
- package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +18 -2
- package/lib/typescript/src/components/Dropdown/Dropdown.d.ts +62 -0
- package/lib/typescript/src/components/DropdownInput/DropdownInput.d.ts +107 -0
- package/lib/typescript/src/components/FormField/FormField.d.ts +76 -19
- package/lib/typescript/src/components/IconButton/IconButton.d.ts +7 -1
- package/lib/typescript/src/components/Image/Image.d.ts +8 -1
- package/lib/typescript/src/components/LottieIntroBlock/LottieIntroBlock.d.ts +58 -0
- package/lib/typescript/src/components/LottiePlayer/LottiePlayer.d.ts +85 -0
- package/lib/typescript/src/components/LottiePlayer/LottiePlayer.web.d.ts +28 -0
- package/lib/typescript/src/components/LottiePlayer/loadNativeLottieView.d.ts +11 -0
- package/lib/typescript/src/components/LottiePlayer/loadWebLottieView.d.ts +11 -0
- package/lib/typescript/src/components/PageHero/PageHero.d.ts +79 -0
- package/lib/typescript/src/components/PoweredByLabel/PoweredByLabel.d.ts +70 -0
- package/lib/typescript/src/components/Text/Text.d.ts +31 -2
- package/lib/typescript/src/components/Tooltip/Tooltip.d.ts +13 -2
- package/lib/typescript/src/components/index.d.ts +8 -1
- package/lib/typescript/src/icons/Icon.d.ts +7 -1
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/skeleton/Skeleton.d.ts +60 -0
- package/lib/typescript/src/skeleton/SkeletonGroup.d.ts +78 -0
- package/lib/typescript/src/skeleton/index.d.ts +5 -0
- package/lib/typescript/src/skeleton/shimmer-tokens.d.ts +160 -0
- package/lib/typescript/src/skeleton/useReducedMotion.d.ts +15 -0
- package/package.json +11 -3
- package/src/components/AccountCard/AccountCard.tsx +376 -0
- package/src/components/ActionFooter/ActionFooter.tsx +152 -86
- package/src/components/AppBar/AppBar.tsx +25 -14
- package/src/components/Avatar/Avatar.tsx +26 -0
- package/src/components/Badge/Badge.tsx +27 -0
- package/src/components/Button/Button.tsx +40 -0
- package/src/components/CardBankAccount/CardBankAccount.tsx +29 -3
- package/src/components/CheckboxItem/CheckboxItem.tsx +65 -30
- package/src/components/Dropdown/Dropdown.tsx +331 -0
- package/src/components/DropdownInput/DropdownInput.tsx +819 -0
- package/src/components/FormField/FormField.tsx +542 -215
- package/src/components/IconButton/IconButton.tsx +27 -0
- package/src/components/Image/Image.tsx +25 -0
- package/src/components/LottieIntroBlock/LottieIntroBlock.tsx +202 -0
- package/src/components/LottiePlayer/LottiePlayer.tsx +145 -0
- package/src/components/LottiePlayer/LottiePlayer.web.tsx +94 -0
- package/src/components/LottiePlayer/loadNativeLottieView.tsx +87 -0
- package/src/components/LottiePlayer/loadWebLottieView.tsx +64 -0
- package/src/components/PageHero/PageHero.tsx +257 -0
- package/src/components/PoweredByLabel/PoweredByLabel.tsx +221 -0
- package/src/components/PoweredByLabel/finvu.png +0 -0
- package/src/components/RechargeCard/RechargeCard.tsx +32 -24
- package/src/components/Text/Text.tsx +78 -3
- package/src/components/Tooltip/Tooltip.tsx +50 -25
- package/src/components/index.ts +16 -1
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- package/src/icons/Icon.tsx +17 -0
- package/src/icons/registry.ts +1 -1
- package/src/index.ts +1 -0
- package/src/skeleton/Skeleton.tsx +298 -0
- package/src/skeleton/SkeletonGroup.tsx +193 -0
- package/src/skeleton/index.ts +10 -0
- package/src/skeleton/shimmer-tokens.ts +221 -0
- package/src/skeleton/useReducedMotion.ts +72 -0
|
@@ -1,263 +1,590 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import {
|
|
1
|
+
import React, { useCallback, useMemo, useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
TextInput as RNTextInput,
|
|
6
|
+
type StyleProp,
|
|
7
|
+
type TextInputProps as RNTextInputProps,
|
|
8
|
+
type TextStyle,
|
|
9
|
+
type ViewStyle,
|
|
10
|
+
} from 'react-native'
|
|
3
11
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
4
|
-
import { EMPTY_MODES } from '../../utils/react-utils'
|
|
5
12
|
import { useTokens } from '../../design-tokens/JFSThemeProvider'
|
|
6
|
-
import
|
|
13
|
+
import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
|
|
7
14
|
import SupportText from '../SupportText/SupportText'
|
|
8
15
|
import type { SupportTextStatus } from '../SupportText/SupportTextIcon'
|
|
16
|
+
import Icon from '../../icons/Icon'
|
|
17
|
+
import { useFormContext } from '../Form/Form'
|
|
9
18
|
|
|
10
|
-
export type FormFieldType = 'text' | 'password' | 'email' | 'search'
|
|
19
|
+
export type FormFieldType = 'text' | 'password' | 'email' | 'search' | 'number' | 'phone' | 'url'
|
|
11
20
|
|
|
12
21
|
export type FormFieldProps = {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
22
|
+
/** Label rendered above the input. */
|
|
23
|
+
label?: string
|
|
24
|
+
/** Placeholder text shown when the input is empty. */
|
|
25
|
+
placeholder?: string
|
|
26
|
+
/** Current value of the input (controlled). */
|
|
27
|
+
value?: string
|
|
28
|
+
/** Called when the input text changes. */
|
|
29
|
+
onChangeText?: (text: string) => void
|
|
30
|
+
/**
|
|
31
|
+
* Logical input type. Mapped to keyboard, secure entry and capitalization.
|
|
32
|
+
* @default 'text'
|
|
33
|
+
*/
|
|
34
|
+
type?: FormFieldType
|
|
35
|
+
/**
|
|
36
|
+
* Field name. When used inside `<Form>`, this is the key used to look up
|
|
37
|
+
* server-side `validationErrors` and to notify the form when the value
|
|
38
|
+
* changes so the error can clear.
|
|
39
|
+
*/
|
|
40
|
+
name?: string
|
|
41
|
+
/**
|
|
42
|
+
* Optional element rendered before the input text (Figma slot "start").
|
|
43
|
+
* No leading element is rendered by default — the Figma FormField design
|
|
44
|
+
* does not include one.
|
|
45
|
+
*/
|
|
46
|
+
leading?: React.ReactNode
|
|
47
|
+
/**
|
|
48
|
+
* Optional element rendered after the input text (Figma slot "end").
|
|
49
|
+
* Typically used for an inline button (e.g. "Apply", "Show") or a status
|
|
50
|
+
* icon.
|
|
51
|
+
*/
|
|
52
|
+
trailing?: React.ReactNode
|
|
53
|
+
/**
|
|
54
|
+
* Convenience prop to render a built-in icon as the leading element.
|
|
55
|
+
* Ignored when `leading` is provided. Defaults to `undefined` — meaning
|
|
56
|
+
* no leading icon, matching the Figma design.
|
|
57
|
+
*/
|
|
58
|
+
leadingIconName?: string
|
|
59
|
+
/** Shows a required asterisk next to the label. */
|
|
60
|
+
isRequired?: boolean
|
|
61
|
+
/** Disables interaction and dims the field. Resolves to "Read Only" state. */
|
|
62
|
+
isDisabled?: boolean
|
|
63
|
+
/** Marks the field as invalid and shows `errorMessage`. */
|
|
64
|
+
isInvalid?: boolean
|
|
65
|
+
/**
|
|
66
|
+
* Renders the field in read-only mode (non-interactive but not dimmed).
|
|
67
|
+
* Resolves to the "Read Only" state.
|
|
68
|
+
*/
|
|
69
|
+
isReadOnly?: boolean
|
|
70
|
+
/** Helper text displayed below the input. */
|
|
71
|
+
supportText?: string
|
|
72
|
+
/** Replaces `supportText` when `isInvalid` is true. */
|
|
73
|
+
errorMessage?: string
|
|
74
|
+
/** Maximum number of characters accepted. */
|
|
75
|
+
maxLength?: number
|
|
76
|
+
/** When true, focuses the input on mount. */
|
|
77
|
+
autoFocus?: boolean
|
|
78
|
+
/** Modes for design token resolution. */
|
|
79
|
+
modes?: Record<string, any>
|
|
80
|
+
/** Style overrides for the outermost wrapper. */
|
|
81
|
+
style?: StyleProp<ViewStyle>
|
|
82
|
+
/** Style overrides for the input row container. */
|
|
83
|
+
inputStyle?: StyleProp<ViewStyle>
|
|
84
|
+
/** Style overrides for the input text. */
|
|
85
|
+
inputTextStyle?: StyleProp<TextStyle>
|
|
86
|
+
/** Called when the input receives focus. */
|
|
87
|
+
onFocus?: RNTextInputProps['onFocus']
|
|
88
|
+
/** Called when the input loses focus. */
|
|
89
|
+
onBlur?: RNTextInputProps['onBlur']
|
|
90
|
+
/** Called when the user submits the input (e.g. presses return). */
|
|
91
|
+
onSubmitEditing?: RNTextInputProps['onSubmitEditing']
|
|
92
|
+
/** Accessibility label. Defaults to `label` or `placeholder`. */
|
|
93
|
+
accessibilityLabel?: string
|
|
94
|
+
/** Accessibility hint. */
|
|
95
|
+
accessibilityHint?: string
|
|
96
|
+
/** Test identifier. */
|
|
97
|
+
testID?: string
|
|
98
|
+
}
|
|
50
99
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
[globalModes, propModes],
|
|
55
|
-
)
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Token resolution
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
56
103
|
|
|
57
|
-
|
|
104
|
+
function toNumber(value: unknown, fallback: number): number {
|
|
105
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
106
|
+
if (typeof value === 'string') {
|
|
107
|
+
const parsed = parseFloat(value)
|
|
108
|
+
if (Number.isFinite(parsed)) return parsed
|
|
109
|
+
}
|
|
110
|
+
return fallback
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function toFontWeight(value: unknown, fallback: TextStyle['fontWeight']): TextStyle['fontWeight'] {
|
|
114
|
+
if (typeof value === 'number') return value.toString() as TextStyle['fontWeight']
|
|
115
|
+
if (typeof value === 'string' && value.length > 0) return value as TextStyle['fontWeight']
|
|
116
|
+
return fallback
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function useFormFieldTokens(modes: Record<string, any>) {
|
|
120
|
+
return useMemo(() => {
|
|
121
|
+
// Wrapper
|
|
122
|
+
const gap = toNumber(getVariableByName('formField/gap', modes), 8)
|
|
123
|
+
|
|
124
|
+
// Label (Figma: 14/17 medium, color #0c0d10)
|
|
125
|
+
const labelColor =
|
|
126
|
+
(getVariableByName('formField/label/color', modes) as string) || '#0c0d10'
|
|
127
|
+
const labelFontFamily =
|
|
128
|
+
(getVariableByName('formField/label/fontFamily', modes) as string) ||
|
|
129
|
+
'JioType Var'
|
|
130
|
+
const labelFontSize = toNumber(
|
|
131
|
+
getVariableByName('formField/label/fontSize', modes),
|
|
132
|
+
14,
|
|
133
|
+
)
|
|
134
|
+
const labelLineHeight = toNumber(
|
|
135
|
+
getVariableByName('formField/label/lineHeight', modes),
|
|
136
|
+
17,
|
|
137
|
+
)
|
|
138
|
+
const labelFontWeight = toFontWeight(
|
|
139
|
+
getVariableByName('formField/label/fontWeight', modes),
|
|
140
|
+
'500',
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
// Input row (Figma: 12 px padding-h, 8 px gap, 8 px radius, 1 px border)
|
|
144
|
+
const inputPaddingH = toNumber(
|
|
145
|
+
getVariableByName('formField/input/padding/horizontal', modes),
|
|
146
|
+
12,
|
|
147
|
+
)
|
|
148
|
+
const inputGap = toNumber(getVariableByName('formField/input/gap', modes), 8)
|
|
149
|
+
const inputRadius = toNumber(
|
|
150
|
+
getVariableByName('formField/input/radius', modes),
|
|
151
|
+
8,
|
|
152
|
+
)
|
|
153
|
+
const inputBorderSize = toNumber(
|
|
154
|
+
getVariableByName('formField/input/border/size', modes),
|
|
155
|
+
1,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
// Input text (Figma: 16/45 regular)
|
|
159
|
+
const inputFontSize = toNumber(
|
|
160
|
+
getVariableByName('formField/input/label/fontSize', modes),
|
|
161
|
+
16,
|
|
162
|
+
)
|
|
163
|
+
const inputLineHeight = toNumber(
|
|
164
|
+
getVariableByName('formField/input/label/lineHeight', modes),
|
|
165
|
+
45,
|
|
166
|
+
)
|
|
167
|
+
const inputFontFamily =
|
|
168
|
+
(getVariableByName('formField/input/label/fontFamily', modes) as string) ||
|
|
169
|
+
'JioType Var'
|
|
170
|
+
const inputFontWeight = toFontWeight(
|
|
171
|
+
getVariableByName('formField/input/label/fontWeight', modes),
|
|
172
|
+
'400',
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
const inputBackground =
|
|
176
|
+
(getVariableByName('formField/input/background', modes) as string) ||
|
|
177
|
+
'#ffffff'
|
|
178
|
+
const inputBorderColor =
|
|
179
|
+
(getVariableByName('formField/input/border/color', modes) as string) ||
|
|
180
|
+
'#b5b6b7'
|
|
58
181
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
182
|
+
if (__DEV__) {
|
|
183
|
+
console.warn('[FormField] border color (modes changed)', {
|
|
184
|
+
'FormField States': modes['FormField States'],
|
|
185
|
+
inputBorderColor,
|
|
186
|
+
'formField/input/border/color': getVariableByName(
|
|
187
|
+
'formField/input/border/color',
|
|
188
|
+
modes,
|
|
189
|
+
),
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const inputTextColor =
|
|
194
|
+
(getVariableByName('formField/input/label/color', modes) as string) ||
|
|
195
|
+
'#24262b'
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
gap,
|
|
199
|
+
labelColor,
|
|
200
|
+
labelFontFamily,
|
|
201
|
+
labelFontSize,
|
|
202
|
+
labelLineHeight,
|
|
203
|
+
labelFontWeight,
|
|
204
|
+
inputPaddingH,
|
|
205
|
+
inputGap,
|
|
206
|
+
inputRadius,
|
|
207
|
+
inputBorderSize,
|
|
208
|
+
inputFontSize,
|
|
209
|
+
inputLineHeight,
|
|
210
|
+
inputFontFamily,
|
|
211
|
+
inputFontWeight,
|
|
212
|
+
inputBackground,
|
|
213
|
+
inputBorderColor,
|
|
214
|
+
inputTextColor,
|
|
215
|
+
}
|
|
216
|
+
}, [modes])
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// Helpers
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
type DerivedTypeProps = {
|
|
224
|
+
secureTextEntry: boolean
|
|
225
|
+
keyboardType: RNTextInputProps['keyboardType']
|
|
226
|
+
autoCapitalize: RNTextInputProps['autoCapitalize']
|
|
227
|
+
autoComplete: RNTextInputProps['autoComplete']
|
|
228
|
+
textContentType: RNTextInputProps['textContentType']
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function deriveTypeProps(type: FormFieldType): DerivedTypeProps {
|
|
232
|
+
switch (type) {
|
|
233
|
+
case 'password':
|
|
234
|
+
return {
|
|
235
|
+
secureTextEntry: true,
|
|
236
|
+
keyboardType: 'default',
|
|
237
|
+
autoCapitalize: 'none',
|
|
238
|
+
autoComplete: 'password',
|
|
239
|
+
textContentType: 'password',
|
|
240
|
+
}
|
|
241
|
+
case 'email':
|
|
242
|
+
return {
|
|
243
|
+
secureTextEntry: false,
|
|
244
|
+
keyboardType: 'email-address',
|
|
245
|
+
autoCapitalize: 'none',
|
|
246
|
+
autoComplete: 'email',
|
|
247
|
+
textContentType: 'emailAddress',
|
|
248
|
+
}
|
|
249
|
+
case 'number':
|
|
250
|
+
return {
|
|
251
|
+
secureTextEntry: false,
|
|
252
|
+
keyboardType: 'numeric',
|
|
253
|
+
autoCapitalize: 'none',
|
|
254
|
+
autoComplete: 'off',
|
|
255
|
+
textContentType: 'none',
|
|
256
|
+
}
|
|
257
|
+
case 'phone':
|
|
258
|
+
return {
|
|
259
|
+
secureTextEntry: false,
|
|
260
|
+
keyboardType: 'phone-pad',
|
|
261
|
+
autoCapitalize: 'none',
|
|
262
|
+
autoComplete: 'tel',
|
|
263
|
+
textContentType: 'telephoneNumber',
|
|
264
|
+
}
|
|
265
|
+
case 'url':
|
|
266
|
+
return {
|
|
267
|
+
secureTextEntry: false,
|
|
268
|
+
keyboardType: 'url',
|
|
269
|
+
autoCapitalize: 'none',
|
|
270
|
+
autoComplete: 'url',
|
|
271
|
+
textContentType: 'URL',
|
|
272
|
+
}
|
|
273
|
+
case 'search':
|
|
274
|
+
return {
|
|
275
|
+
secureTextEntry: false,
|
|
276
|
+
keyboardType: 'default',
|
|
277
|
+
autoCapitalize: 'none',
|
|
278
|
+
autoComplete: 'off',
|
|
279
|
+
textContentType: 'none',
|
|
280
|
+
}
|
|
281
|
+
case 'text':
|
|
282
|
+
default:
|
|
283
|
+
return {
|
|
284
|
+
secureTextEntry: false,
|
|
285
|
+
keyboardType: 'default',
|
|
286
|
+
autoCapitalize: 'sentences',
|
|
287
|
+
autoComplete: 'off',
|
|
288
|
+
textContentType: 'none',
|
|
289
|
+
}
|
|
165
290
|
}
|
|
166
291
|
}
|
|
167
292
|
|
|
293
|
+
function firstError(error: string | string[] | undefined): string | undefined {
|
|
294
|
+
if (!error) return undefined
|
|
295
|
+
if (Array.isArray(error)) return error[0]
|
|
296
|
+
return error
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
// Component
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
|
|
168
303
|
function FormField({
|
|
169
304
|
label,
|
|
170
305
|
placeholder,
|
|
171
|
-
value
|
|
306
|
+
value,
|
|
172
307
|
onChangeText,
|
|
173
308
|
type = 'text',
|
|
309
|
+
name,
|
|
174
310
|
leading,
|
|
175
311
|
trailing,
|
|
176
312
|
leadingIconName,
|
|
177
313
|
isRequired = false,
|
|
178
314
|
isDisabled = false,
|
|
179
315
|
isInvalid = false,
|
|
316
|
+
isReadOnly = false,
|
|
180
317
|
supportText,
|
|
181
318
|
errorMessage,
|
|
182
|
-
|
|
319
|
+
maxLength,
|
|
320
|
+
autoFocus = false,
|
|
321
|
+
modes: propModes = EMPTY_MODES,
|
|
183
322
|
style,
|
|
323
|
+
inputStyle,
|
|
324
|
+
inputTextStyle,
|
|
184
325
|
onFocus,
|
|
185
326
|
onBlur,
|
|
327
|
+
onSubmitEditing,
|
|
186
328
|
accessibilityLabel,
|
|
187
329
|
accessibilityHint,
|
|
330
|
+
testID,
|
|
188
331
|
}: FormFieldProps) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
332
|
+
// -- Form context integration -------------------------------------------
|
|
333
|
+
const formCtx = useFormContext()
|
|
334
|
+
const formError =
|
|
335
|
+
name && formCtx ? firstError(formCtx.validationErrors[name]) : undefined
|
|
336
|
+
const resolvedIsInvalid = isInvalid || Boolean(formError)
|
|
337
|
+
const resolvedErrorMessage = errorMessage ?? formError
|
|
338
|
+
|
|
339
|
+
// -- Mode resolution ----------------------------------------------------
|
|
340
|
+
const { modes: globalModes } = useTokens()
|
|
341
|
+
const baseModes = useMemo(
|
|
342
|
+
() => ({ ...globalModes, ...propModes }),
|
|
343
|
+
[globalModes, propModes],
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
const [isFocused, setIsFocused] = useState(false)
|
|
347
|
+
const interactive = !isDisabled && !isReadOnly
|
|
348
|
+
|
|
349
|
+
// FormField States cascade — error > read only/disabled > active (focused) > idle.
|
|
350
|
+
// Disabled maps to "Read Only" since there is no dedicated disabled mode and
|
|
351
|
+
// the visual treatment is closest. This is only the DEFAULT — an explicit
|
|
352
|
+
// `modes['FormField States']` passed in via props or the global theme
|
|
353
|
+
// always wins so consumers can force a state (e.g. for documentation).
|
|
354
|
+
const derivedStateMode: 'Idle' | 'Active' | 'Read Only' | 'Error' = useMemo(() => {
|
|
355
|
+
if (resolvedIsInvalid) return 'Error'
|
|
356
|
+
if (isReadOnly || isDisabled) return 'Read Only'
|
|
357
|
+
if (isFocused) return 'Active'
|
|
358
|
+
return 'Idle'
|
|
359
|
+
}, [resolvedIsInvalid, isReadOnly, isDisabled, isFocused])
|
|
360
|
+
|
|
361
|
+
const modes = useMemo(() => {
|
|
362
|
+
const explicitStateMode = baseModes['FormField States'] as
|
|
363
|
+
| 'Idle'
|
|
364
|
+
| 'Active'
|
|
365
|
+
| 'Read Only'
|
|
366
|
+
| 'Error'
|
|
367
|
+
| undefined
|
|
368
|
+
const stateMode = explicitStateMode ?? derivedStateMode
|
|
369
|
+
|
|
370
|
+
const explicitStatus = baseModes.Status as string | undefined
|
|
371
|
+
// Default SupportText token mode is Auto (Figma resolves foreground from
|
|
372
|
+
// context). Pass modes={{ Status: 'Error' }} etc. to override.
|
|
373
|
+
const status = explicitStatus ?? 'Auto'
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
...baseModes,
|
|
377
|
+
'FormField States': stateMode,
|
|
378
|
+
Status: status,
|
|
379
|
+
}
|
|
380
|
+
}, [baseModes, derivedStateMode])
|
|
381
|
+
|
|
382
|
+
const tokens = useFormFieldTokens(modes)
|
|
383
|
+
|
|
384
|
+
// -- Type-derived input props ------------------------------------------
|
|
385
|
+
const typeProps = useMemo(() => deriveTypeProps(type), [type])
|
|
386
|
+
|
|
387
|
+
// -- Event handlers ----------------------------------------------------
|
|
388
|
+
const handleFocus = useCallback<NonNullable<RNTextInputProps['onFocus']>>(
|
|
389
|
+
(e) => {
|
|
390
|
+
setIsFocused(true)
|
|
391
|
+
onFocus?.(e)
|
|
392
|
+
},
|
|
393
|
+
[onFocus],
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
const handleBlur = useCallback<NonNullable<RNTextInputProps['onBlur']>>(
|
|
397
|
+
(e) => {
|
|
398
|
+
setIsFocused(false)
|
|
399
|
+
onBlur?.(e)
|
|
400
|
+
},
|
|
401
|
+
[onBlur],
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
const handleChangeText = useCallback(
|
|
405
|
+
(next: string) => {
|
|
406
|
+
onChangeText?.(next)
|
|
407
|
+
if (name && formCtx) formCtx.onFieldChange(name)
|
|
408
|
+
},
|
|
409
|
+
[onChangeText, name, formCtx],
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
// -- Styles ------------------------------------------------------------
|
|
413
|
+
const wrapperStyle: ViewStyle = useMemo(
|
|
414
|
+
() => ({
|
|
415
|
+
gap: tokens.gap,
|
|
416
|
+
opacity: isDisabled ? 0.5 : 1,
|
|
417
|
+
}),
|
|
418
|
+
[tokens.gap, isDisabled],
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
const labelRowStyle: ViewStyle = useMemo(
|
|
422
|
+
() => ({ flexDirection: 'row', alignItems: 'baseline' }),
|
|
423
|
+
[],
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
const labelTextStyle: TextStyle = useMemo(
|
|
427
|
+
() => ({
|
|
428
|
+
color: tokens.labelColor,
|
|
429
|
+
fontFamily: tokens.labelFontFamily,
|
|
430
|
+
fontSize: tokens.labelFontSize,
|
|
431
|
+
lineHeight: tokens.labelLineHeight,
|
|
432
|
+
fontWeight: tokens.labelFontWeight,
|
|
433
|
+
}),
|
|
434
|
+
[
|
|
435
|
+
tokens.labelColor,
|
|
436
|
+
tokens.labelFontFamily,
|
|
437
|
+
tokens.labelFontSize,
|
|
438
|
+
tokens.labelLineHeight,
|
|
439
|
+
tokens.labelFontWeight,
|
|
440
|
+
],
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
const requiredIndicatorStyle: TextStyle = useMemo(
|
|
444
|
+
() => ({ ...labelTextStyle, color: '#d93d3d' }),
|
|
445
|
+
[labelTextStyle],
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
const inputRowStyle: ViewStyle = useMemo(
|
|
449
|
+
() => ({
|
|
450
|
+
flexDirection: 'row',
|
|
451
|
+
alignItems: 'center',
|
|
452
|
+
backgroundColor: tokens.inputBackground,
|
|
453
|
+
borderColor: tokens.inputBorderColor,
|
|
454
|
+
borderWidth: tokens.inputBorderSize,
|
|
455
|
+
borderStyle: 'solid',
|
|
456
|
+
borderRadius: tokens.inputRadius,
|
|
457
|
+
paddingHorizontal: tokens.inputPaddingH,
|
|
458
|
+
paddingVertical: 0,
|
|
459
|
+
gap: tokens.inputGap,
|
|
460
|
+
minHeight: tokens.inputLineHeight,
|
|
461
|
+
width: '100%',
|
|
462
|
+
}),
|
|
463
|
+
[
|
|
464
|
+
tokens.inputBackground,
|
|
465
|
+
tokens.inputBorderColor,
|
|
466
|
+
tokens.inputBorderSize,
|
|
467
|
+
tokens.inputRadius,
|
|
468
|
+
tokens.inputPaddingH,
|
|
469
|
+
tokens.inputGap,
|
|
470
|
+
tokens.inputLineHeight,
|
|
471
|
+
],
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
const inputTextStyles: TextStyle = useMemo(
|
|
475
|
+
() => ({
|
|
476
|
+
flex: 1,
|
|
477
|
+
color: tokens.inputTextColor,
|
|
478
|
+
fontFamily: tokens.inputFontFamily,
|
|
479
|
+
fontSize: tokens.inputFontSize,
|
|
480
|
+
lineHeight: tokens.inputLineHeight,
|
|
481
|
+
fontWeight: tokens.inputFontWeight,
|
|
482
|
+
padding: 0,
|
|
483
|
+
margin: 0,
|
|
484
|
+
// Remove the default web focus ring; the input row's border acts as the
|
|
485
|
+
// focus indicator via the FormField States cascade.
|
|
486
|
+
outlineStyle: 'none' as TextStyle['outlineStyle'],
|
|
487
|
+
outlineWidth: 0,
|
|
488
|
+
outlineColor: 'transparent',
|
|
489
|
+
}),
|
|
490
|
+
[
|
|
491
|
+
tokens.inputTextColor,
|
|
492
|
+
tokens.inputFontFamily,
|
|
493
|
+
tokens.inputFontSize,
|
|
494
|
+
tokens.inputLineHeight,
|
|
495
|
+
tokens.inputFontWeight,
|
|
496
|
+
],
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
const placeholderColor = useMemo(() => {
|
|
500
|
+
// Slightly muted version of the resolved text color, mirroring the
|
|
501
|
+
// sibling TextInput behavior.
|
|
502
|
+
const c = tokens.inputTextColor
|
|
503
|
+
if (typeof c !== 'string') return undefined
|
|
504
|
+
if (c.startsWith('rgb(')) {
|
|
505
|
+
return c.replace('rgb(', 'rgba(').replace(')', ', 0.55)')
|
|
506
|
+
}
|
|
507
|
+
return '#888a8d'
|
|
508
|
+
}, [tokens.inputTextColor])
|
|
509
|
+
|
|
510
|
+
// -- Slots --------------------------------------------------------------
|
|
511
|
+
const leadingElement = leading ?? (leadingIconName
|
|
512
|
+
? <Icon name={leadingIconName} size={20} color={tokens.inputTextColor} />
|
|
513
|
+
: null)
|
|
514
|
+
const processedLeading = leadingElement
|
|
515
|
+
? cloneChildrenWithModes(leadingElement, modes)
|
|
516
|
+
: null
|
|
517
|
+
const processedTrailing = trailing
|
|
518
|
+
? cloneChildrenWithModes(trailing, modes)
|
|
519
|
+
: null
|
|
520
|
+
|
|
521
|
+
// -- Support text -------------------------------------------------------
|
|
522
|
+
const supportStatus: SupportTextStatus = resolvedIsInvalid ? 'Error' : 'Neutral'
|
|
523
|
+
const supportLabel =
|
|
524
|
+
resolvedIsInvalid && resolvedErrorMessage ? resolvedErrorMessage : supportText
|
|
525
|
+
|
|
526
|
+
// -- Accessibility ------------------------------------------------------
|
|
527
|
+
const resolvedA11yLabel =
|
|
528
|
+
accessibilityLabel || label || placeholder || 'Form field'
|
|
215
529
|
|
|
216
530
|
return (
|
|
217
531
|
<View
|
|
218
532
|
style={[wrapperStyle, style]}
|
|
219
533
|
pointerEvents={isDisabled ? 'none' : 'auto'}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
accessibilityLabel={resolvedA11yLabel}
|
|
223
|
-
accessibilityState={{
|
|
224
|
-
disabled: isDisabled,
|
|
225
|
-
}}
|
|
534
|
+
testID={testID}
|
|
535
|
+
accessible={false}
|
|
226
536
|
>
|
|
227
537
|
{label != null && (
|
|
228
|
-
<View style={
|
|
229
|
-
<Text style={
|
|
230
|
-
{isRequired &&
|
|
231
|
-
<Text style={requiredIndicatorStyle}> *</Text>
|
|
232
|
-
)}
|
|
538
|
+
<View style={labelRowStyle}>
|
|
539
|
+
<Text style={labelTextStyle}>{label}</Text>
|
|
540
|
+
{isRequired && <Text style={requiredIndicatorStyle}> *</Text>}
|
|
233
541
|
</View>
|
|
234
542
|
)}
|
|
235
543
|
|
|
236
|
-
<
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
544
|
+
<View style={[inputRowStyle, inputStyle]}>
|
|
545
|
+
{processedLeading != null && (
|
|
546
|
+
<View
|
|
547
|
+
accessibilityElementsHidden
|
|
548
|
+
importantForAccessibility="no"
|
|
549
|
+
>
|
|
550
|
+
{processedLeading}
|
|
551
|
+
</View>
|
|
552
|
+
)}
|
|
553
|
+
<RNTextInput
|
|
554
|
+
style={[inputTextStyles, inputTextStyle]}
|
|
555
|
+
value={value ?? ''}
|
|
556
|
+
onChangeText={handleChangeText}
|
|
557
|
+
onFocus={handleFocus}
|
|
558
|
+
onBlur={handleBlur}
|
|
559
|
+
onSubmitEditing={onSubmitEditing}
|
|
560
|
+
placeholder={placeholder ?? ''}
|
|
561
|
+
placeholderTextColor={placeholderColor}
|
|
562
|
+
editable={interactive}
|
|
563
|
+
maxLength={maxLength}
|
|
564
|
+
autoFocus={autoFocus}
|
|
565
|
+
secureTextEntry={typeProps.secureTextEntry}
|
|
566
|
+
keyboardType={typeProps.keyboardType}
|
|
567
|
+
autoCapitalize={typeProps.autoCapitalize}
|
|
568
|
+
autoComplete={typeProps.autoComplete}
|
|
569
|
+
textContentType={typeProps.textContentType}
|
|
570
|
+
accessibilityLabel={resolvedA11yLabel}
|
|
571
|
+
accessibilityHint={accessibilityHint}
|
|
572
|
+
/>
|
|
573
|
+
{processedTrailing != null && (
|
|
574
|
+
<View
|
|
575
|
+
accessibilityElementsHidden
|
|
576
|
+
importantForAccessibility="no"
|
|
577
|
+
>
|
|
578
|
+
{processedTrailing}
|
|
579
|
+
</View>
|
|
580
|
+
)}
|
|
581
|
+
</View>
|
|
582
|
+
|
|
583
|
+
{supportLabel != null && supportLabel !== '' && (
|
|
257
584
|
<SupportText
|
|
258
585
|
label={supportLabel}
|
|
259
586
|
status={supportStatus}
|
|
260
|
-
modes={
|
|
587
|
+
modes={modes}
|
|
261
588
|
/>
|
|
262
589
|
)}
|
|
263
590
|
</View>
|