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
@@ -0,0 +1,191 @@
1
+ package com.platformcomponents
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.ReadableArray
5
+ import com.facebook.react.bridge.ReadableMap
6
+ import com.facebook.react.uimanager.ReactStylesDiffMap
7
+ import com.facebook.react.uimanager.SimpleViewManager
8
+ import com.facebook.react.uimanager.StateWrapper
9
+ import com.facebook.react.uimanager.ThemedReactContext
10
+ import com.facebook.react.uimanager.UIManagerHelper
11
+ import com.facebook.react.uimanager.ViewManagerDelegate
12
+ import com.facebook.react.uimanager.events.Event
13
+ import com.facebook.react.uimanager.events.RCTEventEmitter
14
+ import com.facebook.react.viewmanagers.PCButtonGroupManagerDelegate
15
+ import com.facebook.react.viewmanagers.PCButtonGroupManagerInterface
16
+ import com.platformcomponents.PCButtonSupport.doubleOr
17
+ import com.platformcomponents.PCButtonSupport.stringOr
18
+
19
+ class PCButtonGroupViewManager :
20
+ SimpleViewManager<PCButtonGroupView>(),
21
+ PCButtonGroupManagerInterface<PCButtonGroupView> {
22
+
23
+ companion object {
24
+ private const val TAG = "PCButtonGroup"
25
+ }
26
+
27
+ private val delegate: ViewManagerDelegate<PCButtonGroupView> =
28
+ PCButtonGroupManagerDelegate(this)
29
+
30
+ override fun getName(): String = "PCButtonGroup"
31
+
32
+ override fun getDelegate(): ViewManagerDelegate<PCButtonGroupView> = delegate
33
+
34
+ override fun createViewInstance(reactContext: ThemedReactContext): PCButtonGroupView {
35
+ return PCButtonGroupView(reactContext)
36
+ }
37
+
38
+ /**
39
+ * Pass the StateWrapper to the view so it can update Fabric state with its
40
+ * natural size, and report it right away so the first layout already fits.
41
+ */
42
+ override fun updateState(
43
+ view: PCButtonGroupView,
44
+ props: ReactStylesDiffMap,
45
+ stateWrapper: StateWrapper
46
+ ): Any? {
47
+ view.stateWrapper = stateWrapper
48
+ view.reportIntrinsicSize()
49
+ return null
50
+ }
51
+
52
+ override fun addEventEmitters(reactContext: ThemedReactContext, view: PCButtonGroupView) {
53
+ val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.id)
54
+
55
+ view.onPress = { index, value ->
56
+ dispatcher?.dispatchEvent(PressEvent(view.id, index, value))
57
+ }
58
+ view.onSelectionChange = { values ->
59
+ dispatcher?.dispatchEvent(SelectionEvent(view.id, values))
60
+ }
61
+ }
62
+
63
+ // buttons: array of {label, value, disabled, iconType, iconName, iconUri,
64
+ // iconScale, iconTinted, accessibilityLabel}
65
+ override fun setButtons(view: PCButtonGroupView, value: ReadableArray?) {
66
+ val out = ArrayList<PCButtonGroupView.Item>()
67
+ if (value != null) {
68
+ for (i in 0 until value.size()) {
69
+ val m = value.getMap(i) ?: continue
70
+ out.add(
71
+ PCButtonGroupView.Item(
72
+ label = m.stringOr("label", ""),
73
+ value = m.stringOr("value", ""),
74
+ disabled = m.stringOr("disabled", "enabled") == "disabled",
75
+ icon = PCButtonSupport.parseIcon(m),
76
+ accessibilityLabel = m.stringOr("accessibilityLabel", "")
77
+ )
78
+ )
79
+ }
80
+ }
81
+ view.applyItems(out)
82
+ }
83
+
84
+ override fun setVariant(view: PCButtonGroupView, value: String?) {
85
+ view.applyVariant(value)
86
+ }
87
+
88
+ override fun setSize(view: PCButtonGroupView, value: String?) {
89
+ view.applySize(value)
90
+ }
91
+
92
+ override fun setShape(view: PCButtonGroupView, value: String?) {
93
+ view.applyShape(value)
94
+ }
95
+
96
+ // Spec sentinel: "true" | "false"
97
+ override fun setConnected(view: PCButtonGroupView, value: String?) {
98
+ view.applyConnected(value == "true")
99
+ }
100
+
101
+ // Negative means the style's default spacing
102
+ override fun setSpacing(view: PCButtonGroupView, value: Double) {
103
+ view.applySpacing(value.toFloat())
104
+ }
105
+
106
+ override fun setSelection(view: PCButtonGroupView, value: String?) {
107
+ view.applySelection(value)
108
+ }
109
+
110
+ override fun setSelectedValues(view: PCButtonGroupView, value: ReadableArray?) {
111
+ val out = ArrayList<String>()
112
+ if (value != null) {
113
+ for (i in 0 until value.size()) {
114
+ value.getString(i)?.let { out.add(it) }
115
+ }
116
+ }
117
+ view.applySelectedValues(out)
118
+ }
119
+
120
+ override fun setSelectionRequired(view: PCButtonGroupView, value: String?) {
121
+ view.applySelectionRequired(value == "true")
122
+ }
123
+
124
+ override fun setInteractivity(view: PCButtonGroupView, value: String?) {
125
+ view.applyInteractivity(value)
126
+ }
127
+
128
+ // Colors arrive already processed by React Native (ARGB ints)
129
+ override fun setColor(view: PCButtonGroupView, value: Int?) {
130
+ view.applyColors(value, view.foregroundColor, view.rippleColor, view.strokeColor)
131
+ }
132
+
133
+ override fun setForegroundColor(view: PCButtonGroupView, value: Int?) {
134
+ view.applyColors(view.containerColor, value, view.rippleColor, view.strokeColor)
135
+ }
136
+
137
+ override fun setAndroidRippleColor(view: PCButtonGroupView, value: Int?) {
138
+ view.applyColors(view.containerColor, view.foregroundColor, value, view.strokeColor)
139
+ }
140
+
141
+ override fun setAndroidStrokeColor(view: PCButtonGroupView, value: Int?) {
142
+ view.applyColors(view.containerColor, view.foregroundColor, view.rippleColor, value)
143
+ }
144
+
145
+ // labelStyle: {fontFamily, fontSize, fontWeight, fontStyle}; empty / 0 = default
146
+ override fun setLabelStyle(view: PCButtonGroupView, value: ReadableMap?) {
147
+ view.applyLabelStyle(
148
+ fontFamily = value?.stringOr("fontFamily", "") ?: "",
149
+ fontSize = value?.doubleOr("fontSize", 0.0)?.toFloat() ?: 0f,
150
+ fontWeight = value?.stringOr("fontWeight", "") ?: "",
151
+ fontStyle = value?.stringOr("fontStyle", "") ?: ""
152
+ )
153
+ }
154
+
155
+ // android: {overflow, material}
156
+ override fun setAndroid(view: PCButtonGroupView, value: ReadableMap?) {
157
+ view.applyOverflow(value?.stringOr("overflow", "none"))
158
+ view.applyMaterial(value?.stringOr("material", "expressive"))
159
+ }
160
+
161
+ // --- Events ---
162
+ private class PressEvent(
163
+ surfaceId: Int,
164
+ private val index: Int,
165
+ private val value: String
166
+ ) : Event<PressEvent>(surfaceId) {
167
+ override fun getEventName(): String = "topButtonPress"
168
+ override fun dispatch(rctEventEmitter: RCTEventEmitter) {
169
+ val payload = Arguments.createMap().apply {
170
+ putInt("index", index)
171
+ putString("value", value)
172
+ }
173
+ rctEventEmitter.receiveEvent(viewTag, eventName, payload)
174
+ }
175
+ }
176
+
177
+ private class SelectionEvent(
178
+ surfaceId: Int,
179
+ private val values: List<String>
180
+ ) : Event<SelectionEvent>(surfaceId) {
181
+ override fun getEventName(): String = "topGroupSelectionChange"
182
+ override fun dispatch(rctEventEmitter: RCTEventEmitter) {
183
+ val array = Arguments.createArray()
184
+ values.forEach { array.pushString(it) }
185
+ val payload = Arguments.createMap().apply {
186
+ putArray("values", array)
187
+ }
188
+ rctEventEmitter.receiveEvent(viewTag, eventName, payload)
189
+ }
190
+ }
191
+ }
@@ -0,0 +1,127 @@
1
+ package com.platformcomponents
2
+
3
+ import android.content.res.ColorStateList
4
+ import android.graphics.Color
5
+ import android.graphics.drawable.BitmapDrawable
6
+ import android.util.TypedValue
7
+ import android.widget.TextView
8
+ import com.facebook.react.bridge.ReadableMap
9
+ import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper
10
+ import com.facebook.react.views.text.ReactTypefaceUtils
11
+ import com.google.android.material.button.MaterialButton
12
+
13
+ /** Icon, font and color handling shared by Button and ButtonGroup. */
14
+ object PCButtonSupport {
15
+ data class Icon(
16
+ /** "", "sfSymbol" (ignored on Android), "drawable" or "image" */
17
+ val type: String,
18
+ val name: String,
19
+ val uri: String,
20
+ val scale: Float,
21
+ val tinted: Boolean
22
+ ) {
23
+ val isPresent: Boolean get() = type == "drawable" || type == "image"
24
+ }
25
+
26
+ val NO_ICON = Icon("", "", "", 1f, true)
27
+
28
+ fun ReadableMap.stringOr(key: String, fallback: String): String =
29
+ if (hasKey(key) && !isNull(key)) getString(key) ?: fallback else fallback
30
+
31
+ fun ReadableMap.doubleOr(key: String, fallback: Double): Double =
32
+ if (hasKey(key) && !isNull(key)) getDouble(key) else fallback
33
+
34
+ /** Reads the flat icon fields (iconType, iconName, ...) from a props map. */
35
+ fun parseIcon(map: ReadableMap?): Icon {
36
+ if (map == null) return NO_ICON
37
+ val scale = map.doubleOr("iconScale", 1.0)
38
+ return Icon(
39
+ type = map.stringOr("iconType", ""),
40
+ name = map.stringOr("iconName", ""),
41
+ uri = map.stringOr("iconUri", ""),
42
+ scale = if (scale > 0) scale.toFloat() else 1f,
43
+ tinted = map.stringOr("iconTinted", "true") != "false"
44
+ )
45
+ }
46
+
47
+ /**
48
+ * Resolves a button's icon. Drawable names and release-bundled assets (which
49
+ * React Native ships as drawable resources) resolve synchronously; other image
50
+ * URIs load in the background and are applied when they arrive, if
51
+ * [isCurrent] still holds.
52
+ */
53
+ fun applyIcon(button: MaterialButton, icon: Icon, isCurrent: () -> Boolean, onLoaded: () -> Unit) {
54
+ if (!icon.tinted) {
55
+ button.iconTint = null
56
+ }
57
+ val context = button.context
58
+ when (icon.type) {
59
+ "drawable" -> {
60
+ button.icon = ResourceDrawableIdHelper.instance.getResourceDrawable(context, icon.name)
61
+ }
62
+ "image" -> {
63
+ val uri = icon.uri
64
+ if (!uri.contains(':')) {
65
+ button.icon = ResourceDrawableIdHelper.instance.getResourceDrawable(context, uri)
66
+ } else {
67
+ PCImageLoader.load(context, uri, icon.scale) { bitmap ->
68
+ if (bitmap == null || !isCurrent()) return@load
69
+ button.icon = BitmapDrawable(context.resources, bitmap)
70
+ onLoaded()
71
+ }
72
+ }
73
+ }
74
+ }
75
+ }
76
+
77
+ /** Applies the label font on top of the style's; empty / 0 keep the default. */
78
+ fun applyFont(button: TextView, family: String, size: Float, weight: String, style: String) {
79
+ if (size > 0) {
80
+ button.setTextSize(TypedValue.COMPLEX_UNIT_SP, size)
81
+ }
82
+ if (family.isNotEmpty() || weight.isNotEmpty() || style.isNotEmpty()) {
83
+ button.typeface = ReactTypefaceUtils.applyStyles(
84
+ button.typeface,
85
+ ReactTypefaceUtils.parseFontStyle(style.ifEmpty { null }),
86
+ ReactTypefaceUtils.parseFontWeight(weight.ifEmpty { null }),
87
+ family.ifEmpty { null },
88
+ button.context.assets
89
+ )
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Applies the color props on top of the style's colors, keeping the style's
95
+ * disabled colors.
96
+ */
97
+ fun applyColors(
98
+ button: MaterialButton,
99
+ container: Int?,
100
+ foreground: Int?,
101
+ ripple: Int?,
102
+ stroke: Int?,
103
+ iconTinted: Boolean
104
+ ) {
105
+ val disabledState = intArrayOf(-android.R.attr.state_enabled)
106
+ val states = arrayOf(disabledState, intArrayOf())
107
+
108
+ container?.let { color ->
109
+ val theme = button.backgroundTintList
110
+ val disabled = theme?.getColorForState(disabledState, theme.defaultColor) ?: Color.TRANSPARENT
111
+ button.backgroundTintList = ColorStateList(states, intArrayOf(disabled, color))
112
+ }
113
+
114
+ foreground?.let { color ->
115
+ val theme = button.textColors
116
+ val disabled = theme.getColorForState(disabledState, theme.defaultColor)
117
+ val tint = ColorStateList(states, intArrayOf(disabled, color))
118
+ button.setTextColor(tint)
119
+ if (iconTinted) {
120
+ button.iconTint = tint
121
+ }
122
+ }
123
+
124
+ ripple?.let { button.rippleColor = ColorStateList.valueOf(it) }
125
+ stroke?.let { button.strokeColor = ColorStateList.valueOf(it) }
126
+ }
127
+ }
@@ -0,0 +1,345 @@
1
+ package com.platformcomponents
2
+
3
+ import android.content.Context
4
+ import android.text.TextUtils
5
+ import android.view.View
6
+ import android.view.ViewParent
7
+ import android.widget.FrameLayout
8
+ import com.facebook.react.bridge.WritableNativeMap
9
+ import com.facebook.react.uimanager.PixelUtil
10
+ import com.facebook.react.uimanager.ReactCompoundViewGroup
11
+ import com.facebook.react.uimanager.StateWrapper
12
+ import com.facebook.react.views.scroll.ReactScrollViewHelper
13
+ import com.google.android.material.button.MaterialButton
14
+
15
+ /**
16
+ * A Material 3 Expressive button. Hosts a MaterialButton, rebuilt whenever a
17
+ * prop that the widget reads at construction changes, and reports its
18
+ * intrinsic size to Fabric so the React view wraps its content.
19
+ */
20
+ class PCButtonView(context: Context) :
21
+ FrameLayout(context),
22
+ ReactScrollViewHelper.HasStateWrapper,
23
+ ReactCompoundViewGroup {
24
+
25
+ companion object {
26
+ private const val TAG = "PCButton"
27
+ }
28
+
29
+ // --- State Wrapper for Fabric state updates ---
30
+ override var stateWrapper: StateWrapper? = null
31
+
32
+ private var lastReportedWidth: Float = 0f
33
+ private var lastReportedHeight: Float = 0f
34
+
35
+ // --- Props ---
36
+ var label: String = ""
37
+ var icon: PCButtonSupport.Icon = PCButtonSupport.NO_ICON
38
+ var variant: PCExpressive.Variant = PCExpressive.Variant.FILLED
39
+ var size: String = "small"
40
+ var shape: String = "round"
41
+ var interactivity: String = "enabled" // "enabled" | "disabled"
42
+ var spokenLabel: String = ""
43
+ var expressive: Boolean = true // android.material: "expressive" | "m3"
44
+
45
+ // --- Styling (null / empty = Material theme default) ---
46
+ var containerColor: Int? = null
47
+ var foregroundColor: Int? = null
48
+ var rippleColor: Int? = null
49
+ var strokeColor: Int? = null
50
+ var labelFontFamily: String = ""
51
+ var labelFontSize: Float = 0f
52
+ var labelFontWeight: String = ""
53
+ var labelFontStyle: String = ""
54
+
55
+ // --- Events ---
56
+ var onPress: (() -> Unit)? = null
57
+
58
+ // --- UI ---
59
+ private var button: MaterialButton? = null
60
+
61
+ /** Bumped on every rebuild so late image loads can't touch a stale button. */
62
+ private var rebuildGeneration = 0
63
+
64
+ /** The FloatingToolbar button overlay the widget was built with (0 = none). */
65
+ private var builtToolbarOverlay = 0
66
+
67
+ // --- Native theme ---
68
+ // Widgets read theme colors when they are created, so they are rebuilt when the
69
+ // native theme changes (brand color, light/dark switch).
70
+ private var builtThemeVersion = PCNativeTheme.version
71
+ private val nativeThemeListener = PCNativeTheme.Listener {
72
+ PCThemeSupport.clearColorStateListCaches(this)
73
+ rebuildUI()
74
+ }
75
+
76
+ init {
77
+ clipChildren = false
78
+ clipToPadding = false
79
+ rebuildUI()
80
+ }
81
+
82
+ // ---- Public apply* (called by manager) ----
83
+
84
+ fun applyLabel(value: String) {
85
+ if (label == value) return
86
+ label = value
87
+ rebuildUI()
88
+ }
89
+
90
+ fun applyIcon(value: PCButtonSupport.Icon) {
91
+ if (icon == value) return
92
+ icon = value
93
+ rebuildUI()
94
+ }
95
+
96
+ fun applyVariant(value: String?) {
97
+ val parsed = PCExpressive.parseVariant(value)
98
+ if (variant == parsed) return
99
+ variant = parsed
100
+ rebuildUI()
101
+ }
102
+
103
+ fun applySize(value: String?) {
104
+ val parsed = PCExpressive.parseSize(value)
105
+ if (size == parsed) return
106
+ size = parsed
107
+ rebuildUI()
108
+ }
109
+
110
+ fun applyShape(value: String?) {
111
+ val parsed = PCExpressive.parseShape(value)
112
+ if (shape == parsed) return
113
+ shape = parsed
114
+ rebuildUI()
115
+ }
116
+
117
+ fun applyInteractivity(value: String?) {
118
+ val newValue = if (value == "disabled") "disabled" else "enabled"
119
+ if (interactivity == newValue) return
120
+ interactivity = newValue
121
+ button?.isEnabled = newValue == "enabled"
122
+ }
123
+
124
+ fun applyMaterial(value: String?) {
125
+ val parsed = PCExpressive.parseExpressive(value)
126
+ if (expressive == parsed) return
127
+ expressive = parsed
128
+ rebuildUI()
129
+ }
130
+
131
+ fun applySpokenLabel(value: String) {
132
+ if (spokenLabel == value) return
133
+ spokenLabel = value
134
+ button?.contentDescription = spokenLabel.ifEmpty { null }
135
+ }
136
+
137
+ fun applyColors(container: Int?, foreground: Int?, ripple: Int?, stroke: Int?) {
138
+ if (containerColor == container && foregroundColor == foreground &&
139
+ rippleColor == ripple && strokeColor == stroke
140
+ ) return
141
+ containerColor = container
142
+ foregroundColor = foreground
143
+ rippleColor = ripple
144
+ strokeColor = stroke
145
+ rebuildUI()
146
+ }
147
+
148
+ fun applyLabelStyle(fontFamily: String, fontSize: Float, fontWeight: String, fontStyle: String) {
149
+ if (labelFontFamily == fontFamily && labelFontSize == fontSize &&
150
+ labelFontWeight == fontWeight && labelFontStyle == fontStyle
151
+ ) return
152
+ labelFontFamily = fontFamily
153
+ labelFontSize = fontSize
154
+ labelFontWeight = fontWeight
155
+ labelFontStyle = fontStyle
156
+ rebuildUI()
157
+ }
158
+
159
+ /** Called by the enclosing FloatingToolbar when its variant changes. */
160
+ fun refreshToolbarStyle() {
161
+ if (toolbarOverlay() != builtToolbarOverlay) rebuildUI()
162
+ }
163
+
164
+ // The button has a generated view id, which React Native's touch handling would
165
+ // take for a React tag and dispatch JS touch events to an unrelated view. Claim
166
+ // the touch for this view instead; the button still receives it natively.
167
+ override fun interceptsTouchEvent(touchX: Float, touchY: Float): Boolean = true
168
+
169
+ override fun reactTagForTouch(touchX: Float, touchY: Float): Int = id
170
+
171
+ override fun onAttachedToWindow() {
172
+ super.onAttachedToWindow()
173
+ PCNativeTheme.addListener(nativeThemeListener)
174
+ PCNativeTheme.attach(context)
175
+ if (builtThemeVersion != PCNativeTheme.version) {
176
+ nativeThemeListener.onNativeThemeChanged()
177
+ } else {
178
+ // Inside a FloatingToolbar the toolbar's button styles apply; the parent
179
+ // chain is only complete once attached.
180
+ refreshToolbarStyle()
181
+ }
182
+ }
183
+
184
+ override fun onDetachedFromWindow() {
185
+ PCNativeTheme.removeListener(nativeThemeListener)
186
+ super.onDetachedFromWindow()
187
+ }
188
+
189
+ private fun toolbarOverlay(): Int {
190
+ var p: ViewParent? = parent
191
+ while (p != null) {
192
+ if (p is PCFloatingToolbarView) return p.buttonOverlay
193
+ p = p.parent
194
+ }
195
+ return 0
196
+ }
197
+
198
+ // ---- UI Building ----
199
+
200
+ private fun rebuildUI() {
201
+ builtThemeVersion = PCNativeTheme.version
202
+ builtToolbarOverlay = toolbarOverlay()
203
+ removeAllViews()
204
+ rebuildGeneration += 1
205
+ val generation = rebuildGeneration
206
+
207
+ // Material widgets need a Material theme; fall back to Material 3 defaults instead of crashing.
208
+ val base = PCThemeSupport.materialContext(context, "Button")
209
+ val iconOnly = label.isEmpty() && icon.isPresent
210
+
211
+ val b = PCExpressive.createButton(
212
+ base, variant, size, shape, iconOnly, builtToolbarOverlay, expressive
213
+ ).apply {
214
+ id = View.generateViewId()
215
+ text = label
216
+ isAllCaps = false // Preserve original text casing
217
+ ellipsize = TextUtils.TruncateAt.END
218
+ maxLines = 1
219
+ contentDescription = spokenLabel.ifEmpty { null }
220
+ isEnabled = interactivity == "enabled"
221
+ if (iconOnly) {
222
+ // Centre the icon instead of leaving it at the start edge
223
+ iconGravity = MaterialButton.ICON_GRAVITY_TEXT_START
224
+ }
225
+ setOnClickListener { onPress?.invoke() }
226
+ }
227
+
228
+ PCButtonSupport.applyIcon(b, icon, { generation == rebuildGeneration }) { requestLayout() }
229
+ PCButtonSupport.applyFont(b, labelFontFamily, labelFontSize, labelFontWeight, labelFontStyle)
230
+ PCButtonSupport.applyColors(b, containerColor, foregroundColor, rippleColor, strokeColor, icon.tinted)
231
+
232
+ addView(b, LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT))
233
+ button = b
234
+ requestLayout()
235
+ }
236
+
237
+ // ---- Layout ----
238
+
239
+ private var manualLayoutPending = false
240
+
241
+ /**
242
+ * React Native's root view ignores requestLayout after the initial pass, so
243
+ * a native view that changes its own subtree later (rebuilding for a prop
244
+ * change, an icon finishing loading) has to measure and lay itself out.
245
+ * Coalesced to one pass per frame.
246
+ */
247
+ override fun requestLayout() {
248
+ super.requestLayout()
249
+ if (!manualLayoutPending) {
250
+ manualLayoutPending = true
251
+ post { runManualLayout() }
252
+ }
253
+ }
254
+
255
+ private fun runManualLayout() {
256
+ manualLayoutPending = false
257
+ // Nothing to do until Yoga has placed us; the regular pass covers that.
258
+ if (width == 0 || height == 0) return
259
+ measure(
260
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
261
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
262
+ )
263
+ layout(left, top, right, bottom)
264
+ }
265
+
266
+ // ---- Measurement ----
267
+
268
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
269
+ val child = button
270
+ if (child == null) {
271
+ setMeasuredDimension(0, 0)
272
+ return
273
+ }
274
+
275
+ // The button's natural size; Yoga normally hands us exactly that (see
276
+ // updateFrameSizeState), or a narrower width when the row is full.
277
+ val unspecified = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
278
+ child.measure(unspecified, unspecified)
279
+ val width = resolveDimension(widthMeasureSpec, child.measuredWidth)
280
+ val height = resolveDimension(heightMeasureSpec, child.measuredHeight)
281
+
282
+ child.measure(
283
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
284
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
285
+ )
286
+ setMeasuredDimension(width, height)
287
+ }
288
+
289
+ private fun resolveDimension(spec: Int, desired: Int): Int {
290
+ val size = MeasureSpec.getSize(spec)
291
+ return when (MeasureSpec.getMode(spec)) {
292
+ // Fabric may give us 0 before the state is applied; keep the natural size then.
293
+ MeasureSpec.EXACTLY -> if (size > 0) size else desired
294
+ MeasureSpec.AT_MOST -> minOf(desired, size)
295
+ else -> desired
296
+ }
297
+ }
298
+
299
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
300
+ super.onLayout(changed, left, top, right, bottom)
301
+ updateFrameSizeState()
302
+ }
303
+
304
+ /** Reports the intrinsic size to Fabric; called as soon as the state wrapper is available. */
305
+ fun reportIntrinsicSize() = updateFrameSizeState()
306
+
307
+ /**
308
+ * Update Fabric state with the button's natural size, so the shadow node
309
+ * wraps its content in Yoga layout.
310
+ */
311
+ private fun updateFrameSizeState() {
312
+ val wrapper = stateWrapper ?: return
313
+ val child = button ?: return
314
+
315
+ // The natural width, and the height at the width we actually have when
316
+ // that is narrower.
317
+ val unspecified = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
318
+ child.measure(unspecified, unspecified)
319
+ val naturalWidth = child.measuredWidth
320
+ val fittedWidth = if (width in 1 until naturalWidth) width else naturalWidth
321
+ child.measure(MeasureSpec.makeMeasureSpec(fittedWidth, MeasureSpec.EXACTLY), unspecified)
322
+ val widthDp = PixelUtil.toDIPFromPixel(naturalWidth.toFloat())
323
+ val heightDp = PixelUtil.toDIPFromPixel(child.measuredHeight.toFloat())
324
+
325
+ // Measuring re-wraps the text; restore the laid-out size so the label is
326
+ // ellipsized to the bounds it is drawn in.
327
+ if (child.width > 0 && child.height > 0) {
328
+ child.measure(
329
+ MeasureSpec.makeMeasureSpec(child.width, MeasureSpec.EXACTLY),
330
+ MeasureSpec.makeMeasureSpec(child.height, MeasureSpec.EXACTLY)
331
+ )
332
+ }
333
+
334
+ if (widthDp != lastReportedWidth || heightDp != lastReportedHeight) {
335
+ lastReportedWidth = widthDp
336
+ lastReportedHeight = heightDp
337
+
338
+ val stateData = WritableNativeMap().apply {
339
+ putDouble("width", widthDp.toDouble())
340
+ putDouble("height", heightDp.toDouble())
341
+ }
342
+ wrapper.updateState(stateData)
343
+ }
344
+ }
345
+ }