react-native-platform-components 0.3.1 → 0.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 +109 -102
- package/android/src/main/java/com/platformcomponents/PCConstants.kt +17 -0
- package/android/src/main/java/com/platformcomponents/PCDatePickerView.kt +10 -3
- package/android/src/main/java/com/platformcomponents/PCSelectionMenuView.kt +1 -1
- package/ios/PCConstants.swift +34 -0
- package/ios/PCDatePickerView.swift +12 -4
- package/ios/PCSelectionMenu.swift +24 -12
- package/package.json +32 -26
package/README.md
CHANGED
|
@@ -1,43 +1,22 @@
|
|
|
1
1
|
# react-native-platform-components
|
|
2
2
|
|
|
3
|
-
> 🚧 In development — not ready for public use.
|
|
4
|
-
|
|
5
3
|
High-quality **native UI components for React Native**, implemented with platform-first APIs and exposed through clean, typed JavaScript interfaces.
|
|
6
4
|
|
|
7
|
-
This library focuses on **true native behavior**, not JavaScript re-implementations —
|
|
5
|
+
This library focuses on **true native behavior**, not JavaScript re-implementations — providing:
|
|
8
6
|
|
|
9
|
-
- **SelectionMenu** – native selection menus (Material on Android, system menus on iOS
|
|
10
|
-
- **DatePicker** – native date & time pickers with modal and
|
|
7
|
+
- **SelectionMenu** – native selection menus (Material on Android, system menus on iOS)
|
|
8
|
+
- **DatePicker** – native date & time pickers with modal and embedded presentations
|
|
11
9
|
|
|
12
10
|
The goal is to provide components that:
|
|
13
11
|
|
|
14
12
|
- Feel **100% native** on each platform
|
|
15
|
-
- Support modern platform design systems (Material
|
|
13
|
+
- Support modern platform design systems (Material 3 on Android, system pickers on iOS)
|
|
16
14
|
- Offer **headless** and **inline** modes for maximum layout control
|
|
17
15
|
- Integrate cleanly with **React Native Codegen / Fabric**
|
|
18
|
-
- Degrade gracefully on **Web**
|
|
19
|
-
|
|
20
|
-
---
|
|
21
|
-
|
|
22
|
-
## 🎥 Demos
|
|
23
|
-
|
|
24
|
-
### SelectionMenu
|
|
25
|
-
Native Material / system selection menus with headless and inline modes.
|
|
26
|
-
|
|
27
|
-
📹 **Demo video:**
|
|
28
|
-
👉 *(add SelectionMenu demo link here)*
|
|
29
16
|
|
|
30
17
|
---
|
|
31
18
|
|
|
32
|
-
|
|
33
|
-
Native date & time pickers using platform system UI.
|
|
34
|
-
|
|
35
|
-
📹 **Demo video:**
|
|
36
|
-
👉 *(add DatePicker demo link here)*
|
|
37
|
-
|
|
38
|
-
---
|
|
39
|
-
|
|
40
|
-
## 📦 Installation
|
|
19
|
+
## Installation
|
|
41
20
|
|
|
42
21
|
```sh
|
|
43
22
|
npm install react-native-platform-components
|
|
@@ -53,22 +32,17 @@ pod install
|
|
|
53
32
|
```
|
|
54
33
|
|
|
55
34
|
- Minimum iOS version: **iOS 13+**
|
|
56
|
-
- Uses `UIDatePicker` and
|
|
35
|
+
- Uses `UIDatePicker` and SwiftUI Menu
|
|
57
36
|
|
|
58
37
|
### Android
|
|
59
38
|
|
|
60
|
-
- Uses native Android Views
|
|
61
|
-
- Supports **Material
|
|
39
|
+
- Uses native Android Views with Material Design
|
|
40
|
+
- Supports **Material 3** styling
|
|
62
41
|
- No additional setup required beyond autolinking
|
|
63
42
|
|
|
64
|
-
### Web
|
|
65
|
-
|
|
66
|
-
- **SelectionMenu** is supported with a web-appropriate fallback
|
|
67
|
-
- **DatePicker** currently targets native platforms only
|
|
68
|
-
|
|
69
43
|
---
|
|
70
44
|
|
|
71
|
-
##
|
|
45
|
+
## Quick Start
|
|
72
46
|
|
|
73
47
|
### SelectionMenu (Headless)
|
|
74
48
|
|
|
@@ -104,6 +78,33 @@ export function Example() {
|
|
|
104
78
|
}
|
|
105
79
|
```
|
|
106
80
|
|
|
81
|
+
### SelectionMenu (Inline)
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import { SelectionMenu } from 'react-native-platform-components';
|
|
85
|
+
|
|
86
|
+
const options = [
|
|
87
|
+
{ label: 'Apple', data: 'apple' },
|
|
88
|
+
{ label: 'Banana', data: 'banana' },
|
|
89
|
+
{ label: 'Orange', data: 'orange' },
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
export function Example() {
|
|
93
|
+
const [value, setValue] = React.useState<string | null>(null);
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<SelectionMenu
|
|
97
|
+
options={options}
|
|
98
|
+
selected={value}
|
|
99
|
+
inlineMode
|
|
100
|
+
placeholder="Select fruit"
|
|
101
|
+
onSelect={(data) => setValue(data)}
|
|
102
|
+
android={{ material: 'm3' }}
|
|
103
|
+
/>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
107
108
|
---
|
|
108
109
|
|
|
109
110
|
### DatePicker (Modal)
|
|
@@ -134,45 +135,51 @@ export function Example() {
|
|
|
134
135
|
}
|
|
135
136
|
```
|
|
136
137
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
## 🧩 Components
|
|
140
|
-
|
|
141
|
-
## SelectionMenu
|
|
138
|
+
### DatePicker (Embedded)
|
|
142
139
|
|
|
143
|
-
|
|
140
|
+
```tsx
|
|
141
|
+
import { DatePicker } from 'react-native-platform-components';
|
|
144
142
|
|
|
145
|
-
|
|
143
|
+
export function Example() {
|
|
144
|
+
const [date, setDate] = React.useState<Date | null>(new Date());
|
|
146
145
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
return (
|
|
147
|
+
<DatePicker
|
|
148
|
+
date={date}
|
|
149
|
+
presentation="embedded"
|
|
150
|
+
mode="dateAndTime"
|
|
151
|
+
onConfirm={(d) => setDate(d)}
|
|
152
|
+
/>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
```
|
|
150
156
|
|
|
151
|
-
|
|
152
|
-
label: string;
|
|
153
|
-
data: string;
|
|
154
|
-
}[];
|
|
157
|
+
---
|
|
155
158
|
|
|
156
|
-
|
|
159
|
+
## Components
|
|
157
160
|
|
|
158
|
-
|
|
159
|
-
placeholder?: string;
|
|
161
|
+
## SelectionMenu
|
|
160
162
|
|
|
161
|
-
|
|
162
|
-
visible?: boolean;
|
|
163
|
+
Native selection menu with **inline** and **headless** modes.
|
|
163
164
|
|
|
164
|
-
|
|
165
|
+
### Props
|
|
165
166
|
|
|
166
|
-
|
|
167
|
-
|
|
167
|
+
| Prop | Type | Description |
|
|
168
|
+
|------|------|-------------|
|
|
169
|
+
| `options` | `{ label: string; data: string }[]` | Array of options to display |
|
|
170
|
+
| `selected` | `string \| null` | Currently selected option's `data` value |
|
|
171
|
+
| `disabled` | `boolean` | Disables the menu |
|
|
172
|
+
| `placeholder` | `string` | Placeholder text when no selection |
|
|
173
|
+
| `inlineMode` | `boolean` | If true, renders native inline picker UI |
|
|
174
|
+
| `visible` | `boolean` | Controls headless mode menu visibility |
|
|
175
|
+
| `onSelect` | `(data, label, index) => void` | Called when user selects an option |
|
|
176
|
+
| `onRequestClose` | `() => void` | Called when menu is dismissed without selection |
|
|
177
|
+
| `android.material` | `'system' \| 'm3'` | Material Design style preference |
|
|
168
178
|
|
|
169
|
-
|
|
179
|
+
### Modes
|
|
170
180
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
};
|
|
174
|
-
};
|
|
175
|
-
```
|
|
181
|
+
- **Headless mode** (default): Menu visibility controlled by `visible` prop. Use for custom trigger UI.
|
|
182
|
+
- **Inline mode** (`inlineMode={true}`): Native picker UI rendered inline. Menu managed internally.
|
|
176
183
|
|
|
177
184
|
---
|
|
178
185
|
|
|
@@ -182,54 +189,54 @@ Native date & time picker using **platform system pickers**.
|
|
|
182
189
|
|
|
183
190
|
### Props
|
|
184
191
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
positiveButtonTitle?: string;
|
|
217
|
-
negativeButtonTitle?: string;
|
|
218
|
-
};
|
|
219
|
-
};
|
|
220
|
-
```
|
|
192
|
+
| Prop | Type | Description |
|
|
193
|
+
|------|------|-------------|
|
|
194
|
+
| `date` | `Date \| null` | Controlled date value |
|
|
195
|
+
| `minDate` | `Date \| null` | Minimum selectable date |
|
|
196
|
+
| `maxDate` | `Date \| null` | Maximum selectable date |
|
|
197
|
+
| `locale` | `string` | Locale identifier (e.g., `'en-US'`) |
|
|
198
|
+
| `timeZoneName` | `string` | Time zone identifier |
|
|
199
|
+
| `mode` | `'date' \| 'time' \| 'dateAndTime' \| 'countDownTimer'` | Picker mode |
|
|
200
|
+
| `presentation` | `'modal' \| 'embedded'` | Presentation style |
|
|
201
|
+
| `visible` | `boolean` | Controls modal visibility (modal mode only) |
|
|
202
|
+
| `onConfirm` | `(date: Date) => void` | Called when user confirms selection |
|
|
203
|
+
| `onClosed` | `() => void` | Called when modal is dismissed |
|
|
204
|
+
|
|
205
|
+
### iOS Props (`ios`)
|
|
206
|
+
|
|
207
|
+
| Prop | Type | Description |
|
|
208
|
+
|------|------|-------------|
|
|
209
|
+
| `preferredStyle` | `'automatic' \| 'compact' \| 'inline' \| 'wheels'` | iOS date picker style |
|
|
210
|
+
| `countDownDurationSeconds` | `number` | Duration for countdown timer mode |
|
|
211
|
+
| `minuteInterval` | `number` | Minute interval (1-30) |
|
|
212
|
+
| `roundsToMinuteInterval` | `'inherit' \| 'round' \| 'noRound'` | Rounding behavior |
|
|
213
|
+
|
|
214
|
+
### Android Props (`android`)
|
|
215
|
+
|
|
216
|
+
| Prop | Type | Description |
|
|
217
|
+
|------|------|-------------|
|
|
218
|
+
| `firstDayOfWeek` | `number` | First day of week (1-7, Sunday=1) |
|
|
219
|
+
| `material` | `'system' \| 'm3'` | Material Design style |
|
|
220
|
+
| `dialogTitle` | `string` | Custom dialog title |
|
|
221
|
+
| `positiveButtonTitle` | `string` | Custom confirm button text |
|
|
222
|
+
| `negativeButtonTitle` | `string` | Custom cancel button text |
|
|
221
223
|
|
|
222
224
|
---
|
|
223
225
|
|
|
224
|
-
##
|
|
226
|
+
## Design Philosophy
|
|
225
227
|
|
|
226
228
|
- **Native first** — no JS re-implementation of pickers
|
|
227
229
|
- **Headless-friendly** — works with any custom UI
|
|
228
|
-
- **Codegen-safe** — string unions & sentinel values
|
|
230
|
+
- **Codegen-safe** — string unions & sentinel values for type safety
|
|
229
231
|
- **Predictable behavior** — no surprise re-renders or layout hacks
|
|
232
|
+
- **Platform conventions** — respects native UX patterns
|
|
230
233
|
|
|
231
234
|
---
|
|
232
235
|
|
|
233
|
-
##
|
|
236
|
+
## Contributing
|
|
237
|
+
|
|
238
|
+
See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.
|
|
239
|
+
|
|
240
|
+
## License
|
|
234
241
|
|
|
235
242
|
MIT
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
package com.platformcomponents
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Centralized sizing constants for Platform Components Android implementations.
|
|
5
|
+
* These values control touch targets, default dimensions, and fallback sizes.
|
|
6
|
+
*/
|
|
7
|
+
object PCConstants {
|
|
8
|
+
// MARK: - Touch Targets
|
|
9
|
+
|
|
10
|
+
/** Minimum touch target height in dp (Material Design 3 recommends 48dp, we use 56dp for text fields) */
|
|
11
|
+
const val MIN_TOUCH_TARGET_HEIGHT_DP = 56f
|
|
12
|
+
|
|
13
|
+
// MARK: - Fallback Dimensions
|
|
14
|
+
|
|
15
|
+
/** Fallback width in dp when constraint width is unavailable */
|
|
16
|
+
const val FALLBACK_WIDTH_DP = 320f
|
|
17
|
+
}
|
|
@@ -5,6 +5,7 @@ import android.content.Context
|
|
|
5
5
|
import android.content.ContextWrapper
|
|
6
6
|
import android.content.DialogInterface
|
|
7
7
|
import android.os.Build
|
|
8
|
+
import android.util.Log
|
|
8
9
|
import android.view.Gravity
|
|
9
10
|
import android.view.View
|
|
10
11
|
import android.view.ViewGroup
|
|
@@ -27,6 +28,10 @@ import kotlin.math.min
|
|
|
27
28
|
|
|
28
29
|
class PCDatePickerView(context: Context) : FrameLayout(context) {
|
|
29
30
|
|
|
31
|
+
companion object {
|
|
32
|
+
private const val TAG = "PCDatePicker"
|
|
33
|
+
}
|
|
34
|
+
|
|
30
35
|
// --- Public props (set by manager) ---
|
|
31
36
|
private var mode: String = "date" // "date" | "time" | "dateAndTime"
|
|
32
37
|
private var presentation: String = "modal" // "inline" | "modal" | "popover" | "sheet" | "auto" (we treat non-inline as modal-ish)
|
|
@@ -80,6 +85,7 @@ class PCDatePickerView(context: Context) : FrameLayout(context) {
|
|
|
80
85
|
"date", "time", "dateAndTime" -> value
|
|
81
86
|
else -> "date"
|
|
82
87
|
}
|
|
88
|
+
Log.d(TAG, "applyMode mode=$mode")
|
|
83
89
|
rebuildUI()
|
|
84
90
|
}
|
|
85
91
|
|
|
@@ -94,6 +100,7 @@ class PCDatePickerView(context: Context) : FrameLayout(context) {
|
|
|
94
100
|
"open", "closed" -> value
|
|
95
101
|
else -> "closed"
|
|
96
102
|
}
|
|
103
|
+
Log.d(TAG, "applyVisible visible=$visible isInline=${isInline()}")
|
|
97
104
|
if (isInline()) return
|
|
98
105
|
|
|
99
106
|
if (visible == "open") presentIfNeeded() else dismissIfNeeded()
|
|
@@ -313,12 +320,13 @@ class PCDatePickerView(context: Context) : FrameLayout(context) {
|
|
|
313
320
|
private fun presentIfNeeded() {
|
|
314
321
|
if (showingModal) return
|
|
315
322
|
val act = findFragmentActivity() ?: run {
|
|
316
|
-
|
|
323
|
+
Log.w(TAG, "presentIfNeeded: no FragmentActivity found")
|
|
317
324
|
onCancel?.invoke()
|
|
318
325
|
showingModal = false
|
|
319
326
|
return
|
|
320
327
|
}
|
|
321
328
|
|
|
329
|
+
Log.d(TAG, "presentIfNeeded mode=$mode material=$androidMaterialMode")
|
|
322
330
|
showingModal = true
|
|
323
331
|
|
|
324
332
|
when (mode) {
|
|
@@ -641,9 +649,8 @@ class PCDatePickerView(context: Context) : FrameLayout(context) {
|
|
|
641
649
|
// -----------------------------
|
|
642
650
|
|
|
643
651
|
private fun onCancelOrClose() {
|
|
652
|
+
Log.d(TAG, "onCancelOrClose")
|
|
644
653
|
showingModal = false
|
|
645
|
-
// JS typically sets visible="closed" in response to onCancel/onConfirm,
|
|
646
|
-
// but we defensively mark ourselves closed.
|
|
647
654
|
}
|
|
648
655
|
|
|
649
656
|
private fun clamp(valueMs: Long): Long {
|
|
@@ -59,7 +59,7 @@ class PCSelectionMenuView(context: Context) : FrameLayout(context) {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
private val minInlineHeightPx: Int by lazy {
|
|
62
|
-
(
|
|
62
|
+
(PCConstants.MIN_TOUCH_TARGET_HEIGHT_DP * resources.displayMetrics.density).toInt()
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
// Headless needs a non-zero anchor rect for dropdown
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// Centralized sizing constants for Platform Components iOS implementations.
|
|
4
|
+
/// These values control touch targets, popover sizes, and fallback dimensions.
|
|
5
|
+
enum PCConstants {
|
|
6
|
+
// MARK: - Touch Targets
|
|
7
|
+
|
|
8
|
+
/// Minimum touch target height (Apple HIG recommends 44pt)
|
|
9
|
+
static let minTouchTargetHeight: CGFloat = 44
|
|
10
|
+
|
|
11
|
+
// MARK: - Popover Sizing
|
|
12
|
+
|
|
13
|
+
/// Default popover width for selection menus
|
|
14
|
+
static let popoverWidth: CGFloat = 250
|
|
15
|
+
|
|
16
|
+
/// Maximum popover height before scrolling
|
|
17
|
+
static let popoverMaxHeight: CGFloat = 400
|
|
18
|
+
|
|
19
|
+
/// Row height in selection menu popover
|
|
20
|
+
static let popoverRowHeight: CGFloat = 44
|
|
21
|
+
|
|
22
|
+
/// Vertical padding in selection menu popover (top + bottom)
|
|
23
|
+
static let popoverVerticalPadding: CGFloat = 16
|
|
24
|
+
|
|
25
|
+
// MARK: - Fallback Dimensions
|
|
26
|
+
|
|
27
|
+
/// Fallback width when constraint width is unavailable
|
|
28
|
+
static let fallbackWidth: CGFloat = 320
|
|
29
|
+
|
|
30
|
+
// MARK: - Timing
|
|
31
|
+
|
|
32
|
+
/// Delay before presenting headless menu (allows layout to settle)
|
|
33
|
+
static let headlessPresentationDelay: TimeInterval = 0.1
|
|
34
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import os.log
|
|
1
2
|
import UIKit
|
|
2
3
|
|
|
4
|
+
private let logger = Logger(subsystem: "com.platformcomponents", category: "DatePicker")
|
|
5
|
+
|
|
3
6
|
@objcMembers
|
|
4
7
|
public final class PCDatePickerView: UIControl,
|
|
5
8
|
UIPopoverPresentationControllerDelegate,
|
|
@@ -131,7 +134,7 @@ public final class PCDatePickerView: UIControl,
|
|
|
131
134
|
picker.setNeedsLayout()
|
|
132
135
|
picker.layoutIfNeeded()
|
|
133
136
|
let fitted = picker.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
|
|
134
|
-
return CGSize(width: UIView.noIntrinsicMetric, height: max(
|
|
137
|
+
return CGSize(width: UIView.noIntrinsicMetric, height: max(PCConstants.minTouchTargetHeight, fitted.height))
|
|
135
138
|
}
|
|
136
139
|
|
|
137
140
|
/// ✅ Called by your measuring pipeline.
|
|
@@ -144,13 +147,13 @@ public final class PCDatePickerView: UIControl,
|
|
|
144
147
|
|
|
145
148
|
let width =
|
|
146
149
|
(constrainedSize.width.isFinite && constrainedSize.width > 1)
|
|
147
|
-
? constrainedSize.width :
|
|
150
|
+
? constrainedSize.width : PCConstants.fallbackWidth
|
|
148
151
|
let fitted = picker.systemLayoutSizeFitting(
|
|
149
152
|
CGSize(width: width, height: UIView.layoutFittingCompressedSize.height),
|
|
150
153
|
withHorizontalFittingPriority: .required,
|
|
151
154
|
verticalFittingPriority: .fittingSizeLevel
|
|
152
155
|
)
|
|
153
|
-
return CGSize(width: constrainedSize.width, height: max(
|
|
156
|
+
return CGSize(width: constrainedSize.width, height: max(PCConstants.minTouchTargetHeight, fitted.height))
|
|
154
157
|
}
|
|
155
158
|
|
|
156
159
|
/// Separate sizing for popover content.
|
|
@@ -204,7 +207,11 @@ public final class PCDatePickerView: UIControl,
|
|
|
204
207
|
|
|
205
208
|
private func presentIfNeeded() {
|
|
206
209
|
guard modalVC == nil else { return }
|
|
207
|
-
guard let top = topViewController() else {
|
|
210
|
+
guard let top = topViewController() else {
|
|
211
|
+
logger.warning("presentIfNeeded: no view controller found")
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
logger.debug("presentIfNeeded: presenting modal picker")
|
|
208
215
|
|
|
209
216
|
// Prevent “settle” events right as we present.
|
|
210
217
|
suppressNextChangesBriefly()
|
|
@@ -239,6 +246,7 @@ public final class PCDatePickerView: UIControl,
|
|
|
239
246
|
|
|
240
247
|
private func dismissIfNeeded(emitCancel: Bool) {
|
|
241
248
|
guard let vc = modalVC else { return }
|
|
249
|
+
logger.debug("dismissIfNeeded: dismissing modal, emitCancel=\(emitCancel)")
|
|
242
250
|
modalVC = nil
|
|
243
251
|
vc.dismiss(animated: true) { [weak self] in
|
|
244
252
|
guard let self else { return }
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import os.log
|
|
1
2
|
import SwiftUI
|
|
2
3
|
import UIKit
|
|
3
4
|
|
|
5
|
+
private let logger = Logger(subsystem: "com.platformcomponents", category: "SelectionMenu")
|
|
6
|
+
|
|
4
7
|
// MARK: - Option model (bridged from ObjC++ as dictionaries)
|
|
5
8
|
|
|
6
9
|
struct PCSelectionMenuOption {
|
|
@@ -259,24 +262,31 @@ public final class PCSelectionMenuView: UIControl {
|
|
|
259
262
|
let opts = parsedOptions
|
|
260
263
|
guard !opts.isEmpty else { return }
|
|
261
264
|
|
|
262
|
-
|
|
265
|
+
logger.debug("presentHeadlessMenuIfNeeded: scheduling presentation with \(opts.count) options")
|
|
266
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + PCConstants.headlessPresentationDelay) {
|
|
263
267
|
let menuVC = PCMenuViewController(
|
|
264
268
|
options: opts,
|
|
265
269
|
onSelect: { [weak self] idx in
|
|
266
270
|
guard let self else { return }
|
|
267
271
|
let opt = opts[idx]
|
|
272
|
+
logger.debug("headless menu selected: index=\(idx), data=\(opt.data)")
|
|
268
273
|
self.selectedData = opt.data
|
|
269
274
|
self.onSelect?(idx, opt.label, opt.data)
|
|
270
275
|
},
|
|
271
276
|
onCancel: { [weak self] in
|
|
277
|
+
logger.debug("headless menu cancelled")
|
|
272
278
|
self?.onRequestClose?()
|
|
273
279
|
}
|
|
274
280
|
)
|
|
275
281
|
|
|
276
282
|
menuVC.modalPresentationStyle = .popover
|
|
283
|
+
let popoverHeight = min(
|
|
284
|
+
CGFloat(opts.count) * PCConstants.popoverRowHeight + PCConstants.popoverVerticalPadding,
|
|
285
|
+
PCConstants.popoverMaxHeight
|
|
286
|
+
)
|
|
277
287
|
menuVC.preferredContentSize = CGSize(
|
|
278
|
-
width:
|
|
279
|
-
height:
|
|
288
|
+
width: PCConstants.popoverWidth,
|
|
289
|
+
height: popoverHeight
|
|
280
290
|
)
|
|
281
291
|
|
|
282
292
|
if let popover = menuVC.popoverPresentationController {
|
|
@@ -290,19 +300,18 @@ public final class PCSelectionMenuView: UIControl {
|
|
|
290
300
|
}
|
|
291
301
|
}
|
|
292
302
|
|
|
293
|
-
// MARK: -
|
|
303
|
+
// MARK: - Sizing
|
|
294
304
|
|
|
295
305
|
public override func sizeThatFits(_ size: CGSize) -> CGSize {
|
|
296
306
|
if anchorMode != "inline" { return CGSize(width: size.width, height: 1) }
|
|
297
307
|
|
|
298
|
-
let minH: CGFloat = 44
|
|
299
308
|
guard let host = hostingController else {
|
|
300
|
-
return CGSize(width: size.width, height:
|
|
309
|
+
return CGSize(width: size.width, height: PCConstants.minTouchTargetHeight)
|
|
301
310
|
}
|
|
302
311
|
|
|
303
|
-
let w = (size.width > 1) ? size.width :
|
|
312
|
+
let w = (size.width > 1) ? size.width : PCConstants.fallbackWidth
|
|
304
313
|
let fitted = host.sizeThatFits(in: CGSize(width: w, height: .greatestFiniteMagnitude))
|
|
305
|
-
return CGSize(width: size.width, height: max(
|
|
314
|
+
return CGSize(width: size.width, height: max(PCConstants.minTouchTargetHeight, fitted.height))
|
|
306
315
|
}
|
|
307
316
|
|
|
308
317
|
public override var intrinsicContentSize: CGSize {
|
|
@@ -310,7 +319,9 @@ public final class PCSelectionMenuView: UIControl {
|
|
|
310
319
|
return CGSize(width: UIView.noIntrinsicMetric, height: 1)
|
|
311
320
|
}
|
|
312
321
|
let h = max(
|
|
313
|
-
|
|
322
|
+
PCConstants.minTouchTargetHeight,
|
|
323
|
+
sizeThatFits(CGSize(width: bounds.width, height: .greatestFiniteMagnitude)).height
|
|
324
|
+
)
|
|
314
325
|
return CGSize(width: UIView.noIntrinsicMetric, height: h)
|
|
315
326
|
}
|
|
316
327
|
}
|
|
@@ -348,10 +359,11 @@ private class PCMenuViewController: UIViewController, UITableViewDelegate, UITab
|
|
|
348
359
|
tableView.dataSource = self
|
|
349
360
|
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
|
|
350
361
|
tableView.backgroundColor = .clear
|
|
351
|
-
tableView.separatorStyle = .none
|
|
362
|
+
tableView.separatorStyle = .none
|
|
352
363
|
tableView.isScrollEnabled = true
|
|
353
|
-
tableView.rowHeight =
|
|
354
|
-
|
|
364
|
+
tableView.rowHeight = PCConstants.popoverRowHeight
|
|
365
|
+
let verticalPad = PCConstants.popoverVerticalPadding / 2
|
|
366
|
+
tableView.contentInset = UIEdgeInsets(top: verticalPad, left: 0, bottom: verticalPad, right: 0)
|
|
355
367
|
tableView.translatesAutoresizingMaskIntoConstraints = false
|
|
356
368
|
|
|
357
369
|
blurView.contentView.addSubview(tableView)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-platform-components",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "A cross-platform toolkit of native UI components for React Native.",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|
|
@@ -63,32 +63,32 @@
|
|
|
63
63
|
"registry": "https://registry.npmjs.org/"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
-
"@commitlint/config-conventional": "^
|
|
67
|
-
"@eslint/compat": "^
|
|
68
|
-
"@eslint/eslintrc": "^3.3.
|
|
69
|
-
"@eslint/js": "^9.
|
|
70
|
-
"@react-native-community/cli": "20.0
|
|
71
|
-
"@react-native/babel-preset": "0.
|
|
72
|
-
"@react-native/eslint-config": "^0.
|
|
73
|
-
"@release-it/conventional-changelog": "^10.0.
|
|
74
|
-
"@types/jest": "^
|
|
75
|
-
"@types/react": "^19.
|
|
66
|
+
"@commitlint/config-conventional": "^20.3.1",
|
|
67
|
+
"@eslint/compat": "^2.0.1",
|
|
68
|
+
"@eslint/eslintrc": "^3.3.3",
|
|
69
|
+
"@eslint/js": "^9.39.2",
|
|
70
|
+
"@react-native-community/cli": "20.1.0",
|
|
71
|
+
"@react-native/babel-preset": "0.83.1",
|
|
72
|
+
"@react-native/eslint-config": "^0.83.1",
|
|
73
|
+
"@release-it/conventional-changelog": "^10.0.4",
|
|
74
|
+
"@types/jest": "^30.0.0",
|
|
75
|
+
"@types/react": "^19.2.9",
|
|
76
76
|
"@types/react-test-renderer": "^19.1.0",
|
|
77
|
-
"commitlint": "^
|
|
78
|
-
"del-cli": "^
|
|
79
|
-
"eslint": "^9.
|
|
77
|
+
"commitlint": "^20.3.1",
|
|
78
|
+
"del-cli": "^7.0.0",
|
|
79
|
+
"eslint": "^9.39.2",
|
|
80
80
|
"eslint-config-prettier": "^10.1.8",
|
|
81
|
-
"eslint-plugin-prettier": "^5.5.
|
|
82
|
-
"jest": "^
|
|
83
|
-
"lefthook": "^2.0.
|
|
84
|
-
"prettier": "^
|
|
85
|
-
"react": "19.
|
|
86
|
-
"react-native": "0.
|
|
87
|
-
"react-native-builder-bob": "^0.40.
|
|
88
|
-
"react-test-renderer": "19.
|
|
81
|
+
"eslint-plugin-prettier": "^5.5.5",
|
|
82
|
+
"jest": "^30.2.0",
|
|
83
|
+
"lefthook": "^2.0.15",
|
|
84
|
+
"prettier": "^3.8.1",
|
|
85
|
+
"react": "19.2.3",
|
|
86
|
+
"react-native": "0.83.1",
|
|
87
|
+
"react-native-builder-bob": "^0.40.17",
|
|
88
|
+
"react-test-renderer": "19.2.3",
|
|
89
89
|
"release-it": "^19.2.4",
|
|
90
|
-
"turbo": "^2.5
|
|
91
|
-
"typescript": "^5.9.
|
|
90
|
+
"turbo": "^2.7.5",
|
|
91
|
+
"typescript": "^5.9.3"
|
|
92
92
|
},
|
|
93
93
|
"peerDependencies": {
|
|
94
94
|
"react": "*",
|
|
@@ -145,10 +145,16 @@
|
|
|
145
145
|
"release-it": {
|
|
146
146
|
"git": {
|
|
147
147
|
"commitMessage": "chore: release ${version}",
|
|
148
|
-
"tagName": "v${version}"
|
|
148
|
+
"tagName": "v${version}",
|
|
149
|
+
"requireCleanWorkingDir": false
|
|
149
150
|
},
|
|
150
151
|
"npm": {
|
|
151
|
-
"publish":
|
|
152
|
+
"publish": true,
|
|
153
|
+
"skipChecks": true,
|
|
154
|
+
"publishArgs": [
|
|
155
|
+
"--access",
|
|
156
|
+
"public"
|
|
157
|
+
]
|
|
152
158
|
},
|
|
153
159
|
"github": {
|
|
154
160
|
"release": true
|