react-native-platform-components 1.3.0 → 1.5.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 (636) hide show
  1. package/PlatformComponents.podspec +1 -1
  2. package/README.md +47 -51
  3. package/android/src/main/java/com/platformcomponents/PCButtonGroupView.kt +664 -0
  4. package/android/src/main/java/com/platformcomponents/PCButtonGroupViewManager.kt +241 -0
  5. package/android/src/main/java/com/platformcomponents/PCButtonSupport.kt +159 -0
  6. package/android/src/main/java/com/platformcomponents/PCButtonView.kt +527 -0
  7. package/android/src/main/java/com/platformcomponents/PCButtonViewManager.kt +204 -0
  8. package/android/src/main/java/com/platformcomponents/PCContextMenuView.kt +41 -151
  9. package/android/src/main/java/com/platformcomponents/PCContextMenuViewManager.kt +7 -63
  10. package/android/src/main/java/com/platformcomponents/PCDatePickerView.kt +310 -115
  11. package/android/src/main/java/com/platformcomponents/PCDatePickerViewManager.kt +23 -3
  12. package/android/src/main/java/com/platformcomponents/PCExpressive.kt +112 -0
  13. package/android/src/main/java/com/platformcomponents/PCFloatingActionButtonView.kt +485 -0
  14. package/android/src/main/java/com/platformcomponents/PCFloatingActionButtonViewManager.kt +109 -0
  15. package/android/src/main/java/com/platformcomponents/PCFloatingToolbarView.kt +247 -0
  16. package/android/src/main/java/com/platformcomponents/PCFloatingToolbarViewManager.kt +73 -0
  17. package/android/src/main/java/com/platformcomponents/PCHaptics.kt +46 -0
  18. package/android/src/main/java/com/platformcomponents/PCImageLoader.kt +42 -7
  19. package/android/src/main/java/com/platformcomponents/PCLiquidGlassContainerView.kt +32 -0
  20. package/android/src/main/java/com/platformcomponents/PCLiquidGlassContainerViewManager.kt +31 -0
  21. package/android/src/main/java/com/platformcomponents/PCLiquidGlassView.kt +32 -6
  22. package/android/src/main/java/com/platformcomponents/PCLiquidGlassViewManager.kt +4 -0
  23. package/android/src/main/java/com/platformcomponents/PCMenuSupport.kt +280 -0
  24. package/android/src/main/java/com/platformcomponents/PCNavigationBarSupport.kt +332 -0
  25. package/android/src/main/java/com/platformcomponents/PCNavigationRailView.kt +448 -0
  26. package/android/src/main/java/com/platformcomponents/PCNavigationRailViewManager.kt +176 -0
  27. package/android/src/main/java/com/platformcomponents/PCSegmentedControlView.kt +35 -17
  28. package/android/src/main/java/com/platformcomponents/PCSegmentedControlViewManager.kt +20 -3
  29. package/android/src/main/java/com/platformcomponents/PCSelectionMenuView.kt +371 -39
  30. package/android/src/main/java/com/platformcomponents/PCSelectionMenuViewManager.kt +18 -4
  31. package/android/src/main/java/com/platformcomponents/PCTabBarView.kt +397 -0
  32. package/android/src/main/java/com/platformcomponents/PCTabBarViewManager.kt +182 -0
  33. package/android/src/main/java/com/platformcomponents/PCTextFieldView.kt +1393 -0
  34. package/android/src/main/java/com/platformcomponents/PCTextFieldViewManager.kt +354 -0
  35. package/android/src/main/java/com/platformcomponents/PCThemeSupport.kt +37 -0
  36. package/android/src/main/java/com/platformcomponents/PlatformComponentsPackage.kt +8 -0
  37. package/android/src/main/res/drawable/pc_ic_search.xml +12 -0
  38. package/android/src/main/res/values/attrs.xml +13 -0
  39. package/android/src/main/res/values/ids.xml +5 -0
  40. package/android/src/main/res/values/styles.xml +207 -0
  41. package/ios/PCButton.h +10 -0
  42. package/ios/PCButton.mm +311 -0
  43. package/ios/PCButtonGroup.h +10 -0
  44. package/ios/PCButtonGroup.mm +323 -0
  45. package/ios/PCButtonGroupView.swift +473 -0
  46. package/ios/PCButtonSupport.swift +312 -0
  47. package/ios/PCButtonView.swift +438 -0
  48. package/ios/PCConstants.swift +23 -2
  49. package/ios/PCContextMenu.mm +28 -110
  50. package/ios/PCContextMenu.swift +106 -167
  51. package/ios/PCDatePicker.mm +21 -9
  52. package/ios/PCDatePickerView.swift +94 -20
  53. package/ios/PCFloatingActionButton.h +10 -0
  54. package/ios/PCFloatingActionButton.mm +177 -0
  55. package/ios/PCFloatingActionButtonView.swift +426 -0
  56. package/ios/PCFloatingToolbar.h +10 -0
  57. package/ios/PCFloatingToolbar.mm +103 -0
  58. package/ios/PCFloatingToolbarView.swift +336 -0
  59. package/ios/PCHaptics.swift +91 -0
  60. package/ios/PCImageLoader.swift +76 -12
  61. package/ios/PCLiquidGlass.mm +36 -0
  62. package/ios/PCLiquidGlass.swift +221 -114
  63. package/ios/PCLiquidGlassContainer.h +10 -0
  64. package/ios/PCLiquidGlassContainer.mm +84 -0
  65. package/ios/PCLiquidGlassContainer.swift +69 -0
  66. package/ios/PCMenuItems.h +76 -0
  67. package/ios/PCMenuSupport.swift +350 -0
  68. package/ios/PCNavigationRail.h +10 -0
  69. package/ios/PCNavigationRail.mm +270 -0
  70. package/ios/PCNavigationRailView.swift +415 -0
  71. package/ios/PCSegmentedControl.mm +17 -2
  72. package/ios/PCSegmentedControl.swift +57 -7
  73. package/ios/PCSelectionMenu.mm +36 -8
  74. package/ios/PCSelectionMenu.swift +224 -48
  75. package/ios/PCTabBar.h +10 -0
  76. package/ios/PCTabBar.mm +285 -0
  77. package/ios/PCTabBarAccessory.h +10 -0
  78. package/ios/PCTabBarAccessory.mm +93 -0
  79. package/ios/PCTabBarAccessoryView.swift +96 -0
  80. package/ios/PCTabBarView.swift +671 -0
  81. package/ios/PCTextField.h +10 -0
  82. package/ios/PCTextField.mm +546 -0
  83. package/ios/PCTextFieldView.swift +1558 -0
  84. package/lib/commonjs/Button.js +149 -0
  85. package/lib/commonjs/Button.js.map +1 -0
  86. package/lib/commonjs/ButtonGroup.js +109 -0
  87. package/lib/commonjs/ButtonGroup.js.map +1 -0
  88. package/lib/commonjs/ButtonGroupNativeComponent.ts +191 -0
  89. package/lib/commonjs/ButtonNativeComponent.ts +192 -0
  90. package/lib/commonjs/ContextMenu.js +9 -48
  91. package/lib/commonjs/ContextMenu.js.map +1 -1
  92. package/lib/commonjs/ContextMenuNativeComponent.ts +47 -49
  93. package/lib/commonjs/DatePicker.js +98 -2
  94. package/lib/commonjs/DatePicker.js.map +1 -1
  95. package/lib/commonjs/DatePickerNativeComponent.ts +19 -2
  96. package/lib/commonjs/FloatingActionButton.js +60 -0
  97. package/lib/commonjs/FloatingActionButton.js.map +1 -0
  98. package/lib/commonjs/FloatingActionButtonNativeComponent.ts +58 -0
  99. package/lib/commonjs/FloatingToolbar.js +77 -0
  100. package/lib/commonjs/FloatingToolbar.js.map +1 -0
  101. package/lib/commonjs/FloatingToolbarNativeComponent.ts +47 -0
  102. package/lib/commonjs/LiquidGlass.js +7 -1
  103. package/lib/commonjs/LiquidGlass.js.map +1 -1
  104. package/lib/commonjs/LiquidGlassContainer.js +32 -0
  105. package/lib/commonjs/LiquidGlassContainer.js.map +1 -0
  106. package/lib/commonjs/LiquidGlassContainerNativeComponent.ts +17 -0
  107. package/lib/commonjs/LiquidGlassNativeComponent.ts +8 -0
  108. package/lib/commonjs/NavigationRail.js +104 -0
  109. package/lib/commonjs/NavigationRail.js.map +1 -0
  110. package/lib/commonjs/NavigationRailNativeComponent.ts +107 -0
  111. package/lib/commonjs/PlatformComponentsProvider.js +18 -0
  112. package/lib/commonjs/PlatformComponentsProvider.js.map +1 -0
  113. package/lib/commonjs/SegmentedControl.js +18 -85
  114. package/lib/commonjs/SegmentedControl.js.map +1 -1
  115. package/lib/commonjs/SegmentedControlNativeComponent.ts +14 -0
  116. package/lib/commonjs/SelectionMenu.js +27 -3
  117. package/lib/commonjs/SelectionMenu.js.map +1 -1
  118. package/lib/commonjs/SelectionMenuNativeComponent.ts +26 -4
  119. package/lib/commonjs/SplitButton.js +98 -0
  120. package/lib/commonjs/SplitButton.js.map +1 -0
  121. package/lib/commonjs/TabBar.js +203 -0
  122. package/lib/commonjs/TabBar.js.map +1 -0
  123. package/lib/commonjs/TabBarAccessoryNativeComponent.ts +19 -0
  124. package/lib/commonjs/TabBarNativeComponent.ts +145 -0
  125. package/lib/commonjs/TextField.js +358 -0
  126. package/lib/commonjs/TextField.js.map +1 -0
  127. package/lib/commonjs/TextFieldNativeComponent.ts +324 -0
  128. package/lib/commonjs/focusRegistry.js +79 -0
  129. package/lib/commonjs/focusRegistry.js.map +1 -0
  130. package/lib/commonjs/haptics.js +2 -0
  131. package/lib/commonjs/haptics.js.map +1 -0
  132. package/lib/commonjs/icons.js +106 -0
  133. package/lib/commonjs/icons.js.map +1 -0
  134. package/lib/commonjs/index.js +143 -0
  135. package/lib/commonjs/index.js.map +1 -1
  136. package/lib/commonjs/index.web.js +157 -63
  137. package/lib/commonjs/index.web.js.map +1 -1
  138. package/lib/commonjs/jest.js +910 -0
  139. package/lib/commonjs/jest.js.map +1 -0
  140. package/lib/commonjs/labelStyle.js +27 -0
  141. package/lib/commonjs/labelStyle.js.map +1 -0
  142. package/lib/commonjs/menuItems.js +68 -0
  143. package/lib/commonjs/menuItems.js.map +1 -0
  144. package/lib/commonjs/reactNativeInternals.d.js +2 -0
  145. package/lib/commonjs/reactNativeInternals.d.js.map +1 -0
  146. package/lib/commonjs/selectionIdentifiers.js +18 -0
  147. package/lib/commonjs/selectionIdentifiers.js.map +1 -0
  148. package/lib/commonjs/submitBehavior.js +19 -0
  149. package/lib/commonjs/submitBehavior.js.map +1 -0
  150. package/lib/commonjs/tabItems.js +49 -0
  151. package/lib/commonjs/tabItems.js.map +1 -0
  152. package/lib/commonjs/web/Button.js +170 -0
  153. package/lib/commonjs/web/Button.js.map +1 -0
  154. package/lib/commonjs/web/ButtonGroup.js +119 -0
  155. package/lib/commonjs/web/ButtonGroup.js.map +1 -0
  156. package/lib/commonjs/web/DatePicker.js +168 -0
  157. package/lib/commonjs/web/DatePicker.js.map +1 -0
  158. package/lib/commonjs/web/DateRangePicker.js +137 -0
  159. package/lib/commonjs/web/DateRangePicker.js.map +1 -0
  160. package/lib/commonjs/web/Dialog.js +54 -0
  161. package/lib/commonjs/web/Dialog.js.map +1 -0
  162. package/lib/commonjs/web/FloatingActionButton.js +98 -0
  163. package/lib/commonjs/web/FloatingActionButton.js.map +1 -0
  164. package/lib/commonjs/web/Icon.js +34 -0
  165. package/lib/commonjs/web/Icon.js.map +1 -0
  166. package/lib/commonjs/web/NativeTheme.js +28 -0
  167. package/lib/commonjs/web/NativeTheme.js.map +1 -0
  168. package/lib/commonjs/web/NavigationRail.js +139 -0
  169. package/lib/commonjs/web/NavigationRail.js.map +1 -0
  170. package/lib/commonjs/web/PlatformComponentsProvider.js +51 -0
  171. package/lib/commonjs/web/PlatformComponentsProvider.js.map +1 -0
  172. package/lib/commonjs/web/SegmentedControl.js +117 -0
  173. package/lib/commonjs/web/SegmentedControl.js.map +1 -0
  174. package/lib/commonjs/web/SelectionMenu.js +142 -0
  175. package/lib/commonjs/web/SelectionMenu.js.map +1 -0
  176. package/lib/commonjs/web/SplitButton.js +35 -0
  177. package/lib/commonjs/web/SplitButton.js.map +1 -0
  178. package/lib/commonjs/web/TabBar.js +131 -0
  179. package/lib/commonjs/web/TabBar.js.map +1 -0
  180. package/lib/commonjs/web/TextField.js +322 -0
  181. package/lib/commonjs/web/TextField.js.map +1 -0
  182. package/lib/commonjs/web/components.js +36 -0
  183. package/lib/commonjs/web/components.js.map +1 -0
  184. package/lib/commonjs/web/containers.js +121 -0
  185. package/lib/commonjs/web/containers.js.map +1 -0
  186. package/lib/commonjs/web/dateInput.js +116 -0
  187. package/lib/commonjs/web/dateInput.js.map +1 -0
  188. package/lib/commonjs/web/shared.js +192 -0
  189. package/lib/commonjs/web/shared.js.map +1 -0
  190. package/lib/commonjs/webComponents.js +6 -0
  191. package/lib/commonjs/webComponents.js.map +1 -0
  192. package/lib/module/Button.js +142 -0
  193. package/lib/module/Button.js.map +1 -0
  194. package/lib/module/ButtonGroup.js +102 -0
  195. package/lib/module/ButtonGroup.js.map +1 -0
  196. package/lib/module/ButtonGroupNativeComponent.ts +191 -0
  197. package/lib/module/ButtonNativeComponent.ts +192 -0
  198. package/lib/module/ContextMenu.js +9 -48
  199. package/lib/module/ContextMenu.js.map +1 -1
  200. package/lib/module/ContextMenuNativeComponent.ts +47 -49
  201. package/lib/module/DatePicker.js +98 -4
  202. package/lib/module/DatePicker.js.map +1 -1
  203. package/lib/module/DatePickerNativeComponent.ts +19 -2
  204. package/lib/module/FloatingActionButton.js +53 -0
  205. package/lib/module/FloatingActionButton.js.map +1 -0
  206. package/lib/module/FloatingActionButtonNativeComponent.ts +58 -0
  207. package/lib/module/FloatingToolbar.js +70 -0
  208. package/lib/module/FloatingToolbar.js.map +1 -0
  209. package/lib/module/FloatingToolbarNativeComponent.ts +47 -0
  210. package/lib/module/LiquidGlass.js +7 -1
  211. package/lib/module/LiquidGlass.js.map +1 -1
  212. package/lib/module/LiquidGlassContainer.js +26 -0
  213. package/lib/module/LiquidGlassContainer.js.map +1 -0
  214. package/lib/module/LiquidGlassContainerNativeComponent.ts +17 -0
  215. package/lib/module/LiquidGlassNativeComponent.ts +8 -0
  216. package/lib/module/NavigationRail.js +99 -0
  217. package/lib/module/NavigationRail.js.map +1 -0
  218. package/lib/module/NavigationRailNativeComponent.ts +107 -0
  219. package/lib/module/PlatformComponentsProvider.js +13 -0
  220. package/lib/module/PlatformComponentsProvider.js.map +1 -0
  221. package/lib/module/SegmentedControl.js +18 -85
  222. package/lib/module/SegmentedControl.js.map +1 -1
  223. package/lib/module/SegmentedControlNativeComponent.ts +14 -0
  224. package/lib/module/SelectionMenu.js +27 -3
  225. package/lib/module/SelectionMenu.js.map +1 -1
  226. package/lib/module/SelectionMenuNativeComponent.ts +26 -4
  227. package/lib/module/SplitButton.js +91 -0
  228. package/lib/module/SplitButton.js.map +1 -0
  229. package/lib/module/TabBar.js +196 -0
  230. package/lib/module/TabBar.js.map +1 -0
  231. package/lib/module/TabBarAccessoryNativeComponent.ts +19 -0
  232. package/lib/module/TabBarNativeComponent.ts +145 -0
  233. package/lib/module/TextField.js +352 -0
  234. package/lib/module/TextField.js.map +1 -0
  235. package/lib/module/TextFieldNativeComponent.ts +324 -0
  236. package/lib/module/focusRegistry.js +75 -0
  237. package/lib/module/focusRegistry.js.map +1 -0
  238. package/lib/module/haptics.js +2 -0
  239. package/lib/module/haptics.js.map +1 -0
  240. package/lib/module/icons.js +101 -0
  241. package/lib/module/icons.js.map +1 -0
  242. package/lib/module/index.js +13 -0
  243. package/lib/module/index.js.map +1 -1
  244. package/lib/module/index.web.js +12 -59
  245. package/lib/module/index.web.js.map +1 -1
  246. package/lib/module/jest.js +840 -0
  247. package/lib/module/jest.js.map +1 -0
  248. package/lib/module/labelStyle.js +23 -0
  249. package/lib/module/labelStyle.js.map +1 -0
  250. package/lib/module/menuItems.js +65 -0
  251. package/lib/module/menuItems.js.map +1 -0
  252. package/lib/module/reactNativeInternals.d.js +2 -0
  253. package/lib/module/reactNativeInternals.d.js.map +1 -0
  254. package/lib/module/selectionIdentifiers.js +14 -0
  255. package/lib/module/selectionIdentifiers.js.map +1 -0
  256. package/lib/module/submitBehavior.js +15 -0
  257. package/lib/module/submitBehavior.js.map +1 -0
  258. package/lib/module/tabItems.js +45 -0
  259. package/lib/module/tabItems.js.map +1 -0
  260. package/lib/module/web/Button.js +164 -0
  261. package/lib/module/web/Button.js.map +1 -0
  262. package/lib/module/web/ButtonGroup.js +113 -0
  263. package/lib/module/web/ButtonGroup.js.map +1 -0
  264. package/lib/module/web/DatePicker.js +162 -0
  265. package/lib/module/web/DatePicker.js.map +1 -0
  266. package/lib/module/web/DateRangePicker.js +131 -0
  267. package/lib/module/web/DateRangePicker.js.map +1 -0
  268. package/lib/module/web/Dialog.js +48 -0
  269. package/lib/module/web/Dialog.js.map +1 -0
  270. package/lib/module/web/FloatingActionButton.js +94 -0
  271. package/lib/module/web/FloatingActionButton.js.map +1 -0
  272. package/lib/module/web/Icon.js +29 -0
  273. package/lib/module/web/Icon.js.map +1 -0
  274. package/lib/module/web/NativeTheme.js +23 -0
  275. package/lib/module/web/NativeTheme.js.map +1 -0
  276. package/lib/module/web/NavigationRail.js +133 -0
  277. package/lib/module/web/NavigationRail.js.map +1 -0
  278. package/lib/module/web/PlatformComponentsProvider.js +44 -0
  279. package/lib/module/web/PlatformComponentsProvider.js.map +1 -0
  280. package/lib/module/web/SegmentedControl.js +112 -0
  281. package/lib/module/web/SegmentedControl.js.map +1 -0
  282. package/lib/module/web/SelectionMenu.js +137 -0
  283. package/lib/module/web/SelectionMenu.js.map +1 -0
  284. package/lib/module/web/SplitButton.js +30 -0
  285. package/lib/module/web/SplitButton.js.map +1 -0
  286. package/lib/module/web/TabBar.js +126 -0
  287. package/lib/module/web/TabBar.js.map +1 -0
  288. package/lib/module/web/TextField.js +316 -0
  289. package/lib/module/web/TextField.js.map +1 -0
  290. package/lib/module/web/components.js +33 -0
  291. package/lib/module/web/components.js.map +1 -0
  292. package/lib/module/web/containers.js +112 -0
  293. package/lib/module/web/containers.js.map +1 -0
  294. package/lib/module/web/dateInput.js +109 -0
  295. package/lib/module/web/dateInput.js.map +1 -0
  296. package/lib/module/web/shared.js +179 -0
  297. package/lib/module/web/shared.js.map +1 -0
  298. package/lib/module/webComponents.js +4 -0
  299. package/lib/module/webComponents.js.map +1 -0
  300. package/lib/typescript/commonjs/docs/sidebars.d.ts.map +1 -1
  301. package/lib/typescript/commonjs/src/Button.d.ts +184 -0
  302. package/lib/typescript/commonjs/src/Button.d.ts.map +1 -0
  303. package/lib/typescript/commonjs/src/ButtonGroup.d.ts +117 -0
  304. package/lib/typescript/commonjs/src/ButtonGroup.d.ts.map +1 -0
  305. package/lib/typescript/commonjs/src/ButtonGroupNativeComponent.d.ts +154 -0
  306. package/lib/typescript/commonjs/src/ButtonGroupNativeComponent.d.ts.map +1 -0
  307. package/lib/typescript/commonjs/src/ButtonNativeComponent.d.ts +148 -0
  308. package/lib/typescript/commonjs/src/ButtonNativeComponent.d.ts.map +1 -0
  309. package/lib/typescript/commonjs/src/ContextMenu.d.ts +20 -33
  310. package/lib/typescript/commonjs/src/ContextMenu.d.ts.map +1 -1
  311. package/lib/typescript/commonjs/src/ContextMenuNativeComponent.d.ts +45 -47
  312. package/lib/typescript/commonjs/src/ContextMenuNativeComponent.d.ts.map +1 -1
  313. package/lib/typescript/commonjs/src/DatePicker.d.ts +74 -1
  314. package/lib/typescript/commonjs/src/DatePicker.d.ts.map +1 -1
  315. package/lib/typescript/commonjs/src/DatePickerNativeComponent.d.ts +16 -1
  316. package/lib/typescript/commonjs/src/DatePickerNativeComponent.d.ts.map +1 -1
  317. package/lib/typescript/commonjs/src/FloatingActionButton.d.ts +67 -0
  318. package/lib/typescript/commonjs/src/FloatingActionButton.d.ts.map +1 -0
  319. package/lib/typescript/commonjs/src/FloatingActionButtonNativeComponent.d.ts +43 -0
  320. package/lib/typescript/commonjs/src/FloatingActionButtonNativeComponent.d.ts.map +1 -0
  321. package/lib/typescript/commonjs/src/FloatingToolbar.d.ts +75 -0
  322. package/lib/typescript/commonjs/src/FloatingToolbar.d.ts.map +1 -0
  323. package/lib/typescript/commonjs/src/FloatingToolbarNativeComponent.d.ts +36 -0
  324. package/lib/typescript/commonjs/src/FloatingToolbarNativeComponent.d.ts.map +1 -0
  325. package/lib/typescript/commonjs/src/LiquidGlass.d.ts +17 -0
  326. package/lib/typescript/commonjs/src/LiquidGlass.d.ts.map +1 -1
  327. package/lib/typescript/commonjs/src/LiquidGlassContainer.d.ts +27 -0
  328. package/lib/typescript/commonjs/src/LiquidGlassContainer.d.ts.map +1 -0
  329. package/lib/typescript/commonjs/src/LiquidGlassContainerNativeComponent.d.ts +13 -0
  330. package/lib/typescript/commonjs/src/LiquidGlassContainerNativeComponent.d.ts.map +1 -0
  331. package/lib/typescript/commonjs/src/LiquidGlassNativeComponent.d.ts +7 -0
  332. package/lib/typescript/commonjs/src/LiquidGlassNativeComponent.d.ts.map +1 -1
  333. package/lib/typescript/commonjs/src/NavigationRail.d.ts +86 -0
  334. package/lib/typescript/commonjs/src/NavigationRail.d.ts.map +1 -0
  335. package/lib/typescript/commonjs/src/NavigationRailNativeComponent.d.ts +81 -0
  336. package/lib/typescript/commonjs/src/NavigationRailNativeComponent.d.ts.map +1 -0
  337. package/lib/typescript/commonjs/src/PlatformComponentsProvider.d.ts +6 -0
  338. package/lib/typescript/commonjs/src/PlatformComponentsProvider.d.ts.map +1 -0
  339. package/lib/typescript/commonjs/src/SegmentedControl.d.ts +35 -47
  340. package/lib/typescript/commonjs/src/SegmentedControl.d.ts.map +1 -1
  341. package/lib/typescript/commonjs/src/SegmentedControlNativeComponent.d.ts +11 -0
  342. package/lib/typescript/commonjs/src/SegmentedControlNativeComponent.d.ts.map +1 -1
  343. package/lib/typescript/commonjs/src/SelectionMenu.d.ts +44 -2
  344. package/lib/typescript/commonjs/src/SelectionMenu.d.ts.map +1 -1
  345. package/lib/typescript/commonjs/src/SelectionMenuNativeComponent.d.ts +20 -4
  346. package/lib/typescript/commonjs/src/SelectionMenuNativeComponent.d.ts.map +1 -1
  347. package/lib/typescript/commonjs/src/SplitButton.d.ts +83 -0
  348. package/lib/typescript/commonjs/src/SplitButton.d.ts.map +1 -0
  349. package/lib/typescript/commonjs/src/TabBar.d.ts +144 -0
  350. package/lib/typescript/commonjs/src/TabBar.d.ts.map +1 -0
  351. package/lib/typescript/commonjs/src/TabBarAccessoryNativeComponent.d.ts +13 -0
  352. package/lib/typescript/commonjs/src/TabBarAccessoryNativeComponent.d.ts.map +1 -0
  353. package/lib/typescript/commonjs/src/TabBarNativeComponent.d.ts +110 -0
  354. package/lib/typescript/commonjs/src/TabBarNativeComponent.d.ts.map +1 -0
  355. package/lib/typescript/commonjs/src/TextField.d.ts +332 -0
  356. package/lib/typescript/commonjs/src/TextField.d.ts.map +1 -0
  357. package/lib/typescript/commonjs/src/TextFieldNativeComponent.d.ts +240 -0
  358. package/lib/typescript/commonjs/src/TextFieldNativeComponent.d.ts.map +1 -0
  359. package/lib/typescript/commonjs/src/focusRegistry.d.ts +13 -0
  360. package/lib/typescript/commonjs/src/focusRegistry.d.ts.map +1 -0
  361. package/lib/typescript/commonjs/src/haptics.d.ts +20 -0
  362. package/lib/typescript/commonjs/src/haptics.d.ts.map +1 -0
  363. package/lib/typescript/commonjs/src/icons.d.ts +58 -0
  364. package/lib/typescript/commonjs/src/icons.d.ts.map +1 -0
  365. package/lib/typescript/commonjs/src/index.d.ts +13 -0
  366. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  367. package/lib/typescript/commonjs/src/index.web.d.ts +25 -133
  368. package/lib/typescript/commonjs/src/index.web.d.ts.map +1 -1
  369. package/lib/typescript/commonjs/src/jest.d.ts +139 -0
  370. package/lib/typescript/commonjs/src/jest.d.ts.map +1 -0
  371. package/lib/typescript/commonjs/src/labelStyle.d.ts +20 -0
  372. package/lib/typescript/commonjs/src/labelStyle.d.ts.map +1 -0
  373. package/lib/typescript/commonjs/src/menuItems.d.ts +100 -0
  374. package/lib/typescript/commonjs/src/menuItems.d.ts.map +1 -0
  375. package/lib/typescript/commonjs/src/selectionIdentifiers.d.ts +3 -0
  376. package/lib/typescript/commonjs/src/selectionIdentifiers.d.ts.map +1 -0
  377. package/lib/typescript/commonjs/src/sharedTypes.d.ts +6 -1
  378. package/lib/typescript/commonjs/src/sharedTypes.d.ts.map +1 -1
  379. package/lib/typescript/commonjs/src/submitBehavior.d.ts +9 -0
  380. package/lib/typescript/commonjs/src/submitBehavior.d.ts.map +1 -0
  381. package/lib/typescript/commonjs/src/tabItems.d.ts +55 -0
  382. package/lib/typescript/commonjs/src/tabItems.d.ts.map +1 -0
  383. package/lib/typescript/commonjs/src/web/Button.d.ts +9 -0
  384. package/lib/typescript/commonjs/src/web/Button.d.ts.map +1 -0
  385. package/lib/typescript/commonjs/src/web/ButtonGroup.d.ts +11 -0
  386. package/lib/typescript/commonjs/src/web/ButtonGroup.d.ts.map +1 -0
  387. package/lib/typescript/commonjs/src/web/DatePicker.d.ts +14 -0
  388. package/lib/typescript/commonjs/src/web/DatePicker.d.ts.map +1 -0
  389. package/lib/typescript/commonjs/src/web/DateRangePicker.d.ts +11 -0
  390. package/lib/typescript/commonjs/src/web/DateRangePicker.d.ts.map +1 -0
  391. package/lib/typescript/commonjs/src/web/Dialog.d.ts +12 -0
  392. package/lib/typescript/commonjs/src/web/Dialog.d.ts.map +1 -0
  393. package/lib/typescript/commonjs/src/web/FloatingActionButton.d.ts +8 -0
  394. package/lib/typescript/commonjs/src/web/FloatingActionButton.d.ts.map +1 -0
  395. package/lib/typescript/commonjs/src/web/Icon.d.ts +12 -0
  396. package/lib/typescript/commonjs/src/web/Icon.d.ts.map +1 -0
  397. package/lib/typescript/commonjs/src/web/NativeTheme.d.ts +10 -0
  398. package/lib/typescript/commonjs/src/web/NativeTheme.d.ts.map +1 -0
  399. package/lib/typescript/commonjs/src/web/NavigationRail.d.ts +8 -0
  400. package/lib/typescript/commonjs/src/web/NavigationRail.d.ts.map +1 -0
  401. package/lib/typescript/commonjs/src/web/PlatformComponentsProvider.d.ts +10 -0
  402. package/lib/typescript/commonjs/src/web/PlatformComponentsProvider.d.ts.map +1 -0
  403. package/lib/typescript/commonjs/src/web/SegmentedControl.d.ts +8 -0
  404. package/lib/typescript/commonjs/src/web/SegmentedControl.d.ts.map +1 -0
  405. package/lib/typescript/commonjs/src/web/SelectionMenu.d.ts +10 -0
  406. package/lib/typescript/commonjs/src/web/SelectionMenu.d.ts.map +1 -0
  407. package/lib/typescript/commonjs/src/web/SplitButton.d.ts +9 -0
  408. package/lib/typescript/commonjs/src/web/SplitButton.d.ts.map +1 -0
  409. package/lib/typescript/commonjs/src/web/TabBar.d.ts +9 -0
  410. package/lib/typescript/commonjs/src/web/TabBar.d.ts.map +1 -0
  411. package/lib/typescript/commonjs/src/web/TextField.d.ts +9 -0
  412. package/lib/typescript/commonjs/src/web/TextField.d.ts.map +1 -0
  413. package/lib/typescript/commonjs/src/web/components.d.ts +17 -0
  414. package/lib/typescript/commonjs/src/web/components.d.ts.map +1 -0
  415. package/lib/typescript/commonjs/src/web/containers.d.ts +24 -0
  416. package/lib/typescript/commonjs/src/web/containers.d.ts.map +1 -0
  417. package/lib/typescript/commonjs/src/web/dateInput.d.ts +20 -0
  418. package/lib/typescript/commonjs/src/web/dateInput.d.ts.map +1 -0
  419. package/lib/typescript/commonjs/src/web/shared.d.ts +43 -0
  420. package/lib/typescript/commonjs/src/web/shared.d.ts.map +1 -0
  421. package/lib/typescript/commonjs/src/webComponents.d.ts +43 -0
  422. package/lib/typescript/commonjs/src/webComponents.d.ts.map +1 -0
  423. package/lib/typescript/module/docs/sidebars.d.ts.map +1 -1
  424. package/lib/typescript/module/src/Button.d.ts +184 -0
  425. package/lib/typescript/module/src/Button.d.ts.map +1 -0
  426. package/lib/typescript/module/src/ButtonGroup.d.ts +117 -0
  427. package/lib/typescript/module/src/ButtonGroup.d.ts.map +1 -0
  428. package/lib/typescript/module/src/ButtonGroupNativeComponent.d.ts +154 -0
  429. package/lib/typescript/module/src/ButtonGroupNativeComponent.d.ts.map +1 -0
  430. package/lib/typescript/module/src/ButtonNativeComponent.d.ts +148 -0
  431. package/lib/typescript/module/src/ButtonNativeComponent.d.ts.map +1 -0
  432. package/lib/typescript/module/src/ContextMenu.d.ts +20 -33
  433. package/lib/typescript/module/src/ContextMenu.d.ts.map +1 -1
  434. package/lib/typescript/module/src/ContextMenuNativeComponent.d.ts +45 -47
  435. package/lib/typescript/module/src/ContextMenuNativeComponent.d.ts.map +1 -1
  436. package/lib/typescript/module/src/DatePicker.d.ts +74 -1
  437. package/lib/typescript/module/src/DatePicker.d.ts.map +1 -1
  438. package/lib/typescript/module/src/DatePickerNativeComponent.d.ts +16 -1
  439. package/lib/typescript/module/src/DatePickerNativeComponent.d.ts.map +1 -1
  440. package/lib/typescript/module/src/FloatingActionButton.d.ts +67 -0
  441. package/lib/typescript/module/src/FloatingActionButton.d.ts.map +1 -0
  442. package/lib/typescript/module/src/FloatingActionButtonNativeComponent.d.ts +43 -0
  443. package/lib/typescript/module/src/FloatingActionButtonNativeComponent.d.ts.map +1 -0
  444. package/lib/typescript/module/src/FloatingToolbar.d.ts +75 -0
  445. package/lib/typescript/module/src/FloatingToolbar.d.ts.map +1 -0
  446. package/lib/typescript/module/src/FloatingToolbarNativeComponent.d.ts +36 -0
  447. package/lib/typescript/module/src/FloatingToolbarNativeComponent.d.ts.map +1 -0
  448. package/lib/typescript/module/src/LiquidGlass.d.ts +17 -0
  449. package/lib/typescript/module/src/LiquidGlass.d.ts.map +1 -1
  450. package/lib/typescript/module/src/LiquidGlassContainer.d.ts +27 -0
  451. package/lib/typescript/module/src/LiquidGlassContainer.d.ts.map +1 -0
  452. package/lib/typescript/module/src/LiquidGlassContainerNativeComponent.d.ts +13 -0
  453. package/lib/typescript/module/src/LiquidGlassContainerNativeComponent.d.ts.map +1 -0
  454. package/lib/typescript/module/src/LiquidGlassNativeComponent.d.ts +7 -0
  455. package/lib/typescript/module/src/LiquidGlassNativeComponent.d.ts.map +1 -1
  456. package/lib/typescript/module/src/NavigationRail.d.ts +86 -0
  457. package/lib/typescript/module/src/NavigationRail.d.ts.map +1 -0
  458. package/lib/typescript/module/src/NavigationRailNativeComponent.d.ts +81 -0
  459. package/lib/typescript/module/src/NavigationRailNativeComponent.d.ts.map +1 -0
  460. package/lib/typescript/module/src/PlatformComponentsProvider.d.ts +6 -0
  461. package/lib/typescript/module/src/PlatformComponentsProvider.d.ts.map +1 -0
  462. package/lib/typescript/module/src/SegmentedControl.d.ts +35 -47
  463. package/lib/typescript/module/src/SegmentedControl.d.ts.map +1 -1
  464. package/lib/typescript/module/src/SegmentedControlNativeComponent.d.ts +11 -0
  465. package/lib/typescript/module/src/SegmentedControlNativeComponent.d.ts.map +1 -1
  466. package/lib/typescript/module/src/SelectionMenu.d.ts +44 -2
  467. package/lib/typescript/module/src/SelectionMenu.d.ts.map +1 -1
  468. package/lib/typescript/module/src/SelectionMenuNativeComponent.d.ts +20 -4
  469. package/lib/typescript/module/src/SelectionMenuNativeComponent.d.ts.map +1 -1
  470. package/lib/typescript/module/src/SplitButton.d.ts +83 -0
  471. package/lib/typescript/module/src/SplitButton.d.ts.map +1 -0
  472. package/lib/typescript/module/src/TabBar.d.ts +144 -0
  473. package/lib/typescript/module/src/TabBar.d.ts.map +1 -0
  474. package/lib/typescript/module/src/TabBarAccessoryNativeComponent.d.ts +13 -0
  475. package/lib/typescript/module/src/TabBarAccessoryNativeComponent.d.ts.map +1 -0
  476. package/lib/typescript/module/src/TabBarNativeComponent.d.ts +110 -0
  477. package/lib/typescript/module/src/TabBarNativeComponent.d.ts.map +1 -0
  478. package/lib/typescript/module/src/TextField.d.ts +332 -0
  479. package/lib/typescript/module/src/TextField.d.ts.map +1 -0
  480. package/lib/typescript/module/src/TextFieldNativeComponent.d.ts +240 -0
  481. package/lib/typescript/module/src/TextFieldNativeComponent.d.ts.map +1 -0
  482. package/lib/typescript/module/src/focusRegistry.d.ts +13 -0
  483. package/lib/typescript/module/src/focusRegistry.d.ts.map +1 -0
  484. package/lib/typescript/module/src/haptics.d.ts +20 -0
  485. package/lib/typescript/module/src/haptics.d.ts.map +1 -0
  486. package/lib/typescript/module/src/icons.d.ts +58 -0
  487. package/lib/typescript/module/src/icons.d.ts.map +1 -0
  488. package/lib/typescript/module/src/index.d.ts +13 -0
  489. package/lib/typescript/module/src/index.d.ts.map +1 -1
  490. package/lib/typescript/module/src/index.web.d.ts +25 -133
  491. package/lib/typescript/module/src/index.web.d.ts.map +1 -1
  492. package/lib/typescript/module/src/jest.d.ts +139 -0
  493. package/lib/typescript/module/src/jest.d.ts.map +1 -0
  494. package/lib/typescript/module/src/labelStyle.d.ts +20 -0
  495. package/lib/typescript/module/src/labelStyle.d.ts.map +1 -0
  496. package/lib/typescript/module/src/menuItems.d.ts +100 -0
  497. package/lib/typescript/module/src/menuItems.d.ts.map +1 -0
  498. package/lib/typescript/module/src/selectionIdentifiers.d.ts +3 -0
  499. package/lib/typescript/module/src/selectionIdentifiers.d.ts.map +1 -0
  500. package/lib/typescript/module/src/sharedTypes.d.ts +6 -1
  501. package/lib/typescript/module/src/sharedTypes.d.ts.map +1 -1
  502. package/lib/typescript/module/src/submitBehavior.d.ts +9 -0
  503. package/lib/typescript/module/src/submitBehavior.d.ts.map +1 -0
  504. package/lib/typescript/module/src/tabItems.d.ts +55 -0
  505. package/lib/typescript/module/src/tabItems.d.ts.map +1 -0
  506. package/lib/typescript/module/src/web/Button.d.ts +9 -0
  507. package/lib/typescript/module/src/web/Button.d.ts.map +1 -0
  508. package/lib/typescript/module/src/web/ButtonGroup.d.ts +11 -0
  509. package/lib/typescript/module/src/web/ButtonGroup.d.ts.map +1 -0
  510. package/lib/typescript/module/src/web/DatePicker.d.ts +14 -0
  511. package/lib/typescript/module/src/web/DatePicker.d.ts.map +1 -0
  512. package/lib/typescript/module/src/web/DateRangePicker.d.ts +11 -0
  513. package/lib/typescript/module/src/web/DateRangePicker.d.ts.map +1 -0
  514. package/lib/typescript/module/src/web/Dialog.d.ts +12 -0
  515. package/lib/typescript/module/src/web/Dialog.d.ts.map +1 -0
  516. package/lib/typescript/module/src/web/FloatingActionButton.d.ts +8 -0
  517. package/lib/typescript/module/src/web/FloatingActionButton.d.ts.map +1 -0
  518. package/lib/typescript/module/src/web/Icon.d.ts +12 -0
  519. package/lib/typescript/module/src/web/Icon.d.ts.map +1 -0
  520. package/lib/typescript/module/src/web/NativeTheme.d.ts +10 -0
  521. package/lib/typescript/module/src/web/NativeTheme.d.ts.map +1 -0
  522. package/lib/typescript/module/src/web/NavigationRail.d.ts +8 -0
  523. package/lib/typescript/module/src/web/NavigationRail.d.ts.map +1 -0
  524. package/lib/typescript/module/src/web/PlatformComponentsProvider.d.ts +10 -0
  525. package/lib/typescript/module/src/web/PlatformComponentsProvider.d.ts.map +1 -0
  526. package/lib/typescript/module/src/web/SegmentedControl.d.ts +8 -0
  527. package/lib/typescript/module/src/web/SegmentedControl.d.ts.map +1 -0
  528. package/lib/typescript/module/src/web/SelectionMenu.d.ts +10 -0
  529. package/lib/typescript/module/src/web/SelectionMenu.d.ts.map +1 -0
  530. package/lib/typescript/module/src/web/SplitButton.d.ts +9 -0
  531. package/lib/typescript/module/src/web/SplitButton.d.ts.map +1 -0
  532. package/lib/typescript/module/src/web/TabBar.d.ts +9 -0
  533. package/lib/typescript/module/src/web/TabBar.d.ts.map +1 -0
  534. package/lib/typescript/module/src/web/TextField.d.ts +9 -0
  535. package/lib/typescript/module/src/web/TextField.d.ts.map +1 -0
  536. package/lib/typescript/module/src/web/components.d.ts +17 -0
  537. package/lib/typescript/module/src/web/components.d.ts.map +1 -0
  538. package/lib/typescript/module/src/web/containers.d.ts +24 -0
  539. package/lib/typescript/module/src/web/containers.d.ts.map +1 -0
  540. package/lib/typescript/module/src/web/dateInput.d.ts +20 -0
  541. package/lib/typescript/module/src/web/dateInput.d.ts.map +1 -0
  542. package/lib/typescript/module/src/web/shared.d.ts +43 -0
  543. package/lib/typescript/module/src/web/shared.d.ts.map +1 -0
  544. package/lib/typescript/module/src/webComponents.d.ts +43 -0
  545. package/lib/typescript/module/src/webComponents.d.ts.map +1 -0
  546. package/package.json +55 -4
  547. package/plugin/build/index.d.ts +3 -2
  548. package/plugin/build/index.d.ts.map +1 -1
  549. package/react-native.config.js +6 -0
  550. package/shared/PCButtonComponentDescriptors-custom.h +22 -0
  551. package/shared/PCButtonGroupComponentDescriptors-custom.h +22 -0
  552. package/shared/PCButtonGroupShadowNode-custom.cpp +48 -0
  553. package/shared/PCButtonGroupShadowNode-custom.h +55 -0
  554. package/shared/PCButtonGroupState-custom.h +61 -0
  555. package/shared/PCButtonShadowNode-custom.cpp +48 -0
  556. package/shared/PCButtonShadowNode-custom.h +55 -0
  557. package/shared/PCButtonState-custom.h +61 -0
  558. package/shared/PCFloatingActionButtonComponentDescriptors-custom.h +22 -0
  559. package/shared/PCFloatingActionButtonShadowNode-custom.cpp +48 -0
  560. package/shared/PCFloatingActionButtonShadowNode-custom.h +54 -0
  561. package/shared/PCFloatingActionButtonState-custom.h +61 -0
  562. package/shared/PCNavigationRailComponentDescriptors-custom.h +47 -0
  563. package/shared/PCNavigationRailShadowNode-custom.h +42 -0
  564. package/shared/PCNavigationRailState-custom.h +62 -0
  565. package/shared/PCTabBarComponentDescriptors-custom.h +22 -0
  566. package/shared/PCTabBarShadowNode-custom.cpp +54 -0
  567. package/shared/PCTabBarShadowNode-custom.h +54 -0
  568. package/shared/PCTabBarState-custom.h +62 -0
  569. package/shared/PCTextFieldComponentDescriptors-custom.h +22 -0
  570. package/shared/PCTextFieldShadowNode-custom.cpp +47 -0
  571. package/shared/PCTextFieldShadowNode-custom.h +55 -0
  572. package/shared/PCTextFieldState-custom.h +61 -0
  573. package/shared/react/renderer/components/PlatformComponentsViewSpec/ComponentDescriptors.h +6 -0
  574. package/src/Button.tsx +359 -0
  575. package/src/ButtonGroup.tsx +251 -0
  576. package/src/ButtonGroupNativeComponent.ts +191 -0
  577. package/src/ButtonNativeComponent.ts +192 -0
  578. package/src/ContextMenu.tsx +34 -81
  579. package/src/ContextMenuNativeComponent.ts +47 -49
  580. package/src/DatePicker.tsx +182 -4
  581. package/src/DatePickerNativeComponent.ts +19 -2
  582. package/src/FloatingActionButton.tsx +121 -0
  583. package/src/FloatingActionButtonNativeComponent.ts +58 -0
  584. package/src/FloatingToolbar.tsx +164 -0
  585. package/src/FloatingToolbarNativeComponent.ts +47 -0
  586. package/src/LiquidGlass.tsx +24 -1
  587. package/src/LiquidGlassContainer.tsx +42 -0
  588. package/src/LiquidGlassContainerNativeComponent.ts +17 -0
  589. package/src/LiquidGlassNativeComponent.ts +8 -0
  590. package/src/NavigationRail.tsx +203 -0
  591. package/src/NavigationRailNativeComponent.ts +107 -0
  592. package/src/PlatformComponentsProvider.tsx +15 -0
  593. package/src/SegmentedControl.tsx +57 -119
  594. package/src/SegmentedControlNativeComponent.ts +14 -0
  595. package/src/SelectionMenu.tsx +81 -6
  596. package/src/SelectionMenuNativeComponent.ts +26 -4
  597. package/src/SplitButton.tsx +198 -0
  598. package/src/TabBar.tsx +373 -0
  599. package/src/TabBarAccessoryNativeComponent.ts +19 -0
  600. package/src/TabBarNativeComponent.ts +145 -0
  601. package/src/TextField.tsx +908 -0
  602. package/src/TextFieldNativeComponent.ts +324 -0
  603. package/src/focusRegistry.ts +90 -0
  604. package/src/haptics.ts +31 -0
  605. package/src/icons.ts +143 -0
  606. package/src/index.tsx +13 -0
  607. package/src/index.web.tsx +53 -199
  608. package/src/jest.tsx +958 -0
  609. package/src/labelStyle.ts +36 -0
  610. package/src/menuItems.ts +152 -0
  611. package/src/reactNativeInternals.d.ts +22 -0
  612. package/src/selectionIdentifiers.ts +17 -0
  613. package/src/sharedTypes.ts +7 -1
  614. package/src/submitBehavior.ts +16 -0
  615. package/src/tabItems.ts +113 -0
  616. package/src/web/Button.tsx +201 -0
  617. package/src/web/ButtonGroup.tsx +154 -0
  618. package/src/web/DatePicker.tsx +204 -0
  619. package/src/web/DateRangePicker.tsx +135 -0
  620. package/src/web/Dialog.tsx +54 -0
  621. package/src/web/FloatingActionButton.tsx +76 -0
  622. package/src/web/Icon.tsx +33 -0
  623. package/src/web/NativeTheme.ts +23 -0
  624. package/src/web/NavigationRail.tsx +148 -0
  625. package/src/web/PlatformComponentsProvider.tsx +48 -0
  626. package/src/web/SegmentedControl.tsx +127 -0
  627. package/src/web/SelectionMenu.tsx +149 -0
  628. package/src/web/SplitButton.tsx +32 -0
  629. package/src/web/TabBar.tsx +150 -0
  630. package/src/web/TextField.tsx +361 -0
  631. package/src/web/components.tsx +71 -0
  632. package/src/web/containers.tsx +130 -0
  633. package/src/web/dateInput.ts +137 -0
  634. package/src/web/shared.ts +176 -0
  635. package/src/webComponents.ts +45 -0
  636. package/android/src/main/java/com/platformcomponents/DateConstraints.kt +0 -83
