react-native-platform-components 0.5.4 → 0.6.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 (31) hide show
  1. package/README.md +296 -84
  2. package/android/src/main/java/com/platformcomponents/PCContextMenuView.kt +419 -0
  3. package/android/src/main/java/com/platformcomponents/PCContextMenuViewManager.kt +200 -0
  4. package/android/src/main/java/com/platformcomponents/PlatformComponentsPackage.kt +1 -0
  5. package/ios/PCContextMenu.h +12 -0
  6. package/ios/PCContextMenu.mm +247 -0
  7. package/ios/PCContextMenu.swift +346 -0
  8. package/ios/PCDatePickerView.swift +39 -0
  9. package/lib/module/ContextMenu.js +111 -0
  10. package/lib/module/ContextMenu.js.map +1 -0
  11. package/lib/module/ContextMenuNativeComponent.ts +141 -0
  12. package/lib/module/SelectionMenu.js +6 -6
  13. package/lib/module/SelectionMenu.js.map +1 -1
  14. package/lib/module/index.js +1 -0
  15. package/lib/module/index.js.map +1 -1
  16. package/lib/typescript/src/ContextMenu.d.ts +79 -0
  17. package/lib/typescript/src/ContextMenu.d.ts.map +1 -0
  18. package/lib/typescript/src/ContextMenuNativeComponent.d.ts +122 -0
  19. package/lib/typescript/src/ContextMenuNativeComponent.d.ts.map +1 -0
  20. package/lib/typescript/src/SelectionMenu.d.ts +6 -5
  21. package/lib/typescript/src/SelectionMenu.d.ts.map +1 -1
  22. package/lib/typescript/src/index.d.ts +1 -0
  23. package/lib/typescript/src/index.d.ts.map +1 -1
  24. package/lib/typescript/src/sharedTypes.d.ts +3 -1
  25. package/lib/typescript/src/sharedTypes.d.ts.map +1 -1
  26. package/package.json +6 -3
  27. package/src/ContextMenu.tsx +209 -0
  28. package/src/ContextMenuNativeComponent.ts +141 -0
  29. package/src/SelectionMenu.tsx +13 -12
  30. package/src/index.tsx +1 -0
  31. package/src/sharedTypes.ts +4 -1
package/README.md CHANGED
@@ -15,6 +15,14 @@
15
15
  <td><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/ios-datepicker.gif" height="350" /></td>
16
16
  <td><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/android-datepicker.gif" height="350" /></td>
17
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>
18
26
  <tr>
19
27
  <td align="center"><strong>iOS SelectionMenu</strong></td>
20
28
  <td align="center"><strong>Android SelectionMenu</strong></td>
@@ -29,8 +37,9 @@
29
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>
30
38
  <p>This library focuses on <strong>true native behavior</strong>, not JavaScript re-implementations — providing:</p>
31
39
  <ul>
32
- <li><strong>SelectionMenu</strong> – native selection menus (Material on Android, system menus on iOS)</li>
33
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>
34
43
  </ul>
35
44
  <p>The goal is to provide components that:</p>
36
45
  <ul>
@@ -61,11 +70,11 @@ pod install
61
70
  ```
62
71
 
63
72
  - Minimum iOS version: **iOS 13+**
64
- - Uses `UIDatePicker` and SwiftUI Menu
73
+ - Uses `UIDatePicker`, SwiftUI Menu, and `UIContextMenuInteraction`
65
74
 
66
75
  ### Android
67
76
 
68
- - Uses native Android Views with Material Design
77
+ - Uses native Android Views with Material Design (including `PopupMenu` for context menus)
69
78
  - Supports **Material 3** styling
70
79
  - No additional setup required beyond autolinking
71
80
 
@@ -73,62 +82,53 @@ pod install
73
82
 
74
83
  ## Quick Start
75
84
 
76
- ### SelectionMenu (Headless)
85
+ ### DatePicker (Modal)
77
86
 
78
87
  ```tsx
79
- import { SelectionMenu } from 'react-native-platform-components';
80
-
81
- const options = [
82
- { label: 'Apple', data: 'apple' },
83
- { label: 'Banana', data: 'banana' },
84
- { label: 'Orange', data: 'orange' },
85
- ];
88
+ import { DatePicker } from 'react-native-platform-components';
86
89
 
