react-native-platform-components 0.5.3 → 0.5.5

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 (32) hide show
  1. package/README.md +13 -8
  2. package/android/build.gradle +3 -1
  3. package/android/src/main/java/com/platformcomponents/PCConstants.kt +3 -0
  4. package/android/src/main/java/com/platformcomponents/PCDatePickerView.kt +53 -1
  5. package/android/src/main/java/com/platformcomponents/PCDatePickerViewManager.kt +14 -0
  6. package/android/src/main/java/com/platformcomponents/PCSelectionMenuView.kt +169 -10
  7. package/android/src/main/java/com/platformcomponents/PCSelectionMenuViewManager.kt +14 -0
  8. package/android/src/main/jni/CMakeLists.txt +47 -0
  9. package/android/src/main/jni/OnLoad.cpp +33 -0
  10. package/ios/PCDatePickerView.swift +58 -2
  11. package/ios/PCSelectionMenu.mm +42 -0
  12. package/ios/PCSelectionMenu.swift +17 -0
  13. package/lib/module/SelectionMenu.js +7 -14
  14. package/lib/module/SelectionMenu.js.map +1 -1
  15. package/lib/typescript/src/SelectionMenu.d.ts +6 -5
  16. package/lib/typescript/src/SelectionMenu.d.ts.map +1 -1
  17. package/lib/typescript/src/sharedTypes.d.ts +3 -1
  18. package/lib/typescript/src/sharedTypes.d.ts.map +1 -1
  19. package/package.json +3 -2
  20. package/react-native.config.js +13 -0
  21. package/shared/PCDatePickerComponentDescriptors-custom.h +14 -43
  22. package/shared/PCDatePickerShadowNode-custom.cpp +35 -0
  23. package/shared/PCDatePickerShadowNode-custom.h +40 -18
  24. package/shared/PCDatePickerState-custom.h +53 -1
  25. package/shared/PCSelectionMenuComponentDescriptors-custom.h +15 -18
  26. package/shared/PCSelectionMenuShadowNode-custom.cpp +42 -21
  27. package/shared/PCSelectionMenuShadowNode-custom.h +23 -10
  28. package/shared/PCSelectionMenuState-custom.h +65 -0
  29. package/shared/README.md +179 -0
  30. package/shared/react/renderer/components/PlatformComponentsViewSpec/ComponentDescriptors.h +9 -0
  31. package/src/SelectionMenu.tsx +15 -24
  32. package/src/sharedTypes.ts +4 -1
@@ -2,31 +2,43 @@
2
2
 
3
3
  #include <react/renderer/components/view/ConcreteViewShadowNode.h>
4
4
 
5
- #import <react/renderer/components/PlatformComponentsViewSpec/ComponentDescriptors.h>
6
- #import <react/renderer/components/PlatformComponentsViewSpec/EventEmitters.h>
7
- #import <react/renderer/components/PlatformComponentsViewSpec/Props.h>
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 "PCSelectionMenuState-custom.h"
8
11
 
