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,256 @@
1
+ // PCButtonGroup.mm
2
+
3
+ #import "PCButtonGroup.h"
4
+
5
+ #import <React/RCTComponentViewFactory.h>
6
+ #import <React/RCTConversions.h>
7
+ #import <React/RCTFabricComponentsPlugins.h>
8
+ #import <React/RCTFont.h>
9
+
10
+ #import <react/renderer/components/PlatformComponentsViewSpec/ComponentDescriptors.h>
11
+ #import <react/renderer/components/PlatformComponentsViewSpec/EventEmitters.h>
12
+ #import <react/renderer/components/PlatformComponentsViewSpec/Props.h>
13
+ #import <react/renderer/core/LayoutPrimitives.h>
14
+
15
+ #if __has_include(<PlatformComponents/PlatformComponents-Swift.h>)
16
+ #import <PlatformComponents/PlatformComponents-Swift.h>
17
+ #else
18
+ #import "PlatformComponents-Swift.h"
19
+ #endif
20
+
21
+ #import "PCButtonGroupComponentDescriptors-custom.h"
22
+ #import "PCButtonGroupShadowNode-custom.h"
23
+ #import "PCButtonGroupState-custom.h"
24
+
25
+ using namespace facebook::react;
26
+
27
+ namespace {
28
+ static inline bool ButtonsEqual(
29
+ const std::vector<PCButtonGroupButtonsStruct> &a,
30
+ const std::vector<PCButtonGroupButtonsStruct> &b) {
31
+ if (a.size() != b.size()) return false;
32
+ for (size_t i = 0; i < a.size(); i++) {
33
+ if (a[i].label != b[i].label) return false;
34
+ if (a[i].value != b[i].value) return false;
35
+ if (a[i].disabled != b[i].disabled) return false;
36
+ if (a[i].iconType != b[i].iconType) return false;
37
+ if (a[i].iconName != b[i].iconName) return false;
38
+ if (a[i].iconUri != b[i].iconUri) return false;
39
+ if (a[i].iconScale != b[i].iconScale) return false;
40
+ if (a[i].iconTinted != b[i].iconTinted) return false;
41
+ if (a[i].accessibilityLabel != b[i].accessibilityLabel) return false;
42
+ }
43
+ return true;
44
+ }
45
+
46
+ static inline NSString *NSStringFromStd(const std::string &s, NSString *fallback) {
47
+ return s.empty() ? fallback : [NSString stringWithUTF8String:s.c_str()];
48
+ }
49
+
50
+ static inline NSArray<NSString *> *NSArrayFromStd(const std::vector<std::string> &values) {
51
+ NSMutableArray<NSString *> *arr = [NSMutableArray arrayWithCapacity:values.size()];
52
+ for (const auto &value : values) {
53
+ [arr addObject:NSStringFromStd(value, @"")];
54
+ }
55
+ return arr;
56
+ }
57
+
58
+ static inline bool LabelStyleEqual(
59
+ const PCButtonGroupLabelStyleStruct &a,
60
+ const PCButtonGroupLabelStyleStruct &b) {
61
+ return a.fontFamily == b.fontFamily && a.fontSize == b.fontSize &&
62
+ a.fontWeight == b.fontWeight && a.fontStyle == b.fontStyle;
63
+ }
64
+
65
+ /// Builds the label font from RN-style font props, or nil when every field is
66
+ /// unset so the buttons keep the configuration's font.
67
+ static UIFont *FontFromLabelStyle(const PCButtonGroupLabelStyleStruct &style) {
68
+ if (style.fontFamily.empty() && style.fontSize <= 0 &&
69
+ style.fontWeight.empty() && style.fontStyle.empty()) {
70
+ return nil;
71
+ }
72
+ return [RCTFont updateFont:[UIFont preferredFontForTextStyle:UIFontTextStyleBody]
73
+ withFamily:NSStringFromStd(style.fontFamily, nil)
74
+ size:style.fontSize > 0 ? @(style.fontSize) : nil
75
+ weight:NSStringFromStd(style.fontWeight, nil)
76
+ style:NSStringFromStd(style.fontStyle, nil)
77
+ variant:nil
78
+ scaleMultiplier:1.0];
79
+ }
80
+ } // namespace
81
+
82
+ @interface PCButtonGroup ()
83
+
84
+ - (void)updateMeasurements;
85
+
86
+ @end
87
+
88
+ @implementation PCButtonGroup {
89
+ PCButtonGroupView *_view;
90
+ MeasuringPCButtonGroupShadowNode::ConcreteState::Shared _state;
91
+ }
92
+
93
+ + (ComponentDescriptorProvider)componentDescriptorProvider {
94
+ return concreteComponentDescriptorProvider<MeasuringPCButtonGroupComponentDescriptor>();
95
+ }
96
+
97
+ - (instancetype)initWithFrame:(CGRect)frame {
98
+ if (self = [super initWithFrame:frame]) {
99
+ _view = [PCButtonGroupView new];
100
+ self.contentView = _view;
101
+
102
+ __weak __typeof(self) weakSelf = self;
103
+
104
+ _view.onPress = ^(NSInteger index, NSString *value) {
105
+ __typeof(self) strongSelf = weakSelf;
106
+ if (!strongSelf) return;
107
+
108
+ auto eventEmitter =
109
+ std::static_pointer_cast<const PCButtonGroupEventEmitter>(strongSelf->_eventEmitter);
110
+ if (!eventEmitter) return;
111
+
112
+ PCButtonGroupEventEmitter::OnButtonPress payload = {
113
+ .index = (int)index,
114
+ .value = value.UTF8String,
115
+ };
116
+ eventEmitter->onButtonPress(payload);
117
+ };
118
+
119
+ _view.onSelectionChange = ^(NSArray<NSString *> *values) {
120
+ __typeof(self) strongSelf = weakSelf;
121
+ if (!strongSelf) return;
122
+
123
+ auto eventEmitter =
124
+ std::static_pointer_cast<const PCButtonGroupEventEmitter>(strongSelf->_eventEmitter);
125
+ if (!eventEmitter) return;
126
+
127
+ PCButtonGroupEventEmitter::OnGroupSelectionChange payload;
128
+ for (NSString *value in values) {
129
+ payload.values.push_back(value.UTF8String);
130
+ }
131
+ eventEmitter->onGroupSelectionChange(payload);
132
+ };
133
+
134
+ _view.onNeedsRemeasure = ^{
135
+ __typeof(self) strongSelf = weakSelf;
136
+ if (!strongSelf) return;
137
+ [strongSelf updateMeasurements];
138
+ };
139
+ }
140
+ return self;
141
+ }
142
+
143
+ - (void)updateProps:(Props::Shared const &)props
144
+ oldProps:(Props::Shared const &)oldProps {
145
+ const auto &newProps = *std::static_pointer_cast<const PCButtonGroupProps>(props);
146
+ const auto prevProps = std::static_pointer_cast<const PCButtonGroupProps>(oldProps);
147
+
148
+ // buttons: [{label, value, disabled, iconType, iconName, iconUri, iconScale,
149
+ // iconTinted, accessibilityLabel}]
150
+ if (!prevProps || !ButtonsEqual(newProps.buttons, prevProps->buttons)) {
151
+ NSMutableArray *arr = [NSMutableArray new];
152
+ for (const auto &button : newProps.buttons) {
153
+ [arr addObject:@{
154
+ @"label": NSStringFromStd(button.label, @""),
155
+ @"value": NSStringFromStd(button.value, @""),
156
+ @"disabled": NSStringFromStd(button.disabled, @"enabled"),
157
+ @"iconType": NSStringFromStd(button.iconType, @""),
158
+ @"iconName": NSStringFromStd(button.iconName, @""),
159
+ @"iconUri": NSStringFromStd(button.iconUri, @""),
160
+ @"iconScale": @(button.iconScale),
161
+ @"iconTinted": NSStringFromStd(button.iconTinted, @"true"),
162
+ @"accessibilityLabel": NSStringFromStd(button.accessibilityLabel, @""),
163
+ }];
164
+ }
165
+ _view.buttons = arr;
166
+ }
167
+
168
+ if (!prevProps || newProps.variant != prevProps->variant) {
169
+ _view.variant = NSStringFromStd(newProps.variant, @"outlined");
170
+ }
171
+
172
+ if (!prevProps || newProps.size != prevProps->size) {
173
+ _view.size = NSStringFromStd(newProps.size, @"small");
174
+ }
175
+
176
+ if (!prevProps || newProps.shape != prevProps->shape) {
177
+ _view.shape = NSStringFromStd(newProps.shape, @"");
178
+ }
179
+
180
+ if (!prevProps || newProps.connected != prevProps->connected) {
181
+ _view.connected = (newProps.connected == "true");
182
+ }
183
+
184
+ if (!prevProps || newProps.spacing != prevProps->spacing) {
185
+ _view.spacing = newProps.spacing;
186
+ }
187
+
188
+ if (!prevProps || newProps.selection != prevProps->selection) {
189
+ _view.selection = NSStringFromStd(newProps.selection, @"none");
190
+ }
191
+
192
+ if (!prevProps || newProps.selectedValues != prevProps->selectedValues) {
193
+ _view.selectedValues = NSArrayFromStd(newProps.selectedValues);
194
+ }
195
+
196
+ if (!prevProps || newProps.selectionRequired != prevProps->selectionRequired) {
197
+ _view.selectionRequired = (newProps.selectionRequired == "true");
198
+ }
199
+
200
+ if (!prevProps || newProps.interactivity != prevProps->interactivity) {
201
+ _view.interactivity = NSStringFromStd(newProps.interactivity, @"enabled");
202
+ }
203
+
204
+ // Colors arrive as SharedColor (already processed by React Native)
205
+ if (!prevProps || newProps.color != prevProps->color) {
206
+ _view.containerColor = RCTUIColorFromSharedColor(newProps.color);
207
+ }
208
+
209
+ if (!prevProps || newProps.foregroundColor != prevProps->foregroundColor) {
210
+ _view.foregroundColor = RCTUIColorFromSharedColor(newProps.foregroundColor);
211
+ }
212
+
213
+ // labelStyle: {fontFamily, fontSize, fontWeight, fontStyle}
214
+ if (!prevProps || !LabelStyleEqual(newProps.labelStyle, prevProps->labelStyle)) {
215
+ _view.labelFont = FontFromLabelStyle(newProps.labelStyle);
216
+ }
217
+
218
+ // androidRippleColor / androidStrokeColor / android: Android only
219
+
220
+ [super updateProps:props oldProps:oldProps];
221
+
222
+ // Update measurements when props change that affect layout
223
+ [self updateMeasurements];
224
+ }
225
+
226
+ #pragma mark - State (Measuring)
227
+
228
+ - (void)updateState:(const State::Shared &)state
229
+ oldState:(const State::Shared &)oldState {
230
+ _state = std::static_pointer_cast<const MeasuringPCButtonGroupShadowNode::ConcreteState>(state);
231
+
232
+ if (oldState == nullptr) {
233
+ // First time: compute initial size.
234
+ [self updateMeasurements];
235
+ }
236
+
237
+ [super updateState:state oldState:oldState];
238
+ }
239
+
240
+ - (void)updateMeasurements {
241
+ if (_state == nullptr)
242
+ return;
243
+
244
+ // The group's natural size; Yoga clamps it to the available width
245
+ CGSize size = [_view sizeForLayoutWithConstrainedTo:CGSizeZero];
246
+
247
+ PCButtonGroupStateFrameSize next;
248
+ next.frameSize = {(Float)size.width, (Float)size.height};
249
+ _state->updateState(std::move(next));
250
+ }
251
+
252
+ @end
253
+
254
+ Class<RCTComponentViewProtocol> PCButtonGroupCls(void) {
255
+ return PCButtonGroup.class;
256
+ }
@@ -0,0 +1,254 @@
1
+ import UIKit
2
+
3
+ /// A row of native buttons (`UIButton` with configurations), with optional
4
+ /// single / multiple selection shown through the buttons' selected state.
5
+ @objcMembers
6
+ public final class PCButtonGroupView: UIView {
7
+ private struct Item {
8
+ let label: String
9
+ let value: String
10
+ let disabled: Bool
11
+ let icon: PCButtonSupport.Icon
12
+ let accessibilityLabel: String
13
+
14
+ /// What VoiceOver announces for the button.
15
+ var spokenLabel: String { accessibilityLabel.isEmpty ? label : accessibilityLabel }
16
+ }
17
+
18
+ // MARK: - Props (set from ObjC++)
19
+
20
+ /// ObjC++ sets this as an array of dictionaries; see `rebuild` for keys.
21
+ public var buttons: [Any] = [] { didSet { rebuild() } }
22
+
23
+ /// "filled" | "tonal" | "outlined" | "text" | "elevated"
24
+ public var variant: String = "outlined" {
25
+ didSet { if oldValue != variant { applyConfigurations() } }
26
+ }
27
+
28
+ /// "xsmall" | "small" | "medium" | "large" | "xlarge"
29
+ public var size: String = "small" {
30
+ didSet { if oldValue != size { applyConfigurations() } }
31
+ }
32
+
33
+ /// "" (UIKit default) | "round" | "square"
34
+ public var shape: String = "" {
35
+ didSet { if oldValue != shape { applyConfigurations() } }
36
+ }
37
+
38
+ /// Connected groups sit closer together (a hairline gap)
39
+ public var connected: Bool = false {
40
+ didSet { if oldValue != connected { applyLayout() } }
41
+ }
42
+
43
+ /// Gap between buttons in points; negative means the default
44
+ public var spacing: CGFloat = -1 {
45
+ didSet { if oldValue != spacing { applyLayout() } }
46
+ }
47
+
48
+ /// "none" | "single" | "multiple"
49
+ public var selection: String = "none" {
50
+ didSet { if oldValue != selection { applyConfigurations() } }
51
+ }
52
+
53
+ /// Controlled selection by value
54
+ public var selectedValues: [String] = [] {
55
+ didSet { if oldValue != selectedValues { applyConfigurations() } }
56
+ }
57
+
58
+ /// Whether at least one button must stay selected
59
+ public var selectionRequired: Bool = false
60
+
61
+ /// "enabled" | "disabled"
62
+ public var interactivity: String = "enabled" {
63
+ didSet { updateEnabled() }
64
+ }
65
+
66
+ /// Container color; nil keeps the configuration's color
67
+ public var containerColor: UIColor? {
68
+ didSet { applyConfigurations() }
69
+ }
70
+
71
+ /// Label and icon color; nil keeps the configuration's color
72
+ public var foregroundColor: UIColor? {
73
+ didSet { applyConfigurations() }
74
+ }
75
+
76
+ /// Label font; nil keeps the configuration's font
77
+ public var labelFont: UIFont? {
78
+ didSet { applyConfigurations() }
79
+ }
80
+
81
+ // MARK: - Events back to ObjC++
82
+
83
+ public var onPress: ((Int, String) -> Void)? // (index, value)
84
+ public var onSelectionChange: (([String]) -> Void)?
85
+
86
+ /// Called when content changed after layout (an icon finished loading),
87
+ /// so the Fabric measurement can be refreshed.
88
+ public var onNeedsRemeasure: (() -> Void)?
89
+
90
+ // MARK: - Internal
91
+
92
+ private let stack = UIStackView()
93
+ private var items: [Item] = []
94
+ private var uiButtons: [UIButton] = []
95
+ private var iconImages: [UIImage?] = []
96
+
97
+ /// Bumped on every rebuild so late image loads can't touch stale buttons.
98
+ private var rebuildGeneration = 0
99
+
100
+ // MARK: - Init
101
+
102
+ public override init(frame: CGRect) {
103
+ super.init(frame: frame)
104
+ setup()
105
+ }
106
+
107
+ public required init?(coder: NSCoder) {
108
+ super.init(coder: coder)
109
+ setup()
110
+ }
111
+
112
+ private func setup() {
113
+ stack.axis = .horizontal
114
+ stack.alignment = .fill
115
+ stack.translatesAutoresizingMaskIntoConstraints = false
116
+ addSubview(stack)
117
+
118
+ NSLayoutConstraint.activate([
119
+ stack.topAnchor.constraint(equalTo: topAnchor),
120
+ stack.bottomAnchor.constraint(equalTo: bottomAnchor),
121
+ stack.leadingAnchor.constraint(equalTo: leadingAnchor),
122
+ stack.trailingAnchor.constraint(equalTo: trailingAnchor),
123
+ ])
124
+
125
+ applyLayout()
126
+ }
127
+
128
+ // MARK: - Props handling
129
+
130
+ private func rebuild() {
131
+ items = buttons.compactMap { any in
132
+ guard let dict = any as? [String: Any] else { return nil }
133
+ return Item(
134
+ label: (dict["label"] as? String) ?? "",
135
+ value: (dict["value"] as? String) ?? "",
136
+ disabled: (dict["disabled"] as? String) == "disabled",
137
+ icon: PCButtonSupport.Icon(dictionary: dict),
138
+ accessibilityLabel: (dict["accessibilityLabel"] as? String) ?? ""
139
+ )
140
+ }
141
+
142
+ rebuildGeneration += 1
143
+ let generation = rebuildGeneration
144
+
145
+ uiButtons.forEach { $0.removeFromSuperview() }
146
+ uiButtons = []
147
+ iconImages = Array(repeating: nil, count: items.count)
148
+
149
+ for (index, item) in items.enumerated() {
150
+ let button = UIButton(type: .system)
151
+ button.tag = index
152
+ button.addTarget(self, action: #selector(tapped(_:)), for: .touchUpInside)
153
+ stack.addArrangedSubview(button)
154
+ uiButtons.append(button)
155
+
156
+ iconImages[index] = PCButtonSupport.image(for: item.icon) { [weak self] image in
157
+ guard let self, self.rebuildGeneration == generation, index < self.iconImages.count else { return }
158
+ self.iconImages[index] = image
159
+ self.applyConfigurations()
160
+ self.onNeedsRemeasure?()
161
+ }
162
+ }
163
+
164
+ applyConfigurations()
165
+ updateEnabled()
166
+ }
167
+
168
+ private func applyLayout() {
169
+ stack.spacing = spacing >= 0 ? spacing : (connected ? 2 : 8)
170
+ // Buttons size to their content; extra or missing width is shared in
171
+ // proportion when the group is stretched or squeezed.
172
+ stack.distribution = .fillProportionally
173
+ invalidateIntrinsicContentSize()
174
+ }
175
+
176
+ private func applyConfigurations() {
177
+ for (index, button) in uiButtons.enumerated() where index < items.count {
178
+ let item = items[index]
179
+ let selected = selection != "none" && selectedValues.contains(item.value)
180
+ button.isSelected = selected
181
+ button.configuration = PCButtonSupport.makeConfiguration(
182
+ variant: variant,
183
+ selected: selected,
184
+ size: size,
185
+ shape: shape,
186
+ label: item.label,
187
+ image: iconImages[index],
188
+ containerColor: containerColor,
189
+ foregroundColor: foregroundColor,
190
+ font: labelFont
191
+ )
192
+ button.accessibilityLabel = item.spokenLabel.isEmpty ? nil : item.spokenLabel
193
+ }
194
+ invalidateIntrinsicContentSize()
195
+ }
196
+
197
+ private func updateEnabled() {
198
+ let enabled = interactivity != "disabled"
199
+ alpha = enabled ? 1.0 : 0.5
200
+ for (index, button) in uiButtons.enumerated() where index < items.count {
201
+ button.isEnabled = enabled && !items[index].disabled
202
+ }
203
+ }
204
+
205
+ // MARK: - Selection
206
+
207
+ @objc private func tapped(_ sender: UIButton) {
208
+ let index = sender.tag
209
+ guard index >= 0, index < items.count else { return }
210
+ let item = items[index]
211
+ onPress?(index, item.value)
212
+
213
+ guard selection != "none", let next = nextSelection(tapping: item.value) else { return }
214
+ selectedValues = next
215
+ onSelectionChange?(next)
216
+ }
217
+
218
+ /// The selection after a tap, or nil when the tap changes nothing.
219
+ private func nextSelection(tapping value: String) -> [String]? {
220
+ let isSelected = selectedValues.contains(value)
221
+ if selection == "single" {
222
+ if isSelected {
223
+ return selectionRequired ? nil : []
224
+ }
225
+ return [value]
226
+ }
227
+
228
+ // multiple
229
+ if isSelected {
230
+ if selectionRequired && selectedValues.count == 1 { return nil }
231
+ return selectedValues.filter { $0 != value }
232
+ }
233
+ var chosen = Set(selectedValues)
234
+ chosen.insert(value)
235
+ // Report in button order
236
+ return items.map(\.value).filter { chosen.contains($0) }
237
+ }
238
+
239
+ // MARK: - Sizing
240
+
241
+ public override var intrinsicContentSize: CGSize {
242
+ stack.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
243
+ }
244
+
245
+ public override func sizeThatFits(_ size: CGSize) -> CGSize {
246
+ stack.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
247
+ }
248
+
249
+ /// Called by the measuring pipeline to get the size for Yoga layout.
250
+ @objc public func sizeForLayout(withConstrainedTo constrainedSize: CGSize) -> CGSize {
251
+ let fitted = stack.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
252
+ return CGSize(width: ceil(fitted.width), height: ceil(fitted.height))
253
+ }
254
+ }
@@ -0,0 +1,141 @@
1
+ import UIKit
2
+
3
+ /// UIButton configuration and icon handling shared by PCButtonView and
4
+ /// PCButtonGroupView.
5
+ enum PCButtonSupport {
6
+ struct Icon: Equatable {
7
+ /// "", "sfSymbol", "drawable" (ignored on iOS) or "image"
8
+ var type: String = ""
9
+ var name: String = ""
10
+ var uri: String = ""
11
+ var scale: CGFloat = 1
12
+ var tinted: Bool = true
13
+
14
+ var isPresent: Bool { type == "sfSymbol" || type == "image" }
15
+
16
+ static let none = Icon()
17
+
18
+ init() {}
19
+
20
+ init(type: String, name: String, uri: String, scale: CGFloat, tinted: Bool) {
21
+ self.type = type
22
+ self.name = name
23
+ self.uri = uri
24
+ self.scale = scale > 0 ? scale : 1
25
+ self.tinted = tinted
26
+ }
27
+
28
+ /// Reads the flat icon fields (iconType, iconName, ...) bridged as a dictionary.
29
+ init(dictionary: [String: Any]) {
30
+ let scale = (dictionary["iconScale"] as? NSNumber).map { CGFloat(truncating: $0) } ?? 1
31
+ self.init(
32
+ type: (dictionary["iconType"] as? String) ?? "",
33
+ name: (dictionary["iconName"] as? String) ?? "",
34
+ uri: (dictionary["iconUri"] as? String) ?? "",
35
+ scale: scale,
36
+ tinted: (dictionary["iconTinted"] as? String) != "false"
37
+ )
38
+ }
39
+ }
40
+
41
+ /// The configuration for a variant. A selected button (single / multiple
42
+ /// selection in a group) uses the filled style, as UIKit's own toggle
43
+ /// buttons do.
44
+ ///
45
+ /// | Variant | Configuration |
46
+ /// | filled | .filled() |
47
+ /// | tonal | .tinted() |
48
+ /// | outlined | .bordered() |
49
+ /// | text | .plain() |
50
+ /// | elevated | .gray() |
51
+ static func configuration(variant: String, selected: Bool) -> UIButton.Configuration {
52
+ if selected { return .filled() }
53
+ switch variant {
54
+ case "tonal": return .tinted()
55
+ case "outlined": return .bordered()
56
+ case "text": return .plain()
57
+ case "elevated": return .gray()
58
+ default: return .filled()
59
+ }
60
+ }
61
+
62
+ /// Material's five sizes onto UIKit's four.
63
+ static func buttonSize(_ size: String) -> UIButton.Configuration.Size {
64
+ switch size {
65
+ case "xsmall": return .mini
66
+ case "medium": return .medium
67
+ case "large", "xlarge": return .large
68
+ default: return .small
69
+ }
70
+ }
71
+
72
+ /// `nil` keeps UIKit's dynamic corner style.
73
+ static func cornerStyle(_ shape: String) -> UIButton.Configuration.CornerStyle? {
74
+ switch shape {
75
+ case "round": return .capsule
76
+ case "square": return .large
77
+ default: return nil
78
+ }
79
+ }
80
+
81
+ static func makeConfiguration(
82
+ variant: String,
83
+ selected: Bool,
84
+ size: String,
85
+ shape: String,
86
+ label: String,
87
+ image: UIImage?,
88
+ containerColor: UIColor?,
89
+ foregroundColor: UIColor?,
90
+ font: UIFont?
91
+ ) -> UIButton.Configuration {
92
+ var config = configuration(variant: variant, selected: selected)
93
+ config.title = label.isEmpty ? nil : label
94
+ config.image = image
95
+ config.imagePadding = (image != nil && !label.isEmpty) ? 8 : 0
96
+ config.buttonSize = buttonSize(size)
97
+ config.titleLineBreakMode = .byTruncatingTail
98
+ if let corner = cornerStyle(shape) {
99
+ config.cornerStyle = corner
100
+ }
101
+ if let containerColor {
102
+ config.baseBackgroundColor = containerColor
103
+ }
104
+ if let foregroundColor {
105
+ config.baseForegroundColor = foregroundColor
106
+ }
107
+ if let font {
108
+ config.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in
109
+ var outgoing = incoming
110
+ outgoing.font = font
111
+ return outgoing
112
+ }
113
+ }
114
+ return config
115
+ }
116
+
117
+ /// Resolves the icon's image. SF Symbols resolve synchronously; images
118
+ /// that are still loading return nil and call `completion` on the main
119
+ /// thread once they arrive.
120
+ static func image(for icon: Icon, completion: @escaping (UIImage?) -> Void) -> UIImage? {
121
+ switch icon.type {
122
+ case "sfSymbol":
123
+ return UIImage(systemName: icon.name).map { decorate($0, tinted: icon.tinted) }
124
+
125
+ case "image":
126
+ let cached = PCImageLoader.shared.image(uri: icon.uri, scale: icon.scale) { image in
127
+ completion(image.map { decorate($0, tinted: icon.tinted) })
128
+ }
129
+ return cached.map { decorate($0, tinted: icon.tinted) }
130
+
131
+ default:
132
+ return nil
133
+ }
134
+ }
135
+
136
+ /// Template images take the button's foreground color; untinted images
137
+ /// keep their own colors.
138
+ static func decorate(_ image: UIImage, tinted: Bool) -> UIImage {
139
+ image.withRenderingMode(tinted ? .alwaysTemplate : .alwaysOriginal)
140
+ }
141
+ }