react-native-platform-components 0.6.1 → 0.7.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 (40) hide show
  1. package/README.md +153 -44
  2. package/android/src/main/java/com/platformcomponents/PCSegmentedControlView.kt +241 -0
  3. package/android/src/main/java/com/platformcomponents/PCSegmentedControlViewManager.kt +105 -0
  4. package/android/src/main/java/com/platformcomponents/PlatformComponentsPackage.kt +1 -0
  5. package/ios/PCDatePickerView.swift +16 -13
  6. package/ios/PCSegmentedControl.h +10 -0
  7. package/ios/PCSegmentedControl.mm +194 -0
  8. package/ios/PCSegmentedControl.swift +200 -0
  9. package/lib/commonjs/SegmentedControl.js +93 -0
  10. package/lib/commonjs/SegmentedControl.js.map +1 -0
  11. package/lib/commonjs/SegmentedControlNativeComponent.ts +79 -0
  12. package/lib/commonjs/index.js +11 -0
  13. package/lib/commonjs/index.js.map +1 -1
  14. package/lib/module/SegmentedControl.js +87 -0
  15. package/lib/module/SegmentedControl.js.map +1 -0
  16. package/lib/module/SegmentedControlNativeComponent.ts +79 -0
  17. package/lib/module/index.js +1 -0
  18. package/lib/module/index.js.map +1 -1
  19. package/lib/typescript/commonjs/src/SegmentedControl.d.ts +62 -0
  20. package/lib/typescript/commonjs/src/SegmentedControl.d.ts.map +1 -0
  21. package/lib/typescript/commonjs/src/SegmentedControlNativeComponent.d.ts +63 -0
  22. package/lib/typescript/commonjs/src/SegmentedControlNativeComponent.d.ts.map +1 -0
  23. package/lib/typescript/commonjs/src/index.d.ts +1 -0
  24. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  25. package/lib/typescript/module/src/SegmentedControl.d.ts +62 -0
  26. package/lib/typescript/module/src/SegmentedControl.d.ts.map +1 -0
  27. package/lib/typescript/module/src/SegmentedControlNativeComponent.d.ts +63 -0
  28. package/lib/typescript/module/src/SegmentedControlNativeComponent.d.ts.map +1 -0
  29. package/lib/typescript/module/src/index.d.ts +1 -0
  30. package/lib/typescript/module/src/index.d.ts.map +1 -1
  31. package/package.json +4 -3
  32. package/react-native.config.js +1 -0
  33. package/shared/PCSegmentedControlComponentDescriptors-custom.h +22 -0
  34. package/shared/PCSegmentedControlShadowNode-custom.cpp +54 -0
  35. package/shared/PCSegmentedControlShadowNode-custom.h +56 -0
  36. package/shared/PCSegmentedControlState-custom.h +62 -0
  37. package/shared/react/renderer/components/PlatformComponentsViewSpec/ComponentDescriptors.h +1 -0
  38. package/src/SegmentedControl.tsx +178 -0
  39. package/src/SegmentedControlNativeComponent.ts +79 -0
  40. package/src/index.tsx +1 -0