9
12
  namespace facebook::react {
10
13
 
11
14
  extern const char PCSelectionMenuComponentName[];
12
15
 
13
16
  /**
14
- * ShadowNode for SelectionMenu.
17
+ * Custom ShadowNode for SelectionMenu that supports Yoga measurement.
15
18
  *
16
19
  * Key behavior:
17
- * - Provides a non-zero default measured height (minRowHeight) so the view
18
- * remains tappable when JS does not specify an explicit height.
20
+ * - Native side measures the actual picker and updates state with frameSize
21
+ * - measureContent() returns the size from state for proper Yoga layout
22
+ * - Falls back to platform-specific defaults if state hasn't been set yet
19
23
  */
20
24
  class MeasuringPCSelectionMenuShadowNode final : public ConcreteViewShadowNode<
21
25
  PCSelectionMenuComponentName,
22
26
  PCSelectionMenuProps,
23
27
  PCSelectionMenuEventEmitter,
24
- PCSelectionMenuState> {
28
+ PCSelectionMenuStateFrameSize> {
25
29
  public:
26
30
  using ConcreteViewShadowNode::ConcreteViewShadowNode;
27
31
 
28
- static constexpr float kMinRowHeight = 44.0f;
29
-
32
+ // Fallback heights used when native hasn't reported measurements yet
33
+ // iOS standard row height
34
+ static constexpr float kFallbackHeightIOS = 44.0f;
35
+
36
+ // Android System Spinner height
37
+ static constexpr float kFallbackHeightAndroid = 56.0f;
38
+
39
+ // Android M3 TextInputLayout with floating label height
40
+ static constexpr float kFallbackHeightAndroidM3 = 72.0f;
41
+
30
42
  static ShadowNodeTraits BaseTraits() {
31
43
  auto traits = ConcreteViewShadowNode::BaseTraits();
32
44
  traits.set(ShadowNodeTraits::Trait::LeafYogaNode);
@@ -36,7 +48,8 @@ class MeasuringPCSelectionMenuShadowNode final : public ConcreteViewShadowNode<
36
48
 
37
49
  /**
38
50
  * Called by Yoga when it needs the intrinsic size of the component.
39
- * We ensure a sensible minimum height so the view doesn't measure to 0.
51
+ * Returns the size provided by native through state, with fallback to
52
+ * platform-specific defaults if state hasn't been set.
40
53
  */
41
54
  Size measureContent(
42
55
  const LayoutContext& layoutContext,
@@ -0,0 +1,65 @@
1
+ #pragma once
2
+
3
+ #include <react/renderer/core/LayoutPrimitives.h>
4
+ #include <memory>
5
+
6
+ #ifdef RN_SERIALIZABLE_STATE
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 SelectionMenu that holds the measured frame size from native.
16
+ * This allows the native side to measure the actual picker and communicate
17
+ * the size to the shadow node for proper Yoga layout.
18
+ *
19
+ * Note: Does NOT inherit from StateData (which is final). Custom state types
20
+ * are standalone structs that satisfy the ConcreteState template requirements.
21
+ */
22
+ struct PCSelectionMenuStateFrameSize {
23
+ using Shared = std::shared_ptr<const PCSelectionMenuStateFrameSize>;
24
+
25
+ Size frameSize{}; // {width, height} in points
26
+
27
+ PCSelectionMenuStateFrameSize() = default;
28
+
29
+ explicit PCSelectionMenuStateFrameSize(Size size) : frameSize(size) {}
30
+
31
+ bool operator==(const PCSelectionMenuStateFrameSize& other) const {
32
+ return frameSize.width == other.frameSize.width &&
33
+ frameSize.height == other.frameSize.height;
34
+ }
35
+
36
+ bool operator!=(const PCSelectionMenuStateFrameSize& other) const {
37
+ return !(*this == other);
38
+ }
39
+
40
+ #ifdef RN_SERIALIZABLE_STATE
41
+ // Required for Android state serialization
42
+ PCSelectionMenuStateFrameSize(
43
+ const PCSelectionMenuStateFrameSize& previousState,
44
+ folly::dynamic data)
45
+ : frameSize(previousState.frameSize) {
46
+ // Parse frame size from dynamic data if provided
47
+ if (data.isObject()) {
48
+ if (data.count("width") && data.count("height")) {
49
+ frameSize.width = static_cast<Float>(data["width"].asDouble());
50
+ frameSize.height = static_cast<Float>(data["height"].asDouble());
51
+ }
52
+ }
53
+ }
54
+
55
+ folly::dynamic getDynamic() const {
56
+ return folly::dynamic::object("width", frameSize.width)("height", frameSize.height);
57
+ }
58
+
59
+ MapBuffer getMapBuffer() const {
60
+ return MapBufferBuilder::EMPTY();
61
+ }
62
+ #endif
63
+ };
64
+
65
+ } // namespace facebook::react
@@ -0,0 +1,179 @@
1
+ # Shared C++ Code for Yoga Shadow Node Measurement
2
+
3
+ This directory contains cross-platform C++ code that enables native components to report their measured sizes to React Native's Yoga layout engine. This allows components like `DatePicker` and `SelectionMenu` to have "perfect" sizing based on their actual native content, rather than relying on hardcoded dimensions.
4
+
5
+ ## Architecture Overview
6
+
7
+ ```
8
+ ┌─────────────────────────────────────────────────────────────────────────┐
9
+ │ React Native (JS) │
10
+ │ <DatePicker /> <SelectionMenu /> │
11
+ └─────────────────────────────────────────────────────────────────────────┘
12
+
13
+
14
+ ┌─────────────────────────────────────────────────────────────────────────┐
15
+ │ Yoga Layout Engine (C++) │
16
+ │ │
17
+ │ Calls measureContent() on shadow nodes to determine intrinsic size │
18
+ └─────────────────────────────────────────────────────────────────────────┘
19
+
20
+
21
+ ┌─────────────────────────────────────────────────────────────────────────┐
22
+ │ Custom Shadow Nodes (this directory) │
23
+ │ │
24
+ │ MeasuringPCDatePickerShadowNode │
25
+ │ MeasuringPCSelectionMenuShadowNode │
26
+ │ │
27
+ │ - Marked as LeafYogaNode + MeasurableYogaNode │
28
+ │ - Override measureContent() to return size from state │
29
+ └─────────────────────────────────────────────────────────────────────────┘
30
+
31
+
32
+ ┌─────────────────────────────────────────────────────────────────────────┐
33
+ │ Custom State (this directory) │
34
+ │ │
35
+ │ PCDatePickerStateFrameSize { Size frameSize } │
36
+ │ PCSelectionMenuStateFrameSize { Size frameSize } │
37
+ │ │
38
+ │ - Holds measured dimensions from native │
39
+ │ - Supports serialization for Android (folly::dynamic) │
40
+ └─────────────────────────────────────────────────────────────────────────┘
41
+
42
+
43
+ ┌─────────────────────────────────────────────────────────────────────────┐
44
+ │ Native Views (iOS/Android) │
45
+ │ │
46
+ │ iOS: PCDatePickerView, PCSelectionMenuView │
47
+ │ Android: PCDatePickerView, PCSelectionMenuView │
48
+ │ │
49
+ │ - Measure actual native content │
50
+ │ - Call state->updateState() with measured frameSize │
51
+ │ - Triggers Yoga re-layout with correct dimensions │
52
+ └─────────────────────────────────────────────────────────────────────────┘
53
+ ```
54
+
55
+ ## How It Works
56
+
57
+ ### 1. Shadow Node Traits
58
+
59
+ Shadow nodes are marked with two critical traits:
60
+
61
+ ```cpp
62
+ static ShadowNodeTraits BaseTraits() {
63
+ auto traits = ConcreteViewShadowNode::BaseTraits();
64
+ traits.set(ShadowNodeTraits::Trait::LeafYogaNode); // No Yoga children
65
+ traits.set(ShadowNodeTraits::Trait::MeasurableYogaNode); // Has measureContent()
66
+ return traits;
67
+ }
68
+ ```
69
+
70
+ - **LeafYogaNode**: Tells Yoga this node has no children to layout
71
+ - **MeasurableYogaNode**: Tells Yoga to call `measureContent()` for intrinsic sizing
72
+
73
+ ### 2. Measurement Flow
74
+
75
+ ```cpp
76
+ Size measureContent(
77
+ const LayoutContext& layoutContext,
78
+ const LayoutConstraints& layoutConstraints) const override {
79
+
80
+ // Get the measured size from native (via state)
81
+ const auto& stateData = this->getStateData();
82
+ Float measuredW = stateData.frameSize.width;
83
+ Float measuredH = stateData.frameSize.height;
84
+
85
+ // Apply layout constraints and return
86
+ return layoutConstraints.clamp(Size{measuredW, measuredH});
87
+ }
88
+ ```
89
+
90
+ ### 3. Native → Shadow Node Communication (State)
91
+
92
+ Native views measure their content and update the shadow node's state:
93
+
94
+ **iOS (Objective-C++):**
95
+ ```objc
96
+ - (void)updateMeasurements {
97
+ CGSize size = [_nativeView sizeForLayoutWithConstrainedTo:...];
98
+
99
+ PCDatePickerStateFrameSize next;
100
+ next.frameSize = {(Float)size.width, (Float)size.height};
101
+ _state->updateState(std::move(next));
102
+ }
103
+ ```
104
+
105
+ **Android (Kotlin + JNI):**
106
+ ```kotlin
107
+ private fun updateMeasurements(width: Float, height: Float) {
108
+ updateState(width, height) // JNI call to C++
109
+ }
110
+ ```
111
+
112
+ ### 4. State Serialization (Android)
113
+
114
+ Android requires state to be serializable via `folly::dynamic`:
115
+
116
+ ```cpp
117
+ struct PCDatePickerStateFrameSize {
118
+ #ifdef RN_SERIALIZABLE_STATE
119
+ // Constructor from dynamic data
120
+ PCDatePickerStateFrameSize(
121
+ const PCDatePickerStateFrameSize& previousState,
122
+ folly::dynamic data) {
123
+ if (data.isObject() && data.count("width") && data.count("height")) {
124
+ frameSize.width = static_cast<Float>(data["width"].asDouble());
125
+ frameSize.height = static_cast<Float>(data["height"].asDouble());
126
+ }
127
+ }
128
+
129
+ folly::dynamic getDynamic() const {
130
+ return folly::dynamic::object("width", frameSize.width)("height", frameSize.height);
131
+ }
132
+ #endif
133
+ };
134
+ ```
135
+
136
+ ## File Structure
137
+
138
+ | File | Purpose |
139
+ |------|---------|
140
+ | `PC*ShadowNode-custom.h` | Shadow node class declaration with `measureContent()` |
141
+ | `PC*ShadowNode-custom.cpp` | `measureContent()` implementation |
142
+ | `PC*State-custom.h` | State struct holding `frameSize` from native |
143
+ | `PC*ComponentDescriptors-custom.h` | Type alias for component descriptor using custom shadow node |
144
+
145
+ ## Fallback Behavior
146
+
147
+ When native hasn't yet reported measurements (state is empty), the shadow nodes use platform-specific fallback heights:
148
+
149
+ ```cpp
150
+ // SelectionMenu fallbacks
151
+ static constexpr float kFallbackHeightIOS = 44.0f; // iOS standard row
152
+ static constexpr float kFallbackHeightAndroid = 56.0f; // Android Spinner
153
+ static constexpr float kFallbackHeightAndroidM3 = 72.0f; // Android M3 TextInputLayout
154
+ ```
155
+
156
+ This prevents layout jumps on initial render before native measurement completes.
157
+
158
+ ## Integration Points
159
+
160
+ ### iOS
161
+
162
+ - `ios/PCDatePicker.mm` - Calls `updateMeasurements` when props change
163
+ - `ios/PCSelectionMenu.mm` - Calls `updateMeasurements` when props change
164
+
165
+ ### Android
166
+
167
+ - `android/.../PCDatePickerView.kt` - Calls JNI `updateState()` after measure
168
+ - `android/.../PCSelectionMenuView.kt` - Calls JNI `updateState()` after measure
169
+ - `android/src/main/jni/OnLoad.cpp` - JNI bridge for state updates
170
+
171
+ ## Common Pitfalls
172
+
173
+ 1. **Circular includes**: Shadow node headers should NOT include ComponentDescriptors.h. Use forward declarations instead.
174
+
175
+ 2. **State timing**: Native measurement may happen after initial Yoga layout. Always provide sensible fallback values.
176
+
177
+ 3. **Width constraints**: When width isn't measured (0), use `layoutConstraints.maximumSize.width` as the width.
178
+
179
+ 4. **Android serialization**: The `RN_SERIALIZABLE_STATE` macro gates Android-specific serialization code.
@@ -0,0 +1,9 @@
1
+ #pragma once
2
+
3
+ // Include the codegen-generated component descriptors using include_next
4
+ // This allows us to shadow the header while still including the original
5
+ #include_next <react/renderer/components/PlatformComponentsViewSpec/ComponentDescriptors.h>
6
+
7
+ // Include our custom component descriptors which use measuring shadow nodes
8
+ #include "PCSelectionMenuComponentDescriptors-custom.h"
9
+ #include "PCDatePickerComponentDescriptors-custom.h"
@@ -1,13 +1,13 @@
1
1
  // SelectionMenu.tsx
2
2
  import React, { useCallback, useMemo } from 'react';
3
- import { Platform, StyleSheet, type ViewProps } from 'react-native';
3
+ import { type ViewProps } from 'react-native';
4
4
 
5
5
  import NativeSelectionMenu, {
6
6
  type SelectionMenuOption,
7
7
  type SelectionMenuSelectEvent,
8
8
  } from './SelectionMenuNativeComponent';
9
9
 
10
- import type { AndroidMaterialMode } from './sharedTypes';
10
+ import type { AndroidMaterialMode, Presentation } from './sharedTypes';
11
11
 
12
12
  export interface SelectionMenuProps extends ViewProps {
13
13
  /** Options are label + data (payload) */
@@ -23,13 +23,14 @@ export interface SelectionMenuProps extends ViewProps {
23
23
  placeholder?: string;
24
24
 
25
25
  /**
26
- * If true, native renders its own inline anchor and manages open/close internally.
27
- * If false (default), component is headless and controlled by `visible`.
26
+ * Presentation mode:
27
+ * - 'modal' (default): Headless mode, controlled by `visible` prop.
28
+ * - 'embedded': Native renders its own inline anchor and manages open/close internally.
28
29
  */
29
- inlineMode?: boolean;
30
+ presentation?: Presentation;
30
31
 
31
32
  /**
32
- * Headless mode only (inlineMode === false):
33
+ * Modal mode only (presentation === 'modal'):
33
34
  * controls whether the native menu UI is presented.
34
35
  */
35
36
  visible?: boolean;
@@ -64,11 +65,11 @@ function normalizeSelectedData(selected: string | null): string {
64
65
  }
65
66
 
66
67
  function normalizeNativeVisible(
67
- inlineMode: boolean | undefined,
68
+ presentation: Presentation | undefined,
68
69
  visible: boolean | undefined
69
70
  ): 'open' | 'closed' | undefined {
70
- // Inline mode ignores visible; keep it undefined so native isn't spammed.
71
- if (inlineMode) return undefined;
71
+ // Embedded mode ignores visible; keep it undefined so native isn't spammed.
72
+ if (presentation === 'embedded') return undefined;
72
73
  return visible ? 'open' : 'closed';
73
74
  }
74
75
 
@@ -79,7 +80,7 @@ export function SelectionMenu(props: SelectionMenuProps): React.ReactElement {
79
80
  selected,
80
81
  disabled,
81
82
  placeholder,
82
- inlineMode,
83
+ presentation = 'modal',
83
84
  visible,
84
85
  onSelect,
85
86
  onRequestClose,
@@ -94,8 +95,8 @@ export function SelectionMenu(props: SelectionMenuProps): React.ReactElement {
94
95
  );
95
96
 
96
97
  const nativeVisible = useMemo(
97
- () => normalizeNativeVisible(inlineMode, visible),
98
- [inlineMode, visible]
98
+ () => normalizeNativeVisible(presentation, visible),
99
+ [presentation, visible]
99
100
  );
100
101
 
101
102
  const handleSelect = useCallback(
@@ -116,20 +117,14 @@ export function SelectionMenu(props: SelectionMenuProps): React.ReactElement {
116
117
  return { material: android.material };
117
118
  }, [android]);
118
119
 
119
- const isAndroidM3Inline =
120
- android?.material &&
121
- inlineMode &&
122
- android.material === 'm3' &&
123
- Platform.OS === 'android';
124
-
125
120
  return (
126
121
  <NativeSelectionMenu
127
- style={[style, isAndroidM3Inline && styles.androidInline]}
122
+ style={style}
128
123
  options={options}
129
124
  selectedData={selectedData}
130
125
  interactivity={disabled ? 'disabled' : 'enabled'}
131
126
  placeholder={placeholder}
132
- anchorMode={inlineMode ? 'inline' : 'headless'}
127
+ anchorMode={presentation === 'embedded' ? 'inline' : 'headless'}
133
128
  visible={nativeVisible}
134
129
  onSelect={onSelect ? handleSelect : undefined}
135
130
  onRequestClose={onRequestClose ? handleRequestClose : undefined}
@@ -139,7 +134,3 @@ export function SelectionMenu(props: SelectionMenuProps): React.ReactElement {
139
134
  />
140
135
  );
141
136
  }
142
-
143
- const styles = StyleSheet.create({
144
- androidInline: { minHeight: 60 },
145
- });
@@ -1,9 +1,12 @@
1
1
  // SharedTypes.ts
2
2
  import type { CodegenTypes } from 'react-native';
3
3
 
4
- /** Shared open/closed control state. */
4
+ /** Shared "open/closed" control state. */
5
5
  export type Visible = 'open' | 'closed';
6
6
 
7
+ /** Shared presentation mode for pickers/menus. */
8
+ export type Presentation = 'modal' | 'embedded';
9
+
7
10
  /** Shared Material preference (Android). */
8
11
  export type AndroidMaterialMode = 'system' | 'm3';
9
12