react-native-platform-components 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/README.md +33 -31
  2. package/android/src/main/java/com/platformcomponents/PCButtonGroupView.kt +498 -0
  3. package/android/src/main/java/com/platformcomponents/PCButtonGroupViewManager.kt +191 -0
  4. package/android/src/main/java/com/platformcomponents/PCButtonSupport.kt +127 -0
  5. package/android/src/main/java/com/platformcomponents/PCButtonView.kt +345 -0
  6. package/android/src/main/java/com/platformcomponents/PCButtonViewManager.kt +126 -0
  7. package/android/src/main/java/com/platformcomponents/PCExpressive.kt +107 -0
  8. package/android/src/main/java/com/platformcomponents/PCFloatingToolbarView.kt +115 -0
  9. package/android/src/main/java/com/platformcomponents/PCFloatingToolbarViewManager.kt +65 -0
  10. package/android/src/main/java/com/platformcomponents/PCNativeThemeModule.kt +3 -0
  11. package/android/src/main/java/com/platformcomponents/PCSegmentedControlView.kt +13 -5
  12. package/android/src/main/java/com/platformcomponents/PCSegmentedControlViewManager.kt +6 -1
  13. package/android/src/main/java/com/platformcomponents/PCTextFieldView.kt +1018 -0
  14. package/android/src/main/java/com/platformcomponents/PCTextFieldViewManager.kt +248 -0
  15. package/android/src/main/java/com/platformcomponents/PlatformComponentsPackage.kt +13 -6
  16. package/android/src/main/jni/CMakeLists.txt +13 -1
  17. package/android/src/main/res/values/attrs.xml +13 -0
  18. package/android/src/main/res/values/styles.xml +183 -0
  19. package/ios/PCButton.h +10 -0
  20. package/ios/PCButton.mm +196 -0
  21. package/ios/PCButtonGroup.h +10 -0
  22. package/ios/PCButtonGroup.mm +256 -0
  23. package/ios/PCButtonGroupView.swift +254 -0
  24. package/ios/PCButtonSupport.swift +141 -0
  25. package/ios/PCButtonView.swift +155 -0
  26. package/ios/PCFloatingToolbar.h +10 -0
  27. package/ios/PCFloatingToolbar.mm +76 -0
  28. package/ios/PCFloatingToolbarView.swift +95 -0
  29. package/ios/PCNativeThemeModule.mm +13 -0
  30. package/ios/PCSelectionMenu.swift +9 -1
  31. package/ios/PCTextField.h +10 -0
  32. package/ios/PCTextField.mm +391 -0
  33. package/ios/PCTextFieldView.swift +1012 -0
  34. package/lib/commonjs/Button.js +77 -0
  35. package/lib/commonjs/Button.js.map +1 -0
  36. package/lib/commonjs/ButtonGroup.js +99 -0
  37. package/lib/commonjs/ButtonGroup.js.map +1 -0
  38. package/lib/commonjs/ButtonGroupNativeComponent.ts +121 -0
  39. package/lib/commonjs/ButtonNativeComponent.ts +76 -0
  40. package/lib/commonjs/ContextMenuNativeComponent.ts +5 -4
  41. package/lib/commonjs/DatePickerNativeComponent.ts +17 -11
  42. package/lib/commonjs/FloatingToolbar.js +69 -0
  43. package/lib/commonjs/FloatingToolbar.js.map +1 -0
  44. package/lib/commonjs/FloatingToolbarNativeComponent.ts +31 -0
  45. package/lib/commonjs/LiquidGlass.js +24 -4
  46. package/lib/commonjs/LiquidGlass.js.map +1 -1
  47. package/lib/commonjs/LiquidGlassNativeComponent.ts +6 -5
  48. package/lib/commonjs/NativePlatformComponentsTheme.js.map +1 -1
  49. package/lib/commonjs/SegmentedControl.js +11 -85
  50. package/lib/commonjs/SegmentedControl.js.map +1 -1
  51. package/lib/commonjs/SegmentedControlNativeComponent.ts +15 -11
  52. package/lib/commonjs/SelectionMenuNativeComponent.ts +6 -5
  53. package/lib/commonjs/TextField.js +195 -0
  54. package/lib/commonjs/TextField.js.map +1 -0
  55. package/lib/commonjs/TextFieldNativeComponent.ts +218 -0
  56. package/lib/commonjs/codegenTypes.js +6 -0
  57. package/lib/commonjs/codegenTypes.js.map +1 -0
  58. package/lib/commonjs/focusRegistry.js +78 -0
  59. package/lib/commonjs/focusRegistry.js.map +1 -0
  60. package/lib/commonjs/icons.js +92 -0
  61. package/lib/commonjs/icons.js.map +1 -0
  62. package/lib/commonjs/index.js +66 -0
  63. package/lib/commonjs/index.js.map +1 -1
  64. package/lib/commonjs/index.web.js +32 -2
  65. package/lib/commonjs/index.web.js.map +1 -1
  66. package/lib/commonjs/labelStyle.js +27 -0
  67. package/lib/commonjs/labelStyle.js.map +1 -0
  68. package/lib/commonjs/reactNativeInternals.d.js +2 -0
  69. package/lib/commonjs/reactNativeInternals.d.js.map +1 -0
  70. package/lib/module/Button.js +70 -0
  71. package/lib/module/Button.js.map +1 -0
  72. package/lib/module/ButtonGroup.js +92 -0
  73. package/lib/module/ButtonGroup.js.map +1 -0
  74. package/lib/module/ButtonGroupNativeComponent.ts +121 -0
  75. package/lib/module/ButtonNativeComponent.ts +76 -0
  76. package/lib/module/ContextMenuNativeComponent.ts +5 -4
  77. package/lib/module/DatePickerNativeComponent.ts +17 -11
  78. package/lib/module/FloatingToolbar.js +62 -0
  79. package/lib/module/FloatingToolbar.js.map +1 -0
  80. package/lib/module/FloatingToolbarNativeComponent.ts +31 -0
  81. package/lib/module/LiquidGlass.js +24 -4
  82. package/lib/module/LiquidGlass.js.map +1 -1
  83. package/lib/module/LiquidGlassNativeComponent.ts +6 -5
  84. package/lib/module/NativePlatformComponentsTheme.js.map +1 -1
  85. package/lib/module/SegmentedControl.js +12 -87
  86. package/lib/module/SegmentedControl.js.map +1 -1
  87. package/lib/module/SegmentedControlNativeComponent.ts +15 -11
  88. package/lib/module/SelectionMenuNativeComponent.ts +6 -5
  89. package/lib/module/TextField.js +189 -0
  90. package/lib/module/TextField.js.map +1 -0
  91. package/lib/module/TextFieldNativeComponent.ts +218 -0
  92. package/lib/module/codegenTypes.js +4 -0
  93. package/lib/module/codegenTypes.js.map +1 -0
  94. package/lib/module/focusRegistry.js +74 -0
  95. package/lib/module/focusRegistry.js.map +1 -0
  96. package/lib/module/icons.js +87 -0
  97. package/lib/module/icons.js.map +1 -0
  98. package/lib/module/index.js +6 -0
  99. package/lib/module/index.js.map +1 -1
  100. package/lib/module/index.web.js +28 -0
  101. package/lib/module/index.web.js.map +1 -1
  102. package/lib/module/labelStyle.js +23 -0
  103. package/lib/module/labelStyle.js.map +1 -0
  104. package/lib/module/reactNativeInternals.d.js +2 -0
  105. package/lib/module/reactNativeInternals.d.js.map +1 -0
  106. package/lib/typescript/commonjs/docs/sidebars.d.ts.map +1 -1
  107. package/lib/typescript/commonjs/src/Button.d.ts +76 -0
  108. package/lib/typescript/commonjs/src/Button.d.ts.map +1 -0
  109. package/lib/typescript/commonjs/src/ButtonGroup.d.ts +102 -0
  110. package/lib/typescript/commonjs/src/ButtonGroup.d.ts.map +1 -0
  111. package/lib/typescript/commonjs/src/ButtonGroupNativeComponent.d.ts +93 -0
  112. package/lib/typescript/commonjs/src/ButtonGroupNativeComponent.d.ts.map +1 -0
  113. package/lib/typescript/commonjs/src/ButtonNativeComponent.d.ts +57 -0
  114. package/lib/typescript/commonjs/src/ButtonNativeComponent.d.ts.map +1 -0
  115. package/lib/typescript/commonjs/src/ContextMenuNativeComponent.d.ts +5 -4
  116. package/lib/typescript/commonjs/src/ContextMenuNativeComponent.d.ts.map +1 -1
  117. package/lib/typescript/commonjs/src/DatePickerNativeComponent.d.ts +12 -11
  118. package/lib/typescript/commonjs/src/DatePickerNativeComponent.d.ts.map +1 -1
  119. package/lib/typescript/commonjs/src/FloatingToolbar.d.ts +45 -0
  120. package/lib/typescript/commonjs/src/FloatingToolbar.d.ts.map +1 -0
  121. package/lib/typescript/commonjs/src/FloatingToolbarNativeComponent.d.ts +24 -0
  122. package/lib/typescript/commonjs/src/FloatingToolbarNativeComponent.d.ts.map +1 -0
  123. package/lib/typescript/commonjs/src/LiquidGlass.d.ts +4 -3
  124. package/lib/typescript/commonjs/src/LiquidGlass.d.ts.map +1 -1
  125. package/lib/typescript/commonjs/src/LiquidGlassNativeComponent.d.ts +6 -5
  126. package/lib/typescript/commonjs/src/LiquidGlassNativeComponent.d.ts.map +1 -1
  127. package/lib/typescript/commonjs/src/NativePlatformComponentsTheme.d.ts +10 -2
  128. package/lib/typescript/commonjs/src/NativePlatformComponentsTheme.d.ts.map +1 -1
  129. package/lib/typescript/commonjs/src/SegmentedControl.d.ts +17 -46
  130. package/lib/typescript/commonjs/src/SegmentedControl.d.ts.map +1 -1
  131. package/lib/typescript/commonjs/src/SegmentedControlNativeComponent.d.ts +9 -6
  132. package/lib/typescript/commonjs/src/SegmentedControlNativeComponent.d.ts.map +1 -1
  133. package/lib/typescript/commonjs/src/SelectionMenuNativeComponent.d.ts +6 -5
  134. package/lib/typescript/commonjs/src/SelectionMenuNativeComponent.d.ts.map +1 -1
  135. package/lib/typescript/commonjs/src/TextField.d.ts +174 -0
  136. package/lib/typescript/commonjs/src/TextField.d.ts.map +1 -0
  137. package/lib/typescript/commonjs/src/TextFieldNativeComponent.d.ts +162 -0
  138. package/lib/typescript/commonjs/src/TextFieldNativeComponent.d.ts.map +1 -0
  139. package/lib/typescript/commonjs/src/codegenTypes.d.ts +12 -0
  140. package/lib/typescript/commonjs/src/codegenTypes.d.ts.map +1 -0
  141. package/lib/typescript/commonjs/src/focusRegistry.d.ts +13 -0
  142. package/lib/typescript/commonjs/src/focusRegistry.d.ts.map +1 -0
  143. package/lib/typescript/commonjs/src/icons.d.ts +52 -0
  144. package/lib/typescript/commonjs/src/icons.d.ts.map +1 -0
  145. package/lib/typescript/commonjs/src/index.d.ts +6 -0
  146. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  147. package/lib/typescript/commonjs/src/index.web.d.ts +121 -0
  148. package/lib/typescript/commonjs/src/index.web.d.ts.map +1 -1
  149. package/lib/typescript/commonjs/src/labelStyle.d.ts +20 -0
  150. package/lib/typescript/commonjs/src/labelStyle.d.ts.map +1 -0
  151. package/lib/typescript/commonjs/src/sharedTypes.d.ts +8 -3
  152. package/lib/typescript/commonjs/src/sharedTypes.d.ts.map +1 -1
  153. package/lib/typescript/module/docs/sidebars.d.ts.map +1 -1
  154. package/lib/typescript/module/src/Button.d.ts +76 -0
  155. package/lib/typescript/module/src/Button.d.ts.map +1 -0
  156. package/lib/typescript/module/src/ButtonGroup.d.ts +102 -0
  157. package/lib/typescript/module/src/ButtonGroup.d.ts.map +1 -0
  158. package/lib/typescript/module/src/ButtonGroupNativeComponent.d.ts +93 -0
  159. package/lib/typescript/module/src/ButtonGroupNativeComponent.d.ts.map +1 -0
  160. package/lib/typescript/module/src/ButtonNativeComponent.d.ts +57 -0
  161. package/lib/typescript/module/src/ButtonNativeComponent.d.ts.map +1 -0
  162. package/lib/typescript/module/src/ContextMenuNativeComponent.d.ts +5 -4
  163. package/lib/typescript/module/src/ContextMenuNativeComponent.d.ts.map +1 -1
  164. package/lib/typescript/module/src/DatePickerNativeComponent.d.ts +12 -11
  165. package/lib/typescript/module/src/DatePickerNativeComponent.d.ts.map +1 -1
  166. package/lib/typescript/module/src/FloatingToolbar.d.ts +45 -0
  167. package/lib/typescript/module/src/FloatingToolbar.d.ts.map +1 -0
  168. package/lib/typescript/module/src/FloatingToolbarNativeComponent.d.ts +24 -0
  169. package/lib/typescript/module/src/FloatingToolbarNativeComponent.d.ts.map +1 -0
  170. package/lib/typescript/module/src/LiquidGlass.d.ts +4 -3
  171. package/lib/typescript/module/src/LiquidGlass.d.ts.map +1 -1
  172. package/lib/typescript/module/src/LiquidGlassNativeComponent.d.ts +6 -5
  173. package/lib/typescript/module/src/LiquidGlassNativeComponent.d.ts.map +1 -1
  174. package/lib/typescript/module/src/NativePlatformComponentsTheme.d.ts +10 -2
  175. package/lib/typescript/module/src/NativePlatformComponentsTheme.d.ts.map +1 -1
  176. package/lib/typescript/module/src/SegmentedControl.d.ts +17 -46
  177. package/lib/typescript/module/src/SegmentedControl.d.ts.map +1 -1
  178. package/lib/typescript/module/src/SegmentedControlNativeComponent.d.ts +9 -6
  179. package/lib/typescript/module/src/SegmentedControlNativeComponent.d.ts.map +1 -1
  180. package/lib/typescript/module/src/SelectionMenuNativeComponent.d.ts +6 -5
  181. package/lib/typescript/module/src/SelectionMenuNativeComponent.d.ts.map +1 -1
  182. package/lib/typescript/module/src/TextField.d.ts +174 -0
  183. package/lib/typescript/module/src/TextField.d.ts.map +1 -0
  184. package/lib/typescript/module/src/TextFieldNativeComponent.d.ts +162 -0
  185. package/lib/typescript/module/src/TextFieldNativeComponent.d.ts.map +1 -0
  186. package/lib/typescript/module/src/codegenTypes.d.ts +12 -0
  187. package/lib/typescript/module/src/codegenTypes.d.ts.map +1 -0
  188. package/lib/typescript/module/src/focusRegistry.d.ts +13 -0
  189. package/lib/typescript/module/src/focusRegistry.d.ts.map +1 -0
  190. package/lib/typescript/module/src/icons.d.ts +52 -0
  191. package/lib/typescript/module/src/icons.d.ts.map +1 -0
  192. package/lib/typescript/module/src/index.d.ts +6 -0
  193. package/lib/typescript/module/src/index.d.ts.map +1 -1
  194. package/lib/typescript/module/src/index.web.d.ts +121 -0
  195. package/lib/typescript/module/src/index.web.d.ts.map +1 -1
  196. package/lib/typescript/module/src/labelStyle.d.ts +20 -0
  197. package/lib/typescript/module/src/labelStyle.d.ts.map +1 -0
  198. package/lib/typescript/module/src/sharedTypes.d.ts +8 -3
  199. package/lib/typescript/module/src/sharedTypes.d.ts.map +1 -1
  200. package/package.json +28 -6
  201. package/react-native.config.js +3 -0
  202. package/shared/PCButtonComponentDescriptors-custom.h +22 -0
  203. package/shared/PCButtonGroupComponentDescriptors-custom.h +22 -0
  204. package/shared/PCButtonGroupShadowNode-custom.cpp +48 -0
  205. package/shared/PCButtonGroupShadowNode-custom.h +55 -0
  206. package/shared/PCButtonGroupState-custom.h +61 -0
  207. package/shared/PCButtonShadowNode-custom.cpp +48 -0
  208. package/shared/PCButtonShadowNode-custom.h +55 -0
  209. package/shared/PCButtonState-custom.h +61 -0
  210. package/shared/PCDatePickerState-custom.h +2 -2
  211. package/shared/PCSegmentedControlState-custom.h +2 -2
  212. package/shared/PCSelectionMenuState-custom.h +2 -2
  213. package/shared/PCTextFieldComponentDescriptors-custom.h +22 -0
  214. package/shared/PCTextFieldShadowNode-custom.cpp +47 -0
  215. package/shared/PCTextFieldShadowNode-custom.h +55 -0
  216. package/shared/PCTextFieldState-custom.h +61 -0
  217. package/shared/react/renderer/components/PlatformComponentsViewSpec/ComponentDescriptors.h +3 -0
  218. package/src/Button.tsx +144 -0
  219. package/src/ButtonGroup.tsx +233 -0
  220. package/src/ButtonGroupNativeComponent.ts +121 -0
  221. package/src/ButtonNativeComponent.ts +76 -0
  222. package/src/ContextMenuNativeComponent.ts +5 -4
  223. package/src/DatePickerNativeComponent.ts +17 -11
  224. package/src/FloatingToolbar.tsx +113 -0
  225. package/src/FloatingToolbarNativeComponent.ts +31 -0
  226. package/src/LiquidGlass.tsx +24 -5
  227. package/src/LiquidGlassNativeComponent.ts +6 -5
  228. package/src/NativePlatformComponentsTheme.ts +11 -2
  229. package/src/SegmentedControl.tsx +26 -118
  230. package/src/SegmentedControlNativeComponent.ts +15 -11
  231. package/src/SelectionMenuNativeComponent.ts +6 -5
  232. package/src/TextField.tsx +494 -0
  233. package/src/TextFieldNativeComponent.ts +218 -0
  234. package/src/codegenTypes.ts +40 -0
  235. package/src/focusRegistry.ts +89 -0
  236. package/src/icons.ts +116 -0
  237. package/src/index.tsx +6 -0
  238. package/src/index.web.tsx +194 -0
  239. package/src/labelStyle.ts +36 -0
  240. package/src/reactNativeInternals.d.ts +22 -0
  241. package/src/sharedTypes.ts +9 -3
  242. /package/{app.plugin.cjs → app.plugin.js} +0 -0