@@ -0,0 +1,1558 @@
1
+ import UIKit
2
+
3
+ /// A native text field: a caption label, a `UITextField` (or a growing
4
+ /// `UITextView` for multi-line text) and supporting text below, with the
5
+ /// text traits iOS adds each year exposed as props.
6
+ ///
7
+ /// The text and its edit counter live here. User edits report the counter
8
+ /// with the text; JS pushes a controlled value back with the last counter it
9
+ /// saw, and a push older than the latest edit is dropped so a slow JS
10
+ /// round-trip can't erase what was typed meanwhile.
11
+ @objcMembers
12
+ public final class PCTextFieldView: UIView, UITextFieldDelegate, UITextViewDelegate, UIGestureRecognizerDelegate {
13
+ // MARK: - Props (set from ObjC++)
14
+
15
+ public var label: String = "" {
16
+ didSet { if oldValue != label { applyLabel() } }
17
+ }
18
+
19
+ public var placeholder: String = "" {
20
+ didSet { if oldValue != placeholder { applyPlaceholder() } }
21
+ }
22
+
23
+ public var supportingText: String = "" {
24
+ didSet { if oldValue != supportingText { applyFooter() } }
25
+ }
26
+
27
+ /// "none" | "error"
28
+ public var errorState: String = "none" {
29
+ didSet { if oldValue != errorState { applyFooter(); applyColors() } }
30
+ }
31
+
32
+ public var errorText: String = "" {
33
+ didSet { if oldValue != errorText { applyFooter() } }
34
+ }
35
+
36
+ public var prefix: String = "" {
37
+ didSet { if oldValue != prefix { applyAccessories() } }
38
+ }
39
+
40
+ public var suffix: String = "" {
41
+ didSet { if oldValue != suffix { applyAccessories() } }
42
+ }
43
+
44
+ /// "never" | "while-editing" | "unless-editing" | "always"
45
+ public var clearButtonMode: String = "never" {
46
+ didSet { if oldValue != clearButtonMode { applyAccessories() } }
47
+ }
48
+
49
+ public var passwordToggle: Bool = false {
50
+ didSet { if oldValue != passwordToggle { applyAccessories() } }
51
+ }
52
+
53
+ public var characterCount: Bool = false {
54
+ didSet { if oldValue != characterCount { applyFooter() } }
55
+ }
56
+
57
+ /// 0 = unlimited
58
+ public var maxLength: Int = 0 {
59
+ didSet { if oldValue != maxLength { applyFooter() } }
60
+ }
61
+
62
+ public var keyboardType: String = "default" {
63
+ didSet { if oldValue != keyboardType { applyTraits() } }
64
+ }
65
+
66
+ public var returnKeyType: String = "default" {
67
+ didSet { if oldValue != returnKeyType { applyTraits() } }
68
+ }
69
+
70
+ public var autoCapitalize: String = "sentences" {
71
+ didSet { if oldValue != autoCapitalize { applyTraits() } }
72
+ }
73
+
74
+ public var autoCorrect: Bool = true {
75
+ didSet { if oldValue != autoCorrect { applyTraits() } }
76
+ }
77
+
78
+ public var secureTextEntry: Bool = false {
79
+ didSet { if oldValue != secureTextEntry { revealed = false; applyTraits(); applyAccessories() } }
80
+ }
81
+
82
+ public var multiline: Bool = false {
83
+ didSet { if oldValue != multiline { rebuildInput() } }
84
+ }
85
+
86
+ /// What the return key does: "submit" | "blurAndSubmit" | "newline";
87
+ /// "" keeps the defaults (a newline in multi-line fields, else blur and submit)
88
+ public var submitBehavior: String = ""
89
+
90
+ /// UITextInputPasswordRules descriptor; "" = none
91
+ public var passwordRules: String = "" {
92
+ didSet { if oldValue != passwordRules { applyTraits() } }
93
+ }
94
+
95
+ /// The keyboard toolbar's items (kind, itemId, title, systemItem, icon
96
+ /// fields, prominent, accessibilityLabel, testID); empty = no toolbar
97
+ public var keyboardToolbarItems: [NSDictionary] = [] {
98
+ didSet { if oldValue != keyboardToolbarItems { applyKeyboardToolbar() } }
99
+ }
100
+
101
+ /// "enabled" | "disabled"
102
+ public var interactivity: String = "enabled" {
103
+ didSet { if oldValue != interactivity { applyEnabled() } }
104
+ }
105
+
106
+ public var autoFocus: Bool = false {
107
+ didSet { if autoFocus, window != nil { performAutoFocus() } }
108
+ }
109
+
110
+ public var selectTextOnFocus: Bool = false
111
+
112
+ /// React Native autoComplete value; "" = default
113
+ public var autoComplete: String = "" {
114
+ didSet { if oldValue != autoComplete { applyTraits() } }
115
+ }
116
+
117
+ /// "default" | "light" | "dark"
118
+ public var keyboardAppearance: String = "default" {
119
+ didSet { if oldValue != keyboardAppearance { applyTraits() } }
120
+ }
121
+
122
+ /// Input font; nil keeps the body text style
123
+ public var textFont: UIFont? {
124
+ didSet { applyFonts() }
125
+ }
126
+
127
+ /// Screen-reader label; empty uses the label
128
+ public var spokenLabel: String = "" {
129
+ didSet { applyAccessibility() }
130
+ }
131
+
132
+ // iOS text traits, "" for the system default
133
+ public var writingTools: String = "" {
134
+ didSet { if oldValue != writingTools { applyTraits() } }
135
+ }
136
+
137
+ public var inlinePrediction: String = "" {
138
+ didSet { if oldValue != inlinePrediction { applyTraits() } }
139
+ }
140
+
141
+ public var smartQuotes: String = "" {
142
+ didSet { if oldValue != smartQuotes { applyTraits() } }
143
+ }
144
+
145
+ public var smartDashes: String = "" {
146
+ didSet { if oldValue != smartDashes { applyTraits() } }
147
+ }
148
+
149
+ public var smartInsertDelete: String = "" {
150
+ didSet { if oldValue != smartInsertDelete { applyTraits() } }
151
+ }
152
+
153
+ public var mathExpressionCompletion: String = "" {
154
+ didSet { if oldValue != mathExpressionCompletion { applyTraits() } }
155
+ }
156
+
157
+ /// "" | "roundedRect" | "none" | "line" | "bezel"
158
+ public var borderStyle: String = "" {
159
+ didSet { if oldValue != borderStyle { applyBorderStyle() } }
160
+ }
161
+
162
+ /// "" | "above" | "leading": the caption above the field, or in a column
163
+ /// beside it, the layout of the grouped forms in Contacts and Settings
164
+ public var labelPlacement: String = "" {
165
+ didSet { if oldValue != labelPlacement { applyLabel(); applyFonts(); applyColors() } }
166
+ }
167
+
168
+ /// Width of the leading label column; 0 = default
169
+ public var labelWidth: CGFloat = 0 {
170
+ didSet { if oldValue != labelWidth { setNeedsLayout(); onNeedsRemeasure?() } }
171
+ }
172
+
173
+ /// Test id of the inner input, where E2E drivers type (see PCTextField.mm)
174
+ public var inputTestID: String = "" {
175
+ didSet { if oldValue != inputTestID { applyAccessibility() } }
176
+ }
177
+
178
+ public var leadingIconTestID: String = "" {
179
+ didSet { if oldValue != leadingIconTestID { applyAccessories() } }
180
+ }
181
+
182
+ public var leadingIconSpokenLabel: String = "" {
183
+ didSet { if oldValue != leadingIconSpokenLabel { applyAccessories() } }
184
+ }
185
+
186
+ public var trailingIconTestID: String = "" {
187
+ didSet { if oldValue != trailingIconTestID { applyAccessories() } }
188
+ }
189
+
190
+ public var trailingIconSpokenLabel: String = "" {
191
+ didSet { if oldValue != trailingIconSpokenLabel { applyAccessories() } }
192
+ }
193
+
194
+ // Colors; nil keeps the system color
195
+ public var activeColor: UIColor? {
196
+ didSet { if oldValue != activeColor { applyBorderStyle(); applyColors() } }
197
+ }
198
+
199
+ public var outlineColor: UIColor? {
200
+ didSet { if oldValue != outlineColor { applyBorderStyle(); applyColors() } }
201
+ }
202
+
203
+ public var errorColor: UIColor? {
204
+ didSet { if oldValue != errorColor { applyColors() } }
205
+ }
206
+
207
+ public var containerColor: UIColor? {
208
+ didSet { if oldValue != containerColor { applyBorderStyle(); applyColors() } }
209
+ }
210
+
211
+ public var textColor: UIColor? {
212
+ didSet { if oldValue != textColor { applyColors() } }
213
+ }
214
+
215
+ public var placeholderTextColor: UIColor? {
216
+ didSet { if oldValue != placeholderTextColor { applyPlaceholder() } }
217
+ }
218
+
219
+ /// Cap on the Dynamic Type scale of every text; values below 1 mean no cap
220
+ public var maxFontSizeMultiplier: CGFloat = 0 {
221
+ didSet { if oldValue != maxFontSizeMultiplier { applyFonts(); applyAccessories() } }
222
+ }
223
+
224
+ /// "" | "left" | "center" | "right"
225
+ public var textAlign: String = "" {
226
+ didSet { if oldValue != textAlign { applyAlignment() } }
227
+ }
228
+
229
+ /// Multi-line height bounds in lines; 0 = unset
230
+ public var minLines: Int = 0 {
231
+ didSet { if oldValue != minLines { setNeedsLayout(); onNeedsRemeasure?() } }
232
+ }
233
+
234
+ public var maxLines: Int = 0 {
235
+ didSet { if oldValue != maxLines { setNeedsLayout(); onNeedsRemeasure?() } }
236
+ }
237
+
238
+ /// A non-editable field that reports presses, as a picker's anchor
239
+ public var pressable: Bool = false {
240
+ didSet { if oldValue != pressable { applyEnabled() } }
241
+ }
242
+
243
+ private var leadingLabel: Bool { labelPlacement == "leading" && !label.isEmpty }
244
+
245
+ /// Whether the field acts as a button: pressable and not editable
246
+ private var actsAsButton: Bool { pressable && interactivity == "disabled" }
247
+
248
+ /// The rounded rect drawn here instead of by UIKit, whose border can't be colored
249
+ private var drawsBox: Bool {
250
+ (borderStyle.isEmpty || borderStyle == "roundedRect")
251
+ && (activeColor != nil || outlineColor != nil || containerColor != nil)
252
+ }
253
+ private var leadingColumnWidth: CGFloat { labelWidth > 0 ? labelWidth : 100 }
254
+ private let leadingSpacing: CGFloat = 8
255
+
256
+ // MARK: - Events back to ObjC++
257
+
258
+ public var onChange: ((String, Int) -> Void)?
259
+ public var onFocusChange: ((Bool, String) -> Void)?
260
+ public var onSubmit: ((String) -> Void)?
261
+ public var onSelectionChange: ((Int, Int) -> Void)?
262
+ public var onKeyboardToolbarPress: ((String) -> Void)?
263
+ public var onTrailingIconPress: (() -> Void)?
264
+ public var onPress: (() -> Void)?
265
+
266
+ /// Called when content changed in a way that affects the height (a
267
+ /// multi-line edit, an error message appearing), so the Fabric
268
+ /// measurement can be refreshed.
269
+ public var onNeedsRemeasure: (() -> Void)?
270
+
271
+ // MARK: - Text state
272
+
273
+ public private(set) var text: String = ""
274
+ private var nativeEventCount = 0
275
+ private var initialTextApplied = false
276
+ private var autoFocusDone = false
277
+
278
+ /// Whether a secure field is currently showing its text (password toggle)
279
+ private var revealed = false
280
+
281
+ /// The selection JS last heard of, so a change is reported once. Changes
282
+ /// made from JS (text or selection) update it without an event.
283
+ private var lastReportedSelection = NSRange(location: NSNotFound, length: 0)
284
+ private var applyingFromJS = false
285
+
286
+ /// The toolbar above the keyboard, while there are toolbar items
287
+ private var keyboardToolbar: UIToolbar?
288
+ private var toolbarGeneration = 0
289
+
290
+ // MARK: - Subviews
291
+
292
+ private let captionLabel = UILabel()
293
+ private let textField = PCInputTextField()
294
+ private let textView = PCPlaceholderTextView()
295
+ private let footerLabel = UILabel()
296
+ private let counterLabel = UILabel()
297
+
298
+ private var leadingIcon = PCButtonSupport.Icon.none
299
+ private var trailingIcon = PCButtonSupport.Icon.none
300
+ private var leadingImage: UIImage?
301
+ private var trailingImage: UIImage?
302
+ private var leadingIconGeneration = 0
303
+ private var trailingIconGeneration = 0
304
+
305
+ // MARK: - Layout metrics
306
+
307
+ private let captionSpacing: CGFloat = 6
308
+ private let footerSpacing: CGFloat = 4
309
+ private let singleLineMinHeight: CGFloat = 34
310
+ private let textViewInsets = UIEdgeInsets(top: 7, left: 4, bottom: 7, right: 4)
311
+
312
+ // MARK: - Init
313
+
314
+ public override init(frame: CGRect) {
315
+ super.init(frame: frame)
316
+ setup()
317
+ }
318
+
319
+ public required init?(coder: NSCoder) {
320
+ super.init(coder: coder)
321
+ setup()
322
+ }
323
+
324
+ private func setup() {
325
+ captionLabel.numberOfLines = 1
326
+ captionLabel.adjustsFontForContentSizeCategory = true
327
+
328
+ footerLabel.numberOfLines = 0
329
+ footerLabel.adjustsFontForContentSizeCategory = true
330
+
331
+ counterLabel.numberOfLines = 1
332
+ counterLabel.adjustsFontForContentSizeCategory = true
333
+ counterLabel.textAlignment = .right
334
+ counterLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
335
+
336
+ textField.delegate = self
337
+ textField.borderStyle = .roundedRect
338
+ textField.adjustsFontForContentSizeCategory = true
339
+ textField.addTarget(self, action: #selector(fieldEditingChanged), for: .editingChanged)
340
+
341
+ textView.delegate = self
342
+ textView.isScrollEnabled = false
343
+ textView.adjustsFontForContentSizeCategory = true
344
+ textView.textContainerInset = textViewInsets
345
+ textView.textContainer.lineFragmentPadding = 4
346
+ textView.backgroundColor = .clear
347
+
348
+ addSubview(captionLabel)
349
+ addSubview(textField)
350
+ addSubview(footerLabel)
351
+ addSubview(counterLabel)
352
+
353
+ // Presses of a field that acts as a button; accessory buttons keep their own
354
+ pressRecognizer.addTarget(self, action: #selector(handlePress))
355
+ pressRecognizer.delegate = self
356
+ pressRecognizer.isEnabled = false
357
+ addGestureRecognizer(pressRecognizer)
358
+ textField.onActivate = { [weak self] in self?.activateAsButton() ?? false }
359
+ textView.onActivate = { [weak self] in self?.activateAsButton() ?? false }
360
+
361
+ applyFonts()
362
+ applyColors()
363
+ applyBorderStyle()
364
+ applyTraits()
365
+ applyLabel()
366
+ applyPlaceholder()
367
+ applyFooter()
368
+ applyAccessories()
369
+ applyAccessibility()
370
+ applyAlignment()
371
+ }
372
+
373
+ // MARK: - Text
374
+
375
+ /// The text the field starts with; only the first value is used (see setText).
376
+ public func applyInitialText(_ value: String) {
377
+ guard !initialTextApplied else { return }
378
+ initialTextApplied = true
379
+ setTextInternal(value, moveCursorToEnd: true)
380
+ }
381
+
382
+ /// A controlled value from JS. Dropped when the user has edited since
383
+ /// `eventCount` was reported: the edit event that follows carries the
384
+ /// text JS should reconcile against.
385
+ public func setText(_ value: String, eventCount: Int) {
386
+ guard eventCount >= nativeEventCount, value != text else { return }
387
+ // Replacing text mid-composition (Japanese, Chinese keyboards) would
388
+ // break the composition; the next edit reconciles instead
389
+ guard activeInput.markedTextRange == nil else { return }
390
+ setTextInternal(value, moveCursorToEnd: false)
391
+ }
392
+
393
+ /// Clears the text as if the user had, so JS gets a change event.
394
+ public func clear() {
395
+ guard !text.isEmpty else { return }
396
+ setTextInternal("", moveCursorToEnd: true)
397
+ reportEdit()
398
+ lastReportedSelection = NSRange(location: NSNotFound, length: 0)
399
+ reportSelection()
400
+ }
401
+
402
+ /// A selection from JS, in UTF-16 offsets clamped to the text. Dropped
403
+ /// like a stale `setText` when the user has edited since `eventCount`.
404
+ public func setSelection(start: Int, end: Int, eventCount: Int) {
405
+ guard eventCount >= nativeEventCount else { return }
406
+ guard activeInput.markedTextRange == nil else { return }
407
+ let length = (text as NSString).length
408
+ let from = min(max(0, start), length)
409
+ let to = min(max(from, end), length)
410
+ let range = NSRange(location: from, length: to - from)
411
+ applyingFromJS = true
412
+ if multiline {
413
+ textView.selectedRange = range
414
+ } else if let startPosition = textField.position(from: textField.beginningOfDocument, offset: from),
415
+ let endPosition = textField.position(from: startPosition, offset: to - from) {
416
+ textField.selectedTextRange = textField.textRange(from: startPosition, to: endPosition)
417
+ }
418
+ applyingFromJS = false
419
+ lastReportedSelection = currentSelection() ?? range
420
+ }
421
+
422
+ public func focus() {
423
+ activeInput.becomeFirstResponder()
424
+ }
425
+
426
+ public func blur() {
427
+ activeInput.resignFirstResponder()
428
+ }
429
+
430
+ public func setLeadingIcon(type: String, name: String, request: String, uri: String, scale: CGFloat, tinted: Bool) {
431
+ let next = PCButtonSupport.Icon(type: type, name: name, uri: uri, scale: scale, tinted: tinted, request: request)
432
+ guard next != leadingIcon else { return }
433
+ leadingIcon = next
434
+ leadingIconGeneration += 1
435
+ let generation = leadingIconGeneration
436
+ leadingImage = PCButtonSupport.image(for: next) { [weak self] image in
437
+ guard let self, self.leadingIconGeneration == generation else { return }
438
+ self.leadingImage = image
439
+ self.applyAccessories()
440
+ }
441
+ applyAccessories()
442
+ }
443
+
444
+ public func setTrailingIcon(type: String, name: String, request: String, uri: String, scale: CGFloat, tinted: Bool) {
445
+ let next = PCButtonSupport.Icon(type: type, name: name, uri: uri, scale: scale, tinted: tinted, request: request)
446
+ guard next != trailingIcon else { return }
447
+ trailingIcon = next
448
+ trailingIconGeneration += 1
449
+ let generation = trailingIconGeneration
450
+ trailingImage = PCButtonSupport.image(for: next) { [weak self] image in
451
+ guard let self, self.trailingIconGeneration == generation else { return }
452
+ self.trailingImage = image
453
+ self.applyAccessories()
454
+ }
455
+ applyAccessories()
456
+ }
457
+
458
+ /// Resets the text state for a recycled view.
459
+ public func resetForRecycle() {
460
+ blur()
461
+ leadingIconGeneration += 1
462
+ trailingIconGeneration += 1
463
+ initialTextApplied = false
464
+ autoFocusDone = false
465
+ nativeEventCount = 0
466
+ revealed = false
467
+ setTextInternal("", moveCursorToEnd: true)
468
+ lastReportedSelection = NSRange(location: NSNotFound, length: 0)
469
+ }
470
+
471
+ private var activeInput: UIView & UITextInput {
472
+ multiline ? textView : textField
473
+ }
474
+
475
+ private func setTextInternal(_ value: String, moveCursorToEnd: Bool) {
476
+ // The cursor moves with the text; JS knows, so there is no event
477
+ applyingFromJS = true
478
+ defer {
479
+ applyingFromJS = false
480
+ lastReportedSelection = currentSelection() ?? lastReportedSelection
481
+ }
482
+ if multiline {
483
+ let wasAtEnd = textView.selectedRange.location == (textView.text as NSString).length
484
+ let cursor = textView.selectedRange.location
485
+ textView.text = value
486
+ let length = (value as NSString).length
487
+ textView.selectedRange = NSRange(location: moveCursorToEnd || wasAtEnd ? length : min(cursor, length), length: 0)
488
+ textView.updatePlaceholderVisibility()
489
+ } else {
490
+ let wasAtEnd: Bool
491
+ if let selected = textField.selectedTextRange {
492
+ wasAtEnd = textField.compare(selected.end, to: textField.endOfDocument) == .orderedSame
493
+ } else {
494
+ wasAtEnd = true
495
+ }
496
+ let cursorOffset = textField.selectedTextRange.map {
497
+ textField.offset(from: textField.beginningOfDocument, to: $0.start)
498
+ } ?? 0
499
+ textField.text = value
500
+ let length = (value as NSString).length
501
+ let offset = moveCursorToEnd || wasAtEnd ? length : min(cursorOffset, length)
502
+ if let position = textField.position(from: textField.beginningOfDocument, offset: offset) {
503
+ textField.selectedTextRange = textField.textRange(from: position, to: position)
504
+ }
505
+ }
506
+ text = value
507
+ applyFooter()
508
+ onNeedsRemeasure?()
509
+ }
510
+
511
+ private func reportEdit() {
512
+ nativeEventCount += 1
513
+ onChange?(text, nativeEventCount)
514
+ }
515
+
516
+ /// The selection in UTF-16 offsets, as JS strings count them.
517
+ private func currentSelection() -> NSRange? {
518
+ if multiline { return textView.selectedRange }
519
+ guard let range = textField.selectedTextRange else { return nil }
520
+ let start = textField.offset(from: textField.beginningOfDocument, to: range.start)
521
+ let end = textField.offset(from: textField.beginningOfDocument, to: range.end)
522
+ return NSRange(location: start, length: max(0, end - start))
523
+ }
524
+
525
+ private func reportSelection() {
526
+ guard !applyingFromJS, let selection = currentSelection(), selection != lastReportedSelection else { return }
527
+ lastReportedSelection = selection
528
+ onSelectionChange?(selection.location, selection.location + selection.length)
529
+ }
530
+
531
+ /// The return key: submits unless it inserts a newline, and blurs
532
+ /// unless submitBehavior is "submit". Returns whether it submitted.
533
+ private func handleReturn() -> Bool {
534
+ let behavior = submitBehavior.isEmpty ? (multiline ? "newline" : "blurAndSubmit") : submitBehavior
535
+ if multiline && behavior == "newline" { return false }
536
+ onSubmit?(text)
537
+ if behavior != "submit" { activeInput.resignFirstResponder() }
538
+ return true
539
+ }
540
+
541
+ // MARK: - UITextField
542
+
543
+ @objc private func fieldEditingChanged() {
544
+ text = textField.text ?? ""
545
+ applyFooter()
546
+ reportEdit()
547
+ }
548
+
549
+ public func textFieldDidBeginEditing(_ field: UITextField) {
550
+ if selectTextOnFocus {
551
+ field.selectAll(nil)
552
+ }
553
+ applyColors()
554
+ onFocusChange?(true, text)
555
+ }
556
+
557
+ public func textFieldDidEndEditing(_ field: UITextField) {
558
+ applyColors()
559
+ onFocusChange?(false, text)
560
+ }
561
+
562
+ public func textFieldShouldBeginEditing(_ field: UITextField) -> Bool {
563
+ !actsAsButton
564
+ }
565
+
566
+ /// The clear button doesn't send .editingChanged on every iOS version;
567
+ /// report the cleared text once UIKit has applied it, unless it did.
568
+ public func textFieldShouldClear(_ field: UITextField) -> Bool {
569
+ DispatchQueue.main.async { [weak self] in
570
+ guard let self, (self.textField.text ?? "") != self.text else { return }
571
+ self.fieldEditingChanged()
572
+ }
573
+ return true
574
+ }
575
+
576
+ public func textFieldShouldReturn(_ field: UITextField) -> Bool {
577
+ // Single-line fields blur on submit unless submitBehavior is
578
+ // "submit", as the core TextInput does
579
+ _ = handleReturn()
580
+ return true
581
+ }
582
+
583
+ public func textFieldDidChangeSelection(_ field: UITextField) {
584
+ reportSelection()
585
+ }
586
+
587
+ public func textField(_ field: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
588
+ allowsChange(current: field.text ?? "", range: range, replacement: string, input: field)
589
+ }
590
+
591
+ // MARK: - Press (a field acting as a button)
592
+
593
+ private let pressRecognizer = UITapGestureRecognizer()
594
+
595
+ @objc private func handlePress() {
596
+ guard actsAsButton else { return }
597
+ // Brief highlight, as a plain UIButton gives
598
+ activeInput.alpha = 0.5
599
+ UIView.animate(withDuration: 0.25, delay: 0.05, options: [.allowUserInteraction]) {
600
+ self.activeInput.alpha = 1
601
+ }
602
+ onPress?()
603
+ }
604
+
605
+ private func activateAsButton() -> Bool {
606
+ guard actsAsButton else { return false }
607
+ onPress?()
608
+ return true
609
+ }
610
+
611
+ public func gestureRecognizer(_ recognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
612
+ // The trailing icon and the password toggle handle their own taps
613
+ !(touch.view is PCAccessoryButton)
614
+ }
615
+
616
+ /// UITextField's own tap recognizers would otherwise take the tap (and
617
+ /// then find editing refused), leaving the press unreported
618
+ public func gestureRecognizer(
619
+ _ recognizer: UIGestureRecognizer,
620
+ shouldRecognizeSimultaneouslyWith other: UIGestureRecognizer
621
+ ) -> Bool {
622
+ recognizer === pressRecognizer
623
+ }
624
+
625
+ // MARK: - UITextView
626
+
627
+ public func textViewDidChange(_ view: UITextView) {
628
+ text = view.text ?? ""
629
+ textView.updatePlaceholderVisibility()
630
+ applyFooter()
631
+ reportEdit()
632
+ onNeedsRemeasure?()
633
+ }
634
+
635
+ public func textViewDidBeginEditing(_ view: UITextView) {
636
+ if selectTextOnFocus {
637
+ view.selectAll(nil)
638
+ }
639
+ applyColors()
640
+ onFocusChange?(true, text)
641
+ }
642
+
643
+ public func textViewDidEndEditing(_ view: UITextView) {
644
+ applyColors()
645
+ onFocusChange?(false, text)
646
+ }
647
+
648
+ public func textView(_ view: UITextView, shouldChangeTextIn range: NSRange, replacementText string: String) -> Bool {
649
+ // Return submits instead of inserting a newline, unless submitBehavior is "newline"
650
+ if string == "\n", view.markedTextRange == nil, handleReturn() {
651
+ return false
652
+ }
653
+ return allowsChange(current: view.text ?? "", range: range, replacement: string, input: view)
654
+ }
655
+
656
+ public func textViewDidChangeSelection(_ view: UITextView) {
657
+ reportSelection()
658
+ }
659
+
660
+ /// Enforces `maxLength` in UTF-16 units, like the core TextInput and the
661
+ /// Android length filter, leaving IME composition alone.
662
+ private func allowsChange(current: String, range: NSRange, replacement: String, input: UITextInput) -> Bool {
663
+ guard maxLength > 0, input.markedTextRange == nil else { return true }
664
+ let currentLength = (current as NSString).length
665
+ let nextLength = currentLength - range.length + (replacement as NSString).length
666
+ return nextLength <= maxLength
667
+ }
668
+
669
+ // MARK: - Props handling
670
+
671
+ private func rebuildInput() {
672
+ // `multiline` already has its new value in didSet, so activeInput
673
+ // points to the replacement. Read focus and selection from the old input.
674
+ let previousInput: UIView & UITextInput = multiline ? textField : textView
675
+ let focused = previousInput.isFirstResponder
676
+ let selection = previousInput.selectedTextRange.map { range in
677
+ let start = previousInput.offset(from: previousInput.beginningOfDocument, to: range.start)
678
+ let end = previousInput.offset(from: previousInput.beginningOfDocument, to: range.end)
679
+ return NSRange(location: start, length: max(0, end - start))
680
+ }
681
+ textField.removeFromSuperview()
682
+ textView.removeFromSuperview()
683
+ if multiline {
684
+ textView.text = text
685
+ textView.updatePlaceholderVisibility()
686
+ insertSubview(textView, at: 1)
687
+ } else {
688
+ textField.text = text
689
+ insertSubview(textField, at: 1)
690
+ }
691
+ applyFonts()
692
+ applyColors()
693
+ applyBorderStyle()
694
+ applyTraits()
695
+ applyPlaceholder()
696
+ applyAccessories()
697
+ applyEnabled()
698
+ applyAccessibility()
699
+ applyAlignment()
700
+ if focused { activeInput.becomeFirstResponder() }
701
+ if let selection {
702
+ setSelection(start: selection.location, end: NSMaxRange(selection), eventCount: nativeEventCount)
703
+ }
704
+ setNeedsLayout()
705
+ onNeedsRemeasure?()
706
+ }
707
+
708
+ private func applyLabel() {
709
+ captionLabel.text = label
710
+ captionLabel.isHidden = label.isEmpty
711
+ applyAccessibility()
712
+ setNeedsLayout()
713
+ onNeedsRemeasure?()
714
+ }
715
+
716
+ private func applyPlaceholder() {
717
+ if placeholder.isEmpty {
718
+ textField.placeholder = nil
719
+ } else if let placeholderTextColor {
720
+ textField.attributedPlaceholder = NSAttributedString(
721
+ string: placeholder,
722
+ attributes: [.foregroundColor: placeholderTextColor]
723
+ )
724
+ } else {
725
+ textField.placeholder = placeholder
726
+ }
727
+ textView.placeholder = placeholder
728
+ textView.placeholderLabel.textColor = placeholderTextColor ?? .placeholderText
729
+ }
730
+
731
+ /// Supporting text, error message and counter below the field.
732
+ private func applyFooter() {
733
+ let showsError = errorState == "error"
734
+ let message = showsError && !errorText.isEmpty ? errorText : supportingText
735
+ footerLabel.text = message
736
+ footerLabel.isHidden = message.isEmpty
737
+
738
+ if characterCount {
739
+ let count = (text as NSString).length
740
+ counterLabel.text = maxLength > 0 ? "\(count) / \(maxLength)" : "\(count)"
741
+ counterLabel.isHidden = false
742
+ } else {
743
+ counterLabel.text = nil
744
+ counterLabel.isHidden = true
745
+ }
746
+ applyColors()
747
+ setNeedsLayout()
748
+ onNeedsRemeasure?()
749
+ }
750
+
751
+ /// A text style's font at the current text size, capped at
752
+ /// `maxFontSizeMultiplier` times its size at the default text size.
753
+ /// `base` replaces the style's own font (the `textStyle` font, given at
754
+ /// its default-size point size), scaled along the style's curve.
755
+ private func scaledFont(_ style: UIFont.TextStyle, base: UIFont? = nil) -> UIFont {
756
+ let font = base ?? .preferredFont(
757
+ forTextStyle: style,
758
+ compatibleWith: UITraitCollection(preferredContentSizeCategory: .large)
759
+ )
760
+ let metrics = UIFontMetrics(forTextStyle: style)
761
+ if maxFontSizeMultiplier >= 1 {
762
+ return metrics.scaledFont(
763
+ for: font,
764
+ maximumPointSize: font.pointSize * maxFontSizeMultiplier,
765
+ compatibleWith: traitCollection
766
+ )
767
+ }
768
+ return metrics.scaledFont(for: font, compatibleWith: traitCollection)
769
+ }
770
+
771
+ private var inputFont: UIFont { scaledFont(.body, base: textFont) }
772
+
773
+ private func applyFonts() {
774
+ captionLabel.font = scaledFont(leadingLabel ? .body : .subheadline)
775
+ footerLabel.font = scaledFont(.footnote)
776
+ counterLabel.font = scaledFont(.footnote)
777
+ let inputFont = self.inputFont
778
+ textField.font = inputFont
779
+ textView.font = inputFont
780
+ textView.placeholderLabel.font = inputFont
781
+ setNeedsLayout()
782
+ onNeedsRemeasure?()
783
+ }
784
+
785
+ private func applyColors() {
786
+ let showsError = errorState == "error"
787
+ let focused = activeInput.isFirstResponder
788
+ let error = errorColor ?? .systemRed
789
+ captionLabel.textColor = showsError
790
+ ? error
791
+ : (leadingLabel ? .label : (focused ? (activeColor ?? tintColor) : .secondaryLabel))
792
+ footerLabel.textColor = showsError ? error : .secondaryLabel
793
+ counterLabel.textColor = showsError && maxLength > 0 && (text as NSString).length > maxLength
794
+ ? error
795
+ : .secondaryLabel
796
+
797
+ // The outline of the text view and of a field whose box is drawn here
798
+ let outline: UIColor
799
+ if showsError {
800
+ outline = error
801
+ } else if focused {
802
+ outline = activeColor ?? outlineColor ?? .systemGray4
803
+ } else {
804
+ outline = outlineColor ?? .systemGray4
805
+ }
806
+ let resolved = outline.resolvedColor(with: traitCollection).cgColor
807
+ textView.layer.borderColor = resolved
808
+ textField.layer.borderColor = drawsBox ? resolved : nil
809
+
810
+ // Cursor and selection
811
+ textField.tintColor = activeColor
812
+ textView.tintColor = activeColor
813
+ textField.textColor = textColor ?? .label
814
+ textView.textColor = textColor ?? .label
815
+ }
816
+
817
+ private func applyBorderStyle() {
818
+ let style: UITextField.BorderStyle
819
+ switch borderStyle {
820
+ case "none": style = .none
821
+ case "line": style = .line
822
+ case "bezel": style = .bezel
823
+ default: style = .roundedRect
824
+ }
825
+ // UIKit's rounded rect can't take colors; draw its look instead
826
+ let drawsBox = self.drawsBox
827
+ textField.borderStyle = drawsBox ? .none : style
828
+ textField.layer.borderWidth = drawsBox ? 1 : 0
829
+ textField.layer.cornerRadius = drawsBox ? 5 : 0
830
+ textField.textInsets = drawsBox ? UIEdgeInsets(top: 0, left: 7, bottom: 0, right: 7) : .zero
831
+ if drawsBox {
832
+ textField.backgroundColor = containerColor ?? .systemBackground
833
+ } else {
834
+ textField.backgroundColor = style == .none ? containerColor : nil
835
+ }
836
+ // The text view mirrors the rounded-rect field
837
+ let bordered = style != .none
838
+ textView.layer.borderWidth = bordered ? 1 : 0
839
+ textView.layer.cornerRadius = style == .roundedRect ? 5 : 0
840
+ textView.backgroundColor = containerColor ?? (bordered ? .systemBackground : .clear)
841
+ applyColors()
842
+ setNeedsLayout()
843
+ onNeedsRemeasure?()
844
+ }
845
+
846
+ private func applyEnabled() {
847
+ let enabled = interactivity != "disabled"
848
+ let button = actsAsButton
849
+ // A field acting as a button keeps its enabled look; editing is
850
+ // refused in textFieldShouldBeginEditing and presses come from the
851
+ // gesture recognizer
852
+ textField.isEnabled = enabled || button
853
+ textView.isEditable = enabled
854
+ textView.isUserInteractionEnabled = enabled
855
+ textView.alpha = enabled || button ? 1 : 0.5
856
+ pressRecognizer.isEnabled = button
857
+ if button, activeInput.isFirstResponder { blur() }
858
+ applyAccessibility()
859
+ }
860
+
861
+ private func applyAccessibility() {
862
+ let spoken = spokenLabel.isEmpty ? (label.isEmpty ? nil : label) : spokenLabel
863
+ textField.accessibilityLabel = spoken
864
+ textView.accessibilityLabel = spoken
865
+ let id = inputTestID.isEmpty ? nil : inputTestID
866
+ textField.accessibilityIdentifier = id
867
+ textView.accessibilityIdentifier = id
868
+ for input in [textField as UIView, textView] {
869
+ if actsAsButton {
870
+ input.accessibilityTraits.insert(.button)
871
+ } else {
872
+ input.accessibilityTraits.remove(.button)
873
+ }
874
+ }
875
+ }
876
+
877
+ private func applyAlignment() {
878
+ let alignment: NSTextAlignment
879
+ switch textAlign {
880
+ case "left": alignment = .left
881
+ case "center": alignment = .center
882
+ case "right": alignment = .right
883
+ default: alignment = .natural
884
+ }
885
+ textField.textAlignment = alignment
886
+ textView.textAlignment = alignment
887
+ textView.placeholderLabel.textAlignment = alignment
888
+ }
889
+
890
+ private func applyTraits() {
891
+ applyTraits(to: textField)
892
+ applyTraits(to: textView)
893
+ // The keyboard only picks the traits up when input restarts
894
+ if activeInput.isFirstResponder {
895
+ activeInput.reloadInputViews()
896
+ }
897
+ }
898
+
899
+ /// Both inputs get the traits: UITextField and UITextView declare the
900
+ /// same UITextInputTraits properties, but as their own members.
901
+ private func applyTraits(to input: UITextField) {
902
+ input.keyboardType = PCTextFieldView.keyboardType(keyboardType)
903
+ input.returnKeyType = PCTextFieldView.returnKeyType(returnKeyType)
904
+ input.autocapitalizationType = PCTextFieldView.autocapitalization(autoCapitalize)
905
+ input.autocorrectionType = autoCorrect ? .default : .no
906
+ input.spellCheckingType = autoCorrect ? .default : .no
907
+ input.isSecureTextEntry = secureTextEntry && !revealed
908
+ input.textContentType = PCTextFieldView.contentType(autoComplete)
909
+ input.keyboardAppearance = PCTextFieldView.keyboardAppearance(keyboardAppearance)
910
+ input.passwordRules = passwordRules.isEmpty ? nil : UITextInputPasswordRules(descriptor: passwordRules)
911
+ input.smartQuotesType = PCTextFieldView.smartQuotesType(smartQuotes)
912
+ input.smartDashesType = PCTextFieldView.smartDashesType(smartDashes)
913
+ input.smartInsertDeleteType = PCTextFieldView.smartInsertDeleteType(smartInsertDelete)
914
+ if #available(iOS 17.0, *) {
915
+ input.inlinePredictionType = PCTextFieldView.inlinePredictionType(inlinePrediction)
916
+ }
917
+ if #available(iOS 18.0, *) {
918
+ input.mathExpressionCompletionType = PCTextFieldView.mathCompletionType(mathExpressionCompletion)
919
+ input.writingToolsBehavior = PCTextFieldView.writingToolsBehavior(writingTools)
920
+ }
921
+ }
922
+
923
+ private func applyTraits(to input: UITextView) {
924
+ input.keyboardType = PCTextFieldView.keyboardType(keyboardType)
925
+ input.returnKeyType = PCTextFieldView.returnKeyType(returnKeyType)
926
+ input.autocapitalizationType = PCTextFieldView.autocapitalization(autoCapitalize)
927
+ input.autocorrectionType = autoCorrect ? .default : .no
928
+ input.spellCheckingType = autoCorrect ? .default : .no
929
+ input.isSecureTextEntry = secureTextEntry && !revealed
930
+ input.textContentType = PCTextFieldView.contentType(autoComplete)
931
+ input.keyboardAppearance = PCTextFieldView.keyboardAppearance(keyboardAppearance)
932
+ input.passwordRules = passwordRules.isEmpty ? nil : UITextInputPasswordRules(descriptor: passwordRules)
933
+ input.smartQuotesType = PCTextFieldView.smartQuotesType(smartQuotes)
934
+ input.smartDashesType = PCTextFieldView.smartDashesType(smartDashes)
935
+ input.smartInsertDeleteType = PCTextFieldView.smartInsertDeleteType(smartInsertDelete)
936
+ if #available(iOS 17.0, *) {
937
+ input.inlinePredictionType = PCTextFieldView.inlinePredictionType(inlinePrediction)
938
+ }
939
+ if #available(iOS 18.0, *) {
940
+ input.mathExpressionCompletionType = PCTextFieldView.mathCompletionType(mathExpressionCompletion)
941
+ input.writingToolsBehavior = PCTextFieldView.writingToolsBehavior(writingTools)
942
+ }
943
+ }
944
+
945
+ /// The keyboard toolbar: a `UIToolbar` as the input's
946
+ /// `inputAccessoryView`, with the given buttons, flexible spaces and a
947
+ /// Done button that dismisses the keyboard.
948
+ private func applyKeyboardToolbar() {
949
+ toolbarGeneration += 1
950
+ if keyboardToolbarItems.isEmpty {
951
+ keyboardToolbar = nil
952
+ } else {
953
+ let toolbar = keyboardToolbar ?? UIToolbar(frame: CGRect(x: 0, y: 0, width: 320, height: 44))
954
+ toolbar.items = keyboardToolbarItems.map { toolbarItem(from: $0) }
955
+ toolbar.sizeToFit()
956
+ keyboardToolbar = toolbar
957
+ }
958
+ textField.inputAccessoryView = keyboardToolbar
959
+ textView.inputAccessoryView = keyboardToolbar
960
+ if activeInput.isFirstResponder {
961
+ activeInput.reloadInputViews()
962
+ }
963
+ }
964
+
965
+ private func toolbarItem(from spec: NSDictionary) -> UIBarButtonItem {
966
+ let kind = spec["kind"] as? String ?? "button"
967
+ if kind == "flexibleSpace" {
968
+ return .flexibleSpace()
969
+ }
970
+ let itemId = spec["itemId"] as? String ?? ""
971
+ let title = spec["title"] as? String ?? ""
972
+ let spokenLabel = spec["accessibilityLabel"] as? String ?? ""
973
+ let testID = spec["testID"] as? String ?? ""
974
+ let action: UIAction
975
+ if kind == "done" {
976
+ action = UIAction { [weak self] _ in self?.blur() }
977
+ } else {
978
+ action = UIAction { [weak self] _ in self?.onKeyboardToolbarPress?(itemId) }
979
+ }
980
+
981
+ let item: UIBarButtonItem
982
+ let systemItem = kind == "done" && title.isEmpty
983
+ ? UIBarButtonItem.SystemItem.done
984
+ : PCTextFieldView.toolbarSystemItem(spec["systemItem"] as? String ?? "")
985
+ let icon = PCButtonSupport.Icon(dictionary: spec as? [String: Any] ?? [:])
986
+ if let systemItem {
987
+ item = UIBarButtonItem(systemItem: systemItem, primaryAction: action)
988
+ } else if icon.isPresent {
989
+ item = UIBarButtonItem(title: nil, image: nil, primaryAction: action, menu: nil)
990
+ // The title is what VoiceOver reads for an icon button
991
+ item.accessibilityLabel = title.isEmpty ? nil : title
992
+ let generation = toolbarGeneration
993
+ item.image = PCButtonSupport.image(for: icon) { [weak self, weak item] image in
994
+ guard let self, self.toolbarGeneration == generation else { return }
995
+ item?.image = image
996
+ }
997
+ } else {
998
+ item = UIBarButtonItem(title: title, image: nil, primaryAction: action, menu: nil)
999
+ }
1000
+ if (spec["prominent"] as? String) == "true" {
1001
+ PCTextFieldView.makeProminent(item)
1002
+ }
1003
+ if !spokenLabel.isEmpty {
1004
+ item.accessibilityLabel = spokenLabel
1005
+ }
1006
+ item.accessibilityIdentifier = testID.isEmpty ? nil : testID
1007
+ return item
1008
+ }
1009
+
1010
+ /// The prominent style: tinted glass on iOS 26, bold before
1011
+ private static func makeProminent(_ item: UIBarButtonItem) {
1012
+ #if compiler(>=6.2)
1013
+ if #available(iOS 26.0, *) {
1014
+ item.style = .prominent
1015
+ return
1016
+ }
1017
+ #endif
1018
+ item.style = .done
1019
+ }
1020
+
1021
+ private static func toolbarSystemItem(_ value: String) -> UIBarButtonItem.SystemItem? {
1022
+ switch value {
1023
+ case "done": return .done
1024
+ case "cancel": return .cancel
1025
+ case "save": return .save
1026
+ case "add": return .add
1027
+ case "edit": return .edit
1028
+ case "close": return .close
1029
+ case "search": return .search
1030
+ case "compose": return .compose
1031
+ case "reply": return .reply
1032
+ case "action": return .action
1033
+ case "camera": return .camera
1034
+ case "trash": return .trash
1035
+ case "undo": return .undo
1036
+ case "redo": return .redo
1037
+ default: return nil
1038
+ }
1039
+ }
1040
+
1041
+ /// Prefix and suffix labels, icons, the clear button and the password
1042
+ /// toggle. Single-line only: a text view has no accessory views.
1043
+ private func applyAccessories() {
1044
+ textField.leftView = nil
1045
+ textField.rightView = nil
1046
+ textField.leftViewMode = .never
1047
+ textField.rightViewMode = .never
1048
+
1049
+ if let leading = accessoryView(
1050
+ image: leadingImage, tinted: leadingIcon.tinted, text: prefix, action: nil,
1051
+ testID: leadingIconTestID, spokenLabel: leadingIconSpokenLabel
1052
+ ) {
1053
+ textField.leftView = leading
1054
+ textField.leftViewMode = .always
1055
+ }
1056
+
1057
+ let trailingAction: (() -> Void)?
1058
+ let trailingImageForSlot: UIImage?
1059
+ if trailingImage != nil {
1060
+ trailingImageForSlot = trailingImage
1061
+ trailingAction = { [weak self] in self?.onTrailingIconPress?() }
1062
+ } else if passwordToggle, secureTextEntry || revealed {
1063
+ trailingImageForSlot = UIImage(systemName: revealed ? "eye.slash" : "eye")
1064
+ trailingAction = { [weak self] in self?.toggleReveal() }
1065
+ } else {
1066
+ trailingImageForSlot = nil
1067
+ trailingAction = nil
1068
+ }
1069
+ let trailingTinted = trailingImage != nil ? trailingIcon.tinted : true
1070
+ let customTrailing = trailingImage != nil
1071
+ if let trailing = accessoryView(
1072
+ image: trailingImageForSlot, tinted: trailingTinted, text: suffix, action: trailingAction,
1073
+ testID: customTrailing ? trailingIconTestID : "",
1074
+ spokenLabel: customTrailing ? trailingIconSpokenLabel : ""
1075
+ ) {
1076
+ textField.rightView = trailing
1077
+ textField.rightViewMode = .always
1078
+ }
1079
+
1080
+ // A right view takes the clear button's place, as UIKit draws them in the same slot
1081
+ switch clearButtonMode {
1082
+ case "while-editing": textField.clearButtonMode = .whileEditing
1083
+ case "unless-editing": textField.clearButtonMode = .unlessEditing
1084
+ case "always": textField.clearButtonMode = .always
1085
+ default: textField.clearButtonMode = .never
1086
+ }
1087
+ setNeedsLayout()
1088
+ }
1089
+
1090
+ /// A label and/or an image button for the field's left or right slot.
1091
+ private func accessoryView(
1092
+ image: UIImage?, tinted: Bool, text: String, action: (() -> Void)?,
1093
+ testID: String, spokenLabel: String
1094
+ ) -> UIView? {
1095
+ guard image != nil || !text.isEmpty else { return nil }
1096
+ let stack = UIStackView()
1097
+ stack.axis = .horizontal
1098
+ stack.alignment = .center
1099
+ stack.spacing = 6
1100
+ stack.isLayoutMarginsRelativeArrangement = true
1101
+ stack.layoutMargins = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)
1102
+
1103
+ if let image {
1104
+ let config = UIImage.SymbolConfiguration(font: inputFont)
1105
+ if let action {
1106
+ let button = PCAccessoryButton(type: .system)
1107
+ button.setImage(image.applyingSymbolConfiguration(config) ?? image, for: .normal)
1108
+ button.tintColor = tinted ? .secondaryLabel : nil
1109
+ button.action = action
1110
+ button.accessibilityLabel = passwordToggle && trailingImage == nil
1111
+ ? (revealed ? "Hide password" : "Show password")
1112
+ : (spokenLabel.isEmpty ? nil : spokenLabel)
1113
+ button.accessibilityIdentifier = testID.isEmpty ? nil : testID
1114
+ stack.addArrangedSubview(button)
1115
+ } else {
1116
+ let imageView = UIImageView(image: image.applyingSymbolConfiguration(config) ?? image)
1117
+ imageView.tintColor = tinted ? .secondaryLabel : nil
1118
+ imageView.contentMode = .scaleAspectFit
1119
+ imageView.accessibilityIdentifier = testID.isEmpty ? nil : testID
1120
+ if !spokenLabel.isEmpty {
1121
+ imageView.isAccessibilityElement = true
1122
+ imageView.accessibilityLabel = spokenLabel
1123
+ }
1124
+ stack.addArrangedSubview(imageView)
1125
+ }
1126
+ }
1127
+ if !text.isEmpty {
1128
+ let label = UILabel()
1129
+ label.text = text
1130
+ label.font = inputFont
1131
+ label.textColor = .secondaryLabel
1132
+ label.adjustsFontForContentSizeCategory = true
1133
+ stack.addArrangedSubview(label)
1134
+ }
1135
+ stack.frame.size = stack.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
1136
+ return stack
1137
+ }
1138
+
1139
+ /// Shows or hides the text of a secure field. Toggling `isSecureTextEntry`
1140
+ /// makes UIKit clear the field on the next keystroke; re-inserting the
1141
+ /// text through the text range keeps it and the caret.
1142
+ private func toggleReveal() {
1143
+ revealed.toggle()
1144
+ let current = textField.text ?? ""
1145
+ textField.isSecureTextEntry = secureTextEntry && !revealed
1146
+ if textField.isFirstResponder, let range = textField.textRange(from: textField.beginningOfDocument, to: textField.endOfDocument) {
1147
+ textField.replace(range, withText: current)
1148
+ }
1149
+ applyAccessories()
1150
+ }
1151
+
1152
+ private func performAutoFocus() {
1153
+ guard !autoFocusDone else { return }
1154
+ autoFocusDone = true
1155
+ DispatchQueue.main.async { [weak self] in self?.focus() }
1156
+ }
1157
+
1158
+ public override func didMoveToWindow() {
1159
+ super.didMoveToWindow()
1160
+ if window != nil, autoFocus { performAutoFocus() }
1161
+ }
1162
+
1163
+ public override func tintColorDidChange() {
1164
+ super.tintColorDidChange()
1165
+ applyColors()
1166
+ }
1167
+
1168
+ public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
1169
+ super.traitCollectionDidChange(previousTraitCollection)
1170
+ if previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory {
1171
+ applyFonts()
1172
+ applyAccessories()
1173
+ }
1174
+ if previousTraitCollection?.hasDifferentColorAppearance(comparedTo: traitCollection) == true {
1175
+ applyColors()
1176
+ }
1177
+ }
1178
+
1179
+ // MARK: - Layout
1180
+
1181
+ private struct Metrics {
1182
+ var captionHeight: CGFloat
1183
+ var inputHeight: CGFloat
1184
+ var footerHeight: CGFloat
1185
+ var leading: Bool
1186
+ /// A multi-line field past maxLines scrolls its text
1187
+ var inputScrolls: Bool
1188
+ var total: CGFloat {
1189
+ var height = leading ? max(inputHeight, captionHeight) : inputHeight
1190
+ if captionHeight > 0 && !leading { height += captionHeight + 6 }
1191
+ if footerHeight > 0 { height += footerHeight + 4 }
1192
+ return height
1193
+ }
1194
+ }
1195
+
1196
+ /// The x where the field (and the footer) starts: after a leading label column
1197
+ private func fieldOffset() -> CGFloat {
1198
+ leadingLabel ? leadingColumnWidth + leadingSpacing : 0
1199
+ }
1200
+
1201
+ private func metrics(for width: CGFloat) -> Metrics {
1202
+ let captionHeight = label.isEmpty ? 0 : ceil(captionLabel.font.lineHeight)
1203
+ let fieldWidth = max(1, width - fieldOffset())
1204
+
1205
+ var inputHeight: CGFloat
1206
+ var inputScrolls = false
1207
+ if multiline {
1208
+ let fitted = ceil(textView.sizeThatFits(CGSize(width: fieldWidth, height: .greatestFiniteMagnitude)).height)
1209
+ inputHeight = max(singleLineMinHeight, fitted)
1210
+ let lineHeight = inputFont.lineHeight
1211
+ let chrome = textViewInsets.top + textViewInsets.bottom
1212
+ if minLines > 0 {
1213
+ inputHeight = max(inputHeight, ceil(lineHeight * CGFloat(minLines) + chrome))
1214
+ }
1215
+ if maxLines > 0 {
1216
+ let cap = max(singleLineMinHeight, ceil(lineHeight * CGFloat(maxLines) + chrome))
1217
+ inputScrolls = fitted > cap
1218
+ inputHeight = min(inputHeight, cap)
1219
+ }
1220
+ } else {
1221
+ let fitted = textField.sizeThatFits(CGSize(width: fieldWidth, height: .greatestFiniteMagnitude))
1222
+ inputHeight = max(singleLineMinHeight, ceil(fitted.height))
1223
+ }
1224
+
1225
+ var footerHeight: CGFloat = 0
1226
+ if !footerLabel.isHidden || !counterLabel.isHidden {
1227
+ let counterWidth = counterLabel.isHidden
1228
+ ? 0
1229
+ : ceil(counterLabel.sizeThatFits(CGSize(width: fieldWidth, height: .greatestFiniteMagnitude)).width) + 8
1230
+ let messageWidth = max(1, fieldWidth - counterWidth)
1231
+ let messageHeight = footerLabel.isHidden
1232
+ ? 0
1233
+ : ceil(footerLabel.sizeThatFits(CGSize(width: messageWidth, height: .greatestFiniteMagnitude)).height)
1234
+ let counterHeight = counterLabel.isHidden ? 0 : ceil(counterLabel.font.lineHeight)
1235
+ footerHeight = max(messageHeight, counterHeight)
1236
+ }
1237
+ return Metrics(
1238
+ captionHeight: captionHeight,
1239
+ inputHeight: inputHeight,
1240
+ footerHeight: footerHeight,
1241
+ leading: leadingLabel,
1242
+ inputScrolls: inputScrolls
1243
+ )
1244
+ }
1245
+
1246
+ public override func layoutSubviews() {
1247
+ super.layoutSubviews()
1248
+ let width = bounds.width
1249
+ let m = metrics(for: width)
1250
+ let x = fieldOffset()
1251
+ let fieldWidth = max(0, width - x)
1252
+ var y: CGFloat = 0
1253
+ if multiline, textView.isScrollEnabled != m.inputScrolls {
1254
+ textView.isScrollEnabled = m.inputScrolls
1255
+ }
1256
+
1257
+ if m.leading {
1258
+ // Label column and field side by side, centred on the taller one
1259
+ let rowHeight = max(m.inputHeight, m.captionHeight)
1260
+ captionLabel.frame = CGRect(
1261
+ x: 0,
1262
+ y: (rowHeight - m.captionHeight) / 2,
1263
+ width: leadingColumnWidth,
1264
+ height: m.captionHeight
1265
+ )
1266
+ activeInput.frame = CGRect(x: x, y: (rowHeight - m.inputHeight) / 2, width: fieldWidth, height: m.inputHeight)
1267
+ y += rowHeight
1268
+ } else {
1269
+ if m.captionHeight > 0 {
1270
+ captionLabel.frame = CGRect(x: 0, y: y, width: width, height: m.captionHeight)
1271
+ y += m.captionHeight + captionSpacing
1272
+ }
1273
+ activeInput.frame = CGRect(x: 0, y: y, width: width, height: m.inputHeight)
1274
+ y += m.inputHeight
1275
+ }
1276
+
1277
+ if m.footerHeight > 0 {
1278
+ y += footerSpacing
1279
+ let counterWidth = counterLabel.isHidden
1280
+ ? 0
1281
+ : ceil(counterLabel.sizeThatFits(CGSize(width: fieldWidth, height: .greatestFiniteMagnitude)).width)
1282
+ let messageWidth = max(0, fieldWidth - (counterWidth > 0 ? counterWidth + 8 : 0))
1283
+ footerLabel.frame = CGRect(x: x, y: y, width: messageWidth, height: m.footerHeight)
1284
+ counterLabel.frame = CGRect(x: width - counterWidth, y: y, width: counterWidth, height: m.footerHeight)
1285
+ }
1286
+ }
1287
+
1288
+ /// Called by the measuring pipeline to get the size for Yoga layout: the
1289
+ /// height the field needs at the given width. The width is whatever Yoga
1290
+ /// offers; a text field fills its row.
1291
+ @objc public func sizeForLayout(withConstrainedTo constrainedSize: CGSize) -> CGSize {
1292
+ let width: CGFloat
1293
+ if constrainedSize.width > 0 && constrainedSize.width < CGFloat.greatestFiniteMagnitude {
1294
+ width = constrainedSize.width
1295
+ } else if bounds.width > 0 {
1296
+ width = bounds.width
1297
+ } else {
1298
+ width = PCConstants.fallbackWidth
1299
+ }
1300
+ return CGSize(width: 0, height: ceil(metrics(for: width).total))
1301
+ }
1302
+
1303
+ // MARK: - Trait mapping
1304
+
1305
+ private static func keyboardType(_ value: String) -> UIKeyboardType {
1306
+ switch value {
1307
+ case "number-pad": return .numberPad
1308
+ case "decimal-pad": return .decimalPad
1309
+ case "numeric": return .decimalPad
1310
+ case "email-address": return .emailAddress
1311
+ case "phone-pad": return .phonePad
1312
+ case "url": return .URL
1313
+ case "ascii-capable": return .asciiCapable
1314
+ case "numbers-and-punctuation": return .numbersAndPunctuation
1315
+ case "name-phone-pad": return .namePhonePad
1316
+ case "twitter": return .twitter
1317
+ case "web-search": return .webSearch
1318
+ case "visible-password": return .asciiCapable
1319
+ default: return .default
1320
+ }
1321
+ }
1322
+
1323
+ private static func returnKeyType(_ value: String) -> UIReturnKeyType {
1324
+ switch value {
1325
+ case "done": return .done
1326
+ case "go": return .go
1327
+ case "next": return .next
1328
+ case "search": return .search
1329
+ case "send": return .send
1330
+ default: return .default
1331
+ }
1332
+ }
1333
+
1334
+ private static func autocapitalization(_ value: String) -> UITextAutocapitalizationType {
1335
+ switch value {
1336
+ case "none": return .none
1337
+ case "words": return .words
1338
+ case "characters": return .allCharacters
1339
+ default: return .sentences
1340
+ }
1341
+ }
1342
+
1343
+ private static func keyboardAppearance(_ value: String) -> UIKeyboardAppearance {
1344
+ switch value {
1345
+ case "light": return .light
1346
+ case "dark": return .dark
1347
+ default: return .default
1348
+ }
1349
+ }
1350
+
1351
+ /// React Native autoComplete values onto text content types, as the
1352
+ /// core TextInput maps them, plus the names it leaves out. Values with no
1353
+ /// iOS equivalent, and unknown ones, give no content type.
1354
+ private static func contentType(_ value: String) -> UITextContentType? {
1355
+ switch value {
1356
+ case "username", "username-new": return .username
1357
+ case "password", "current-password": return .password
1358
+ case "new-password", "password-new": return .newPassword
1359
+ case "one-time-code", "sms-otp", "email-otp", "2fa-app-otp": return .oneTimeCode
1360
+ case "email": return .emailAddress
1361
+ case "name": return .name
1362
+ case "given-name", "name-given": return .givenName
1363
+ case "family-name", "name-family": return .familyName
1364
+ case "additional-name", "name-middle": return .middleName
1365
+ case "honorific-prefix", "name-prefix": return .namePrefix
1366
+ case "honorific-suffix", "name-suffix": return .nameSuffix
1367
+ case "nickname": return .nickname
1368
+ case "organization": return .organizationName
1369
+ case "organization-title": return .jobTitle
1370
+ case "tel", "tel-national", "tel-device": return .telephoneNumber
1371
+ case "street-address", "postal-address": return .fullStreetAddress
1372
+ case "address-line1": return .streetAddressLine1
1373
+ case "address-line2", "postal-address-extended": return .streetAddressLine2
1374
+ case "postal-address-locality": return .addressCity
1375
+ case "postal-address-region": return .addressState
1376
+ case "postal-address-dependent-locality": return .sublocality
1377
+ case "postal-code": return .postalCode
1378
+ case "country", "postal-address-country": return .countryName
1379
+ case "cc-number": return .creditCardNumber
1380
+ case "url": return .URL
1381
+ case "flight-number": return .flightNumber
1382
+ default:
1383
+ if #available(iOS 17.0, *) { return contentType17(value) }
1384
+ return nil
1385
+ }
1386
+ }
1387
+
1388
+ @available(iOS 17.0, *)
1389
+ private static func contentType17(_ value: String) -> UITextContentType? {
1390
+ switch value {
1391
+ case "cc-exp": return .creditCardExpiration
1392
+ case "cc-exp-month": return .creditCardExpirationMonth
1393
+ case "cc-exp-year": return .creditCardExpirationYear
1394
+ case "cc-csc": return .creditCardSecurityCode
1395
+ case "cc-name": return .creditCardName
1396
+ case "cc-given-name": return .creditCardGivenName
1397
+ case "cc-middle-name": return .creditCardMiddleName
1398
+ case "cc-family-name": return .creditCardFamilyName
1399
+ case "cc-type": return .creditCardType
1400
+ case "birthdate-full": return .birthdate
1401
+ case "birthdate-day": return .birthdateDay
1402
+ case "birthdate-month": return .birthdateMonth
1403
+ case "birthdate-year": return .birthdateYear
1404
+ default: return nil
1405
+ }
1406
+ }
1407
+
1408
+ private static func smartQuotesType(_ value: String) -> UITextSmartQuotesType {
1409
+ switch value {
1410
+ case "yes": return .yes
1411
+ case "no": return .no
1412
+ default: return .default
1413
+ }
1414
+ }
1415
+
1416
+ private static func smartDashesType(_ value: String) -> UITextSmartDashesType {
1417
+ switch value {
1418
+ case "yes": return .yes
1419
+ case "no": return .no
1420
+ default: return .default
1421
+ }
1422
+ }
1423
+
1424
+ private static func smartInsertDeleteType(_ value: String) -> UITextSmartInsertDeleteType {
1425
+ switch value {
1426
+ case "yes": return .yes
1427
+ case "no": return .no
1428
+ default: return .default
1429
+ }
1430
+ }
1431
+
1432
+ @available(iOS 17.0, *)
1433
+ private static func inlinePredictionType(_ value: String) -> UITextInlinePredictionType {
1434
+ switch value {
1435
+ case "yes": return .yes
1436
+ case "no": return .no
1437
+ default: return .default
1438
+ }
1439
+ }
1440
+
1441
+ @available(iOS 18.0, *)
1442
+ private static func mathCompletionType(_ value: String) -> UITextMathExpressionCompletionType {
1443
+ switch value {
1444
+ case "yes": return .yes
1445
+ case "no": return .no
1446
+ default: return .default
1447
+ }
1448
+ }
1449
+
1450
+ @available(iOS 18.0, *)
1451
+ private static func writingToolsBehavior(_ value: String) -> UIWritingToolsBehavior {
1452
+ switch value {
1453
+ case "complete": return .complete
1454
+ case "limited": return .limited
1455
+ case "none": return .none
1456
+ default: return .default
1457
+ }
1458
+ }
1459
+ }
1460
+
1461
+ // MARK: - Helpers
1462
+
1463
+ /// The single-line input: insets for the box the field view draws, and
1464
+ /// VoiceOver activation for a field acting as a button.
1465
+ final class PCInputTextField: UITextField {
1466
+ var textInsets: UIEdgeInsets = .zero {
1467
+ didSet { if oldValue != textInsets { setNeedsLayout() } }
1468
+ }
1469
+
1470
+ /// Returns true when it handled the activation
1471
+ var onActivate: (() -> Bool)?
1472
+
1473
+ private func inset(_ rect: CGRect) -> CGRect {
1474
+ // Accessory views bring their own margins
1475
+ let left = leftView == nil ? textInsets.left : 0
1476
+ let right = rightView == nil ? textInsets.right : 0
1477
+ return rect.inset(by: UIEdgeInsets(top: textInsets.top, left: left, bottom: textInsets.bottom, right: right))
1478
+ }
1479
+
1480
+ override func textRect(forBounds bounds: CGRect) -> CGRect {
1481
+ inset(super.textRect(forBounds: bounds))
1482
+ }
1483
+
1484
+ override func editingRect(forBounds bounds: CGRect) -> CGRect {
1485
+ inset(super.editingRect(forBounds: bounds))
1486
+ }
1487
+
1488
+ override func placeholderRect(forBounds bounds: CGRect) -> CGRect {
1489
+ inset(super.placeholderRect(forBounds: bounds))
1490
+ }
1491
+
1492
+ override func accessibilityActivate() -> Bool {
1493
+ onActivate?() == true || super.accessibilityActivate()
1494
+ }
1495
+ }
1496
+
1497
+ /// A `UITextView` with a placeholder, drawn where the text starts.
1498
+ final class PCPlaceholderTextView: UITextView {
1499
+ let placeholderLabel = UILabel()
1500
+
1501
+ /// Returns true when it handled the activation
1502
+ var onActivate: (() -> Bool)?
1503
+
1504
+ override func accessibilityActivate() -> Bool {
1505
+ onActivate?() == true || super.accessibilityActivate()
1506
+ }
1507
+
1508
+ var placeholder: String = "" {
1509
+ didSet {
1510
+ placeholderLabel.text = placeholder
1511
+ updatePlaceholderVisibility()
1512
+ }
1513
+ }
1514
+
1515
+ override init(frame: CGRect, textContainer: NSTextContainer?) {
1516
+ super.init(frame: frame, textContainer: textContainer)
1517
+ placeholderLabel.textColor = .placeholderText
1518
+ placeholderLabel.numberOfLines = 0
1519
+ placeholderLabel.adjustsFontForContentSizeCategory = true
1520
+ placeholderLabel.isUserInteractionEnabled = false
1521
+ addSubview(placeholderLabel)
1522
+ }
1523
+
1524
+ required init?(coder: NSCoder) {
1525
+ super.init(coder: coder)
1526
+ }
1527
+
1528
+ func updatePlaceholderVisibility() {
1529
+ placeholderLabel.isHidden = !(text ?? "").isEmpty || placeholder.isEmpty
1530
+ }
1531
+
1532
+ override func layoutSubviews() {
1533
+ super.layoutSubviews()
1534
+ let x = textContainerInset.left + textContainer.lineFragmentPadding
1535
+ let width = max(0, bounds.width - x - textContainerInset.right - textContainer.lineFragmentPadding)
1536
+ let height = placeholderLabel.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude)).height
1537
+ placeholderLabel.frame = CGRect(x: x, y: textContainerInset.top, width: width, height: height)
1538
+ }
1539
+ }
1540
+
1541
+ /// A button with a closure action, for the accessory slots.
1542
+ final class PCAccessoryButton: UIButton {
1543
+ var action: (() -> Void)?
1544
+
1545
+ override init(frame: CGRect) {
1546
+ super.init(frame: frame)
1547
+ addTarget(self, action: #selector(pressed), for: .touchUpInside)
1548
+ }
1549
+
1550
+ required init?(coder: NSCoder) {
1551
+ super.init(coder: coder)
1552
+ addTarget(self, action: #selector(pressed), for: .touchUpInside)
1553
+ }
1554
+
1555
+ @objc private func pressed() {
1556
+ action?()
1557
+ }
1558
+ }