87
90
  export function Example() {
91
+ const [date, setDate] = React.useState<Date | null>(null);
88
92
  const [visible, setVisible] = React.useState(false);
89
- const [value, setValue] = React.useState<string | null>(null);
90
93
 
91
94
  return (
92
95
  <>
93
- <Button title="Open menu" onPress={() => setVisible(true)} />
96
+ <Button title="Pick date" onPress={() => setVisible(true)} />
94
97
 
95
- <SelectionMenu
96
- options={options}
97
- selected={value}
98
+ <DatePicker
99
+ date={date}
98
100
  visible={visible}
99
- onSelect={(data) => {
100
- setValue(data);
101
+ presentation="modal"
102
+ mode="date"
103
+ onConfirm={(d) => {
104
+ setDate(d);
101
105
  setVisible(false);
102
106
  }}
103
- onRequestClose={() => setVisible(false)}
107
+ onClosed={() => setVisible(false)}
108
+ ios={{preferredStyle: 'inline'}}
109
+ android={{material: 'system'}}
104
110
  />
105
111
  </>
106
112
  );
107
113
  }
108
114
  ```
109
115
 
110
- ### SelectionMenu (Inline)
116
+ ### DatePicker (Embedded)
111
117
 
112
118
  ```tsx
113
- import { SelectionMenu } from 'react-native-platform-components';
114
-
115
- const options = [
116
- { label: 'Apple', data: 'apple' },
117
- { label: 'Banana', data: 'banana' },
118
- { label: 'Orange', data: 'orange' },
119
- ];
119
+ import { DatePicker } from 'react-native-platform-components';
120
120
 
121
121
  export function Example() {
122
- const [value, setValue] = React.useState<string | null>(null);
122
+ const [date, setDate] = React.useState<Date | null>(new Date());
123
123
 
124
124
  return (
125
- <SelectionMenu
126
- options={options}
127
- selected={value}
128
- inlineMode
129
- placeholder="Select fruit"
130
- onSelect={(data) => setValue(data)}
131
- android={{ material: 'm3' }}
125
+ <DatePicker
126
+ date={date}
127
+ presentation="embedded"
128
+ mode="date"
129
+ onConfirm={(d) => setDate(d)}
130
+ ios={{preferredStyle: 'inline'}}
131
+ android={{material: 'system'}}
132
132
  />
133
133
  );
134
134
  }
@@ -136,53 +136,130 @@ export function Example() {
136
136
 
137
137
  ---
138
138
 
139
- ### DatePicker (Modal)
139
+ ### ContextMenu (Gesture Mode)
140
140
 
141
141
  ```tsx
142
- import { DatePicker } from 'react-native-platform-components';
142
+ import { ContextMenu } from 'react-native-platform-components';
143
+ import { Platform, View, Text } from 'react-native';
144
+
145
+ export function Example() {
146
+ const [lastAction, setLastAction] = React.useState<string | null>(null);
147
+
148
+ return (
149
+ <ContextMenu
150
+ title="Options"
151
+ actions={[
152
+ {
153
+ id: 'copy',
154
+ title: 'Copy',
155
+ image: Platform.OS === 'ios' ? 'doc.on.doc' : 'content_copy',
156
+ },
157
+ {
158
+ id: 'share',
159
+ title: 'Share',
160
+ image: Platform.OS === 'ios' ? 'square.and.arrow.up' : 'share',
161
+ },
162
+ {
163
+ id: 'delete',
164
+ title: 'Delete',
165
+ image: Platform.OS === 'ios' ? 'trash' : 'delete',
166
+ attributes: { destructive: true },
167
+ },
168
+ ]}
169
+ onPressAction={(id, title) => setLastAction(title)}
170
+ >
171
+ <View style={{ padding: 20, backgroundColor: '#E8F4FD', borderRadius: 8 }}>
172
+ <Text>Long-press me</Text>
173
+ </View>
174
+ </ContextMenu>
175
+ );
176
+ }
177
+ ```
178
+
179
+ ### ContextMenu (Modal Mode)
180
+
181
+ ```tsx
182
+ import { ContextMenu } from 'react-native-platform-components';
183
+ import { View, Text } from 'react-native';
184
+
185
+ export function Example() {
186
+ return (
187
+ <ContextMenu
188
+ title="Actions"
189
+ actions={[
190
+ { id: 'edit', title: 'Edit' },
191
+ { id: 'duplicate', title: 'Duplicate' },
192
+ { id: 'delete', title: 'Delete', attributes: { destructive: true } },
193
+ ]}
194
+ trigger="tap" // or "longPress" (default)
195
+ onPressAction={(id) => console.log('Selected:', id)}
196
+ >
197
+ <View style={{ padding: 16, backgroundColor: '#eee' }}>
198
+ <Text>Tap or long-press me</Text>
199
+ </View>
200
+ </ContextMenu>
201
+ );
202
+ }
203
+ ```
204
+
205
+ ---
206
+
207
+ ### SelectionMenu (Headless)
208
+
209
+ ```tsx
210
+ import { SelectionMenu } from 'react-native-platform-components';
211
+
212
+ const options = [
213
+ { label: 'Apple', data: 'apple' },
214
+ { label: 'Banana', data: 'banana' },
215
+ { label: 'Orange', data: 'orange' },
216
+ ];
143
217
 
144
218
  export function Example() {
145
- const [date, setDate] = React.useState<Date | null>(null);
146
219
  const [visible, setVisible] = React.useState(false);
220
+ const [value, setValue] = React.useState<string | null>(null);
147
221
 
148
222
  return (
149
223
  <>
150
- <Button title="Pick date" onPress={() => setVisible(true)} />
224
+ <Button title="Open menu" onPress={() => setVisible(true)} />
151
225
 
152
- <DatePicker
153
- date={date}
226
+ <SelectionMenu
227
+ options={options}
228
+ selected={value}
154
229
  visible={visible}
155
- presentation="modal"
156
- mode="date"
157
- onConfirm={(d) => {
158
- setDate(d);
230
+ onSelect={(data) => {
231
+ setValue(data);
159
232
  setVisible(false);
160
233
  }}
161
- onClosed={() => setVisible(false)}
162
- ios={{preferredStyle: 'inline'}}
163
- android={{material: 'system'}}
234
+ onRequestClose={() => setVisible(false)}
164
235
  />
165
236
  </>
166
237
  );
167
238
  }
168
239
  ```
169
240
 
170
- ### DatePicker (Embedded)
241
+ ### SelectionMenu (Inline)
171
242
 
172
243
  ```tsx
173
- import { DatePicker } from 'react-native-platform-components';
244
+ import { SelectionMenu } from 'react-native-platform-components';
245
+
246
+ const options = [
247
+ { label: 'Apple', data: 'apple' },
248
+ { label: 'Banana', data: 'banana' },
249
+ { label: 'Orange', data: 'orange' },
250
+ ];
174
251
 
175
252
  export function Example() {
176
- const [date, setDate] = React.useState<Date | null>(new Date());
253
+ const [value, setValue] = React.useState<string | null>(null);
177
254
 
178
255
  return (
179
- <DatePicker
180
- date={date}
256
+ <SelectionMenu
257
+ options={options}
258
+ selected={value}
181
259
  presentation="embedded"
182
- mode="date"
183
- onConfirm={(d) => setDate(d)}
184
- ios={{preferredStyle: 'inline'}}
185
- android={{material: 'system'}}
260
+ placeholder="Select fruit"
261
+ onSelect={(data) => setValue(data)}
262
+ android={{ material: 'm3' }}
186
263
  />
187
264
  );
188
265
  }
@@ -192,33 +269,6 @@ export function Example() {
192
269
 
193
270
  ## Components
194
271
 
195
- ## SelectionMenu
196
-
197
- Native selection menu with **inline** and **headless** modes.
198
-
199
- ### Props
200
-
201
- | Prop | Type | Description |
202
- |------|------|-------------|
203
- | `options` | `{ label: string; data: string }[]` | Array of options to display |
204
- | `selected` | `string \| null` | Currently selected option's `data` value |
205
- | `disabled` | `boolean` | Disables the menu |
206
- | `placeholder` | `string` | Placeholder text when no selection |
207
- | `inlineMode` | `boolean` | If true, renders native inline picker UI |
208
- | `visible` | `boolean` | Controls headless mode menu visibility |
209
- | `onSelect` | `(data, label, index) => void` | Called when user selects an option |
210
- | `onRequestClose` | `() => void` | Called when menu is dismissed without selection |
211
- | `android.material` | `'system' \| 'm3'` | Material Design style preference |
212
-
213
- ### Modes
214
-
215
- - **Headless mode** (default): Menu visibility controlled by `visible` prop. Use for custom trigger UI.
216
- - **Inline mode** (`inlineMode={true}`): Native picker UI rendered inline. Menu managed internally.
217
-
218
- > **Note:** On iOS, headless mode uses a custom popover to enable programmatic presentation. For the full native menu experience (system animations, scroll physics), use inline mode. This is an intentional trade-off: headless gives you control over the trigger UI, inline gives you the complete system menu behavior.
219
-
220
- ---
221
-
222
272
  ## DatePicker
223
273
 
224
274
  Native date & time picker using **platform system pickers**.
@@ -259,6 +309,89 @@ Native date & time picker using **platform system pickers**.
259
309
 
260
310
  ---
261
311
 
312
+ ## ContextMenu
313
+
314
+ Native context menu that wraps content and responds to **long-press** or **tap** gestures.
315
+
316
+ ### Props
317
+
318
+ | Prop | Type | Description |
319
+ |------|------|-------------|
320
+ | `title` | `string` | Menu title (shown as header on iOS) |
321
+ | `actions` | `ContextMenuAction[]` | Array of menu actions |
322
+ | `disabled` | `boolean` | Disables the menu |
323
+ | `trigger` | `'longPress' \| 'tap'` | How the menu opens (default: `'longPress'`) |
324
+ | `onPressAction` | `(actionId, actionTitle) => void` | Called when user selects an action |
325
+ | `onMenuOpen` | `() => void` | Called when menu opens |
326
+ | `onMenuClose` | `() => void` | Called when menu closes |
327
+ | `children` | `ReactNode` | Content to wrap (required) |
328
+
329
+ ### ContextMenuAction
330
+
331
+ | Property | Type | Description |
332
+ |----------|------|-------------|
333
+ | `id` | `string` | Unique identifier returned in callbacks |
334
+ | `title` | `string` | Display text |
335
+ | `subtitle` | `string` | Secondary text (iOS only) |
336
+ | `image` | `string` | Icon name (SF Symbol on iOS, drawable on Android) |
337
+ | `imageColor` | `string` | Tint color for the icon (hex string) |
338
+ | `attributes` | `{ destructive?, disabled?, hidden? }` | Action attributes |
339
+ | `state` | `'off' \| 'on' \| 'mixed'` | Checkmark state |
340
+ | `subactions` | `ContextMenuAction[]` | Nested actions for submenu |
341
+
342
+ ### iOS Props (`ios`)
343
+
344
+ | Prop | Type | Description |
345
+ |------|------|-------------|
346
+ | `enablePreview` | `boolean` | Enable preview when long-pressing |
347
+
348
+ ### Android Props (`android`)
349
+
350
+ | Prop | Type | Description |
351
+ |------|------|-------------|
352
+ | `anchorPosition` | `'left' \| 'right'` | Anchor position for the popup menu |
353
+ | `visible` | `boolean` | Programmatic visibility control (Android only) |
354
+
355
+ ### Trigger Modes
356
+
357
+ - **Long-Press** (default): Long-press on wrapped content triggers the menu.
358
+ - **Tap** (`trigger="tap"`): Single tap on wrapped content triggers the menu.
359
+ - **Programmatic** (Android only): Use `android.visible` to control menu visibility programmatically. iOS does not support programmatic menu opening due to platform limitations.
360
+
361
+ ### Icon Support
362
+
363
+ - **iOS**: Use SF Symbol names (e.g., `'trash'`, `'square.and.arrow.up'`, `'doc.on.doc'`)
364
+ - **Android**: Use drawable resource names or Material icon names
365
+
366
+ ---
367
+
368
+ ## SelectionMenu
369
+
370
+ Native selection menu with **modal** and **embedded** modes.
371
+
372
+ ### Props
373
+
374
+ | Prop | Type | Description |
375
+ |------|------|-------------|
376
+ | `options` | `{ label: string; data: string }[]` | Array of options to display |
377
+ | `selected` | `string \| null` | Currently selected option's `data` value |
378
+ | `disabled` | `boolean` | Disables the menu |
379
+ | `placeholder` | `string` | Placeholder text when no selection |
380
+ | `presentation` | `'modal' \| 'embedded'` | Presentation mode (default: `'modal'`) |
381
+ | `visible` | `boolean` | Controls modal mode menu visibility |
382
+ | `onSelect` | `(data, label, index) => void` | Called when user selects an option |
383
+ | `onRequestClose` | `() => void` | Called when menu is dismissed without selection |
384
+ | `android.material` | `'system' \| 'm3'` | Material Design style preference |
385
+
386
+ ### Modes
387
+
388
+ - **Modal mode** (default): Menu visibility controlled by `visible` prop. Use for custom trigger UI.
389
+ - **Embedded mode** (`presentation="embedded"`): Native picker UI rendered inline. Menu managed internally.
390
+
391
+ > **Note:** On iOS, modal mode uses a custom popover to enable programmatic presentation. For the full native menu experience (system animations, scroll physics), use embedded mode. This is an intentional trade-off: modal gives you control over the trigger UI, embedded gives you the complete system menu behavior.
392
+
393
+ ---
394
+
262
395
  ## Design Philosophy
263
396
 
264
397
  - **Native first** — no JS re-implementation of pickers
@@ -280,6 +413,85 @@ This is intentional. The goal is native fidelity, not pixel-level customization.
280
413
 
281
414
  ---
282
415
 
416
+ ## Icons
417
+
418
+ ContextMenu supports icons on menu items. Icons are specified by name and resolved differently on each platform.
419
+
420
+ ### iOS
421
+
422
+ Use [SF Symbols](https://developer.apple.com/sf-symbols/) names. These are built into iOS and require no additional setup.
423
+
424
+ ```tsx
425
+ // Common SF Symbols
426
+ image: 'doc.on.doc' // Copy
427
+ image: 'square.and.arrow.up' // Share
428
+ image: 'trash' // Delete
429
+ image: 'pencil' // Edit
430
+ image: 'checkmark.circle' // Checkmark
431
+ ```
432
+
433
+ Browse available symbols using Apple's SF Symbols app or [sfsymbols.com](https://sfsymbols.com).
434
+
435
+ ### Android
436
+
437
+ Use drawable resource names from your app's `res/drawable` directory. You must add these resources yourself.
438
+
439
+ ```tsx
440
+ // Reference drawable by name (without extension)
441
+ image: 'content_copy' // res/drawable/content_copy.xml
442
+ image: 'share' // res/drawable/share.xml
443
+ image: 'delete' // res/drawable/delete.xml
444
+ ```
445
+
446
+ **Adding drawable resources:**
447
+
448
+ 1. Create vector drawable XML files in `android/app/src/main/res/drawable/`
449
+ 2. Use [Material Icons](https://fonts.google.com/icons) as a source — download SVG and convert to Android Vector Drawable
450
+ 3. Name the file to match the `image` prop value (e.g., `content_copy.xml` for `image: 'content_copy'`)
451
+
452
+ Example vector drawable (`res/drawable/content_copy.xml`):
453
+
454
+ ```xml
455
+ <vector xmlns:android="http://schemas.android.com/apk/res/android"
456
+ android:width="24dp"
457
+ android:height="24dp"
458
+ android:viewportWidth="24"
459
+ android:viewportHeight="24">
460
+ <path
461
+ android:fillColor="@android:color/white"
462
+ android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/>
463
+ </vector>
464
+ ```
465
+
466
+ ### Cross-platform pattern
467
+
468
+ Use `Platform.OS` to provide the correct icon name for each platform:
469
+
470
+ ```tsx
471
+ import { Platform } from 'react-native';
472
+
473
+ const actions = [
474
+ {
475
+ id: 'copy',
476
+ title: 'Copy',
477
+ image: Platform.OS === 'ios' ? 'doc.on.doc' : 'content_copy',
478
+ },
479
+ {
480
+ id: 'share',
481
+ title: 'Share',
482
+ image: Platform.OS === 'ios' ? 'square.and.arrow.up' : 'share',
483
+ },
484
+ {
485
+ id: 'delete',
486
+ title: 'Delete',
487
+ image: Platform.OS === 'ios' ? 'trash' : 'delete',
488
+ attributes: { destructive: true },
489
+ },
490
+ ];
491
+ ```
492
+
493
+ ---
494
+
283
495
  ## Contributing
284
496
 
285
497
  See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.