@@ -0,0 +1,1018 @@
1
+ package com.platformcomponents
2
+
3
+ import android.content.Context
4
+ import android.graphics.Rect
5
+ import android.graphics.drawable.BitmapDrawable
6
+ import android.graphics.drawable.Drawable
7
+ import android.os.Build
8
+ import android.text.Editable
9
+ import android.text.InputFilter
10
+ import android.text.InputType
11
+ import android.text.TextWatcher
12
+ import android.view.ContextThemeWrapper
13
+ import android.view.Gravity
14
+ import android.view.KeyEvent
15
+ import android.view.View
16
+ import android.view.ViewGroup
17
+ import android.view.inputmethod.EditorInfo
18
+ import android.view.inputmethod.InputMethodManager
19
+ import android.widget.FrameLayout
20
+ import android.widget.LinearLayout
21
+ import android.widget.TextView
22
+ import android.content.res.ColorStateList
23
+ import android.util.TypedValue
24
+ import android.view.MotionEvent
25
+ import androidx.core.widget.TextViewCompat
26
+ import androidx.appcompat.R as AppCompatR
27
+ import com.facebook.react.bridge.WritableNativeMap
28
+ import com.facebook.react.uimanager.PixelUtil
29
+ import com.facebook.react.uimanager.ReactCompoundViewGroup
30
+ import com.facebook.react.uimanager.StateWrapper
31
+ import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper
32
+ import com.facebook.react.views.scroll.ReactScrollViewHelper
33
+ import com.google.android.material.textfield.TextInputEditText
34
+ import com.google.android.material.textfield.TextInputLayout
35
+
36
+ /**
37
+ * A Material 3 text field: a TextInputLayout (floating label, box, icons,
38
+ * supporting text, counter) around a TextInputEditText. With
39
+ * `android.material: 'system'` it is the platform EditText instead (the hint
40
+ * as placeholder, compound-drawable icons, supporting text below), as the
41
+ * other components' system mode gives the AppCompat widget.
42
+ *
43
+ * The widgets are rebuilt for the props they read at construction (the box
44
+ * style, the native theme); everything else is applied to the live widgets.
45
+ * The text and its edit counter live here, so a rebuild keeps them.
46
+ *
47
+ * Text flows one way at a time: user edits report an event count with the
48
+ * text, and JS pushes a controlled value back with the last count it saw.
49
+ * A push that is older than the latest edit is dropped, so a slow JS
50
+ * round-trip can't erase what was typed meanwhile.
51
+ */
52
+ class PCTextFieldView(context: Context) :
53
+ FrameLayout(context),
54
+ ReactScrollViewHelper.HasStateWrapper,
55
+ ReactCompoundViewGroup {
56
+
57
+ companion object {
58
+ private const val TAG = "PCTextField"
59
+ }
60
+
61
+ // --- State Wrapper for Fabric state updates ---
62
+ override var stateWrapper: StateWrapper? = null
63
+
64
+ private var lastReportedHeight: Float = -1f
65
+
66
+ // --- Props ---
67
+ var label: String = ""
68
+ var placeholder: String = ""
69
+ var supportingText: String = ""
70
+ var errorState: String = "none" // "none" | "error"
71
+ var errorText: String = ""
72
+ var prefix: String = ""
73
+ var suffix: String = ""
74
+ var leadingIcon: PCButtonSupport.Icon = PCButtonSupport.NO_ICON
75
+ var trailingIcon: PCButtonSupport.Icon = PCButtonSupport.NO_ICON
76
+ var clearButtonMode: String = "never"
77
+ var passwordToggle: Boolean = false
78
+ var characterCount: Boolean = false
79
+ var maxLength: Int = 0
80
+ var keyboardType: String = "default"
81
+ var returnKeyType: String = "default"
82
+ var autoCapitalize: String = "sentences"
83
+ var autoCorrect: Boolean = true
84
+ var secure: Boolean = false
85
+ var multiline: Boolean = false
86
+ var fieldEnabled: Boolean = true
87
+ var autoFocus: Boolean = false
88
+ var selectTextOnFocus: Boolean = false
89
+ var autoComplete: String = ""
90
+ var spokenLabel: String = ""
91
+ var fontFamily: String = ""
92
+ var fontSize: Float = 0f
93
+ var fontWeight: String = ""
94
+ var fontStyle: String = ""
95
+ var variant: String = "outlined" // "outlined" | "filled"
96
+ var dense: Boolean = false
97
+ var materialMode: String = "m3" // "m3" | "system"
98
+
99
+ // --- Events ---
100
+ var onChange: ((text: String, eventCount: Int) -> Unit)? = null
101
+ var onFocusChange: ((focused: Boolean, text: String) -> Unit)? = null
102
+ var onSubmit: ((text: String) -> Unit)? = null
103
+ var onTrailingIconPress: (() -> Unit)? = null
104
+
105
+ // --- Text state (survives rebuilds) ---
106
+ private var text: String = ""
107
+ private var nativeEventCount = 0
108
+ private var settingTextInternally = false
109
+ private var initialTextApplied = false
110
+ private var autoFocusDone = false
111
+
112
+ // --- UI ---
113
+ /** The top-level widget: the TextInputLayout, or the system field's column. */
114
+ private var widget: View? = null
115
+ private var layout: TextInputLayout? = null
116
+ private var editText: FieldEditText? = null
117
+ private var helperView: TextView? = null
118
+
119
+ /** The Material style's error icon, shown in the end slot while there is an error. */
120
+ private var defaultErrorIcon: Drawable? = null
121
+
122
+ /** Bumped on every rebuild and icon change so late image loads can't touch stale widgets. */
123
+ private var startIconGeneration = 0
124
+ private var endIconGeneration = 0
125
+
126
+ // --- Native theme ---
127
+ // Widgets read theme colors when they are created, so they are rebuilt when the
128
+ // native theme changes (brand color, light/dark switch).
129
+ private var builtThemeVersion = PCNativeTheme.version
130
+ private val nativeThemeListener = PCNativeTheme.Listener {
131
+ PCThemeSupport.clearColorStateListCaches(this)
132
+ rebuildUI()
133
+ }
134
+
135
+ // Declared before init: rebuildUI() attaches the watcher, and Kotlin
136
+ // initializes properties in declaration order.
137
+ private val textWatcher = object : TextWatcher {
138
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
139
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
140
+ override fun afterTextChanged(s: Editable) {
141
+ val next = s.toString()
142
+ if (settingTextInternally) {
143
+ text = next
144
+ return
145
+ }
146
+ if (next == text) return
147
+ text = next
148
+ nativeEventCount += 1
149
+ onChange?.invoke(next, nativeEventCount)
150
+ }
151
+ }
152
+
153
+ private var manualLayoutPending = false
154
+
155
+ init {
156
+ clipChildren = false
157
+ clipToPadding = false
158
+ rebuildUI()
159
+ }
160
+
161
+ // ---- Public apply* (called by manager) ----
162
+
163
+ /** The text the field starts with; only the first value is used (see setTextFromJS). */
164
+ fun applyInitialText(value: String) {
165
+ if (initialTextApplied) return
166
+ initialTextApplied = true
167
+ setTextInternal(value, moveCursorToEnd = true)
168
+ }
169
+
170
+ fun applyLabel(value: String) {
171
+ if (label == value) return
172
+ label = value
173
+ applyHintAndPlaceholder()
174
+ }
175
+
176
+ fun applyPlaceholder(value: String) {
177
+ if (placeholder == value) return
178
+ placeholder = value
179
+ applyHintAndPlaceholder()
180
+ }
181
+
182
+ fun applySupportingText(value: String) {
183
+ if (supportingText == value) return
184
+ supportingText = value
185
+ applyHelperAndError()
186
+ }
187
+
188
+ fun applyError(state: String, message: String) {
189
+ val nextState = if (state == "error") "error" else "none"
190
+ if (errorState == nextState && errorText == message) return
191
+ errorState = nextState
192
+ errorText = message
193
+ applyHelperAndError()
194
+ }
195
+
196
+ fun applyPrefix(value: String) {
197
+ if (prefix == value) return
198
+ prefix = value
199
+ layout?.prefixText = value.ifEmpty { null }
200
+ requestLayout()
201
+ }
202
+
203
+ fun applySuffix(value: String) {
204
+ if (suffix == value) return
205
+ suffix = value
206
+ layout?.suffixText = value.ifEmpty { null }
207
+ requestLayout()
208
+ }
209
+
210
+ fun applyLeadingIcon(value: PCButtonSupport.Icon) {
211
+ if (leadingIcon == value) return
212
+ leadingIcon = value
213
+ applyStartIcon()
214
+ }
215
+
216
+ fun applyTrailingIcon(value: PCButtonSupport.Icon) {
217
+ if (trailingIcon == value) return
218
+ trailingIcon = value
219
+ applyEndIcon()
220
+ }
221
+
222
+ fun applyClearButtonMode(value: String) {
223
+ if (clearButtonMode == value) return
224
+ clearButtonMode = value
225
+ applyEndIcon()
226
+ }
227
+
228
+ fun applyPasswordToggle(value: Boolean) {
229
+ if (passwordToggle == value) return
230
+ passwordToggle = value
231
+ applyEndIcon()
232
+ }
233
+
234
+ fun applyCharacterCount(value: Boolean) {
235
+ if (characterCount == value) return
236
+ characterCount = value
237
+ applyCounterAndLength()
238
+ }
239
+
240
+ fun applyMaxLength(value: Int) {
241
+ if (maxLength == value) return
242
+ maxLength = value
243
+ applyCounterAndLength()
244
+ }
245
+
246
+ fun applyKeyboardType(value: String) {
247
+ if (keyboardType == value) return
248
+ keyboardType = value
249
+ applyInputType()
250
+ }
251
+
252
+ fun applyReturnKeyType(value: String) {
253
+ if (returnKeyType == value) return
254
+ returnKeyType = value
255
+ applyInputType()
256
+ }
257
+
258
+ fun applyAutoCapitalize(value: String) {
259
+ if (autoCapitalize == value) return
260
+ autoCapitalize = value
261
+ applyInputType()
262
+ }
263
+
264
+ fun applyAutoCorrect(value: Boolean) {
265
+ if (autoCorrect == value) return
266
+ autoCorrect = value
267
+ applyInputType()
268
+ }
269
+
270
+ fun applySecure(value: Boolean) {
271
+ if (secure == value) return
272
+ secure = value
273
+ applyInputType()
274
+ }
275
+
276
+ fun applyMultiline(value: Boolean) {
277
+ if (multiline == value) return
278
+ multiline = value
279
+ applyInputType()
280
+ }
281
+
282
+ fun applyInteractivity(value: String?) {
283
+ val next = value != "disabled"
284
+ if (fieldEnabled == next) return
285
+ fieldEnabled = next
286
+ layout?.isEnabled = next
287
+ if (layout == null) editText?.isEnabled = next
288
+ }
289
+
290
+ fun applyAutoFocus(value: Boolean) {
291
+ autoFocus = value
292
+ if (value && isAttachedToWindow) performAutoFocus()
293
+ }
294
+
295
+ fun applySelectTextOnFocus(value: Boolean) {
296
+ if (selectTextOnFocus == value) return
297
+ selectTextOnFocus = value
298
+ editText?.setSelectAllOnFocus(value)
299
+ }
300
+
301
+ fun applyAutoComplete(value: String) {
302
+ if (autoComplete == value) return
303
+ autoComplete = value
304
+ applyAutofill()
305
+ }
306
+
307
+ fun applySpokenLabel(value: String) {
308
+ if (spokenLabel == value) return
309
+ spokenLabel = value
310
+ editText?.contentDescription = value.ifEmpty { null }
311
+ }
312
+
313
+ fun applyTextStyle(family: String, size: Float, weight: String, style: String) {
314
+ if (fontFamily == family && fontSize == size && fontWeight == weight && fontStyle == style) return
315
+ fontFamily = family
316
+ fontSize = size
317
+ fontWeight = weight
318
+ fontStyle = style
319
+ editText?.let { applyFont(it) }
320
+ requestLayout()
321
+ }
322
+
323
+ fun applyVariant(value: String?) {
324
+ val next = if (value == "filled") "filled" else "outlined"
325
+ if (variant == next) return
326
+ variant = next
327
+ rebuildUI()
328
+ }
329
+
330
+ fun applyDensity(value: String?) {
331
+ val next = value == "dense"
332
+ if (dense == next) return
333
+ dense = next
334
+ rebuildUI()
335
+ }
336
+
337
+ fun applyMaterial(value: String?) {
338
+ val next = if (value == "system") "system" else "m3"
339
+ if (materialMode == next) return
340
+ materialMode = next
341
+ rebuildUI()
342
+ }
343
+
344
+ // ---- Commands ----
345
+
346
+ fun focusFromJS() {
347
+ val edit = editText ?: return
348
+ edit.requestFocus()
349
+ if (edit.isInTouchMode) showKeyboard(edit)
350
+ }
351
+
352
+ fun blurFromJS() {
353
+ val edit = editText ?: return
354
+ clearFocusAndMaybeRefocus(edit)
355
+ hideKeyboard()
356
+ }
357
+
358
+ /** Clears the text as if the user had, so JS gets a change event. */
359
+ fun clearFromJS() {
360
+ val edit = editText ?: return
361
+ if (edit.length() == 0) return
362
+ edit.setText("")
363
+ }
364
+
365
+ /**
366
+ * A controlled value from JS. Dropped when the user has edited since
367
+ * [eventCount] was reported: the edit event that follows carries the text
368
+ * JS should reconcile against.
369
+ */
370
+ fun setTextFromJS(eventCount: Int, value: String) {
371
+ if (eventCount < nativeEventCount) return
372
+ if (value == text) return
373
+ setTextInternal(value, moveCursorToEnd = false)
374
+ }
375
+
376
+ // ---- Touch ----
377
+
378
+ // The widgets have generated view ids, which React Native's touch handling
379
+ // would take for React tags and dispatch JS touch events to unrelated views.
380
+ // Claim the touch for this view; the field still receives it natively.
381
+ override fun interceptsTouchEvent(touchX: Float, touchY: Float): Boolean = true
382
+
383
+ override fun reactTagForTouch(touchX: Float, touchY: Float): Int = id
384
+
385
+ // ---- Lifecycle ----
386
+
387
+ override fun onAttachedToWindow() {
388
+ super.onAttachedToWindow()
389
+ PCNativeTheme.addListener(nativeThemeListener)
390
+ PCNativeTheme.attach(context)
391
+ if (builtThemeVersion != PCNativeTheme.version) {
392
+ nativeThemeListener.onNativeThemeChanged()
393
+ }
394
+ if (autoFocus) performAutoFocus()
395
+ }
396
+
397
+ override fun onDetachedFromWindow() {
398
+ PCNativeTheme.removeListener(nativeThemeListener)
399
+ super.onDetachedFromWindow()
400
+ }
401
+
402
+ private fun performAutoFocus() {
403
+ if (autoFocusDone) return
404
+ autoFocusDone = true
405
+ post { focusFromJS() }
406
+ }
407
+
408
+ // ---- UI Building ----
409
+
410
+ private fun rebuildUI() {
411
+ builtThemeVersion = PCNativeTheme.version
412
+ val wasFocused = editText?.isFocused == true
413
+ val selection = editText?.selectionEnd ?: -1
414
+ removeAllViews()
415
+ startIconGeneration += 1
416
+ endIconGeneration += 1
417
+ layout = null
418
+ helperView = null
419
+
420
+ val edit: FieldEditText
421
+ if (materialMode == "system") {
422
+ // The platform field: an AppCompat EditText with a supporting-text line
423
+ val column = LinearLayout(context).apply {
424
+ orientation = LinearLayout.VERTICAL
425
+ layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
426
+ }
427
+ edit = FieldEditText(context).apply {
428
+ id = View.generateViewId()
429
+ layoutParams = LinearLayout.LayoutParams(
430
+ ViewGroup.LayoutParams.MATCH_PARENT,
431
+ ViewGroup.LayoutParams.WRAP_CONTENT
432
+ )
433
+ compoundDrawablePadding = dp(8)
434
+ setSelectAllOnFocus(selectTextOnFocus)
435
+ contentDescription = spokenLabel.ifEmpty { null }
436
+ isEnabled = fieldEnabled
437
+ }
438
+ val helper = TextView(context).apply {
439
+ layoutParams = LinearLayout.LayoutParams(
440
+ ViewGroup.LayoutParams.MATCH_PARENT,
441
+ ViewGroup.LayoutParams.WRAP_CONTENT
442
+ ).apply { marginStart = dp(4); topMargin = dp(2) }
443
+ TextViewCompat.setTextAppearance(this, android.R.style.TextAppearance_Material_Small)
444
+ visibility = View.GONE
445
+ }
446
+ column.addView(edit)
447
+ column.addView(helper)
448
+ addView(column)
449
+ widget = column
450
+ helperView = helper
451
+ editText = edit
452
+ } else {
453
+ // Material widgets need a Material theme; fall back to Material 3 defaults instead of crashing.
454
+ val base = PCThemeSupport.materialContext(context, "TextField")
455
+ val styleOverlay = when {
456
+ variant == "filled" && dense -> R.style.PCTextField_Filled_Dense
457
+ variant == "filled" -> R.style.PCTextField_Filled
458
+ dense -> R.style.PCTextField_Outlined_Dense
459
+ else -> R.style.PCTextField_Outlined
460
+ }
461
+
462
+ val til = TextInputLayout(ContextThemeWrapper(base, styleOverlay)).apply {
463
+ layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
464
+ }
465
+
466
+ // The layout's context carries the style's theme overlay for its edit text
467
+ edit = FieldEditText(til.context).apply {
468
+ id = View.generateViewId()
469
+ layoutParams = LinearLayout.LayoutParams(
470
+ ViewGroup.LayoutParams.MATCH_PARENT,
471
+ ViewGroup.LayoutParams.WRAP_CONTENT
472
+ )
473
+ setSelectAllOnFocus(selectTextOnFocus)
474
+ contentDescription = spokenLabel.ifEmpty { null }
475
+ }
476
+
477
+ til.addView(edit)
478
+ addView(til)
479
+ widget = til
480
+ layout = til
481
+ editText = edit
482
+ defaultErrorIcon = til.errorIconDrawable
483
+ til.prefixText = prefix.ifEmpty { null }
484
+ til.suffixText = suffix.ifEmpty { null }
485
+ til.isEnabled = fieldEnabled
486
+ }
487
+
488
+ applyInputType()
489
+ applyHintAndPlaceholder()
490
+ applyHelperAndError()
491
+ applyStartIcon()
492
+ applyEndIcon()
493
+ applyCounterAndLength()
494
+ applyAutofill()
495
+
496
+ // Restore the text without reporting it as an edit
497
+ settingTextInternally = true
498
+ edit.setText(text)
499
+ val length = edit.length()
500
+ edit.setSelection(if (selection in 0..length) selection else length)
501
+ settingTextInternally = false
502
+
503
+ edit.addTextChangedListener(textWatcher)
504
+ // Focus changes are reported from FieldEditText.onFocusChanged: the Material
505
+ // end-icon delegates (clear text, for one) install their own
506
+ // OnFocusChangeListener on the edit text, replacing any listener set here.
507
+ edit.setOnEditorActionListener { _, actionId, event -> onEditorAction(actionId, event) }
508
+
509
+ if (wasFocused) edit.requestFocus()
510
+ requestLayout()
511
+ }
512
+
513
+ private fun onEditorAction(actionId: Int, event: KeyEvent?): Boolean {
514
+ // Multi-line fields keep the return key for newlines
515
+ if (multiline) return false
516
+ // A hardware / IME Enter reports IME_ACTION_UNSPECIFIED twice (key down and up)
517
+ if (actionId == EditorInfo.IME_ACTION_UNSPECIFIED && event != null && event.action != KeyEvent.ACTION_DOWN) {
518
+ return true
519
+ }
520
+ onSubmit?.invoke(text)
521
+ // Single-line fields blur on submit, as the core TextInput does by default
522
+ blurFromJS()
523
+ return true
524
+ }
525
+
526
+ private fun setTextInternal(value: String, moveCursorToEnd: Boolean) {
527
+ val edit = editText
528
+ if (edit == null) {
529
+ text = value
530
+ return
531
+ }
532
+ settingTextInternally = true
533
+ val wasAtEnd = edit.selectionEnd == edit.length()
534
+ val cursor = edit.selectionStart
535
+ edit.setText(value)
536
+ text = value
537
+ val length = edit.length()
538
+ edit.setSelection(if (moveCursorToEnd || wasAtEnd) length else cursor.coerceIn(0, length))
539
+ settingTextInternally = false
540
+ }
541
+
542
+ // ---- Decorations ----
543
+
544
+ /**
545
+ * With a label the Material layout floats it and shows the placeholder
546
+ * while focused; without one the placeholder is the edit text's own hint,
547
+ * and the box shrinks to the height of a plain field.
548
+ */
549
+ private fun applyHintAndPlaceholder() {
550
+ val edit = editText ?: return
551
+ val til = layout
552
+ if (til == null) {
553
+ // The platform field has one hint: the placeholder, else the label
554
+ edit.hint = placeholder.ifEmpty { label }.ifEmpty { null }
555
+ requestLayout()
556
+ return
557
+ }
558
+ if (label.isNotEmpty()) {
559
+ edit.hint = null
560
+ til.isHintEnabled = true
561
+ til.hint = label
562
+ til.placeholderText = placeholder.ifEmpty { null }
563
+ } else {
564
+ til.placeholderText = null
565
+ til.hint = null
566
+ til.isHintEnabled = false
567
+ edit.hint = placeholder.ifEmpty { null }
568
+ }
569
+ requestLayout()
570
+ }
571
+
572
+ private fun applyHelperAndError() {
573
+ val til = layout
574
+ if (til == null) {
575
+ applySystemHelper()
576
+ return
577
+ }
578
+ til.helperText = supportingText.ifEmpty { null }
579
+ if (errorState == "error") {
580
+ // No message: the supporting text turns to the error color, as the
581
+ // Material spec shows; with nothing to show, keep the outline red only
582
+ til.isErrorEnabled = true
583
+ til.error = errorText.ifEmpty { supportingText.ifEmpty { " " } }
584
+ } else {
585
+ til.error = null
586
+ til.isErrorEnabled = false
587
+ }
588
+ requestLayout()
589
+ }
590
+
591
+ /** The system field's supporting / error line, in the theme's secondary or error color. */
592
+ private fun applySystemHelper() {
593
+ val helper = helperView ?: return
594
+ val showsError = errorState == "error"
595
+ val message = if (showsError && errorText.isNotEmpty()) errorText else supportingText
596
+ helper.text = message
597
+ helper.visibility = if (message.isEmpty()) View.GONE else View.VISIBLE
598
+ val color = if (showsError) {
599
+ themeColor(AppCompatR.attr.colorError, 0xFFB3261E.toInt())
600
+ } else {
601
+ themeColor(android.R.attr.textColorSecondary, 0xFF757575.toInt())
602
+ }
603
+ helper.setTextColor(color)
604
+ requestLayout()
605
+ }
606
+
607
+ private fun themeColor(attr: Int, fallback: Int): Int {
608
+ val value = TypedValue()
609
+ if (!context.theme.resolveAttribute(attr, value, true)) return fallback
610
+ return if (value.resourceId != 0) context.getColor(value.resourceId) else value.data
611
+ }
612
+
613
+ private fun dp(value: Int): Int = (value * resources.displayMetrics.density).toInt()
614
+
615
+ /** Icons of the system field: compound drawables, the trailing one tappable. */
616
+ private var systemStart: Drawable? = null
617
+ private var systemEnd: Drawable? = null
618
+
619
+ private fun applySystemIcons() {
620
+ val edit = editText ?: return
621
+ val start = systemStart
622
+ val end = systemEnd
623
+ edit.setCompoundDrawablesRelativeWithIntrinsicBounds(start, null, end, null)
624
+ val tinted = (start != null && leadingIcon.tinted) || (end != null && trailingIcon.tinted)
625
+ TextViewCompat.setCompoundDrawableTintList(
626
+ edit,
627
+ if (tinted) ColorStateList.valueOf(themeColor(android.R.attr.colorControlNormal, 0xFF757575.toInt())) else null
628
+ )
629
+ if (end != null) {
630
+ edit.setOnTouchListener { v, event ->
631
+ if (event.action == MotionEvent.ACTION_UP) {
632
+ val width = end.bounds.width() + edit.compoundDrawablePadding
633
+ val isRtl = v.layoutDirection == View.LAYOUT_DIRECTION_RTL
634
+ val hit = if (isRtl) event.x <= v.paddingStart + width else event.x >= v.width - v.paddingEnd - width
635
+ if (hit) {
636
+ v.performClick()
637
+ onTrailingIconPress?.invoke()
638
+ return@setOnTouchListener true
639
+ }
640
+ }
641
+ false
642
+ }
643
+ } else {
644
+ edit.setOnTouchListener(null)
645
+ }
646
+ requestLayout()
647
+ }
648
+
649
+ private fun applyStartIcon() {
650
+ startIconGeneration += 1
651
+ val generation = startIconGeneration
652
+ val til = layout
653
+ if (til == null) {
654
+ systemStart = null
655
+ if (leadingIcon.isPresent) {
656
+ loadIcon(leadingIcon) { drawable ->
657
+ if (generation != startIconGeneration || layout != null) return@loadIcon
658
+ systemStart = drawable
659
+ applySystemIcons()
660
+ }
661
+ }
662
+ applySystemIcons()
663
+ return
664
+ }
665
+ if (!leadingIcon.isPresent) {
666
+ til.startIconDrawable = null
667
+ requestLayout()
668
+ return
669
+ }
670
+ loadIcon(leadingIcon) { drawable ->
671
+ if (generation != startIconGeneration || layout !== til) return@loadIcon
672
+ if (!leadingIcon.tinted) til.setStartIconTintList(null)
673
+ til.startIconDrawable = drawable
674
+ requestLayout()
675
+ }
676
+ }
677
+
678
+ /**
679
+ * The end icon slot, by priority: a custom trailing icon, the password
680
+ * toggle, the clear button (shown while the focused field has text).
681
+ */
682
+ private fun applyEndIcon() {
683
+ endIconGeneration += 1
684
+ val generation = endIconGeneration
685
+ val til = layout
686
+ if (til == null) {
687
+ // The platform field has no clear button or password toggle
688
+ systemEnd = null
689
+ if (trailingIcon.isPresent) {
690
+ loadIcon(trailingIcon) { drawable ->
691
+ if (generation != endIconGeneration || layout != null) return@loadIcon
692
+ systemEnd = drawable
693
+ applySystemIcons()
694
+ }
695
+ }
696
+ applySystemIcons()
697
+ return
698
+ }
699
+ when {
700
+ trailingIcon.isPresent -> {
701
+ til.endIconMode = TextInputLayout.END_ICON_CUSTOM
702
+ til.setEndIconOnClickListener { onTrailingIconPress?.invoke() }
703
+ loadIcon(trailingIcon) { drawable ->
704
+ if (generation != endIconGeneration || layout !== til) return@loadIcon
705
+ if (!trailingIcon.tinted) til.setEndIconTintList(null)
706
+ til.endIconDrawable = drawable
707
+ requestLayout()
708
+ }
709
+ }
710
+ passwordToggle -> til.endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE
711
+ clearButtonMode != "never" -> til.endIconMode = TextInputLayout.END_ICON_CLEAR_TEXT
712
+ else -> til.endIconMode = TextInputLayout.END_ICON_NONE
713
+ }
714
+ // Material replaces the end icon with its error icon while an error shows;
715
+ // a password toggle or a custom icon stays, since the user still needs it
716
+ til.errorIconDrawable = if (trailingIcon.isPresent || passwordToggle) null else defaultErrorIcon
717
+ requestLayout()
718
+ }
719
+
720
+ private fun applyCounterAndLength() {
721
+ val edit = editText ?: return
722
+ edit.filters = if (maxLength > 0) arrayOf(InputFilter.LengthFilter(maxLength)) else emptyArray()
723
+ val til = layout ?: return
724
+ til.counterMaxLength = maxLength
725
+ til.isCounterEnabled = characterCount
726
+ requestLayout()
727
+ }
728
+
729
+ private fun applyInputType() {
730
+ val edit = editText ?: return
731
+
732
+ var type = when (keyboardType) {
733
+ "numeric" -> InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL or InputType.TYPE_NUMBER_FLAG_SIGNED
734
+ "number-pad" -> InputType.TYPE_CLASS_NUMBER
735
+ "decimal-pad" -> InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
736
+ "phone-pad" -> InputType.TYPE_CLASS_PHONE
737
+ "email-address" -> InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
738
+ "url" -> InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI
739
+ "visible-password" -> InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
740
+ else -> InputType.TYPE_CLASS_TEXT
741
+ }
742
+ val isText = (type and InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT
743
+ val isNumber = (type and InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_NUMBER
744
+
745
+ if (secure) {
746
+ if (isText) {
747
+ type = (type and InputType.TYPE_MASK_VARIATION.inv()) or InputType.TYPE_TEXT_VARIATION_PASSWORD
748
+ } else if (isNumber) {
749
+ type = type or InputType.TYPE_NUMBER_VARIATION_PASSWORD
750
+ }
751
+ }
752
+
753
+ if (isText && !secure) {
754
+ type = type or when (autoCapitalize) {
755
+ "characters" -> InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
756
+ "words" -> InputType.TYPE_TEXT_FLAG_CAP_WORDS
757
+ "sentences" -> InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
758
+ else -> 0
759
+ }
760
+ type = type or if (autoCorrect) {
761
+ InputType.TYPE_TEXT_FLAG_AUTO_CORRECT
762
+ } else {
763
+ InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
764
+ }
765
+ }
766
+
767
+ if (multiline && isText) {
768
+ type = type or InputType.TYPE_TEXT_FLAG_MULTI_LINE
769
+ }
770
+
771
+ if (edit.inputType != type) {
772
+ // Setting the input type resets the typeface (password fields default
773
+ // to monospace) and the transformation; the font is re-applied below.
774
+ edit.inputType = type
775
+ }
776
+
777
+ edit.imeOptions = when (returnKeyType) {
778
+ "done" -> EditorInfo.IME_ACTION_DONE
779
+ "go" -> EditorInfo.IME_ACTION_GO
780
+ "next" -> EditorInfo.IME_ACTION_NEXT
781
+ "search" -> EditorInfo.IME_ACTION_SEARCH
782
+ "send" -> EditorInfo.IME_ACTION_SEND
783
+ "previous" -> EditorInfo.IME_ACTION_PREVIOUS
784
+ "none" -> EditorInfo.IME_ACTION_NONE
785
+ else -> EditorInfo.IME_ACTION_UNSPECIFIED
786
+ }
787
+
788
+ if (multiline) {
789
+ edit.gravity = Gravity.TOP or Gravity.START
790
+ edit.minLines = 1
791
+ } else {
792
+ edit.gravity = Gravity.CENTER_VERTICAL or Gravity.START
793
+ }
794
+
795
+ applyFont(edit)
796
+ requestLayout()
797
+ }
798
+
799
+ private fun applyFont(edit: TextInputEditText) {
800
+ PCButtonSupport.applyFont(edit, fontFamily, fontSize, fontWeight, fontStyle)
801
+ }
802
+
803
+ private fun applyAutofill() {
804
+ val edit = editText ?: return
805
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
806
+ when (autoComplete) {
807
+ "" -> {
808
+ edit.importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_AUTO
809
+ edit.setAutofillHints()
810
+ }
811
+ "off" -> {
812
+ edit.importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_NO
813
+ edit.setAutofillHints()
814
+ }
815
+ else -> {
816
+ val hint = autofillHint(autoComplete)
817
+ edit.importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_YES
818
+ if (hint != null) edit.setAutofillHints(hint) else edit.setAutofillHints()
819
+ }
820
+ }
821
+ }
822
+
823
+ /** React Native autoComplete values onto the Android autofill hints. */
824
+ private fun autofillHint(value: String): String? = when (value) {
825
+ "username" -> View.AUTOFILL_HINT_USERNAME
826
+ "password" -> View.AUTOFILL_HINT_PASSWORD
827
+ "new-password" -> "newPassword" // HintConstants.AUTOFILL_HINT_NEW_PASSWORD
828
+ "one-time-code" -> "smsOTPCode" // HintConstants.AUTOFILL_HINT_SMS_OTP
829
+ "email" -> View.AUTOFILL_HINT_EMAIL_ADDRESS
830
+ "name" -> View.AUTOFILL_HINT_NAME
831
+ "given-name" -> "personGivenName" // HintConstants.AUTOFILL_HINT_PERSON_NAME_GIVEN
832
+ "family-name" -> "personFamilyName" // HintConstants.AUTOFILL_HINT_PERSON_NAME_FAMILY
833
+ "tel" -> View.AUTOFILL_HINT_PHONE
834
+ "street-address" -> View.AUTOFILL_HINT_POSTAL_ADDRESS
835
+ "postal-code" -> View.AUTOFILL_HINT_POSTAL_CODE
836
+ "country" -> "addressCountry" // HintConstants.AUTOFILL_HINT_POSTAL_ADDRESS_COUNTRY
837
+ "cc-number" -> View.AUTOFILL_HINT_CREDIT_CARD_NUMBER
838
+ "cc-exp" -> View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE
839
+ "cc-csc" -> View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE
840
+ else -> null
841
+ }
842
+
843
+ /**
844
+ * Resolves an icon. Drawable names and release-bundled assets (which React
845
+ * Native ships as drawable resources) resolve synchronously; other image
846
+ * URIs load in the background and are delivered when they arrive.
847
+ */
848
+ private fun loadIcon(icon: PCButtonSupport.Icon, onLoaded: (Drawable?) -> Unit) {
849
+ when (icon.type) {
850
+ "drawable" -> onLoaded(ResourceDrawableIdHelper.instance.getResourceDrawable(context, icon.name))
851
+ "image" -> {
852
+ val uri = icon.uri
853
+ if (!uri.contains(':')) {
854
+ onLoaded(ResourceDrawableIdHelper.instance.getResourceDrawable(context, uri))
855
+ } else {
856
+ PCImageLoader.load(context, uri, icon.scale) { bitmap ->
857
+ onLoaded(bitmap?.let { BitmapDrawable(context.resources, it) })
858
+ }
859
+ }
860
+ }
861
+ else -> onLoaded(null)
862
+ }
863
+ }
864
+
865
+ // ---- Keyboard and focus ----
866
+
867
+ private fun showKeyboard(view: View) {
868
+ val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
869
+ imm?.showSoftInput(view, 0)
870
+ }
871
+
872
+ private fun hideKeyboard() {
873
+ val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
874
+ imm?.hideSoftInputFromWindow(windowToken, 0)
875
+ }
876
+
877
+ /**
878
+ * Drops focus. Android 9 and below would hand it to the next focusable
879
+ * view (often another text field) when the root view refocuses; block
880
+ * that, as the core TextInput does.
881
+ */
882
+ private fun clearFocusAndMaybeRefocus(edit: View) {
883
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P || !edit.isInTouchMode) {
884
+ edit.clearFocus()
885
+ return
886
+ }
887
+ val root = rootView as? ViewGroup
888
+ if (root == null) {
889
+ edit.clearFocus()
890
+ return
891
+ }
892
+ val previous = root.descendantFocusability
893
+ root.descendantFocusability = ViewGroup.FOCUS_BLOCK_DESCENDANTS
894
+ edit.clearFocus()
895
+ root.descendantFocusability = previous
896
+ }
897
+
898
+ /**
899
+ * The edit text. The keyboard's back key hides the keyboard; the field
900
+ * drops focus with it so JS gets onBlur, as the core TextInput does not,
901
+ * but a Material field with a focused label and no keyboard looks stuck.
902
+ */
903
+ private inner class FieldEditText(context: Context) : TextInputEditText(context) {
904
+ override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
905
+ super.onFocusChanged(focused, direction, previouslyFocusedRect)
906
+ onFocusChange?.invoke(focused, this@PCTextFieldView.text)
907
+ }
908
+
909
+ override fun onKeyPreIme(keyCode: Int, event: KeyEvent): Boolean {
910
+ if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP && isFocused) {
911
+ clearFocusAndMaybeRefocus(this)
912
+ }
913
+ return super.onKeyPreIme(keyCode, event)
914
+ }
915
+ }
916
+
917
+ // ---- Layout ----
918
+
919
+ /**
920
+ * React Native's root view ignores requestLayout after the initial pass, so
921
+ * a native view whose subtree changes later (a longer multi-line text, an
922
+ * error message appearing) has to measure and lay itself out. Coalesced to
923
+ * one pass per frame.
924
+ */
925
+ override fun requestLayout() {
926
+ super.requestLayout()
927
+ if (!manualLayoutPending) {
928
+ manualLayoutPending = true
929
+ post { runManualLayout() }
930
+ }
931
+ }
932
+
933
+ private fun runManualLayout() {
934
+ manualLayoutPending = false
935
+ // Nothing to do until Yoga has placed us; the regular pass covers that.
936
+ if (width == 0 || height == 0) return
937
+ measure(
938
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
939
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
940
+ )
941
+ layout(left, top, right, bottom)
942
+ }
943
+
944
+ // ---- Measurement ----
945
+
946
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
947
+ val child = widget
948
+ if (child == null) {
949
+ setMeasuredDimension(0, 0)
950
+ return
951
+ }
952
+
953
+ // The field fills the width it is given and takes the height its
954
+ // content needs; Yoga catches up through the state update.
955
+ val width = resolveWidth(widthMeasureSpec)
956
+ measureWidget(child, width)
957
+ val naturalHeight = child.measuredHeight
958
+ val heightSize = MeasureSpec.getSize(heightMeasureSpec)
959
+ val height = when (MeasureSpec.getMode(heightMeasureSpec)) {
960
+ // Fabric may give us 0 before the state is applied; keep the natural size then.
961
+ MeasureSpec.EXACTLY -> if (heightSize > 0) heightSize else naturalHeight
962
+ MeasureSpec.AT_MOST -> minOf(naturalHeight, heightSize)
963
+ else -> naturalHeight
964
+ }
965
+ setMeasuredDimension(width, height)
966
+ reportHeight(naturalHeight)
967
+ }
968
+
969
+ /** Measures the widget at [width], as tall as its content needs. */
970
+ private fun measureWidget(child: View, width: Int) {
971
+ child.measure(
972
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
973
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
974
+ )
975
+ }
976
+
977
+ private fun resolveWidth(spec: Int): Int {
978
+ val size = MeasureSpec.getSize(spec)
979
+ return if (size > 0) size else (PCConstants.FALLBACK_WIDTH_DP * resources.displayMetrics.density).toInt()
980
+ }
981
+
982
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
983
+ val child = widget ?: return
984
+ // Keep the widget at its natural height even when Yoga hasn't caught up
985
+ child.layout(0, 0, right - left, child.measuredHeight)
986
+ // TextInputLayout makes room for its start icon and prefix (invisible
987
+ // compound drawables on the edit text) from a ViewTreeObserver global
988
+ // layout callback, which only a window traversal delivers. React Native's
989
+ // root doesn't run one for a native subtree that laid itself out, so run
990
+ // it here; when it changes something it asks for another pass.
991
+ layout?.onGlobalLayout()
992
+ }
993
+
994
+ /** Reports the intrinsic height to Fabric; called as soon as the state wrapper is available. */
995
+ fun reportIntrinsicSize() {
996
+ val child = widget ?: return
997
+ val width = if (width > 0) width else (PCConstants.FALLBACK_WIDTH_DP * resources.displayMetrics.density).toInt()
998
+ measureWidget(child, width)
999
+ reportHeight(child.measuredHeight)
1000
+ }
1001
+
1002
+ /**
1003
+ * Update Fabric state with the field's natural height. The width stays 0:
1004
+ * the shadow node fills the available width.
1005
+ */
1006
+ private fun reportHeight(heightPx: Int) {
1007
+ val wrapper = stateWrapper ?: return
1008
+ val heightDp = PixelUtil.toDIPFromPixel(heightPx.toFloat())
1009
+ if (heightDp == lastReportedHeight) return
1010
+ lastReportedHeight = heightDp
1011
+
1012
+ val stateData = WritableNativeMap().apply {
1013
+ putDouble("width", 0.0)
1014
+ putDouble("height", heightDp.toDouble())
1015
+ }
1016
+ wrapper.updateState(stateData)
1017
+ }
1018
+ }