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,61 @@
1
+ #pragma once
2
+
3
+ #include <react/renderer/core/LayoutPrimitives.h>
4
+ #include <memory>
5
+
6
+ #if defined(RN_SERIALIZABLE_STATE) || defined(ANDROID)
7
+ #include <folly/dynamic.h>
8
+ #include <react/renderer/mapbuffer/MapBuffer.h>
9
+ #include <react/renderer/mapbuffer/MapBufferBuilder.h>
10
+ #endif
11
+
12
+ namespace facebook::react {
13
+
14
+ /**
15
+ * Custom state for Button that holds the measured frame size from native.
16
+ * The native side measures the actual button and communicates the size to
17
+ * the shadow node for proper Yoga layout.
18
+ */
19
+ struct PCButtonStateFrameSize {
20
+ using Shared = std::shared_ptr<const PCButtonStateFrameSize>;
21
+
22
+ Size frameSize{}; // {width, height} in points
23
+
24
+ PCButtonStateFrameSize() = default;
25
+
26
+ explicit PCButtonStateFrameSize(Size size) : frameSize(size) {}
27
+
28
+ bool operator==(const PCButtonStateFrameSize& other) const {
29
+ return frameSize.width == other.frameSize.width &&
30
+ frameSize.height == other.frameSize.height;
31
+ }
32
+
33
+ bool operator!=(const PCButtonStateFrameSize& other) const {
34
+ return !(*this == other);
35
+ }
36
+
37
+ #if defined(RN_SERIALIZABLE_STATE) || defined(ANDROID)
38
+ // Required for Android state serialization
39
+ PCButtonStateFrameSize(
40
+ const PCButtonStateFrameSize& previousState,
41
+ folly::dynamic data)
42
+ : frameSize(previousState.frameSize) {
43
+ if (data.isObject()) {
44
+ if (data.count("width") && data.count("height")) {
45
+ frameSize.width = static_cast<Float>(data["width"].asDouble());
46
+ frameSize.height = static_cast<Float>(data["height"].asDouble());
47
+ }
48
+ }
49
+ }
50
+
51
+ folly::dynamic getDynamic() const {
52
+ return folly::dynamic::object("width", frameSize.width)("height", frameSize.height);
53
+ }
54
+
55
+ MapBuffer getMapBuffer() const {
56
+ return MapBufferBuilder::EMPTY();
57
+ }
58
+ #endif
59
+ };
60
+
61
+ } // namespace facebook::react
@@ -0,0 +1,22 @@
1
+ #pragma once
2
+
3
+ #include <react/renderer/core/ConcreteComponentDescriptor.h>
4
+
5
+ // Forward declaration to avoid circular includes
6
+ namespace facebook::react {
7
+ class MeasuringPCTextFieldShadowNode;
8
+ }
9
+
10
+ // Include the actual shadow node definition
11
+ #include "PCTextFieldShadowNode-custom.h"
12
+
13
+ namespace facebook::react {
14
+
15
+ /**
16
+ * Custom component descriptor that uses our measuring shadow node
17
+ * instead of the generated one.
18
+ */
19
+ using MeasuringPCTextFieldComponentDescriptor =
20
+ ConcreteComponentDescriptor<MeasuringPCTextFieldShadowNode>;
21
+
22
+ } // namespace facebook::react
@@ -0,0 +1,47 @@
1
+ #include "PCTextFieldShadowNode-custom.h"
2
+
3
+ #include <react/renderer/core/LayoutConstraints.h>
4
+ #include <algorithm>
5
+
6
+ namespace facebook::react {
7
+
8
+ Size MeasuringPCTextFieldShadowNode::measureContent(
9
+ const LayoutContext& /*layoutContext*/,
10
+ const LayoutConstraints& layoutConstraints) const {
11
+
12
+ const auto& stateData = this->getStateData();
13
+ Float measuredW = stateData.frameSize.width;
14
+ Float measuredH = stateData.frameSize.height;
15
+
16
+ const Float kHuge = static_cast<Float>(1.0e9);
17
+
18
+ // A text field fills the width it is offered; only an unbounded row (no
19
+ // width at all) gets a fixed fallback.
20
+ if (measuredW <= 0) {
21
+ const Float maxW = layoutConstraints.maximumSize.width;
22
+ measuredW = (maxW > 0 && maxW < kHuge) ? maxW : static_cast<Float>(kFallbackWidth);
23
+ }
24
+
25
+ const bool usingFallback = (measuredH <= 0);
26
+ if (usingFallback) {
27
+ #ifdef __ANDROID__
28
+ measuredH = static_cast<Float>(kFallbackHeightAndroid);
29
+ #else
30
+ measuredH = static_cast<Float>(kFallbackHeightIOS);
31
+ #endif
32
+ }
33
+
34
+ measuredW = std::max<Float>(measuredW, layoutConstraints.minimumSize.width);
35
+ measuredW = std::min<Float>(measuredW, layoutConstraints.maximumSize.width);
36
+
37
+ measuredH = std::max<Float>(measuredH, layoutConstraints.minimumSize.height);
38
+ // Only clamp to max height if we have real measured data (not fallback)
39
+ if (!usingFallback && layoutConstraints.maximumSize.height > 0 &&
40
+ layoutConstraints.maximumSize.height < kHuge) {
41
+ measuredH = std::min<Float>(measuredH, layoutConstraints.maximumSize.height);
42
+ }
43
+
44
+ return Size{measuredW, measuredH};
45
+ }
46
+
47
+ } // namespace facebook::react
@@ -0,0 +1,55 @@
1
+ #pragma once
2
+
3
+ #include <react/renderer/components/view/ConcreteViewShadowNode.h>
4
+
5
+ // Only include what we need for the shadow node definition
6
+ // Do NOT include ComponentDescriptors.h here to avoid circular dependency
7
+ #include <react/renderer/components/PlatformComponentsViewSpec/EventEmitters.h>
8
+ #include <react/renderer/components/PlatformComponentsViewSpec/Props.h>
9
+
10
+ #include "PCTextFieldState-custom.h"
11
+
12
+ namespace facebook::react {
13
+
14
+ extern const char PCTextFieldComponentName[];
15
+
16
+ /**
17
+ * Custom ShadowNode for TextField that supports Yoga measurement.
18
+ *
19
+ * - The field takes the width Yoga offers (a text field fills its row)
20
+ * - Native measures the height at that width and updates state with it
21
+ * - Falls back to platform-specific heights until native has reported
22
+ */
23
+ class MeasuringPCTextFieldShadowNode final : public ConcreteViewShadowNode<
24
+ PCTextFieldComponentName,
25
+ PCTextFieldProps,
26
+ PCTextFieldEventEmitter,
27
+ PCTextFieldStateFrameSize> {
28
+ public:
29
+ using ConcreteViewShadowNode::ConcreteViewShadowNode;
30
+
31
+ // Fallback heights used when native hasn't reported measurements yet
32
+ // iOS: a rounded-rect UITextField
33
+ static constexpr float kFallbackHeightIOS = 34.0f;
34
+
35
+ // Android: a Material 3 outlined text field
36
+ static constexpr float kFallbackHeightAndroid = 56.0f;
37
+
38
+ static constexpr float kFallbackWidth = 200.0f;
39
+
40
+ static ShadowNodeTraits BaseTraits() {
41
+ auto traits = ConcreteViewShadowNode::BaseTraits();
42
+ traits.set(ShadowNodeTraits::Trait::LeafYogaNode);
43
+ traits.set(ShadowNodeTraits::Trait::MeasurableYogaNode);
44
+ return traits;
45
+ }
46
+
47
+ /**
48
+ * Called by Yoga when it needs the intrinsic size of the component.
49
+ */
50
+ Size measureContent(
51
+ const LayoutContext& layoutContext,
52
+ const LayoutConstraints& layoutConstraints) const override;
53
+ };
54
+
55
+ } // namespace facebook::react
@@ -0,0 +1,61 @@
1
+ #pragma once
2
+
3
+ #include <react/renderer/core/LayoutPrimitives.h>
4
+ #include <memory>
5
+
6
+ #if defined(RN_SERIALIZABLE_STATE) || defined(ANDROID)
7
+ #include <folly/dynamic.h>
8
+ #include <react/renderer/mapbuffer/MapBuffer.h>
9
+ #include <react/renderer/mapbuffer/MapBufferBuilder.h>
10
+ #endif
11
+
12
+ namespace facebook::react {
13
+
14
+ /**
15
+ * Custom state for TextField that holds the measured frame size from native.
16
+ * The field fills the width it is given; native measures the height the
17
+ * label, text and supporting text need at that width and reports it here.
18
+ */
19
+ struct PCTextFieldStateFrameSize {
20
+ using Shared = std::shared_ptr<const PCTextFieldStateFrameSize>;
21
+
22
+ Size frameSize{}; // {width, height} in points; width 0 = fill the available width
23
+
24
+ PCTextFieldStateFrameSize() = default;
25
+
26
+ explicit PCTextFieldStateFrameSize(Size size) : frameSize(size) {}
27
+
28
+ bool operator==(const PCTextFieldStateFrameSize& other) const {
29
+ return frameSize.width == other.frameSize.width &&
30
+ frameSize.height == other.frameSize.height;
31
+ }
32
+
33
+ bool operator!=(const PCTextFieldStateFrameSize& other) const {
34
+ return !(*this == other);
35
+ }
36
+
37
+ #if defined(RN_SERIALIZABLE_STATE) || defined(ANDROID)
38
+ // Required for Android state serialization
39
+ PCTextFieldStateFrameSize(
40
+ const PCTextFieldStateFrameSize& previousState,
41
+ folly::dynamic data)
42
+ : frameSize(previousState.frameSize) {
43
+ if (data.isObject()) {
44
+ if (data.count("width") && data.count("height")) {
45
+ frameSize.width = static_cast<Float>(data["width"].asDouble());
46
+ frameSize.height = static_cast<Float>(data["height"].asDouble());
47
+ }
48
+ }
49
+ }
50
+
51
+ folly::dynamic getDynamic() const {
52
+ return folly::dynamic::object("width", frameSize.width)("height", frameSize.height);
53
+ }
54
+
55
+ MapBuffer getMapBuffer() const {
56
+ return MapBufferBuilder::EMPTY();
57
+ }
58
+ #endif
59
+ };
60
+
61
+ } // namespace facebook::react
@@ -8,3 +8,6 @@
8
8
  #include "PCSelectionMenuComponentDescriptors-custom.h"
