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.
- package/README.md +24 -30
- package/android/src/main/java/com/platformcomponents/PCButtonGroupView.kt +498 -0
- package/android/src/main/java/com/platformcomponents/PCButtonGroupViewManager.kt +191 -0
- package/android/src/main/java/com/platformcomponents/PCButtonSupport.kt +127 -0
- package/android/src/main/java/com/platformcomponents/PCButtonView.kt +345 -0
- package/android/src/main/java/com/platformcomponents/PCButtonViewManager.kt +126 -0
- package/android/src/main/java/com/platformcomponents/PCExpressive.kt +107 -0
- package/android/src/main/java/com/platformcomponents/PCFloatingToolbarView.kt +115 -0
- package/android/src/main/java/com/platformcomponents/PCFloatingToolbarViewManager.kt +65 -0
- package/android/src/main/java/com/platformcomponents/PCSegmentedControlView.kt +11 -3
- package/android/src/main/java/com/platformcomponents/PCSegmentedControlViewManager.kt +6 -1
- package/android/src/main/java/com/platformcomponents/PCTextFieldView.kt +1018 -0
- package/android/src/main/java/com/platformcomponents/PCTextFieldViewManager.kt +248 -0
- package/android/src/main/java/com/platformcomponents/PlatformComponentsPackage.kt +4 -0
- package/android/src/main/res/values/attrs.xml +13 -0
- package/android/src/main/res/values/styles.xml +183 -0
- package/ios/PCButton.h +10 -0
- package/ios/PCButton.mm +196 -0
- package/ios/PCButtonGroup.h +10 -0
- package/ios/PCButtonGroup.mm +256 -0
- package/ios/PCButtonGroupView.swift +254 -0
- package/ios/PCButtonSupport.swift +141 -0
- package/ios/PCButtonView.swift +155 -0
- package/ios/PCFloatingToolbar.h +10 -0
- package/ios/PCFloatingToolbar.mm +76 -0
- package/ios/PCFloatingToolbarView.swift +95 -0
- package/ios/PCTextField.h +10 -0
- package/ios/PCTextField.mm +391 -0
- package/ios/PCTextFieldView.swift +1012 -0
- package/lib/commonjs/Button.js +77 -0
- package/lib/commonjs/Button.js.map +1 -0
- package/lib/commonjs/ButtonGroup.js +99 -0
- package/lib/commonjs/ButtonGroup.js.map +1 -0
- package/lib/commonjs/ButtonGroupNativeComponent.ts +121 -0
- package/lib/commonjs/ButtonNativeComponent.ts +76 -0
- package/lib/commonjs/FloatingToolbar.js +69 -0
- package/lib/commonjs/FloatingToolbar.js.map +1 -0
- package/lib/commonjs/FloatingToolbarNativeComponent.ts +31 -0
- package/lib/commonjs/SegmentedControl.js +11 -85
- package/lib/commonjs/SegmentedControl.js.map +1 -1
- package/lib/commonjs/SegmentedControlNativeComponent.ts +3 -0
- package/lib/commonjs/TextField.js +195 -0
- package/lib/commonjs/TextField.js.map +1 -0
- package/lib/commonjs/TextFieldNativeComponent.ts +218 -0
- package/lib/commonjs/focusRegistry.js +78 -0
- package/lib/commonjs/focusRegistry.js.map +1 -0
- package/lib/commonjs/icons.js +92 -0
- package/lib/commonjs/icons.js.map +1 -0
- package/lib/commonjs/index.js +66 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +32 -2
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/labelStyle.js +27 -0
- package/lib/commonjs/labelStyle.js.map +1 -0
- package/lib/commonjs/reactNativeInternals.d.js +2 -0
- package/lib/commonjs/reactNativeInternals.d.js.map +1 -0
- package/lib/module/Button.js +70 -0
- package/lib/module/Button.js.map +1 -0
- package/lib/module/ButtonGroup.js +92 -0
- package/lib/module/ButtonGroup.js.map +1 -0
- package/lib/module/ButtonGroupNativeComponent.ts +121 -0
- package/lib/module/ButtonNativeComponent.ts +76 -0
- package/lib/module/FloatingToolbar.js +62 -0
- package/lib/module/FloatingToolbar.js.map +1 -0
- package/lib/module/FloatingToolbarNativeComponent.ts +31 -0
- package/lib/module/SegmentedControl.js +12 -87
- package/lib/module/SegmentedControl.js.map +1 -1
- package/lib/module/SegmentedControlNativeComponent.ts +3 -0
- package/lib/module/TextField.js +189 -0
- package/lib/module/TextField.js.map +1 -0
- package/lib/module/TextFieldNativeComponent.ts +218 -0
- package/lib/module/focusRegistry.js +74 -0
- package/lib/module/focusRegistry.js.map +1 -0
- package/lib/module/icons.js +87 -0
- package/lib/module/icons.js.map +1 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +28 -0
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/labelStyle.js +23 -0
- package/lib/module/labelStyle.js.map +1 -0
- package/lib/module/reactNativeInternals.d.js +2 -0
- package/lib/module/reactNativeInternals.d.js.map +1 -0
- package/lib/typescript/commonjs/docs/sidebars.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/Button.d.ts +76 -0
- package/lib/typescript/commonjs/src/Button.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/ButtonGroup.d.ts +102 -0
- package/lib/typescript/commonjs/src/ButtonGroup.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/ButtonGroupNativeComponent.d.ts +93 -0
- package/lib/typescript/commonjs/src/ButtonGroupNativeComponent.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/ButtonNativeComponent.d.ts +57 -0
- package/lib/typescript/commonjs/src/ButtonNativeComponent.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/FloatingToolbar.d.ts +45 -0
- package/lib/typescript/commonjs/src/FloatingToolbar.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/FloatingToolbarNativeComponent.d.ts +24 -0
- package/lib/typescript/commonjs/src/FloatingToolbarNativeComponent.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/SegmentedControl.d.ts +17 -46
- package/lib/typescript/commonjs/src/SegmentedControl.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/SegmentedControlNativeComponent.d.ts +2 -0
- package/lib/typescript/commonjs/src/SegmentedControlNativeComponent.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/TextField.d.ts +174 -0
- package/lib/typescript/commonjs/src/TextField.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/TextFieldNativeComponent.d.ts +162 -0
- package/lib/typescript/commonjs/src/TextFieldNativeComponent.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/focusRegistry.d.ts +13 -0
- package/lib/typescript/commonjs/src/focusRegistry.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/icons.d.ts +52 -0
- package/lib/typescript/commonjs/src/icons.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/index.d.ts +6 -0
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/index.web.d.ts +121 -0
- package/lib/typescript/commonjs/src/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/labelStyle.d.ts +20 -0
- package/lib/typescript/commonjs/src/labelStyle.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/sharedTypes.d.ts +6 -1
- package/lib/typescript/commonjs/src/sharedTypes.d.ts.map +1 -1
- package/lib/typescript/module/docs/sidebars.d.ts.map +1 -1
- package/lib/typescript/module/src/Button.d.ts +76 -0
- package/lib/typescript/module/src/Button.d.ts.map +1 -0
- package/lib/typescript/module/src/ButtonGroup.d.ts +102 -0
- package/lib/typescript/module/src/ButtonGroup.d.ts.map +1 -0
- package/lib/typescript/module/src/ButtonGroupNativeComponent.d.ts +93 -0
- package/lib/typescript/module/src/ButtonGroupNativeComponent.d.ts.map +1 -0
- package/lib/typescript/module/src/ButtonNativeComponent.d.ts +57 -0
- package/lib/typescript/module/src/ButtonNativeComponent.d.ts.map +1 -0
- package/lib/typescript/module/src/FloatingToolbar.d.ts +45 -0
- package/lib/typescript/module/src/FloatingToolbar.d.ts.map +1 -0
- package/lib/typescript/module/src/FloatingToolbarNativeComponent.d.ts +24 -0
- package/lib/typescript/module/src/FloatingToolbarNativeComponent.d.ts.map +1 -0
- package/lib/typescript/module/src/SegmentedControl.d.ts +17 -46
- package/lib/typescript/module/src/SegmentedControl.d.ts.map +1 -1
- package/lib/typescript/module/src/SegmentedControlNativeComponent.d.ts +2 -0
- package/lib/typescript/module/src/SegmentedControlNativeComponent.d.ts.map +1 -1
- package/lib/typescript/module/src/TextField.d.ts +174 -0
- package/lib/typescript/module/src/TextField.d.ts.map +1 -0
- package/lib/typescript/module/src/TextFieldNativeComponent.d.ts +162 -0
- package/lib/typescript/module/src/TextFieldNativeComponent.d.ts.map +1 -0
- package/lib/typescript/module/src/focusRegistry.d.ts +13 -0
- package/lib/typescript/module/src/focusRegistry.d.ts.map +1 -0
- package/lib/typescript/module/src/icons.d.ts +52 -0
- package/lib/typescript/module/src/icons.d.ts.map +1 -0
- package/lib/typescript/module/src/index.d.ts +6 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/index.web.d.ts +121 -0
- package/lib/typescript/module/src/index.web.d.ts.map +1 -1
- package/lib/typescript/module/src/labelStyle.d.ts +20 -0
- package/lib/typescript/module/src/labelStyle.d.ts.map +1 -0
- package/lib/typescript/module/src/sharedTypes.d.ts +6 -1
- package/lib/typescript/module/src/sharedTypes.d.ts.map +1 -1
- package/package.json +25 -4
- package/react-native.config.js +3 -0
- package/shared/PCButtonComponentDescriptors-custom.h +22 -0
- package/shared/PCButtonGroupComponentDescriptors-custom.h +22 -0
- package/shared/PCButtonGroupShadowNode-custom.cpp +48 -0
- package/shared/PCButtonGroupShadowNode-custom.h +55 -0
- package/shared/PCButtonGroupState-custom.h +61 -0
- package/shared/PCButtonShadowNode-custom.cpp +48 -0
- package/shared/PCButtonShadowNode-custom.h +55 -0
- package/shared/PCButtonState-custom.h +61 -0
- package/shared/PCTextFieldComponentDescriptors-custom.h +22 -0
- package/shared/PCTextFieldShadowNode-custom.cpp +47 -0
- package/shared/PCTextFieldShadowNode-custom.h +55 -0
- package/shared/PCTextFieldState-custom.h +61 -0
- package/shared/react/renderer/components/PlatformComponentsViewSpec/ComponentDescriptors.h +3 -0
- package/src/Button.tsx +144 -0
- package/src/ButtonGroup.tsx +233 -0
- package/src/ButtonGroupNativeComponent.ts +121 -0
- package/src/ButtonNativeComponent.ts +76 -0
- package/src/FloatingToolbar.tsx +113 -0
- package/src/FloatingToolbarNativeComponent.ts +31 -0
- package/src/SegmentedControl.tsx +26 -118
- package/src/SegmentedControlNativeComponent.ts +3 -0
- package/src/TextField.tsx +494 -0
- package/src/TextFieldNativeComponent.ts +218 -0
- package/src/focusRegistry.ts +89 -0
- package/src/icons.ts +116 -0
- package/src/index.tsx +6 -0
- package/src/index.web.tsx +194 -0
- package/src/labelStyle.ts +36 -0
- package/src/reactNativeInternals.d.ts +22 -0
- 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
|
+
}
|