react-native-platform-components 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/README.md +24 -30
  2. package/android/src/main/java/com/platformcomponents/PCButtonGroupView.kt +498 -0
  3. package/android/src/main/java/com/platformcomponents/PCButtonGroupViewManager.kt +191 -0
  4. package/android/src/main/java/com/platformcomponents/PCButtonSupport.kt +127 -0
  5. package/android/src/main/java/com/platformcomponents/PCButtonView.kt +345 -0
  6. package/android/src/main/java/com/platformcomponents/PCButtonViewManager.kt +126 -0
  7. package/android/src/main/java/com/platformcomponents/PCExpressive.kt +107 -0
  8. package/android/src/main/java/com/platformcomponents/PCFloatingToolbarView.kt +115 -0
  9. package/android/src/main/java/com/platformcomponents/PCFloatingToolbarViewManager.kt +65 -0
  10. package/android/src/main/java/com/platformcomponents/PCSegmentedControlView.kt +11 -3
  11. package/android/src/main/java/com/platformcomponents/PCSegmentedControlViewManager.kt +6 -1
  12. package/android/src/main/java/com/platformcomponents/PCTextFieldView.kt +1018 -0
  13. package/android/src/main/java/com/platformcomponents/PCTextFieldViewManager.kt +248 -0
  14. package/android/src/main/java/com/platformcomponents/PlatformComponentsPackage.kt +4 -0
  15. package/android/src/main/res/values/attrs.xml +13 -0
  16. package/android/src/main/res/values/styles.xml +183 -0
  17. package/ios/PCButton.h +10 -0
  18. package/ios/PCButton.mm +196 -0
  19. package/ios/PCButtonGroup.h +10 -0
  20. package/ios/PCButtonGroup.mm +256 -0
  21. package/ios/PCButtonGroupView.swift +254 -0
  22. package/ios/PCButtonSupport.swift +141 -0
  23. package/ios/PCButtonView.swift +155 -0
  24. package/ios/PCFloatingToolbar.h +10 -0
  25. package/ios/PCFloatingToolbar.mm +76 -0
  26. package/ios/PCFloatingToolbarView.swift +95 -0
  27. package/ios/PCTextField.h +10 -0
  28. package/ios/PCTextField.mm +391 -0
  29. package/ios/PCTextFieldView.swift +1012 -0
  30. package/lib/commonjs/Button.js +77 -0
  31. package/lib/commonjs/Button.js.map +1 -0
  32. package/lib/commonjs/ButtonGroup.js +99 -0
  33. package/lib/commonjs/ButtonGroup.js.map +1 -0
  34. package/lib/commonjs/ButtonGroupNativeComponent.ts +121 -0
  35. package/lib/commonjs/ButtonNativeComponent.ts +76 -0
  36. package/lib/commonjs/FloatingToolbar.js +69 -0
  37. package/lib/commonjs/FloatingToolbar.js.map +1 -0
  38. package/lib/commonjs/FloatingToolbarNativeComponent.ts +31 -0
  39. package/lib/commonjs/SegmentedControl.js +11 -85
  40. package/lib/commonjs/SegmentedControl.js.map +1 -1
  41. package/lib/commonjs/SegmentedControlNativeComponent.ts +3 -0
  42. package/lib/commonjs/TextField.js +195 -0
  43. package/lib/commonjs/TextField.js.map +1 -0
  44. package/lib/commonjs/TextFieldNativeComponent.ts +218 -0
  45. package/lib/commonjs/focusRegistry.js +78 -0
  46. package/lib/commonjs/focusRegistry.js.map +1 -0
  47. package/lib/commonjs/icons.js +92 -0
  48. package/lib/commonjs/icons.js.map +1 -0
  49. package/lib/commonjs/index.js +66 -0
  50. package/lib/commonjs/index.js.map +1 -1
  51. package/lib/commonjs/index.web.js +32 -2
  52. package/lib/commonjs/index.web.js.map +1 -1
  53. package/lib/commonjs/labelStyle.js +27 -0
  54. package/lib/commonjs/labelStyle.js.map +1 -0
  55. package/lib/commonjs/reactNativeInternals.d.js +2 -0
  56. package/lib/commonjs/reactNativeInternals.d.js.map +1 -0
  57. package/lib/module/Button.js +70 -0
  58. package/lib/module/Button.js.map +1 -0
  59. package/lib/module/ButtonGroup.js +92 -0
  60. package/lib/module/ButtonGroup.js.map +1 -0
  61. package/lib/module/ButtonGroupNativeComponent.ts +121 -0
  62. package/lib/module/ButtonNativeComponent.ts +76 -0
  63. package/lib/module/FloatingToolbar.js +62 -0
  64. package/lib/module/FloatingToolbar.js.map +1 -0
  65. package/lib/module/FloatingToolbarNativeComponent.ts +31 -0
  66. package/lib/module/SegmentedControl.js +12 -87
  67. package/lib/module/SegmentedControl.js.map +1 -1
  68. package/lib/module/SegmentedControlNativeComponent.ts +3 -0
  69. package/lib/module/TextField.js +189 -0
  70. package/lib/module/TextField.js.map +1 -0
  71. package/lib/module/TextFieldNativeComponent.ts +218 -0
  72. package/lib/module/focusRegistry.js +74 -0
  73. package/lib/module/focusRegistry.js.map +1 -0
  74. package/lib/module/icons.js +87 -0
  75. package/lib/module/icons.js.map +1 -0
  76. package/lib/module/index.js +6 -0
  77. package/lib/module/index.js.map +1 -1
  78. package/lib/module/index.web.js +28 -0
  79. package/lib/module/index.web.js.map +1 -1
  80. package/lib/module/labelStyle.js +23 -0
  81. package/lib/module/labelStyle.js.map +1 -0
  82. package/lib/module/reactNativeInternals.d.js +2 -0
  83. package/lib/module/reactNativeInternals.d.js.map +1 -0
  84. package/lib/typescript/commonjs/docs/sidebars.d.ts.map +1 -1
  85. package/lib/typescript/commonjs/src/Button.d.ts +76 -0
  86. package/lib/typescript/commonjs/src/Button.d.ts.map +1 -0
  87. package/lib/typescript/commonjs/src/ButtonGroup.d.ts +102 -0
  88. package/lib/typescript/commonjs/src/ButtonGroup.d.ts.map +1 -0
  89. package/lib/typescript/commonjs/src/ButtonGroupNativeComponent.d.ts +93 -0
  90. package/lib/typescript/commonjs/src/ButtonGroupNativeComponent.d.ts.map +1 -0
  91. package/lib/typescript/commonjs/src/ButtonNativeComponent.d.ts +57 -0
  92. package/lib/typescript/commonjs/src/ButtonNativeComponent.d.ts.map +1 -0
  93. package/lib/typescript/commonjs/src/FloatingToolbar.d.ts +45 -0
  94. package/lib/typescript/commonjs/src/FloatingToolbar.d.ts.map +1 -0
  95. package/lib/typescript/commonjs/src/FloatingToolbarNativeComponent.d.ts +24 -0
  96. package/lib/typescript/commonjs/src/FloatingToolbarNativeComponent.d.ts.map +1 -0
  97. package/lib/typescript/commonjs/src/SegmentedControl.d.ts +17 -46
  98. package/lib/typescript/commonjs/src/SegmentedControl.d.ts.map +1 -1
  99. package/lib/typescript/commonjs/src/SegmentedControlNativeComponent.d.ts +2 -0
  100. package/lib/typescript/commonjs/src/SegmentedControlNativeComponent.d.ts.map +1 -1
  101. package/lib/typescript/commonjs/src/TextField.d.ts +174 -0
  102. package/lib/typescript/commonjs/src/TextField.d.ts.map +1 -0
  103. package/lib/typescript/commonjs/src/TextFieldNativeComponent.d.ts +162 -0
  104. package/lib/typescript/commonjs/src/TextFieldNativeComponent.d.ts.map +1 -0
  105. package/lib/typescript/commonjs/src/focusRegistry.d.ts +13 -0
  106. package/lib/typescript/commonjs/src/focusRegistry.d.ts.map +1 -0
  107. package/lib/typescript/commonjs/src/icons.d.ts +52 -0
  108. package/lib/typescript/commonjs/src/icons.d.ts.map +1 -0
  109. package/lib/typescript/commonjs/src/index.d.ts +6 -0
  110. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  111. package/lib/typescript/commonjs/src/index.web.d.ts +121 -0
  112. package/lib/typescript/commonjs/src/index.web.d.ts.map +1 -1
  113. package/lib/typescript/commonjs/src/labelStyle.d.ts +20 -0
  114. package/lib/typescript/commonjs/src/labelStyle.d.ts.map +1 -0
  115. package/lib/typescript/commonjs/src/sharedTypes.d.ts +6 -1
  116. package/lib/typescript/commonjs/src/sharedTypes.d.ts.map +1 -1
  117. package/lib/typescript/module/docs/sidebars.d.ts.map +1 -1
  118. package/lib/typescript/module/src/Button.d.ts +76 -0
  119. package/lib/typescript/module/src/Button.d.ts.map +1 -0
  120. package/lib/typescript/module/src/ButtonGroup.d.ts +102 -0
  121. package/lib/typescript/module/src/ButtonGroup.d.ts.map +1 -0
  122. package/lib/typescript/module/src/ButtonGroupNativeComponent.d.ts +93 -0
  123. package/lib/typescript/module/src/ButtonGroupNativeComponent.d.ts.map +1 -0
  124. package/lib/typescript/module/src/ButtonNativeComponent.d.ts +57 -0
  125. package/lib/typescript/module/src/ButtonNativeComponent.d.ts.map +1 -0
  126. package/lib/typescript/module/src/FloatingToolbar.d.ts +45 -0
  127. package/lib/typescript/module/src/FloatingToolbar.d.ts.map +1 -0
  128. package/lib/typescript/module/src/FloatingToolbarNativeComponent.d.ts +24 -0
  129. package/lib/typescript/module/src/FloatingToolbarNativeComponent.d.ts.map +1 -0
  130. package/lib/typescript/module/src/SegmentedControl.d.ts +17 -46
  131. package/lib/typescript/module/src/SegmentedControl.d.ts.map +1 -1
  132. package/lib/typescript/module/src/SegmentedControlNativeComponent.d.ts +2 -0
  133. package/lib/typescript/module/src/SegmentedControlNativeComponent.d.ts.map +1 -1
  134. package/lib/typescript/module/src/TextField.d.ts +174 -0
  135. package/lib/typescript/module/src/TextField.d.ts.map +1 -0
  136. package/lib/typescript/module/src/TextFieldNativeComponent.d.ts +162 -0
  137. package/lib/typescript/module/src/TextFieldNativeComponent.d.ts.map +1 -0
  138. package/lib/typescript/module/src/focusRegistry.d.ts +13 -0
  139. package/lib/typescript/module/src/focusRegistry.d.ts.map +1 -0
  140. package/lib/typescript/module/src/icons.d.ts +52 -0
  141. package/lib/typescript/module/src/icons.d.ts.map +1 -0
  142. package/lib/typescript/module/src/index.d.ts +6 -0
  143. package/lib/typescript/module/src/index.d.ts.map +1 -1
  144. package/lib/typescript/module/src/index.web.d.ts +121 -0
  145. package/lib/typescript/module/src/index.web.d.ts.map +1 -1
  146. package/lib/typescript/module/src/labelStyle.d.ts +20 -0
  147. package/lib/typescript/module/src/labelStyle.d.ts.map +1 -0
  148. package/lib/typescript/module/src/sharedTypes.d.ts +6 -1
  149. package/lib/typescript/module/src/sharedTypes.d.ts.map +1 -1
  150. package/package.json +25 -4
  151. package/react-native.config.js +3 -0
  152. package/shared/PCButtonComponentDescriptors-custom.h +22 -0
  153. package/shared/PCButtonGroupComponentDescriptors-custom.h +22 -0
  154. package/shared/PCButtonGroupShadowNode-custom.cpp +48 -0
  155. package/shared/PCButtonGroupShadowNode-custom.h +55 -0
  156. package/shared/PCButtonGroupState-custom.h +61 -0
  157. package/shared/PCButtonShadowNode-custom.cpp +48 -0
  158. package/shared/PCButtonShadowNode-custom.h +55 -0
  159. package/shared/PCButtonState-custom.h +61 -0
  160. package/shared/PCTextFieldComponentDescriptors-custom.h +22 -0
  161. package/shared/PCTextFieldShadowNode-custom.cpp +47 -0
  162. package/shared/PCTextFieldShadowNode-custom.h +55 -0
  163. package/shared/PCTextFieldState-custom.h +61 -0
  164. package/shared/react/renderer/components/PlatformComponentsViewSpec/ComponentDescriptors.h +3 -0
  165. package/src/Button.tsx +144 -0
  166. package/src/ButtonGroup.tsx +233 -0
  167. package/src/ButtonGroupNativeComponent.ts +121 -0
  168. package/src/ButtonNativeComponent.ts +76 -0
  169. package/src/FloatingToolbar.tsx +113 -0
  170. package/src/FloatingToolbarNativeComponent.ts +31 -0
  171. package/src/SegmentedControl.tsx +26 -118
  172. package/src/SegmentedControlNativeComponent.ts +3 -0
  173. package/src/TextField.tsx +494 -0
  174. package/src/TextFieldNativeComponent.ts +218 -0
  175. package/src/focusRegistry.ts +89 -0
  176. package/src/icons.ts +116 -0
  177. package/src/index.tsx +6 -0
  178. package/src/index.web.tsx +194 -0
  179. package/src/labelStyle.ts +36 -0
  180. package/src/reactNativeInternals.d.ts +22 -0
  181. package/src/sharedTypes.ts +7 -1