9
9
  #include "PCDatePickerComponentDescriptors-custom.h"
10
10
  #include "PCSegmentedControlComponentDescriptors-custom.h"
11
+ #include "PCButtonComponentDescriptors-custom.h"
12
+ #include "PCButtonGroupComponentDescriptors-custom.h"
13
+ #include "PCTextFieldComponentDescriptors-custom.h"
package/src/Button.tsx ADDED
@@ -0,0 +1,144 @@
1
+ // Button.tsx
2
+ import React, { useCallback, useMemo } from 'react';
3
+ import type { ColorValue, ViewProps } from 'react-native';
4
+
5
+ import NativeButton from './ButtonNativeComponent';
6
+ import { resolveIcon, type PlatformIcon } from './icons';
7
+ import { normalizeLabelStyle, type LabelStyle } from './labelStyle';
8
+ import type { AndroidMaterialStyle } from './sharedTypes';
9
+
10
+ /**
11
+ * Emphasis of the button, from highest to lowest.
12
+ *
13
+ * | Variant | Android (Material 3 Expressive) | iOS (`UIButton.Configuration`) |
14
+ * | ---------- | ------------------------------- | ------------------------------ |
15
+ * | `filled` | Filled button | `.filled()` |
16
+ * | `tonal` | Filled tonal button | `.tinted()` |
17
+ * | `outlined` | Outlined button | `.bordered()` |
18
+ * | `text` | Text button | `.plain()` |
19
+ * | `elevated` | Elevated button | `.gray()` |
20
+ */
21
+ export type ButtonVariant =
22
+ 'filled' | 'tonal' | 'outlined' | 'text' | 'elevated';
23
+
24
+ /**
25
+ * Button size. Material 3 Expressive defines five sizes; iOS maps them onto
26
+ * `UIButton.Configuration.Size` (`xsmall` → mini, `xlarge` → large).
27
+ */
28
+ export type ButtonSize = 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge';
29
+
30
+ /**
31
+ * Corner shape. `round` is a pill; `square` keeps rounded corners. Unset
32
+ * means the platform default (round on Android, dynamic on iOS).
33
+ */
34
+ export type ButtonShape = 'round' | 'square';
35
+
36
+ export interface ButtonProps extends ViewProps {
37
+ /** Button text. Omit for an icon-only button. */
38
+ label?: string;
39
+
40
+ /** Icon shown before the label, or alone when there is no label. */
41
+ icon?: PlatformIcon;
42
+
43
+ /** Emphasis. Default: `'filled'`. See {@link ButtonVariant}. */
44
+ variant?: ButtonVariant;
45
+
46
+ /** Size. Default: `'small'`, the Material 3 default. */
47
+ size?: ButtonSize;
48
+
49
+ /** Corner shape. Default: platform default. */
50
+ shape?: ButtonShape;
51
+
52
+ /** Whether the button is disabled. */
53
+ disabled?: boolean;
54
+
55
+ /**
56
+ * Container (background) color.
57
+ * Android: `backgroundTint`. iOS: `baseBackgroundColor`.
58
+ */
59
+ color?: ColorValue;
60
+
61
+ /**
62
+ * Label and icon color.
63
+ * Android: text color and `iconTint`. iOS: `baseForegroundColor`.
64
+ */
65
+ tintColor?: ColorValue;
66
+
67
+ /** Label font. */
68
+ labelStyle?: LabelStyle;
69
+
70
+ /** Screen-reader label. Defaults to `label`. */
71
+ accessibilityLabel?: string;
72
+
73
+ /** Called when the button is pressed. */
74
+ onPress?: () => void;
75
+
76
+ /**
77
+ * Android-specific configuration
78
+ */
79
+ android?: {
80
+ /** Ripple color shown while pressing. */
81
+ rippleColor?: ColorValue;
82
+
83
+ /** Outline color (outlined variant). */
84
+ strokeColor?: ColorValue;
85
+
86
+ /**
87
+ * Material style: Material 3 Expressive (default), or the classic
88
+ * Material 3 button, which has one size and shape (`size` and `shape`
89
+ * are ignored).
90
+ */
91
+ material?: AndroidMaterialStyle;
92
+ };
93
+
94
+ /** Test identifier */
95
+ testID?: string;
96
+ }
97
+
98
+ export function Button(props: ButtonProps): React.ReactElement {
99
+ const {
100
+ label,
101
+ icon,
102
+ variant,
103
+ size,
104
+ shape,
105
+ disabled,
106
+ color,
107
+ tintColor,
108
+ labelStyle,
109
+ accessibilityLabel,
110
+ onPress,
111
+ android,
112
+ ...viewProps
113
+ } = props;
114
+
115
+ const nativeIcon = useMemo(() => resolveIcon(icon), [icon]);
116
+ const nativeLabelStyle = useMemo(
117
+ () => normalizeLabelStyle(labelStyle),
118
+ [labelStyle]
119
+ );
120
+
121
+ const handlePress = useCallback(() => {
122
+ onPress?.();
123
+ }, [onPress]);
124
+
125
+ return (
126
+ <NativeButton
127
+ label={label ?? ''}
128
+ icon={nativeIcon}
129
+ variant={variant ?? 'filled'}
130
+ size={size ?? 'small'}
131
+ shape={shape ?? ''}
132
+ interactivity={disabled ? 'disabled' : 'enabled'}
133
+ color={color}
134
+ foregroundColor={tintColor}
135
+ androidRippleColor={android?.rippleColor}
136
+ androidStrokeColor={android?.strokeColor}
137
+ androidMaterial={android?.material ?? 'expressive'}
138
+ labelStyle={nativeLabelStyle}
139
+ spokenLabel={accessibilityLabel ?? ''}
140
+ onButtonPress={onPress ? handlePress : undefined}
141
+ {...viewProps}
142
+ />
143
+ );
144
+ }
@@ -0,0 +1,233 @@
1
+ // ButtonGroup.tsx
2
+ import React, { useCallback, useMemo } from 'react';
3
+ import type { ColorValue, ViewProps } from 'react-native';
4
+
5
+ import type { ButtonShape, ButtonSize, ButtonVariant } from './Button';
6
+ import NativeButtonGroup, {
7
+ type ButtonGroupButton as NativeButton,
8
+ type ButtonGroupPressEvent,
9
+ type ButtonGroupSelectionEvent,
10
+ } from './ButtonGroupNativeComponent';
11
+ import { resolveIcon, type PlatformIcon } from './icons';
12
+ import { normalizeLabelStyle, type LabelStyle } from './labelStyle';
13
+ import type { AndroidMaterialStyle } from './sharedTypes';
14
+
15
+ /**
16
+ * Selection behavior of the group.
17
+ *
18
+ * - `none` (default): the buttons are plain actions.
19
+ * - `single`: at most one button is selected at a time.
20
+ * - `multiple`: any number of buttons can be selected.
21
+ */
22
+ export type ButtonGroupSelection = 'none' | 'single' | 'multiple';
23
+
24
+ /** What happens to buttons that don't fit the group's width (Android). */
25
+ export type ButtonGroupOverflow = 'none' | 'menu' | 'wrap';
26
+
27
+ export interface ButtonGroupButtonProps {
28
+ /** Button text. Omit for an icon-only button. */
29
+ label?: string;
30
+
31
+ /** Unique value identifier for the button */
32
+ value: string;
33
+
34
+ /** Whether this specific button is disabled */
35
+ disabled?: boolean;
36
+
37
+ /** Optional icon. See {@link PlatformIcon}. */
38
+ icon?: PlatformIcon;
39
+
40
+ /** Screen-reader label. Defaults to `label`. */
41
+ accessibilityLabel?: string;
42
+ }
43
+
44
+ export interface ButtonGroupProps extends ViewProps {
45
+ /** Buttons to display */
46
+ buttons: readonly ButtonGroupButtonProps[];
47
+
48
+ /** Emphasis of every button. Default: `'outlined'`. */
49
+ variant?: ButtonVariant;
50
+
51
+ /** Size of every button. Default: `'small'`. */
52
+ size?: ButtonSize;
53
+
54
+ /** Corner shape of every button. Default: platform default. */
55
+ shape?: ButtonShape;
56
+
57
+ /**
58
+ * Connected group: the buttons share one outline with small inner corners
59
+ * (Material 3 "connected button group"). Default: `true` when `selection`
60
+ * is `'single'` or `'multiple'`, `false` otherwise.
61
+ */
62
+ connected?: boolean;
63
+
64
+ /** Gap between buttons in points. Default: the platform's group spacing. */
65
+ spacing?: number;
66
+
67
+ /** Selection behavior. Default: `'none'`. See {@link ButtonGroupSelection}. */
68
+ selection?: ButtonGroupSelection;
69
+
70
+ /**
71
+ * Values of the selected buttons (controlled). Only the first value is
72
+ * used with `selection: 'single'`.
73
+ */
74
+ selectedValues?: readonly string[];
75
+
76
+ /**
77
+ * Whether at least one button must stay selected. Default: `true` for
78
+ * `single`, `false` for `multiple`.
79
+ */
80
+ selectionRequired?: boolean;
81
+
82
+ /** Whether the entire group is disabled */
83
+ disabled?: boolean;
84
+
85
+ /** Container (background) color of the buttons. */
86
+ color?: ColorValue;
87
+
88
+ /** Label and icon color of the buttons. */
89
+ tintColor?: ColorValue;
90
+
91
+ /** Label font. */
92
+ labelStyle?: LabelStyle;
93
+
94
+ /**
95
+ * Called when a button is pressed, in every selection mode.
96
+ * @param value - The button's value
97
+ * @param index - The button's index
98
+ */
99
+ onPress?: (value: string, index: number) => void;
100
+
101
+ /**
102
+ * Called when the selection changes (`single` / `multiple` selection).
103
+ * @param values - Values of the selected buttons, in button order
104
+ */
105
+ onSelectionChange?: (values: string[]) => void;
106
+
107
+ /**
108
+ * Android-specific configuration
109
+ */
110
+ android?: {
111
+ /**
112
+ * What happens to buttons that don't fit: `'none'` (clipped), `'menu'`
113
+ * (moved into an overflow menu) or `'wrap'` (wrapped onto more rows).
114
+ * Default: `'none'`.
115
+ */
116
+ overflow?: ButtonGroupOverflow;
117
+
118
+ /** Ripple color shown while pressing a button. */
119
+ rippleColor?: ColorValue;
120
+
121
+ /** Outline color (outlined variant). */
122
+ strokeColor?: ColorValue;
123
+
124
+ /**
125
+ * Material style: Material 3 Expressive (default), or the classic
126
+ * Material 3 group, which has one size and shape (`size` and `shape`
127
+ * are ignored).
128
+ */
129
+ material?: AndroidMaterialStyle;
130
+ };
131
+
132
+ /** Test identifier */
133
+ testID?: string;
134
+ }
135
+
136
+ export function ButtonGroup(props: ButtonGroupProps): React.ReactElement {
137
+ const {
138
+ buttons,
139
+ variant,
140
+ size,
141
+ shape,
142
+ connected,
143
+ spacing,
144
+ selection,
145
+ selectedValues,
146
+ selectionRequired,
147
+ disabled,
148
+ color,
149
+ tintColor,
150
+ labelStyle,
151
+ onPress,
152
+ onSelectionChange,
153
+ android,
154
+ ...viewProps
155
+ } = props;
156
+
157
+ const selectionMode = selection ?? 'none';
158
+
159
+ const nativeButtons = useMemo((): NativeButton[] => {
160
+ return buttons.map((button) => ({
161
+ label: button.label ?? '',
162
+ value: button.value,
163
+ disabled: button.disabled ? 'disabled' : 'enabled',
164
+ accessibilityLabel: button.accessibilityLabel ?? '',
165
+ ...resolveIcon(button.icon),
166
+ }));
167
+ }, [buttons]);
168
+
169
+ const nativeSelectedValues = useMemo((): string[] => {
170
+ if (selectionMode === 'none' || !selectedValues) return [];
171
+ return selectionMode === 'single'
172
+ ? selectedValues.slice(0, 1)
173
+ : [...selectedValues];
174
+ }, [selectionMode, selectedValues]);
175
+
176
+ const nativeLabelStyle = useMemo(
177
+ () => normalizeLabelStyle(labelStyle),
178
+ [labelStyle]
179
+ );
180
+
181
+ const handlePress = useCallback(
182
+ (e: { nativeEvent: ButtonGroupPressEvent }) => {
183
+ const { index, value } = e.nativeEvent;
184
+ onPress?.(value, index);
185
+ },
186
+ [onPress]
187
+ );
188
+
189
+ const handleSelectionChange = useCallback(
190
+ (e: { nativeEvent: ButtonGroupSelectionEvent }) => {
191
+ onSelectionChange?.([...e.nativeEvent.values]);
192
+ },
193
+ [onSelectionChange]
194
+ );
195
+
196
+ const nativeAndroid = useMemo(
197
+ () => ({
198
+ overflow: android?.overflow ?? 'none',
199
+ material: android?.material ?? 'expressive',
200
+ }),
201
+ [android]
202
+ );
203
+
204
+ const isConnected = connected ?? selectionMode !== 'none';
205
+ const isSelectionRequired =
206
+ selectionRequired ?? (selectionMode === 'single' ? true : false);
207
+
208
+ return (
209
+ <NativeButtonGroup
210
+ buttons={nativeButtons}
211
+ variant={variant ?? 'outlined'}
212
+ size={size ?? 'small'}
213
+ shape={shape ?? ''}
214
+ connected={isConnected ? 'true' : 'false'}
215
+ spacing={spacing ?? -1}
216
+ selection={selectionMode}
217
+ selectedValues={nativeSelectedValues}
218
+ selectionRequired={isSelectionRequired ? 'true' : 'false'}
219
+ interactivity={disabled ? 'disabled' : 'enabled'}
220
+ color={color}
221
+ foregroundColor={tintColor}
222
+ androidRippleColor={android?.rippleColor}
223
+ androidStrokeColor={android?.strokeColor}
224
+ labelStyle={nativeLabelStyle}
225
+ onButtonPress={onPress ? handlePress : undefined}
226
+ onGroupSelectionChange={
227
+ onSelectionChange ? handleSelectionChange : undefined
228
+ }
229
+ android={nativeAndroid}
230
+ {...viewProps}
231
+ />
232
+ );
233
+ }