package/README.md CHANGED
@@ -3,55 +3,59 @@
3
3
  [![npm version](https://img.shields.io/npm/v/react-native-platform-components.svg)](https://www.npmjs.com/package/react-native-platform-components)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/react-native-platform-components.svg)](https://www.npmjs.com/package/react-native-platform-components)
5
5
 
6
+ High-quality **native UI components for React Native**, implemented with platform-first APIs and exposed through clean, typed JavaScript interfaces.
7
+
8
+ This library focuses on **true native behavior**, not JavaScript re-implementations.
9
+
6
10
  <table>
7
11
  <tr>
8
- <td valign="top">
9
- <table>
10
- <tr>
11
- <td align="center"><strong>iOS DatePicker</strong></td>
12
- <td align="center"><strong>Android DatePicker</strong></td>
13
- </tr>
14
- <tr>
15
- <td><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/ios-datepicker.gif" height="350" /></td>
16
- <td><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/android-datepicker.gif" height="350" /></td>
17
- </tr>
18
- <tr>
19
- <td align="center"><strong>iOS ContextMenu</strong></td>
20
- <td align="center"><strong>Android ContextMenu</strong></td>
21
- </tr>
22
- <tr>
23
- <td><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/ios-contextmenu.gif" height="350" /></td>
24
- <td><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/android-contextmenu.gif" height="350" /></td>
25
- </tr>
26
- <tr>
27
- <td align="center"><strong>iOS SelectionMenu</strong></td>
28
- <td align="center"><strong>Android SelectionMenu</strong></td>
29
- </tr>
30
- <tr>
31
- <td><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/ios-selectionmenu.gif" height="350" /></td>
32
- <td><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/android-selectionmenu.gif" height="350" /></td>
33
- </tr>
34
- </table>
35
- </td>
36
- <td valign="top">
37
- <p>High-quality <strong>native UI components for React Native</strong>, implemented with platform-first APIs and exposed through clean, typed JavaScript interfaces.</p>
38
- <p>This library focuses on <strong>true native behavior</strong>, not JavaScript re-implementations — providing:</p>
39
- <ul>
40
- <li><strong>DatePicker</strong> – native date & time pickers with modal and embedded presentations</li>
41
- <li><strong>ContextMenu</strong> – native context menus with long-press activation (UIContextMenuInteraction on iOS, PopupMenu on Android)</li>
42
- <li><strong>SelectionMenu</strong> – native selection menus (Material on Android, system menus on iOS)</li>
43
- </ul>
44
- <p>The goal is to provide components that:</p>
45
- <ul>
46
- <li>Feel <strong>100% native</strong> on each platform</li>
47
- <li>Support modern platform design systems (Material 3 on Android, system pickers on iOS)</li>
48
- <li>Offer <strong>headless</strong> and <strong>inline</strong> modes for maximum layout control</li>
49
- <li>Integrate cleanly with <strong>React Native Codegen / Fabric</strong></li>
50
- </ul>
51
- </td>
12
+ <td align="center"><strong>iOS DatePicker</strong></td>
13
+ <td align="center"><strong>Android DatePicker</strong></td>
14
+ </tr>
15
+ <tr>
16
+ <td><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/ios-datepicker.gif" height="550" /></td>
17
+ <td><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/android-datepicker.gif" height="550" /></td>
18
+ </tr>
19
+ <tr>
20
+ <td align="center"><strong>iOS ContextMenu</strong></td>
21
+ <td align="center"><strong>Android ContextMenu</strong></td>
22
+ </tr>
23
+ <tr>
24
+ <td><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/ios-contextmenu.gif" height="550" /></td>
25
+ <td><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/android-contextmenu.gif" height="550" /></td>
26
+ </tr>
27
+ <tr>
28
+ <td align="center"><strong>iOS SelectionMenu</strong></td>
29
+ <td align="center"><strong>Android SelectionMenu</strong></td>
30
+ </tr>
31
+ <tr>
32
+ <td><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/ios-selectionmenu.gif" height="550" /></td>
33
+ <td><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/android-selectionmenu.gif" height="550" /></td>
34
+ </tr>
35
+ <tr>
36
+ <td align="center"><strong>iOS SegmentedControl</strong></td>
37
+ <td align="center"><strong>Android SegmentedControl</strong></td>
38
+ </tr>
39
+ <tr>
40
+ <td><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/ios-segmentedcontrol.gif" height="550" /></td>
41
+ <td><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/android-segmentedcontrol.gif" height="550" /></td>
52
42
  </tr>
53
43
  </table>
54
44
 
45
+ ### Components
46
+
47
+ - **DatePicker** – native date & time pickers with modal and embedded presentations
48
+ - **ContextMenu** – native context menus with long-press activation (UIContextMenuInteraction on iOS, PopupMenu on Android)
49
+ - **SelectionMenu** – native selection menus (Material on Android, system menus on iOS)
50
+ - **SegmentedControl** – native segmented controls (UISegmentedControl on iOS, MaterialButtonToggleGroup on Android)
51
+
52
+ ### Goals
53
+
54
+ - Feel **100% native** on each platform
55
+ - Support modern platform design systems (Material 3 on Android, system pickers on iOS)
56
+ - Offer **headless** and **inline** modes for maximum layout control
57
+ - Integrate cleanly with **React Native Codegen / Fabric**
58
+
55
59
  ---
56
60
 
57
61
  ## Installation
@@ -325,6 +329,65 @@ export function Example() {
325
329
 
326
330
  ---
327
331
 
332
+ ### SegmentedControl
333
+
334
+ ```tsx
335
+ import { SegmentedControl } from 'react-native-platform-components';
336
+
337
+ const segments = [
338
+ { label: 'Day', value: 'day' },
339
+ { label: 'Week', value: 'week' },
340
+ { label: 'Month', value: 'month' },
341
+ ];
342
+
343
+ export function Example() {
344
+ const [selected, setSelected] = React.useState('day');
345
+
346
+ return (
347
+ <SegmentedControl
348
+ segments={segments}
349
+ selectedValue={selected}
350
+ onSelect={(value) => setSelected(value)}
351
+ />
352
+ );
353
+ }
354
+ ```
355
+
356
+ ### SegmentedControl (With Icons)
357
+
358
+ ```tsx
359
+ import { SegmentedControl } from 'react-native-platform-components';
360
+ import { Platform } from 'react-native';
361
+
362
+ const segments = [
363
+ {
364
+ label: 'List',
365
+ value: 'list',
366
+ icon: Platform.OS === 'ios' ? 'list.bullet' : 'list_bullet',
367
+ },
368
+ {
369
+ label: 'Grid',
370
+ value: 'grid',
371
+ icon: Platform.OS === 'ios' ? 'square.grid.2x2' : 'grid_view',
372
+ },
373
+ ];
374
+
375
+ export function Example() {
376
+ const [selected, setSelected] = React.useState('list');
377
+
378
+ return (
379
+ <SegmentedControl
380
+ segments={segments}
381
+ selectedValue={selected}
382
+ onSelect={(value) => setSelected(value)}
383
+ ios={{ apportionsSegmentWidthsByContent: true }}
384
+ />
385
+ );
386
+ }
387
+ ```
388
+
389
+ ---
390
+
328
391
  ## Components
329
392
 
330
393
  ## DatePicker
@@ -450,6 +513,50 @@ Native selection menu with **modal** and **embedded** modes.
450
513
 
451
514
  ---
452
515
 
516
+ ## SegmentedControl
517
+
518
+ Native segmented control using **UISegmentedControl** on iOS and **MaterialButtonToggleGroup** on Android.
519
+
520
+ ### Props
521
+
522
+ | Prop | Type | Description |
523
+ |------|------|-------------|
524
+ | `segments` | `SegmentedControlSegment[]` | Array of segments to display |
525
+ | `selectedValue` | `string \| null` | Currently selected segment's `value` |
526
+ | `disabled` | `boolean` | Disables the entire control |
527
+ | `onSelect` | `(value: string, index: number) => void` | Called when user selects a segment |
528
+
529
+ ### SegmentedControlSegment
530
+
531
+ | Property | Type | Description |
532
+ |----------|------|-------------|
533
+ | `label` | `string` | Display text for the segment |
534
+ | `value` | `string` | Unique value returned in callbacks |
535
+ | `disabled` | `boolean` | Disables this specific segment |
536
+ | `icon` | `string` | Icon name (SF Symbol on iOS, drawable on Android) |
537
+
538
+ ### iOS Props (`ios`)
539
+
540
+ | Prop | Type | Description |
541
+ |------|------|-------------|
542
+ | `momentary` | `boolean` | If true, segments don't show selected state |
543
+ | `apportionsSegmentWidthsByContent` | `boolean` | If true, segment widths are proportional to content |
544
+ | `selectedSegmentTintColor` | `string` | Tint color for selected segment (hex string) |
545
+
546
+ ### Android Props (`android`)
547
+
548
+ | Prop | Type | Description |
549
+ |------|------|-------------|
550
+ | `selectionRequired` | `boolean` | If true, one segment must always be selected |
551
+
552
+ ### Icon Support
553
+
554
+ Icons work the same as ContextMenu:
555
+ - **iOS**: Use SF Symbol names (e.g., `'list.bullet'`, `'square.grid.2x2'`)
556
+ - **Android**: Use drawable resource names (e.g., `'list_bullet'`, `'grid_view'`)
557
+
558
+ ---
559
+
453
560
  ## Design Philosophy
454
561
 
455
562
  - **Native first** — no JS re-implementation of pickers
@@ -554,6 +661,8 @@ const actions = [
554
661
 
555
662
  See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.
556
663
 
664
+ **Have a component request?** If there's a native UI component you'd like to see added, [open an issue](https://github.com/JarX-Concepts/react-native-platform-components/issues/new) describing the component and its native APIs on iOS and Android.
665
+
557
666
  ## License
558
667
 
559
668
  MIT
@@ -0,0 +1,241 @@
1
+ package com.platformcomponents
2
+
3
+ import android.content.Context
4
+ import android.text.TextUtils
5
+ import android.view.View
6
+ import android.widget.FrameLayout
7
+ import com.facebook.react.bridge.WritableNativeMap
8
+ import com.facebook.react.uimanager.PixelUtil
9
+ import com.facebook.react.uimanager.StateWrapper
10
+ import com.facebook.react.views.scroll.ReactScrollViewHelper
11
+ import com.google.android.material.button.MaterialButton
12
+ import com.google.android.material.button.MaterialButtonToggleGroup
13
+
14
+ class PCSegmentedControlView(context: Context) : FrameLayout(context), ReactScrollViewHelper.HasStateWrapper {
15
+
16
+ data class Segment(
17
+ val label: String,
18
+ val value: String,
19
+ val disabled: Boolean,
20
+ val icon: String
21
+ )
22
+
23
+ companion object {
24
+ private const val TAG = "PCSegmentedControl"
25
+ }
26
+
27
+ // --- State Wrapper for Fabric state updates ---
28
+ override var stateWrapper: StateWrapper? = null
29
+
30
+ private var lastReportedWidth: Float = 0f
31
+ private var lastReportedHeight: Float = 0f
32
+
33
+ // --- Props ---
34
+ var segments: List<Segment> = emptyList()
35
+ var selectedValue: String = "" // sentinel for none
36
+ var interactivity: String = "enabled" // "enabled" | "disabled"
37
+ var selectionRequired: Boolean = false
38
+
39
+ // --- Events ---
40
+ var onSelect: ((index: Int, value: String) -> Unit)? = null
41
+
42
+ // --- UI ---
43
+ private var toggleGroup: MaterialButtonToggleGroup? = null
44
+ private val buttonIdToSegment: MutableMap<Int, Segment> = mutableMapOf()
45
+ private var suppressCallbacks = false
46
+
47
+ init {
48
+ minimumHeight = (PCConstants.MIN_TOUCH_TARGET_HEIGHT_DP * resources.displayMetrics.density).toInt()
49
+ rebuildUI()
50
+ }
51
+
52
+ // ---- Public apply* (called by manager) ----
53
+
54
+ fun applySegments(newSegments: List<Segment>) {
55
+ if (segments == newSegments) return
56
+ segments = newSegments
57
+ rebuildUI()
58
+ }
59
+
60
+ fun applySelectedValue(value: String) {
61
+ if (selectedValue == value) return
62
+ selectedValue = value
63
+ updateSelection()
64
+ }
65
+
66
+ fun applyInteractivity(value: String?) {
67
+ val newValue = if (value == "disabled") "disabled" else "enabled"
68
+ if (interactivity == newValue) return
69
+ interactivity = newValue
70
+ updateEnabled()
71
+ }
72
+
73
+ fun applyAndroidProps(required: Boolean) {
74
+ if (selectionRequired != required) {
75
+ selectionRequired = required
76
+ rebuildUI()
77
+ }
78
+ }
79
+
80
+ // ---- UI Building ----
81
+
82
+ private fun rebuildUI() {
83
+ removeAllViews()
84
+ buttonIdToSegment.clear()
85
+
86
+ val group = MaterialButtonToggleGroup(context).apply {
87
+ layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
88
+ isSingleSelection = true
89
+ isSelectionRequired = selectionRequired
90
+ }
91
+
92
+ // Calculate if we need compact mode (many segments or long labels)
93
+ val totalLabelLength = segments.sumOf { it.label.length }
94
+ val useCompactMode = segments.size > 3 || totalLabelLength > 20
95
+
96
+ for ((index, segment) in segments.withIndex()) {
97
+ val button = MaterialButton(context, null, com.google.android.material.R.attr.materialButtonOutlinedStyle).apply {
98
+ id = View.generateViewId()
99
+ text = segment.label
100
+ isAllCaps = false // Preserve original text casing
101
+ contentDescription = segment.label // For accessibility and Detox matching
102
+ isEnabled = !segment.disabled && interactivity == "enabled"
103
+
104
+ // Enable text truncation with ellipsis when space is limited
105
+ ellipsize = TextUtils.TruncateAt.END
106
+ maxLines = 1
107
+
108
+ // Reduce horizontal padding in compact mode to fit more content
109
+ if (useCompactMode) {
110
+ val compactPadding = (8 * resources.displayMetrics.density).toInt()
111
+ setPaddingRelative(compactPadding, paddingTop, compactPadding, paddingBottom)
112
+ iconPadding = (4 * resources.displayMetrics.density).toInt()
113
+ }
114
+
115
+ // Set icon if available
116
+ if (segment.icon.isNotEmpty()) {
117
+ val resId = context.resources.getIdentifier(
118
+ segment.icon, "drawable", context.packageName
119
+ )
120
+ if (resId != 0) {
121
+ setIconResource(resId)
122
+ }
123
+ }
124
+
125
+ // Handle click to trigger selection (needed for Detox taps)
126
+ setOnClickListener {
127
+ if (!suppressCallbacks && isEnabled) {
128
+ group.check(id)
129
+ }
130
+ }
131
+ }
132
+
133
+ buttonIdToSegment[button.id] = segment
134
+ group.addView(button)
135
+ }
136
+
137
+ group.addOnButtonCheckedListener { _, checkedId, isChecked ->
138
+ if (suppressCallbacks) return@addOnButtonCheckedListener
139
+ if (!isChecked) return@addOnButtonCheckedListener
140
+
141
+ val segment = buttonIdToSegment[checkedId] ?: return@addOnButtonCheckedListener
142
+ val index = segments.indexOf(segment)
143
+ if (index >= 0) {
144
+ onSelect?.invoke(index, segment.value)
145
+ }
146
+ }
147
+
148
+ addView(group)
149
+ toggleGroup = group
150
+
151
+ updateSelection()
152
+ updateEnabled()
153
+ requestLayout()
154
+ }
155
+
156
+ private fun updateSelection() {
157
+ suppressCallbacks = true
158
+ val group = toggleGroup ?: return
159
+
160
+ if (selectedValue.isEmpty()) {
161
+ group.clearChecked()
162
+ } else {
163
+ for ((id, segment) in buttonIdToSegment) {
164
+ if (segment.value == selectedValue) {
165
+ group.check(id)
166
+ break
167
+ }
168
+ }
169
+ }
170
+ suppressCallbacks = false
171
+ }
172
+
173
+ private fun updateEnabled() {
174
+ val enabled = interactivity == "enabled"
175
+ alpha = if (enabled) 1f else 0.5f
176
+
177
+ val group = toggleGroup ?: return
178
+ for (i in 0 until group.childCount) {
179
+ val button = group.getChildAt(i) as? MaterialButton ?: continue
180
+ val segment = buttonIdToSegment[button.id] ?: continue
181
+ button.isEnabled = enabled && !segment.disabled
182
+ }
183
+ }
184
+
185
+ // ---- Measurement ----
186
+
187
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
188
+ // Measure children with UNSPECIFIED height to get intrinsic size
189
+ val unconstrainedHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
190
+ super.onMeasure(widthMeasureSpec, unconstrainedHeightSpec)
191
+
192
+ // Get the intrinsic height from children
193
+ val childHeight = if (toggleGroup != null) {
194
+ toggleGroup!!.measuredHeight
195
+ } else {
196
+ 0
197
+ }
198
+
199
+ // Use the maximum of child height and minimum touch target
200
+ val intrinsicHeight = childHeight.coerceAtLeast(minimumHeight)
201
+
202
+ // IMPORTANT: Always use intrinsic height regardless of Yoga constraints.
203
+ // Fabric may give us 0 height initially before state is updated.
204
+ // The state update from onLayout will trigger proper re-layout.
205
+ setMeasuredDimension(measuredWidth, intrinsicHeight)
206
+ }
207
+
208
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
209
+ super.onLayout(changed, left, top, right, bottom)
210
+ updateFrameSizeState()
211
+ }
212
+
213
+ /**
214
+ * Update Fabric state with the measured frame size.
215
+ * This allows the shadow node to use actual measured dimensions for Yoga layout.
216
+ */
217
+ private fun updateFrameSizeState() {
218
+ val wrapper = stateWrapper ?: return
219
+ val group = toggleGroup ?: return
220
+
221
+ // Measure the toggle group with exact width and unspecified height
222
+ val widthSpec = MeasureSpec.makeMeasureSpec(width.coerceAtLeast(1), MeasureSpec.EXACTLY)
223
+ val heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
224
+ group.measure(widthSpec, heightSpec)
225
+
226
+ val widthDp = PixelUtil.toDIPFromPixel(width.toFloat())
227
+ val heightDp = PixelUtil.toDIPFromPixel(group.measuredHeight.toFloat())
228
+
229
+ // Only update if changed
230
+ if (widthDp != lastReportedWidth || heightDp != lastReportedHeight) {
231
+ lastReportedWidth = widthDp
232
+ lastReportedHeight = heightDp
233
+
234
+ val stateData = WritableNativeMap().apply {
235
+ putDouble("width", widthDp.toDouble())
236
+ putDouble("height", heightDp.toDouble())
237
+ }
238
+ wrapper.updateState(stateData)
239
+ }
240
+ }
241
+ }
@@ -0,0 +1,105 @@
1
+ package com.platformcomponents
2
+
3
+ import com.facebook.react.bridge.ReadableArray
4
+ import com.facebook.react.bridge.ReadableMap
5
+ import com.facebook.react.uimanager.ReactStylesDiffMap
6
+ import com.facebook.react.uimanager.SimpleViewManager
7
+ import com.facebook.react.uimanager.StateWrapper
8
+ import com.facebook.react.uimanager.ThemedReactContext
9
+ import com.facebook.react.uimanager.ViewManagerDelegate
10
+ import com.facebook.react.uimanager.UIManagerHelper
11
+ import com.facebook.react.uimanager.events.Event
12
+ import com.facebook.react.uimanager.events.RCTEventEmitter
13
+ import com.facebook.react.viewmanagers.PCSegmentedControlManagerDelegate
14
+ import com.facebook.react.viewmanagers.PCSegmentedControlManagerInterface
15
+
16
+ class PCSegmentedControlViewManager :
17
+ SimpleViewManager<PCSegmentedControlView>(),
18
+ PCSegmentedControlManagerInterface<PCSegmentedControlView> {
19
+
20
+ companion object {
21
+ private const val TAG = "PCSegmentedControl"
22
+ }
23
+
24
+ private val delegate: ViewManagerDelegate<PCSegmentedControlView> =
25
+ PCSegmentedControlManagerDelegate(this)
26
+
27
+ override fun getName(): String = "PCSegmentedControl"
28
+
29
+ override fun getDelegate(): ViewManagerDelegate<PCSegmentedControlView> = delegate
30
+
31
+ override fun createViewInstance(reactContext: ThemedReactContext): PCSegmentedControlView {
32
+ return PCSegmentedControlView(reactContext)
33
+ }
34
+
35
+ /**
36
+ * Pass the StateWrapper to the view so it can update Fabric state with measured dimensions.
37
+ */
38
+ override fun updateState(
39
+ view: PCSegmentedControlView,
40
+ props: ReactStylesDiffMap,
41
+ stateWrapper: StateWrapper
42
+ ): Any? {
43
+ view.stateWrapper = stateWrapper
44
+ return null
45
+ }
46
+
47
+ override fun addEventEmitters(reactContext: ThemedReactContext, view: PCSegmentedControlView) {
48
+ val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.id)
49
+
50
+ view.onSelect = { index, value ->
51
+ dispatcher?.dispatchEvent(SelectEvent(view.id, index, value))
52
+ }
53
+ }
54
+
55
+ // segments: array of {label, value, disabled, icon}
56
+ override fun setSegments(view: PCSegmentedControlView, value: ReadableArray?) {
57
+ val out = ArrayList<PCSegmentedControlView.Segment>()
58
+ if (value != null) {
59
+ for (i in 0 until value.size()) {
60
+ val m = value.getMap(i) ?: continue
61
+ val label = if (m.hasKey("label") && !m.isNull("label")) m.getString("label") ?: "" else ""
62
+ val segValue = if (m.hasKey("value") && !m.isNull("value")) m.getString("value") ?: "" else ""
63
+ val disabled = m.hasKey("disabled") && !m.isNull("disabled") && m.getString("disabled") == "disabled"
64
+ val icon = if (m.hasKey("icon") && !m.isNull("icon")) m.getString("icon") ?: "" else ""
65
+ out.add(PCSegmentedControlView.Segment(label = label, value = segValue, disabled = disabled, icon = icon))
66
+ }
67
+ }
68
+ view.applySegments(out)
69
+ }
70
+
71
+ override fun setSelectedValue(view: PCSegmentedControlView, value: String?) {
72
+ // Spec sentinel: empty string means "no selection"
73
+ view.applySelectedValue(value ?: "")
74
+ }
75
+
76
+ override fun setInteractivity(view: PCSegmentedControlView, value: String?) {
77
+ view.applyInteractivity(value)
78
+ }
79
+
80
+ override fun setAndroid(view: PCSegmentedControlView, value: ReadableMap?) {
81
+ val selectionRequired = value != null && value.hasKey("selectionRequired") &&
82
+ !value.isNull("selectionRequired") && value.getString("selectionRequired") == "true"
83
+ view.applyAndroidProps(selectionRequired)
84
+ }
85
+
86
+ override fun setIos(view: PCSegmentedControlView, value: ReadableMap?) {
87
+ // Android ignores iOS config
88
+ }
89
+
90
+ // --- Events ---
91
+ private class SelectEvent(
92
+ surfaceId: Int,
93
+ private val index: Int,
94
+ private val value: String
95
+ ) : Event<SelectEvent>(surfaceId) {
96
+ override fun getEventName(): String = "topSelect"
97
+ override fun dispatch(rctEventEmitter: RCTEventEmitter) {
98
+ val payload = com.facebook.react.bridge.Arguments.createMap().apply {
99
+ putInt("index", index)
100
+ putString("value", value)
101
+ }
102
+ rctEventEmitter.receiveEvent(viewTag, eventName, payload)
103
+ }
104
+ }
105
+ }
@@ -14,6 +14,7 @@ class PlatformComponentsViewPackage : ReactPackage {
14
14
  PCSelectionMenuViewManager(),
15
15
  PCDatePickerViewManager(),
16
16
  PCContextMenuViewManager(),
17
+ PCSegmentedControlViewManager(),
17
18
  )
18
19
  }
19
20
 
@@ -283,30 +283,33 @@ public final class PCDatePickerView: UIControl,
283
283
  // Prevent "settle" events right as we present.
284
284
  suppressNextChangesBriefly()
285
285
 
286
- // Check if using inline style (full calendar) - needs larger popover size
287
- var isInlineStyle = false
288
- if #available(iOS 13.4, *) {
289
- isInlineStyle = picker.preferredDatePickerStyle == .inline
290
- }
291
-
292
286
  let vc = UIViewController()