package/README.md CHANGED
@@ -5,34 +5,22 @@
5
5
  [![CI](https://github.com/JarX-Concepts/react-native-platform-components/actions/workflows/ci.yml/badge.svg)](https://github.com/JarX-Concepts/react-native-platform-components/actions/workflows/ci.yml)
6
6
  [![license](https://img.shields.io/npm/l/react-native-platform-components.svg)](./LICENSE)
7
7
 
8
- Native **DatePicker**, **ContextMenu**, **SelectionMenu**, **SegmentedControl** and **LiquidGlass** for React Native. Every component is the real platform widget on both iOS and Android (UIKit and SwiftUI on iOS, Material 3 on Android), behind one typed, declarative API. No JavaScript re-implementations.
9
-
10
- <table>
11
- <tr>
12
- <td align="center"><a href="https://jarx-concepts.github.io/react-native-platform-components/components/datepicker">DatePicker</a></td>
13
- <td align="center"><a href="https://jarx-concepts.github.io/react-native-platform-components/components/contextmenu">ContextMenu</a></td>
14
- <td align="center"><a href="https://jarx-concepts.github.io/react-native-platform-components/components/selectionmenu">SelectionMenu</a></td>
15
- <td align="center"><a href="https://jarx-concepts.github.io/react-native-platform-components/components/segmentedcontrol">SegmentedControl</a></td>
16
- <td align="center"><a href="https://jarx-concepts.github.io/react-native-platform-components/components/liquidglass">LiquidGlass</a></td>
17
- <td align="center"><a href="https://jarx-concepts.github.io/react-native-platform-components/guides/theming">Theme</a></td>
18
- </tr>
19
- <tr>
20
- <td align="center"><a href="https://jarx-concepts.github.io/react-native-platform-components/components/datepicker"><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/ios-datepicker.gif" width="100" alt="DatePicker on iOS" /></a></td>
21
- <td align="center"><a href="https://jarx-concepts.github.io/react-native-platform-components/components/contextmenu"><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/ios-contextmenu.gif" width="100" alt="ContextMenu on iOS" /></a></td>
22
- <td align="center"><a href="https://jarx-concepts.github.io/react-native-platform-components/components/selectionmenu"><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/ios-selectionmenu.gif" width="100" alt="SelectionMenu on iOS" /></a></td>
23
- <td align="center"><a href="https://jarx-concepts.github.io/react-native-platform-components/components/segmentedcontrol"><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/ios-segmentedcontrol.gif" width="100" alt="SegmentedControl on iOS" /></a></td>
24
- <td align="center"><a href="https://jarx-concepts.github.io/react-native-platform-components/components/liquidglass"><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/ios-liquidglass.gif" width="100" alt="LiquidGlass on iOS" /></a></td>
25
- <td align="center"><a href="https://jarx-concepts.github.io/react-native-platform-components/guides/theming"><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/ios-theme.gif" width="100" alt="Brand color and dark mode on iOS" /></a></td>
26
- </tr>
27
- <tr>
28
- <td align="center"><a href="https://jarx-concepts.github.io/react-native-platform-components/components/datepicker"><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/android-datepicker.gif" width="100" alt="DatePicker on Android" /></a></td>
29
- <td align="center"><a href="https://jarx-concepts.github.io/react-native-platform-components/components/contextmenu"><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/android-contextmenu.gif" width="100" alt="ContextMenu on Android" /></a></td>
30
- <td align="center"><a href="https://jarx-concepts.github.io/react-native-platform-components/components/selectionmenu"><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/android-selectionmenu.gif" width="100" alt="SelectionMenu on Android" /></a></td>
31
- <td align="center"><a href="https://jarx-concepts.github.io/react-native-platform-components/components/segmentedcontrol"><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/android-segmentedcontrol.gif" width="100" alt="SegmentedControl on Android" /></a></td>
32
- <td align="center" valign="middle"><sub>iOS 26+ only.<br/>Plain View on Android.</sub></td>
33
- <td align="center"><a href="https://jarx-concepts.github.io/react-native-platform-components/guides/theming"><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/android-theme.gif" width="100" alt="Brand color and dark mode on Android" /></a></td>
34
- </tr>
35
- </table>
8
+ Native **TextField**, **DatePicker**, **ContextMenu**, **SelectionMenu**, **SegmentedControl**, **Button**, **ButtonGroup**, **FloatingToolbar** and **LiquidGlass** for React Native. Every component is the real platform widget on both iOS and Android (UIKit and SwiftUI on iOS, Material 3 Expressive on Android), behind one typed, declarative API. No JavaScript re-implementations.
9
+
10
+ <p align="center">
11
+ <a href="https://jarx-concepts.github.io/react-native-platform-components">
12
+ <picture>
13
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/hero-dark.webp" />
14
+ <img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/hero-light.webp" alt="DatePicker, TextField, SegmentedControl, Button, ContextMenu, LiquidGlass, FloatingToolbar and SelectionMenu on iOS and Android, and the dark mode from useNativeTheme" />
15
+ </picture>
16
+ </a>
17
+ </p>
18
+ <p align="center"><sub>Every tile is the platform's own widget, captured from the example app. The <a href="https://jarx-concepts.github.io/react-native-platform-components">docs site</a> has full recordings of each component on both platforms; <a href="https://jarx-concepts.github.io/react-native-platform-components/components/liquidglass">LiquidGlass</a> is iOS 26 only.</sub></p>
19
+
20
+ <p align="center">
21
+ <a href="https://jarx-concepts.github.io/react-native-platform-components"><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/showreel.gif" width="640" alt="Showreel: the native theme changing brand color and switching to dark mode, then DatePicker, TextField, SegmentedControl, Button, ContextMenu, SelectionMenu, FloatingToolbar and LiquidGlass in use, iOS on the left and Android on the right" /></a>
22
+ </p>
23
+ <p align="center"><sub>iOS on the left, Android on the right. Recorded by the Detox suite in <code>example/</code>.</sub></p>
36
24
 
37
25
  ```sh
38
26
  npm install react-native-platform-components
@@ -57,10 +45,12 @@ That renders `UISegmentedControl` on iOS and Material 3 segmented buttons (`Mate
57
45
 
58
46
  | You need | This library | Common alternative |
59
47
  | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
48
+ | Text field | Material 3 `TextInputLayout` on Android (floating label, outlined or filled, supporting text, error state, icons, counter); `UITextField` on iOS with Writing Tools, inline predictions and smart punctuation. Same `value` / `onChangeText` contract as `TextInput`. | `TextInput`: a bare `EditText` on Android, no Material field; the iOS 17 and 18 text traits are not exposed. `react-native-paper`: Material 3 drawn in JavaScript. |
60
49
  | Date and time pickers | `UIDatePicker` on iOS; `MaterialDatePicker` / `MaterialTimePicker` or the AppCompat dialogs on Android. Declarative `visible` prop, modal or embedded. | `@react-native-community/datetimepicker`: platform dialogs on Android; Material 3 pickers are an [open request](https://github.com/react-native-datetimepicker/datetimepicker/issues/790). |
61
50
  | Segmented control | `UISegmentedControl` on iOS; Material 3 segmented buttons on Android. Icons and badges on both. | `@react-native-segmented-control/segmented-control`: native on iOS, drawn in JavaScript on Android. |
62
51
  | Context menu | `UIContextMenuInteraction` on iOS; `PopupMenu` on Android. Long-press gesture or modal trigger, icons on both. | `zeego`: native on both platforms, through two additional native dependencies (`react-native-ios-context-menu`, `@react-native-menu/menu`). |
63
52
  | Selection menu | System menus on iOS; Material exposed dropdown or `Spinner` on Android. Headless or inline. | `@react-native-picker/picker`: wheel picker on iOS, dialog or dropdown `Spinner` on Android; no menu-style presentation. |
53
+ | Buttons and toolbars | Material 3 Expressive `MaterialButton`, button groups and `FloatingToolbarLayout` on Android (five sizes, shape morphing); `UIButton` configurations and a Liquid Glass toolbar on iOS. | `react-native-paper`: Material 3 (not Expressive) drawn in JavaScript on both platforms. |
64
54
  | Liquid glass | `UIGlassEffect` on iOS 26+, a fallback `View` elsewhere, `isLiquidGlassSupported` flag. | `@callstack/liquid-glass`, `expo-glass-effect`: same idea on iOS; here it ships with the components above. |
65
55
  | All of the above | One package, one API shape (platform-only props under `ios={{ }}` and `android={{ }}`), Fabric + Codegen typed bindings, Expo config plugin, TypeScript. | Four or five packages with different conventions, install steps and upgrade cadences. |
66
56
 
@@ -68,10 +58,14 @@ That renders `UISegmentedControl` on iOS and Material 3 segmented buttons (`Mate
68
58
 
69
59
  | Component | iOS | Android | Docs |
70
60
  | -------------------- | -------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
61
+ | **TextField** | `UITextField` / `UITextView` | `TextInputLayout` (Material 3: outlined or filled, label, supporting text, icons, counter) | [TextField](https://jarx-concepts.github.io/react-native-platform-components/components/textfield) |
71
62
  | **DatePicker** | `UIDatePicker` | `MaterialDatePicker` / `MaterialTimePicker` or AppCompat dialogs | [DatePicker](https://jarx-concepts.github.io/react-native-platform-components/components/datepicker) |
72
63
  | **ContextMenu** | `UIContextMenuInteraction` | `PopupMenu` | [ContextMenu](https://jarx-concepts.github.io/react-native-platform-components/components/contextmenu) |
73
64
  | **SelectionMenu** | System menus | Material exposed dropdown or `Spinner` | [SelectionMenu](https://jarx-concepts.github.io/react-native-platform-components/components/selectionmenu) |
74
- | **SegmentedControl** | `UISegmentedControl` | `MaterialButtonToggleGroup` (Material 3 segmented buttons) | [SegmentedControl](https://jarx-concepts.github.io/react-native-platform-components/components/segmentedcontrol) |
65
+ | **SegmentedControl** | `UISegmentedControl` | `MaterialButtonToggleGroup` (Material 3 Expressive connected buttons, or classic) | [SegmentedControl](https://jarx-concepts.github.io/react-native-platform-components/components/segmentedcontrol) |
66
+ | **Button** | `UIButton` (configurations) | `MaterialButton` (Material 3 Expressive: five sizes, round or square, shape morph) | [Button](https://jarx-concepts.github.io/react-native-platform-components/components/button) |
67
+ | **ButtonGroup** | Row of `UIButton`s | `MaterialButtonGroup` / `MaterialButtonToggleGroup` (Material 3 Expressive) | [ButtonGroup](https://jarx-concepts.github.io/react-native-platform-components/components/buttongroup) |
68
+ | **FloatingToolbar** | `UIGlassEffect` capsule (iOS 26+), blur below | `FloatingToolbarLayout` (Material 3 Expressive) | [FloatingToolbar](https://jarx-concepts.github.io/react-native-platform-components/components/floatingtoolbar) |
75
69
  | **LiquidGlass** | `UIGlassEffect` (iOS 26+) | Fallback `View` | [LiquidGlass](https://jarx-concepts.github.io/react-native-platform-components/components/liquidglass) |
76
70
 
77
71
  **Have a component request?** If there's a native UI component you'd like to see added, [open an issue](https://github.com/JarX-Concepts/react-native-platform-components/issues/new) describing the component and its native APIs on iOS and Android.
@@ -140,7 +134,7 @@ Full details: [Installation](https://jarx-concepts.github.io/react-native-platfo
140
134
  Everything else lives on the documentation site:
141
135
 
142
136
  - [Quick Start](https://jarx-concepts.github.io/react-native-platform-components/quick-start): copy-paste examples for every component
143
- - [DatePicker](https://jarx-concepts.github.io/react-native-platform-components/components/datepicker), [ContextMenu](https://jarx-concepts.github.io/react-native-platform-components/components/contextmenu), [SelectionMenu](https://jarx-concepts.github.io/react-native-platform-components/components/selectionmenu), [SegmentedControl](https://jarx-concepts.github.io/react-native-platform-components/components/segmentedcontrol), [LiquidGlass](https://jarx-concepts.github.io/react-native-platform-components/components/liquidglass): props, platform options and behavior
137
+ - [DatePicker](https://jarx-concepts.github.io/react-native-platform-components/components/datepicker), [ContextMenu](https://jarx-concepts.github.io/react-native-platform-components/components/contextmenu), [SelectionMenu](https://jarx-concepts.github.io/react-native-platform-components/components/selectionmenu), [SegmentedControl](https://jarx-concepts.github.io/react-native-platform-components/components/segmentedcontrol), [Button](https://jarx-concepts.github.io/react-native-platform-components/components/button), [ButtonGroup](https://jarx-concepts.github.io/react-native-platform-components/components/buttongroup), [FloatingToolbar](https://jarx-concepts.github.io/react-native-platform-components/components/floatingtoolbar), [LiquidGlass](https://jarx-concepts.github.io/react-native-platform-components/components/liquidglass): props, platform options and behavior
144
138
  - [Android Theme Configuration](https://jarx-concepts.github.io/react-native-platform-components/guides/android-theme): Material 3 setup and what happens without it
145
139
  - [Icons](https://jarx-concepts.github.io/react-native-platform-components/guides/icons): SF Symbols, drawables and images
146
140
  - [Theming and Colors](https://jarx-concepts.github.io/react-native-platform-components/guides/theming): light/dark mode and your brand color from JavaScript (`useNativeTheme`), accepted color formats
@@ -0,0 +1,498 @@
1
+ package com.platformcomponents
2
+
3
+ import android.content.Context
4
+ import android.text.TextUtils
5
+ import android.view.ContextThemeWrapper
6
+ import android.view.View
7
+ import android.view.ViewGroup.MarginLayoutParams
8
+ import android.widget.FrameLayout
9
+ import com.facebook.react.bridge.WritableNativeMap
10
+ import com.facebook.react.uimanager.PixelUtil
11
+ import com.facebook.react.uimanager.ReactCompoundViewGroup
12
+ import com.facebook.react.uimanager.StateWrapper
13
+ import com.facebook.react.views.scroll.ReactScrollViewHelper
14
+ import com.google.android.material.button.MaterialButton
15
+ import com.google.android.material.button.MaterialButtonGroup
16
+ import com.google.android.material.button.MaterialButtonToggleGroup
17
+
18
+ /**
19
+ * A Material 3 Expressive button group: MaterialButtonGroup for plain actions,
20
+ * MaterialButtonToggleGroup for single / multiple selection. Rebuilt whenever
21
+ * a prop the widgets read at construction changes.
22
+ */
23
+ class PCButtonGroupView(context: Context) :
24
+ FrameLayout(context),
25
+ ReactScrollViewHelper.HasStateWrapper,
26
+ ReactCompoundViewGroup {
27
+
28
+ data class Item(
29
+ val label: String,
30
+ val value: String,
31
+ val disabled: Boolean,
32
+ val icon: PCButtonSupport.Icon,
33
+ val accessibilityLabel: String
34
+ ) {
35
+ /** What TalkBack announces for the button. */
36
+ val spokenLabel: String get() = accessibilityLabel.ifEmpty { label }
37
+ }
38
+
39
+ companion object {
40
+ private const val TAG = "PCButtonGroup"
41
+ }
42
+
43
+ // --- State Wrapper for Fabric state updates ---
44
+ override var stateWrapper: StateWrapper? = null
45
+
46
+ private var lastReportedWidth: Float = 0f
47
+ private var lastReportedHeight: Float = 0f
48
+
49
+ // --- Props ---
50
+ var items: List<Item> = emptyList()
51
+ var variant: PCExpressive.Variant = PCExpressive.Variant.OUTLINED
52
+ var size: String = "small"
53
+ var shape: String = "round"
54
+ var connected: Boolean = false
55
+ var spacing: Float = -1f // dp; negative = style default
56
+ var selection: String = "none" // "none" | "single" | "multiple"
57
+ var selectedValues: List<String> = emptyList()
58
+ var selectionRequired: Boolean = false
59
+ var interactivity: String = "enabled" // "enabled" | "disabled"
60
+ var overflow: String = "none" // "none" | "menu" | "wrap"
61
+ var expressive: Boolean = true // android.material: "expressive" | "m3"
62
+
63
+ // --- Styling (null / empty = Material theme default) ---
64
+ var containerColor: Int? = null
65
+ var foregroundColor: Int? = null
66
+ var rippleColor: Int? = null
67
+ var strokeColor: Int? = null
68
+ var labelFontFamily: String = ""
69
+ var labelFontSize: Float = 0f
70
+ var labelFontWeight: String = ""
71
+ var labelFontStyle: String = ""
72
+
73
+ // --- Events ---
74
+ var onPress: ((index: Int, value: String) -> Unit)? = null
75
+ var onSelectionChange: ((values: List<String>) -> Unit)? = null
76
+
77
+ // --- UI ---
78
+ private var group: MaterialButtonGroup? = null
79
+ private val buttonIdToIndex: MutableMap<Int, Int> = mutableMapOf()
80
+ private var suppressCallbacks = false
81
+ private var selectionEmitPending = false
82
+
83
+ /** Bumped on every rebuild so late image loads can't touch stale buttons. */
84
+ private var rebuildGeneration = 0
85
+
86
+ // --- Native theme ---
87
+ private var builtThemeVersion = PCNativeTheme.version
88
+ private val nativeThemeListener = PCNativeTheme.Listener {
89
+ PCThemeSupport.clearColorStateListCaches(this)
90
+ rebuildUI()
91
+ }
92
+
93
+ init {
94
+ clipChildren = false
95
+ clipToPadding = false
96
+ rebuildUI()
97
+ }
98
+
99
+ // ---- Public apply* (called by manager) ----
100
+
101
+ fun applyItems(value: List<Item>) {
102
+ if (items == value) return
103
+ items = value
104
+ rebuildUI()
105
+ }
106
+
107
+ fun applyVariant(value: String?) {
108
+ val parsed = PCExpressive.parseVariant(value)
109
+ if (variant == parsed) return
110
+ variant = parsed
111
+ rebuildUI()
112
+ }
113
+
114
+ fun applySize(value: String?) {
115
+ val parsed = PCExpressive.parseSize(value)
116
+ if (size == parsed) return
117
+ size = parsed
118
+ rebuildUI()
119
+ }
120
+
121
+ fun applyShape(value: String?) {
122
+ val parsed = PCExpressive.parseShape(value)
123
+ if (shape == parsed) return
124
+ shape = parsed
125
+ rebuildUI()
126
+ }
127
+
128
+ fun applyConnected(value: Boolean) {
129
+ if (connected == value) return
130
+ connected = value
131
+ rebuildUI()
132
+ }
133
+
134
+ fun applySpacing(value: Float) {
135
+ if (spacing == value) return
136
+ spacing = value
137
+ rebuildUI()
138
+ }
139
+
140
+ fun applySelection(value: String?) {
141
+ val parsed = when (value) {
142
+ "single", "multiple" -> value
143
+ else -> "none"
144
+ }
145
+ if (selection == parsed) return
146
+ selection = parsed
147
+ rebuildUI()
148
+ }
149
+
150
+ fun applySelectedValues(value: List<String>) {
151
+ if (selectedValues == value) return
152
+ selectedValues = value
153
+ updateSelection()
154
+ }
155
+
156
+ fun applySelectionRequired(value: Boolean) {
157
+ if (selectionRequired == value) return
158
+ selectionRequired = value
159
+ (group as? MaterialButtonToggleGroup)?.isSelectionRequired = value
160
+ }
161
+
162
+ fun applyInteractivity(value: String?) {
163
+ val newValue = if (value == "disabled") "disabled" else "enabled"
164
+ if (interactivity == newValue) return
165
+ interactivity = newValue
166
+ updateEnabled()
167
+ }
168
+
169
+ fun applyOverflow(value: String?) {
170
+ val parsed = when (value) {
171
+ "menu", "wrap" -> value
172
+ else -> "none"
173
+ }
174
+ if (overflow == parsed) return
175
+ overflow = parsed
176
+ rebuildUI()
177
+ }
178
+
179
+ fun applyMaterial(value: String?) {
180
+ val parsed = PCExpressive.parseExpressive(value)
181
+ if (expressive == parsed) return
182
+ expressive = parsed
183
+ rebuildUI()
184
+ }
185
+
186
+ fun applyColors(container: Int?, foreground: Int?, ripple: Int?, stroke: Int?) {
187
+ if (containerColor == container && foregroundColor == foreground &&
188
+ rippleColor == ripple && strokeColor == stroke
189
+ ) return
190
+ containerColor = container
191
+ foregroundColor = foreground
192
+ rippleColor = ripple
193
+ strokeColor = stroke
194
+ rebuildUI()
195
+ }
196
+
197
+ fun applyLabelStyle(fontFamily: String, fontSize: Float, fontWeight: String, fontStyle: String) {
198
+ if (labelFontFamily == fontFamily && labelFontSize == fontSize &&
199
+ labelFontWeight == fontWeight && labelFontStyle == fontStyle
200
+ ) return
201
+ labelFontFamily = fontFamily
202
+ labelFontSize = fontSize
203
+ labelFontWeight = fontWeight
204
+ labelFontStyle = fontStyle
205
+ rebuildUI()
206
+ }
207
+
208
+ // The buttons have generated view ids, which React Native's touch handling would
209
+ // take for React tags and dispatch JS touch events to unrelated views. Claim the
210
+ // touch for this view instead; the buttons still receive it natively.
211
+ override fun interceptsTouchEvent(touchX: Float, touchY: Float): Boolean = true
212
+
213
+ override fun reactTagForTouch(touchX: Float, touchY: Float): Int = id
214
+
215
+ override fun onAttachedToWindow() {
216
+ super.onAttachedToWindow()
217
+ PCNativeTheme.addListener(nativeThemeListener)
218
+ PCNativeTheme.attach(context)
219
+ if (builtThemeVersion != PCNativeTheme.version) nativeThemeListener.onNativeThemeChanged()
220
+ }
221
+
222
+ override fun onDetachedFromWindow() {
223
+ PCNativeTheme.removeListener(nativeThemeListener)
224
+ super.onDetachedFromWindow()
225
+ }
226
+
227
+ // ---- UI Building ----
228
+
229
+ private fun rebuildUI() {
230
+ builtThemeVersion = PCNativeTheme.version
231
+ removeAllViews()
232
+ buttonIdToIndex.clear()
233
+ rebuildGeneration += 1
234
+ val generation = rebuildGeneration
235
+
236
+ // Material widgets need a Material theme; fall back to Material 3 defaults instead of crashing.
237
+ val base = PCThemeSupport.materialContext(context, "ButtonGroup")
238
+ val toggle = selection != "none"
239
+
240
+ // The group styles: a standard group (spacing, press size morph) or a
241
+ // connected one (shared outline, small inner corners), Expressive or
242
+ // classic Material 3.
243
+ val overlay = when {
244
+ expressive && toggle && !connected -> R.style.PCExpressiveOverlay_StandardToggleGroup
245
+ expressive && !toggle && connected -> R.style.PCExpressiveOverlay_ConnectedGroup
246
+ expressive -> R.style.PCExpressiveOverlay
247
+ toggle && !connected -> R.style.PCM3Overlay_StandardToggleGroup
248
+ !toggle && connected -> R.style.PCM3Overlay_ConnectedGroup
249
+ else -> 0
250
+ }
251
+ val groupContext = if (overlay != 0) ContextThemeWrapper(base, overlay) else base
252
+
253
+ val g: MaterialButtonGroup =
254
+ if (toggle) {
255
+ MaterialButtonToggleGroup(groupContext).apply {
256
+ isSingleSelection = selection == "single"
257
+ isSelectionRequired = selectionRequired
258
+ }
259
+ } else {
260
+ MaterialButtonGroup(groupContext)
261
+ }
262
+ g.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
263
+ g.clipChildren = false
264
+ g.clipToPadding = false
265
+ if (spacing >= 0) {
266
+ g.spacing = (spacing * resources.displayMetrics.density).toInt()
267
+ }
268
+ g.overflowMode = when (overflow) {
269
+ "menu" -> MaterialButtonGroup.OVERFLOW_MODE_MENU
270
+ "wrap" -> MaterialButtonGroup.OVERFLOW_MODE_WRAP
271
+ else -> MaterialButtonGroup.OVERFLOW_MODE_NONE
272
+ }
273
+
274
+ for ((index, item) in items.withIndex()) {
275
+ val iconOnly = item.label.isEmpty() && item.icon.isPresent
276
+
277
+ val button = PCExpressive.createButton(
278
+ base, variant, size, shape, iconOnly, 0, expressive
279
+ ).apply {
280
+ id = View.generateViewId()
281
+ text = item.label
282
+ isAllCaps = false // Preserve original text casing
283
+ ellipsize = TextUtils.TruncateAt.END
284
+ maxLines = 1
285
+ contentDescription = item.spokenLabel.ifEmpty { null }
286
+ isEnabled = !item.disabled && interactivity == "enabled"
287
+ if (iconOnly) {
288
+ // Centre the icon instead of leaving it at the start edge
289
+ iconGravity = MaterialButton.ICON_GRAVITY_TEXT_START
290
+ }
291
+ setOnClickListener { onPress?.invoke(index, item.value) }
292
+ }
293
+
294
+ PCButtonSupport.applyIcon(button, item.icon, { generation == rebuildGeneration }) { requestLayout() }
295
+ PCButtonSupport.applyFont(button, labelFontFamily, labelFontSize, labelFontWeight, labelFontStyle)
296
+ PCButtonSupport.applyColors(button, containerColor, foregroundColor, rippleColor, strokeColor, item.icon.tinted)
297
+
298
+ buttonIdToIndex[button.id] = index
299
+ // Buttons size to their content. Connected buttons also share any extra
300
+ // width when the group is stretched (and any shortfall when it is
301
+ // squeezed) equally; standard buttons keep their own width.
302
+ val params = MaterialButtonGroup.LayoutParams(
303
+ LayoutParams.WRAP_CONTENT,
304
+ LayoutParams.WRAP_CONTENT,
305
+ if (connected) 1f else 0f
306
+ )
307
+ params.overflowText = item.spokenLabel
308
+ g.addView(button, params)
309
+ }
310
+
311
+ if (g is MaterialButtonToggleGroup) {
312
+ g.addOnButtonCheckedListener { _, _, _ ->
313
+ if (suppressCallbacks) return@addOnButtonCheckedListener
314
+ // A single-selection switch reports the old button unchecked and the
315
+ // new one checked in turn; report the final selection once.
316
+ scheduleSelectionEmit()
317
+ }
318
+ }
319
+
320
+ addView(g)
321
+ group = g
322
+
323
+ updateSelection()
324
+ updateEnabled()
325
+ requestLayout()
326
+ }
327
+
328
+ private fun scheduleSelectionEmit() {
329
+ if (selectionEmitPending) return
330
+ selectionEmitPending = true
331
+ post {
332
+ selectionEmitPending = false
333
+ emitSelection()
334
+ }
335
+ }
336
+
337
+ private fun emitSelection() {
338
+ val g = group as? MaterialButtonToggleGroup ?: return
339
+ val values = g.checkedButtonIds
340
+ .mapNotNull { buttonIdToIndex[it] }
341
+ .sorted()
342
+ .map { items[it].value }
343
+ onSelectionChange?.invoke(values)
344
+ }
345
+
346
+ private fun updateSelection() {
347
+ val g = group as? MaterialButtonToggleGroup ?: return
348
+ suppressCallbacks = true
349
+ for ((id, index) in buttonIdToIndex) {
350
+ val checked = selectedValues.contains(items[index].value)
351
+ if (checked) g.check(id) else g.uncheck(id)
352
+ }
353
+ suppressCallbacks = false
354
+ }
355
+
356
+ private fun updateEnabled() {
357
+ val enabled = interactivity == "enabled"
358
+ alpha = if (enabled) 1f else 0.5f
359
+
360
+ val g = group ?: return
361
+ for (i in 0 until g.childCount) {
362
+ val button = g.getChildAt(i) as? MaterialButton ?: continue
363
+ val index = buttonIdToIndex[button.id] ?: continue
364
+ button.isEnabled = enabled && !items[index].disabled
365
+ }
366
+ }
367
+
368
+ // ---- Layout ----
369
+
370
+ private var manualLayoutPending = false
371
+
372
+ /**
373
+ * React Native's root view ignores requestLayout after the initial pass, so
374
+ * a native view that changes its own subtree later has to measure and lay
375
+ * itself out. Coalesced to one pass per frame.
376
+ */
377
+ override fun requestLayout() {
378
+ super.requestLayout()
379
+ if (!manualLayoutPending) {
380
+ manualLayoutPending = true
381
+ post { runManualLayout() }
382
+ }
383
+ }
384
+
385
+ private fun runManualLayout() {
386
+ manualLayoutPending = false
387
+ // Nothing to do until Yoga has placed us; the regular pass covers that.
388
+ if (width == 0 || height == 0) return
389
+ measure(
390
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
391
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
392
+ )
393
+ layout(left, top, right, bottom)
394
+ }
395
+
396
+ // ---- Measurement ----
397
+
398
+ private val unspecified = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
399
+
400
+ /**
401
+ * The group's natural width: every button on one row. MaterialButtonGroup
402
+ * answers that itself, except in the "wrap" overflow mode, which refuses an
403
+ * AT_MOST width and lays every button on its own row for UNSPECIFIED; there
404
+ * the buttons are summed instead.
405
+ */
406
+ private fun naturalWidth(child: MaterialButtonGroup): Int {
407
+ if (overflow != "wrap") {
408
+ child.measure(unspecified, unspecified)
409
+ return child.measuredWidth
410
+ }
411
+ var total = child.paddingLeft + child.paddingRight
412
+ var visible = 0
413
+ for (i in 0 until child.childCount) {
414
+ val button = child.getChildAt(i)
415
+ if (button.visibility == View.GONE) continue
416
+ button.measure(unspecified, unspecified)
417
+ val params = button.layoutParams as? MarginLayoutParams
418
+ total += button.measuredWidth + (params?.leftMargin ?: 0) + (params?.rightMargin ?: 0)
419
+ visible++
420
+ }
421
+ if (visible > 1) total += child.spacing * (visible - 1)
422
+ return total
423
+ }
424
+
425
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
426
+ val child = group
427
+ if (child == null) {
428
+ setMeasuredDimension(0, 0)
429
+ return
430
+ }
431
+
432
+ val natural = naturalWidth(child)
433
+ val width = resolveDimension(widthMeasureSpec, natural)
434
+ child.measure(MeasureSpec.makeMeasureSpec(minOf(width, natural), MeasureSpec.EXACTLY), unspecified)
435
+ val height = resolveDimension(heightMeasureSpec, child.measuredHeight)
436
+
437
+ child.measure(
438
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
439
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
440
+ )
441
+ setMeasuredDimension(width, height)
442
+ }
443
+
444
+ private fun resolveDimension(spec: Int, desired: Int): Int {
445
+ val size = MeasureSpec.getSize(spec)
446
+ return when (MeasureSpec.getMode(spec)) {
447
+ // Fabric may give us 0 before the state is applied; keep the natural size then.
448
+ MeasureSpec.EXACTLY -> if (size > 0) size else desired
449
+ MeasureSpec.AT_MOST -> minOf(desired, size)
450
+ else -> desired
451
+ }
452
+ }
453
+
454
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
455
+ super.onLayout(changed, left, top, right, bottom)
456
+ updateFrameSizeState()
457
+ }
458
+
459
+ /** Reports the intrinsic size to Fabric; called as soon as the state wrapper is available. */
460
+ fun reportIntrinsicSize() = updateFrameSizeState()
461
+
462
+ /**
463
+ * Update Fabric state with the group's natural size, so the shadow node
464
+ * wraps its content in Yoga layout.
465
+ */
466
+ private fun updateFrameSizeState() {
467
+ val wrapper = stateWrapper ?: return
468
+ val child = group ?: return
469
+
470
+ // The natural width, and the height at the width we actually have when
471
+ // that is narrower (an overflow "wrap" group grows taller then).
472
+ val naturalWidth = naturalWidth(child)
473
+ val fittedWidth = if (width in 1 until naturalWidth) width else naturalWidth
474
+ child.measure(MeasureSpec.makeMeasureSpec(fittedWidth, MeasureSpec.EXACTLY), unspecified)
475
+ val widthDp = PixelUtil.toDIPFromPixel(naturalWidth.toFloat())
476
+ val heightDp = PixelUtil.toDIPFromPixel(child.measuredHeight.toFloat())
477
+
478
+ // Measuring re-wraps the labels; restore the laid-out size so they are
479
+ // ellipsized to the bounds they are drawn in.
480
+ if (child.width > 0 && child.height > 0) {
481
+ child.measure(
482
+ MeasureSpec.makeMeasureSpec(child.width, MeasureSpec.EXACTLY),
483
+ MeasureSpec.makeMeasureSpec(child.height, MeasureSpec.EXACTLY)
484
+ )
485
+ }
486
+
487
+ if (widthDp != lastReportedWidth || heightDp != lastReportedHeight) {
488
+ lastReportedWidth = widthDp
489
+ lastReportedHeight = heightDp
490
+
491
+ val stateData = WritableNativeMap().apply {
492
+ putDouble("width", widthDp.toDouble())
493
+ putDouble("height", heightDp.toDouble())
494
+ }
495
+ wrapper.updateState(stateData)
496
+ }
497
+ }
498
+ }