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
@@ -0,0 +1,209 @@
1
+ // ContextMenu.tsx
2
+ import React, { useCallback, useMemo } from 'react';
3
+ import { type ViewProps } from 'react-native';
4
+
5
+ import NativeContextMenu, {
6
+ type ContextMenuAction as NativeAction,
7
+ type ContextMenuSubaction as NativeSubaction,
8
+ type ContextMenuPressActionEvent,
9
+ } from './ContextMenuNativeComponent';
10
+
11
+ /**
12
+ * Attributes for a context menu action.
13
+ */
14
+ export interface ContextMenuActionAttributes {
15
+ /** Whether the action is destructive (red styling) */
16
+ destructive?: boolean;
17
+ /** Whether the action is disabled (grayed out) */
18
+ disabled?: boolean;
19
+ /** Whether the action is hidden */
20
+ hidden?: boolean;
21
+ }
22
+
23
+ /**
24
+ * A single action in the context menu.
25
+ */
26
+ export interface ContextMenuAction {
27
+ /** Unique identifier returned in callbacks */
28
+ id: string;
29
+ /** Display title */
30
+ title: string;
31
+ /** Secondary text (iOS only) */
32
+ subtitle?: string;
33
+ /** Icon name (SF Symbol on iOS, drawable resource on Android) */
34
+ image?: string;
35
+ /** Tint color for the icon (hex string or named color) */
36
+ imageColor?: string;
37
+ /** Action attributes */
38
+ attributes?: ContextMenuActionAttributes;
39
+ /** Checkmark state */
40
+ state?: 'off' | 'on' | 'mixed';
41
+ /** Nested actions for submenu */
42
+ subactions?: readonly ContextMenuAction[];
43
+ }
44
+
45
+ export interface ContextMenuProps extends ViewProps {
46
+ /** Menu title (shown as header on iOS) */
47
+ title?: string;
48
+
49
+ /** Menu actions */
50
+ actions: readonly ContextMenuAction[];
51
+
52
+ /** Disabled state */
53
+ disabled?: boolean;
54
+
55
+ /**
56
+ * How the menu is triggered:
57
+ * - 'longPress' (default): Long-press opens the menu
58
+ * - 'tap': Single tap opens the menu
59
+ */
60
+ trigger?: 'longPress' | 'tap';
61
+
62
+ /**
63
+ * Called when the user presses an action.
64
+ * Receives the action's id and title.
65
+ */
66
+ onPressAction?: (actionId: string, actionTitle: string) => void;
67
+
68
+ /** Called when the menu opens */
69
+ onMenuOpen?: () => void;
70
+
71
+ /** Called when the menu closes */
72
+ onMenuClose?: () => void;
73
+
74
+ /** The content to wrap */
75
+ children: React.ReactNode;
76
+
77
+ /** iOS-specific props */
78
+ ios?: {
79
+ /** Enable preview when long-pressing */
80
+ enablePreview?: boolean;
81
+ };
82
+
83
+ /** Android-specific props */
84
+ android?: {
85
+ /** Anchor position for the popup menu */
86
+ anchorPosition?: 'left' | 'right';
87
+ /**
88
+ * Programmatic visibility control (Android only).
89
+ * Set to true to open the menu, false to close it.
90
+ * Note: iOS does not support programmatic menu opening.
91
+ */
92
+ visible?: boolean;
93
+ };
94
+
95
+ /** Test identifier */
96
+ testID?: string;
97
+ }
98
+
99
+ /**
100
+ * Convert user-friendly subaction to native format (no further nesting).
101
+ */
102
+ function normalizeSubaction(action: ContextMenuAction): NativeSubaction {
103
+ return {
104
+ id: action.id,
105
+ title: action.title,
106
+ subtitle: action.subtitle,
107
+ image: action.image,
108
+ imageColor: action.imageColor,
109
+ attributes: action.attributes
110
+ ? {
111
+ destructive: action.attributes.destructive ? 'true' : 'false',
112
+ disabled: action.attributes.disabled ? 'true' : 'false',
113
+ hidden: action.attributes.hidden ? 'true' : 'false',
114
+ }
115
+ : undefined,
116
+ state: action.state,
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Convert user-friendly action to native format.
122
+ * Note: Only one level of nesting is supported by the native component.
123
+ */
124
+ function normalizeAction(action: ContextMenuAction): NativeAction {
125
+ return {
126
+ id: action.id,
127
+ title: action.title,
128
+ subtitle: action.subtitle,
129
+ image: action.image,
130
+ imageColor: action.imageColor,
131
+ attributes: action.attributes
132
+ ? {
133
+ destructive: action.attributes.destructive ? 'true' : 'false',
134
+ disabled: action.attributes.disabled ? 'true' : 'false',
135
+ hidden: action.attributes.hidden ? 'true' : 'false',
136
+ }
137
+ : undefined,
138
+ state: action.state,
139
+ subactions: action.subactions?.map(normalizeSubaction),
140
+ };
141
+ }
142
+
143
+ export function ContextMenu(props: ContextMenuProps): React.ReactElement {
144
+ const {
145
+ style,
146
+ title,
147
+ actions,
148
+ disabled,
149
+ trigger = 'longPress',
150
+ onPressAction,
151
+ onMenuOpen,
152
+ onMenuClose,
153
+ children,
154
+ ios,
155
+ android,
156
+ ...viewProps
157
+ } = props;
158
+
159
+ const nativeActions = useMemo(() => actions.map(normalizeAction), [actions]);
160
+
161
+ const handlePressAction = useCallback(
162
+ (e: { nativeEvent: ContextMenuPressActionEvent }) => {
163
+ const { actionId, actionTitle } = e.nativeEvent;
164
+ onPressAction?.(actionId, actionTitle);
165
+ },
166
+ [onPressAction]
167
+ );
168
+
169
+ const handleMenuOpen = useCallback(() => {
170
+ onMenuOpen?.();
171
+ }, [onMenuOpen]);
172
+
173
+ const handleMenuClose = useCallback(() => {
174
+ onMenuClose?.();
175
+ }, [onMenuClose]);
176
+
177
+ const nativeIOS = useMemo(() => {
178
+ if (!ios) return undefined;
179
+ return {
180
+ enablePreview: ios.enablePreview ? 'true' : 'false',
181
+ };
182
+ }, [ios]);
183
+
184
+ const nativeAndroid = useMemo(() => {
185
+ if (!android) return undefined;
186
+ return {
187
+ anchorPosition: android.anchorPosition,
188
+ visible: android.visible ? 'open' : 'closed',
189
+ };
190
+ }, [android]);
191
+
192
+ return (
193
+ <NativeContextMenu
194
+ style={style}
195
+ title={title}
196
+ actions={nativeActions}
197
+ interactivity={disabled ? 'disabled' : 'enabled'}
198
+ trigger={trigger}
199
+ onPressAction={onPressAction ? handlePressAction : undefined}
200
+ onMenuOpen={onMenuOpen ? handleMenuOpen : undefined}
201
+ onMenuClose={onMenuClose ? handleMenuClose : undefined}
202
+ ios={nativeIOS}
203
+ android={nativeAndroid}
204
+ {...viewProps}
205
+ >
206
+ {children}
207
+ </NativeContextMenu>
208
+ );
209
+ }
@@ -0,0 +1,141 @@
1
+ // ContextMenuNativeComponent.ts
2
+ import type { CodegenTypes, HostComponent, ViewProps } from 'react-native';
3
+ import { codegenNativeComponent } from 'react-native';
4
+
5
+ /**
6
+ * Attributes for a context menu action.
7
+ */
8
+ export type ContextMenuActionAttributes = Readonly<{
9
+ /** Whether the action is destructive (red styling) */
10
+ destructive?: string; // 'true' | 'false'
11
+ /** Whether the action is disabled (grayed out) */
12
+ disabled?: string; // 'true' | 'false'
13
+ /** Whether the action is hidden */
14
+ hidden?: string; // 'true' | 'false'
15
+ }>;
16
+
17
+ /**
18
+ * A leaf subaction (no further nesting to avoid codegen recursion issues).
19
+ */
20
+ export type ContextMenuSubaction = Readonly<{
21
+ /** Unique identifier returned in callbacks */
22
+ id: string;
23
+ /** Display title */
24
+ title: string;
25
+ /** Secondary text (iOS only) */
26
+ subtitle?: string;
27
+ /** Icon name (SF Symbol on iOS, drawable resource on Android) */
28
+ image?: string;
29
+ /** Tint color for the icon (hex string, e.g., "#FF0000") */
30
+ imageColor?: string;
31
+ /** Action attributes */
32
+ attributes?: ContextMenuActionAttributes;
33
+ /** Checkmark state: 'off' | 'on' | 'mixed' */
34
+ state?: string;
35
+ }>;
36
+
37
+ /**
38
+ * A single action in the context menu.
39
+ * Actions can be nested one level via `subactions` for submenus.
40
+ */
41
+ export type ContextMenuAction = Readonly<{
42
+ /** Unique identifier returned in callbacks */
43
+ id: string;
44
+ /** Display title */
45
+ title: string;
46
+ /** Secondary text (iOS only) */
47
+ subtitle?: string;
48
+ /** Icon name (SF Symbol on iOS, drawable resource on Android) */
49
+ image?: string;
50
+ /** Tint color for the icon (hex string, e.g., "#FF0000") */
51
+ imageColor?: string;
52
+ /** Action attributes */
53
+ attributes?: ContextMenuActionAttributes;
54
+ /** Checkmark state: 'off' | 'on' | 'mixed' */
55
+ state?: string;
56
+ /** Nested actions for submenu (one level deep) */
57
+ subactions?: ReadonlyArray<ContextMenuSubaction>;
58
+ }>;
59
+
60
+ /**
61
+ * Event emitted when an action is pressed.
62
+ */
63
+ export type ContextMenuPressActionEvent = Readonly<{
64
+ /** The action's unique identifier */
65
+ actionId: string;
66
+ /** The action's title */
67
+ actionTitle: string;
68
+ }>;
69
+
70
+ /** Interactivity state (no booleans for codegen). */
71
+ export type ContextMenuInteractivity = 'enabled' | 'disabled';
72
+
73
+ /** Trigger mode for opening the menu. */
74
+ export type ContextMenuTrigger = 'longPress' | 'tap';
75
+
76
+ /**
77
+ * iOS-specific configuration.
78
+ */
79
+ export type IOSProps = Readonly<{
80
+ /** Enable preview when long-pressing */
81
+ enablePreview?: string; // 'true' | 'false'
82
+ }>;
83
+
84
+ /**
85
+ * Android-specific configuration.
86
+ */
87
+ export type AndroidProps = Readonly<{
88
+ /** Anchor position for the popup menu */
89
+ anchorPosition?: string; // 'left' | 'right'
90
+ /**
91
+ * Programmatic visibility control (Android only).
92
+ * 'open' to show the menu, 'closed' to hide it.
93
+ */
94
+ visible?: string; // 'open' | 'closed'
95
+ }>;
96
+
97
+ export interface ContextMenuProps extends ViewProps {
98
+ /**
99
+ * Menu title (shown as header on iOS).
100
+ */
101
+ title?: string;
102
+
103
+ /**
104
+ * Menu actions.
105
+ */
106
+ actions: ReadonlyArray<ContextMenuAction>;
107
+
108
+ /**
109
+ * Enabled / disabled state.
110
+ */
111
+ interactivity?: string; // ContextMenuInteractivity
112
+
113
+ /**
114
+ * How the menu is triggered:
115
+ * - 'longPress' (default): Long-press opens the menu
116
+ * - 'tap': Single tap opens the menu
117
+ */
118
+ trigger?: string; // ContextMenuTrigger
119
+
120
+ /**
121
+ * Fired when user presses an action.
122
+ */
123
+ onPressAction?: CodegenTypes.BubblingEventHandler<ContextMenuPressActionEvent>;
124
+
125
+ /**
126
+ * Fired when menu opens.
127
+ */
128
+ onMenuOpen?: CodegenTypes.BubblingEventHandler<Readonly<{}>>;
129
+
130
+ /**
131
+ * Fired when menu closes.
132
+ */
133
+ onMenuClose?: CodegenTypes.BubblingEventHandler<Readonly<{}>>;
134
+
135
+ ios?: IOSProps;
136
+ android?: AndroidProps;
137
+ }
138
+
139
+ export default codegenNativeComponent<ContextMenuProps>(
140
+ 'PCContextMenu'
141
+ ) as HostComponent<ContextMenuProps>;
@@ -7,7 +7,7 @@ import NativeSelectionMenu, {
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(
@@ -123,7 +124,7 @@ export function SelectionMenu(props: SelectionMenuProps): React.ReactElement {
123
124
  selectedData={selectedData}
124
125
  interactivity={disabled ? 'disabled' : 'enabled'}
125
126
  placeholder={placeholder}
126
- anchorMode={inlineMode ? 'inline' : 'headless'}
127
+ anchorMode={presentation === 'embedded' ? 'inline' : 'headless'}
127
128
  visible={nativeVisible}
128
129
  onSelect={onSelect ? handleSelect : undefined}
129
130
  onRequestClose={onRequestClose ? handleRequestClose : undefined}
package/src/index.tsx CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './DatePicker';
2
2
  export * from './SelectionMenu';
3
+ export * from './ContextMenu';
3
4
  export * from './sharedTypes';
@@ -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