293
287
  picker.translatesAutoresizingMaskIntoConstraints = false
294
288
  vc.view.addSubview(picker)
295
289
 
296
- // For inline style, use system background and constrain all edges
297
- // For other styles, use clear background and only top/leading constraints
298
- if isInlineStyle {
290
+ let useInlineFallback: Bool
291
+ if #available(iOS 26.0, *) {
292
+ useInlineFallback = false
293
+ } else {
294
+ useInlineFallback = true
295
+ }
296
+
297
+ if useInlineFallback {
298
+ // Pre–Liquid Glass fallback
299
299
  vc.view.backgroundColor = .systemBackground
300
300
  vc.view.isOpaque = true
301
+
301
302
  NSLayoutConstraint.activate([
302
- picker.topAnchor.constraint(equalTo: vc.view.topAnchor, constant: 8),
303
- picker.leadingAnchor.constraint(equalTo: vc.view.leadingAnchor, constant: 8),
304
- picker.trailingAnchor.constraint(equalTo: vc.view.trailingAnchor, constant: -8),
305
- picker.bottomAnchor.constraint(equalTo: vc.view.bottomAnchor, constant: -8),
303
+ picker.topAnchor.constraint(equalTo: vc.view.topAnchor),
304
+ picker.leadingAnchor.constraint(equalTo: vc.view.leadingAnchor),
305
+ picker.trailingAnchor.constraint(equalTo: vc.view.trailingAnchor),
306
+ picker.bottomAnchor.constraint(equalTo: vc.view.bottomAnchor),
306
307
  ])
307
308
  } else {
309
+ // Liquid Glass path
308
310
  vc.view.backgroundColor = .clear
309
311
  vc.view.isOpaque = false
312
+
310
313
  NSLayoutConstraint.activate([
311
314
  picker.topAnchor.constraint(equalTo: vc.view.topAnchor),
312
315
  picker.leadingAnchor.constraint(equalTo: vc.view.leadingAnchor),
@@ -0,0 +1,10 @@
1
+ // PCSegmentedControl.h
2
+
3
+ #import <React/RCTViewComponentView.h>
4
+
5
+ NS_ASSUME_NONNULL_BEGIN
6
+
7
+ @interface PCSegmentedControl : RCTViewComponentView
8
+ @end
9
+
10
+ NS_ASSUME_NONNULL_END