react-native-platform-components 1.5.0 → 1.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.
- package/README.md +19 -18
- package/android/src/main/java/com/platformcomponents/PCActionSheetView.kt +309 -0
- package/android/src/main/java/com/platformcomponents/PCActionSheetViewManager.kt +131 -0
- package/android/src/main/java/com/platformcomponents/PCButtonGroupView.kt +5 -1
- package/android/src/main/java/com/platformcomponents/PCButtonView.kt +5 -1
- package/android/src/main/java/com/platformcomponents/PCContextMenuView.kt +82 -27
- package/android/src/main/java/com/platformcomponents/PCContextMenuViewManager.kt +12 -13
- package/android/src/main/java/com/platformcomponents/PCMenuSupport.kt +66 -2
- package/android/src/main/java/com/platformcomponents/PlatformComponentsPackage.kt +1 -0
- package/android/src/main/res/values/ids.xml +2 -0
- package/ios/PCActionSheet.h +12 -0
- package/ios/PCActionSheet.mm +160 -0
- package/ios/PCActionSheetView.swift +280 -0
- package/ios/PCContextMenu.mm +22 -1
- package/ios/PCContextMenu.swift +218 -15
- package/ios/PCMenuItems.h +2 -1
- package/ios/PCMenuSupport.swift +22 -3
- package/lib/commonjs/ActionSheet.js +85 -0
- package/lib/commonjs/ActionSheet.js.map +1 -0
- package/lib/commonjs/ActionSheetNativeComponent.ts +94 -0
- package/lib/commonjs/ButtonGroupNativeComponent.ts +2 -0
- package/lib/commonjs/ButtonNativeComponent.ts +2 -0
- package/lib/commonjs/ContextMenu.js +50 -4
- package/lib/commonjs/ContextMenu.js.map +1 -1
- package/lib/commonjs/ContextMenuNativeComponent.ts +29 -8
- package/lib/commonjs/index.js +11 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +7 -0
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/jest.js +92 -5
- package/lib/commonjs/jest.js.map +1 -1
- package/lib/commonjs/menuItems.js +2 -1
- package/lib/commonjs/menuItems.js.map +1 -1
- package/lib/commonjs/selectionIdentifiers.js.map +1 -1
- package/lib/commonjs/web/ActionSheet.js +131 -0
- package/lib/commonjs/web/ActionSheet.js.map +1 -0
- package/lib/commonjs/web/components.js +3 -1
- package/lib/commonjs/web/components.js.map +1 -1
- package/lib/commonjs/web/containers.js +1 -0
- package/lib/commonjs/web/containers.js.map +1 -1
- package/lib/module/ActionSheet.js +78 -0
- package/lib/module/ActionSheet.js.map +1 -0
- package/lib/module/ActionSheetNativeComponent.ts +94 -0
- package/lib/module/ButtonGroupNativeComponent.ts +2 -0
- package/lib/module/ButtonNativeComponent.ts +2 -0
- package/lib/module/ContextMenu.js +51 -4
- package/lib/module/ContextMenu.js.map +1 -1
- package/lib/module/ContextMenuNativeComponent.ts +29 -8
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +1 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/jest.js +90 -5
- package/lib/module/jest.js.map +1 -1
- package/lib/module/menuItems.js +2 -1
- package/lib/module/menuItems.js.map +1 -1
- package/lib/module/selectionIdentifiers.js.map +1 -1
- package/lib/module/web/ActionSheet.js +126 -0
- package/lib/module/web/ActionSheet.js.map +1 -0
- package/lib/module/web/components.js +2 -0
- package/lib/module/web/components.js.map +1 -1
- package/lib/module/web/containers.js +1 -0
- package/lib/module/web/containers.js.map +1 -1
- package/lib/typescript/commonjs/docs/sidebars.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/ActionSheet.d.ts +98 -0
- package/lib/typescript/commonjs/src/ActionSheet.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/ActionSheetNativeComponent.d.ts +78 -0
- package/lib/typescript/commonjs/src/ActionSheetNativeComponent.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/ButtonGroupNativeComponent.d.ts +2 -0
- package/lib/typescript/commonjs/src/ButtonGroupNativeComponent.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/ButtonNativeComponent.d.ts +2 -0
- package/lib/typescript/commonjs/src/ButtonNativeComponent.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/ContextMenu.d.ts +22 -6
- package/lib/typescript/commonjs/src/ContextMenu.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/ContextMenuNativeComponent.d.ts +20 -7
- package/lib/typescript/commonjs/src/ContextMenuNativeComponent.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/index.d.ts +1 -0
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/index.web.d.ts +2 -1
- package/lib/typescript/commonjs/src/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/jest.d.ts +17 -3
- package/lib/typescript/commonjs/src/jest.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/menuItems.d.ts +12 -0
- package/lib/typescript/commonjs/src/menuItems.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/selectionIdentifiers.d.ts +1 -1
- package/lib/typescript/commonjs/src/selectionIdentifiers.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/web/ActionSheet.d.ts +11 -0
- package/lib/typescript/commonjs/src/web/ActionSheet.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/components.d.ts +1 -0
- package/lib/typescript/commonjs/src/web/components.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/web/containers.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/webComponents.d.ts +2 -0
- package/lib/typescript/commonjs/src/webComponents.d.ts.map +1 -1
- package/lib/typescript/module/docs/sidebars.d.ts.map +1 -1
- package/lib/typescript/module/src/ActionSheet.d.ts +98 -0
- package/lib/typescript/module/src/ActionSheet.d.ts.map +1 -0
- package/lib/typescript/module/src/ActionSheetNativeComponent.d.ts +78 -0
- package/lib/typescript/module/src/ActionSheetNativeComponent.d.ts.map +1 -0
- package/lib/typescript/module/src/ButtonGroupNativeComponent.d.ts +2 -0
- package/lib/typescript/module/src/ButtonGroupNativeComponent.d.ts.map +1 -1
- package/lib/typescript/module/src/ButtonNativeComponent.d.ts +2 -0
- package/lib/typescript/module/src/ButtonNativeComponent.d.ts.map +1 -1
- package/lib/typescript/module/src/ContextMenu.d.ts +22 -6
- package/lib/typescript/module/src/ContextMenu.d.ts.map +1 -1
- package/lib/typescript/module/src/ContextMenuNativeComponent.d.ts +20 -7
- package/lib/typescript/module/src/ContextMenuNativeComponent.d.ts.map +1 -1
- package/lib/typescript/module/src/index.d.ts +1 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/index.web.d.ts +2 -1
- package/lib/typescript/module/src/index.web.d.ts.map +1 -1
- package/lib/typescript/module/src/jest.d.ts +17 -3
- package/lib/typescript/module/src/jest.d.ts.map +1 -1
- package/lib/typescript/module/src/menuItems.d.ts +12 -0
- package/lib/typescript/module/src/menuItems.d.ts.map +1 -1
- package/lib/typescript/module/src/selectionIdentifiers.d.ts +1 -1
- package/lib/typescript/module/src/selectionIdentifiers.d.ts.map +1 -1
- package/lib/typescript/module/src/web/ActionSheet.d.ts +11 -0
- package/lib/typescript/module/src/web/ActionSheet.d.ts.map +1 -0
- package/lib/typescript/module/src/web/components.d.ts +1 -0
- package/lib/typescript/module/src/web/components.d.ts.map +1 -1
- package/lib/typescript/module/src/web/containers.d.ts.map +1 -1
- package/lib/typescript/module/src/webComponents.d.ts +2 -0
- package/lib/typescript/module/src/webComponents.d.ts.map +1 -1
- package/package.json +9 -2
- package/plugin/build/index.cjs +79 -6
- package/plugin/build/index.d.ts +24 -0
- package/plugin/build/index.d.ts.map +1 -1
- package/plugin/tsconfig.json +2 -1
- package/src/ActionSheet.tsx +193 -0
- package/src/ActionSheetNativeComponent.ts +94 -0
- package/src/ButtonGroupNativeComponent.ts +2 -0
- package/src/ButtonNativeComponent.ts +2 -0
- package/src/ContextMenu.tsx +71 -12
- package/src/ContextMenuNativeComponent.ts +29 -8
- package/src/index.tsx +1 -0
- package/src/index.web.tsx +2 -0
- package/src/jest.tsx +108 -4
- package/src/menuItems.ts +13 -0
- package/src/selectionIdentifiers.ts +1 -1
- package/src/web/ActionSheet.tsx +130 -0
- package/src/web/components.tsx +5 -0
- package/src/web/containers.tsx +1 -0
- package/src/webComponents.ts +2 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// ActionSheetNativeComponent.ts
|
|
2
|
+
import type { HostComponent, ViewProps } from 'react-native';
|
|
3
|
+
import { codegenNativeComponent } from 'react-native';
|
|
4
|
+
import type { BubblingEventHandler, Double } from './codegenTypes';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One action of the sheet. The list is flat (no submenus or sections) and
|
|
8
|
+
* hidden actions are already dropped. Every field is set; empty strings mean
|
|
9
|
+
* "none" and flags are 'true' | 'false'.
|
|
10
|
+
*/
|
|
11
|
+
export type ActionSheetItem = Readonly<{
|
|
12
|
+
/** Unique identifier returned by onSelect */
|
|
13
|
+
id: string;
|
|
14
|
+
title: string;
|
|
15
|
+
/** '' | 'sfSymbol' | 'drawable' | 'image' (Android draws icons; iOS has no public icon API) */
|
|
16
|
+
iconType: string;
|
|
17
|
+
/** SF Symbol / asset name (iOS) or drawable resource name (Android) */
|
|
18
|
+
iconName: string;
|
|
19
|
+
/** Resolved image URI when iconType === 'image' */
|
|
20
|
+
iconRequest: string;
|
|
21
|
+
iconUri: string;
|
|
22
|
+
/** Resolved image scale when iconType === 'image' */
|
|
23
|
+
iconScale: Double;
|
|
24
|
+
/** 'true' | 'false': draw the image as a tinted template */
|
|
25
|
+
iconTinted: string;
|
|
26
|
+
/** Tint color for the icon (hex string, e.g., "#FF0000") */
|
|
27
|
+
imageColor: string;
|
|
28
|
+
/** 'true' | 'false' */
|
|
29
|
+
destructive: string;
|
|
30
|
+
/** 'true' | 'false' */
|
|
31
|
+
disabled: string;
|
|
32
|
+
/** The action's own haptics; '' = the component's */
|
|
33
|
+
haptics: string;
|
|
34
|
+
/** accessibilityIdentifier (iOS) / view tag (Android) of the row */
|
|
35
|
+
testID: string;
|
|
36
|
+
}>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Event emitted when the user picks an action.
|
|
40
|
+
*/
|
|
41
|
+
export type ActionSheetSelectEvent = Readonly<{
|
|
42
|
+
/** The action's unique identifier */
|
|
43
|
+
actionId: string;
|
|
44
|
+
}>;
|
|
45
|
+
|
|
46
|
+
/** Visibility state (no booleans for codegen). */
|
|
47
|
+
export type ActionSheetVisible = 'open' | 'closed';
|
|
48
|
+
|
|
49
|
+
export interface ActionSheetProps extends ViewProps {
|
|
50
|
+
/**
|
|
51
|
+
* 'open' presents the sheet, 'closed' dismisses it.
|
|
52
|
+
*/
|
|
53
|
+
visible?: string; // ActionSheetVisible
|
|
54
|
+
|
|
55
|
+
/** Sheet title; '' = none */
|
|
56
|
+
title?: string;
|
|
57
|
+
|
|
58
|
+
/** Sheet message under the title; '' = none */
|
|
59
|
+
message?: string;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The actions, flat.
|
|
63
|
+
*/
|
|
64
|
+
actions: ReadonlyArray<ActionSheetItem>;
|
|
65
|
+
|
|
66
|
+
/** Title of the iOS cancel action; '' = the localized system "Cancel" */
|
|
67
|
+
cancelTitle?: string;
|
|
68
|
+
|
|
69
|
+
/** accessibilityIdentifier of the iOS cancel action */
|
|
70
|
+
cancelTestID?: string;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Haptic played when an action is picked: '' (none added) | 'none' | 'selection' |
|
|
74
|
+
* 'light' | 'medium' | 'heavy' | 'success' | 'warning' | 'error'
|
|
75
|
+
*/
|
|
76
|
+
haptics?: string;
|
|
77
|
+
|
|
78
|
+
/** Android: largest font scale the sheet's text may reach; 0 = no cap. */
|
|
79
|
+
maxFontSizeMultiplier?: Double;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Fired when the user picks an action.
|
|
83
|
+
*/
|
|
84
|
+
onSelect?: BubblingEventHandler<ActionSheetSelectEvent>;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Fired when the sheet is dismissed without a pick.
|
|
88
|
+
*/
|
|
89
|
+
onCancel?: BubblingEventHandler<Readonly<{}>>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export default codegenNativeComponent<ActionSheetProps>(
|
|
93
|
+
'PCActionSheet'
|
|
94
|
+
) as HostComponent<ActionSheetProps>;
|
|
@@ -75,6 +75,8 @@ export type ButtonGroupMenuItem = Readonly<{
|
|
|
75
75
|
state: string;
|
|
76
76
|
/** The action's own haptics; '' = the component's */
|
|
77
77
|
haptics: string;
|
|
78
|
+
/** Test identifier of the menu row; '' = none */
|
|
79
|
+
testID: string;
|
|
78
80
|
}>;
|
|
79
81
|
|
|
80
82
|
/** Event emitted when a split button menu item is picked. */
|
|
@@ -59,6 +59,8 @@ export type ButtonMenuItem = Readonly<{
|
|
|
59
59
|
state: string;
|
|
60
60
|
/** The action's own haptics; '' = the component's */
|
|
61
61
|
haptics: string;
|
|
62
|
+
/** Test identifier of the menu row; '' = none */
|
|
63
|
+
testID: string;
|
|
62
64
|
}>;
|
|
63
65
|
|
|
64
66
|
/** Event emitted when a toggle button is pressed. */
|
package/src/ContextMenu.tsx
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
// ContextMenu.tsx
|
|
2
|
-
import React, { useCallback, useMemo } from 'react';
|
|
3
|
-
import { type ViewProps } from 'react-native';
|
|
2
|
+
import React, { useCallback, useMemo, useRef } from 'react';
|
|
3
|
+
import { type GestureResponderEvent, type ViewProps } from 'react-native';
|
|
4
4
|
|
|
5
5
|
import NativeContextMenu, {
|
|
6
|
+
Commands,
|
|
6
7
|
type ContextMenuPressActionEvent,
|
|
7
8
|
} from './ContextMenuNativeComponent';
|
|
8
9
|
import { flattenMenuActions, type ContextMenuAction } from './menuItems';
|
|
9
10
|
import type { Haptics } from './haptics';
|
|
10
11
|
|
|
12
|
+
/** How far (in points) a tap may move before it no longer opens the menu. */
|
|
13
|
+
const TAP_SLOP = 10;
|
|
14
|
+
|
|
11
15
|
export type {
|
|
12
16
|
ContextMenuAction,
|
|
13
17
|
ContextMenuActionAttributes,
|
|
@@ -25,10 +29,28 @@ export interface ContextMenuProps extends ViewProps {
|
|
|
25
29
|
|
|
26
30
|
/**
|
|
27
31
|
* How the menu is triggered:
|
|
28
|
-
* - 'longPress' (default): Long-press opens the menu
|
|
29
|
-
*
|
|
32
|
+
* - 'longPress' (default): Long-press opens the menu, also on a button
|
|
33
|
+
* inside the content; taps still reach the content's buttons
|
|
34
|
+
* - 'tap': Single tap opens the menu. A tap on a pressable inside the
|
|
35
|
+
* content goes to that pressable instead
|
|
36
|
+
* - 'none': Only `visible` opens the menu. The wrapped content keeps all
|
|
37
|
+
* its touches, so it can open the menu from one of its own buttons.
|
|
38
|
+
*/
|
|
39
|
+
trigger?: 'longPress' | 'tap' | 'none';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Opens the menu from JS, anchored to the wrapped content. Controlled:
|
|
43
|
+
* set it to true to open the menu, and back to false in `onMenuClose`,
|
|
44
|
+
* which fires however the menu closes (an action, a tap outside, or
|
|
45
|
+
* `visible` turning false). False dismisses an open menu. Leave it unset
|
|
46
|
+
* to leave the menu to the user; menus the user opens still call
|
|
47
|
+
* `onMenuOpen` and `onMenuClose`.
|
|
48
|
+
*
|
|
49
|
+
* iOS 17.4+ (`UIControl.performPrimaryAction()` on an invisible anchor
|
|
50
|
+
* button); earlier iOS has no public API to open a menu without a touch,
|
|
51
|
+
* so `visible` does nothing there.
|
|
30
52
|
*/
|
|
31
|
-
|
|
53
|
+
visible?: boolean;
|
|
32
54
|
|
|
33
55
|
/**
|
|
34
56
|
* Haptic played when the user presses an action. Opening the menu keeps
|
|
@@ -74,9 +96,8 @@ export interface ContextMenuProps extends ViewProps {
|
|
|
74
96
|
*/
|
|
75
97
|
anchorPosition?: 'left' | 'right';
|
|
76
98
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* Note: iOS does not support programmatic menu opening.
|
|
99
|
+
* @deprecated Use the top-level `visible`, which also works on iOS.
|
|
100
|
+
* When both are set, the top-level one wins.
|
|
80
101
|
*/
|
|
81
102
|
visible?: boolean;
|
|
82
103
|
};
|
|
@@ -92,6 +113,7 @@ export function ContextMenu(props: ContextMenuProps): React.ReactElement {
|
|
|
92
113
|
actions,
|
|
93
114
|
disabled,
|
|
94
115
|
trigger = 'longPress',
|
|
116
|
+
visible,
|
|
95
117
|
haptics,
|
|
96
118
|
onPressAction,
|
|
97
119
|
onMenuOpen,
|
|
@@ -105,6 +127,40 @@ export function ContextMenu(props: ContextMenuProps): React.ReactElement {
|
|
|
105
127
|
|
|
106
128
|
const nativeActions = useMemo(() => flattenMenuActions(actions), [actions]);
|
|
107
129
|
|
|
130
|
+
const nativeRef = useRef<React.ComponentRef<typeof NativeContextMenu>>(null);
|
|
131
|
+
// Where the current tap went down; null once it moved too far
|
|
132
|
+
const tapStart = useRef<{ x: number; y: number } | null>(null);
|
|
133
|
+
|
|
134
|
+
// trigger 'tap': React Native gives a touch to the deepest view that asks
|
|
135
|
+
// for it, so the content's own pressables keep their taps. A tap none of
|
|
136
|
+
// them took bubbles up to this view, and opens the menu.
|
|
137
|
+
const tapHandlers = useMemo(() => {
|
|
138
|
+
if (trigger !== 'tap' || disabled) return undefined;
|
|
139
|
+
return {
|
|
140
|
+
onStartShouldSetResponder: () => true,
|
|
141
|
+
onResponderGrant: (e: GestureResponderEvent) => {
|
|
142
|
+
tapStart.current = { x: e.nativeEvent.pageX, y: e.nativeEvent.pageY };
|
|
143
|
+
},
|
|
144
|
+
onResponderMove: (e: GestureResponderEvent) => {
|
|
145
|
+
const start = tapStart.current;
|
|
146
|
+
if (!start) return;
|
|
147
|
+
const { pageX, pageY } = e.nativeEvent;
|
|
148
|
+
if (Math.hypot(pageX - start.x, pageY - start.y) > TAP_SLOP) {
|
|
149
|
+
tapStart.current = null;
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
onResponderRelease: () => {
|
|
153
|
+
const node = nativeRef.current;
|
|
154
|
+
if (tapStart.current && node != null) Commands.handleTap(node);
|
|
155
|
+
tapStart.current = null;
|
|
156
|
+
},
|
|
157
|
+
// A scroll view took the touch over
|
|
158
|
+
onResponderTerminate: () => {
|
|
159
|
+
tapStart.current = null;
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}, [trigger, disabled]);
|
|
163
|
+
|
|
108
164
|
const handlePressAction = useCallback(
|
|
109
165
|
(e: { nativeEvent: ContextMenuPressActionEvent }) => {
|
|
110
166
|
const { actionId, actionTitle } = e.nativeEvent;
|
|
@@ -134,19 +190,22 @@ export function ContextMenu(props: ContextMenuProps): React.ReactElement {
|
|
|
134
190
|
|
|
135
191
|
const nativeAndroid = useMemo(() => {
|
|
136
192
|
if (!android) return undefined;
|
|
137
|
-
return {
|
|
138
|
-
anchorPosition: android.anchorPosition,
|
|
139
|
-
visible: android.visible ? 'open' : 'closed',
|
|
140
|
-
};
|
|
193
|
+
return { anchorPosition: android.anchorPosition };
|
|
141
194
|
}, [android]);
|
|
142
195
|
|
|
196
|
+
// The deprecated android.visible still works, under the top-level prop
|
|
197
|
+
const open = visible ?? android?.visible;
|
|
198
|
+
|
|
143
199
|
return (
|
|
144
200
|
<NativeContextMenu
|
|
201
|
+
ref={nativeRef}
|
|
202
|
+
{...tapHandlers}
|
|
145
203
|
style={style}
|
|
146
204
|
title={title}
|
|
147
205
|
actions={nativeActions}
|
|
148
206
|
interactivity={disabled ? 'disabled' : 'enabled'}
|
|
149
207
|
trigger={trigger}
|
|
208
|
+
visible={open === undefined ? undefined : open ? 'open' : 'closed'}
|
|
150
209
|
haptics={haptics ?? ''}
|
|
151
210
|
onPressAction={onPressAction ? handlePressAction : undefined}
|
|
152
211
|
onMenuOpen={onMenuOpen ? handleMenuOpen : undefined}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// ContextMenuNativeComponent.ts
|
|
2
|
+
import type * as React from 'react';
|
|
2
3
|
import type { HostComponent, ViewProps } from 'react-native';
|
|
3
|
-
import { codegenNativeComponent } from 'react-native';
|
|
4
|
+
import { codegenNativeCommands, codegenNativeComponent } from 'react-native';
|
|
4
5
|
import type { BubblingEventHandler, Double, Int32 } from './codegenTypes';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -43,6 +44,8 @@ export type ContextMenuItem = Readonly<{
|
|
|
43
44
|
state: string;
|
|
44
45
|
/** The action's own haptics; '' = the component's */
|
|
45
46
|
haptics: string;
|
|
47
|
+
/** Test identifier of the menu row; '' = none */
|
|
48
|
+
testID: string;
|
|
46
49
|
}>;
|
|
47
50
|
|
|
48
51
|
/**
|
|
@@ -59,7 +62,7 @@ export type ContextMenuPressActionEvent = Readonly<{
|
|
|
59
62
|
export type ContextMenuInteractivity = 'enabled' | 'disabled';
|
|
60
63
|
|
|
61
64
|
/** Trigger mode for opening the menu. */
|
|
62
|
-
export type ContextMenuTrigger = 'longPress' | 'tap';
|
|
65
|
+
export type ContextMenuTrigger = 'longPress' | 'tap' | 'none';
|
|
63
66
|
|
|
64
67
|
/**
|
|
65
68
|
* iOS-specific configuration.
|
|
@@ -75,11 +78,6 @@ export type IOSProps = Readonly<{
|
|
|
75
78
|
export type AndroidProps = Readonly<{
|
|
76
79
|
/** Anchor position for the popup menu */
|
|
77
80
|
anchorPosition?: string; // 'left' | 'right'
|
|
78
|
-
/**
|
|
79
|
-
* Programmatic visibility control (Android only).
|
|
80
|
-
* 'open' to show the menu, 'closed' to hide it.
|
|
81
|
-
*/
|
|
82
|
-
visible?: string; // 'open' | 'closed'
|
|
83
81
|
}>;
|
|
84
82
|
|
|
85
83
|
export interface ContextMenuProps extends ViewProps {
|
|
@@ -102,9 +100,16 @@ export interface ContextMenuProps extends ViewProps {
|
|
|
102
100
|
* How the menu is triggered:
|
|
103
101
|
* - 'longPress' (default): Long-press opens the menu
|
|
104
102
|
* - 'tap': Single tap opens the menu
|
|
103
|
+
* - 'none': only `visible` opens it; the content keeps its touches
|
|
105
104
|
*/
|
|
106
105
|
trigger?: string; // ContextMenuTrigger
|
|
107
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Programmatic visibility: 'open' presents the menu anchored to the view,
|
|
109
|
+
* 'closed' dismisses it. '' (unset) leaves the menu to the user.
|
|
110
|
+
*/
|
|
111
|
+
visible?: string; // '' | 'open' | 'closed'
|
|
112
|
+
|
|
108
113
|
/**
|
|
109
114
|
* Haptic played when an action is pressed: '' (none added) | 'none' | 'selection' |
|
|
110
115
|
* 'light' | 'medium' | 'heavy' | 'success' | 'warning' | 'error'
|
|
@@ -135,6 +140,22 @@ export interface ContextMenuProps extends ViewProps {
|
|
|
135
140
|
android?: AndroidProps;
|
|
136
141
|
}
|
|
137
142
|
|
|
143
|
+
type NativeContextMenuComponent = HostComponent<ContextMenuProps>;
|
|
144
|
+
|
|
145
|
+
// `React.ElementRef` rather than `React.ComponentRef`: the codegen shipped with
|
|
146
|
+
// React Native 0.81 (and the 0.76 Android floor) only accepts the former.
|
|
147
|
+
interface NativeCommands {
|
|
148
|
+
/**
|
|
149
|
+
* trigger 'tap': the user tapped the content and no inner pressable took
|
|
150
|
+
* the tap. Opens the menu (iOS 17.4+; earlier iOS opens it natively).
|
|
151
|
+
*/
|
|
152
|
+
handleTap: (viewRef: React.ElementRef<NativeContextMenuComponent>) => void;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
|
|
156
|
+
supportedCommands: ['handleTap'],
|
|
157
|
+
});
|
|
158
|
+
|
|
138
159
|
export default codegenNativeComponent<ContextMenuProps>(
|
|
139
160
|
'PCContextMenu'
|
|
140
|
-
) as
|
|
161
|
+
) as NativeContextMenuComponent;
|
package/src/index.tsx
CHANGED
package/src/index.web.tsx
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
export type * from './DatePicker';
|
|
9
9
|
export type * from './SelectionMenu';
|
|
10
10
|
export type * from './ContextMenu';
|
|
11
|
+
export type * from './ActionSheet';
|
|
11
12
|
export type * from './SegmentedControl';
|
|
12
13
|
export type * from './TabBar';
|
|
13
14
|
export type * from './FloatingActionButton';
|
|
@@ -33,6 +34,7 @@ export type {
|
|
|
33
34
|
} from './webComponents';
|
|
34
35
|
export { PlatformComponentsProvider } from './web/PlatformComponentsProvider';
|
|
35
36
|
export {
|
|
37
|
+
ActionSheet,
|
|
36
38
|
Button,
|
|
37
39
|
ButtonGroup,
|
|
38
40
|
SplitButton,
|
package/src/jest.tsx
CHANGED
|
@@ -27,9 +27,10 @@ import {
|
|
|
27
27
|
type ViewProps,
|
|
28
28
|
} from 'react-native';
|
|
29
29
|
|
|
30
|
+
import type { ActionSheetProps } from './ActionSheet';
|
|
30
31
|
import type { ButtonProps } from './Button';
|
|
31
32
|
import type { ButtonGroupProps } from './ButtonGroup';
|
|
32
|
-
import type { ContextMenuProps } from './ContextMenu';
|
|
33
|
+
import type { ContextMenuAction, ContextMenuProps } from './ContextMenu';
|
|
33
34
|
import type { DatePickerProps, DateRangePickerProps } from './DatePicker';
|
|
34
35
|
import type { FloatingActionButtonProps } from './FloatingActionButton';
|
|
35
36
|
import type { FloatingToolbarProps } from './FloatingToolbar';
|
|
@@ -54,6 +55,7 @@ import { nextSelection } from './web/ButtonGroup';
|
|
|
54
55
|
export type * from './DatePicker';
|
|
55
56
|
export type * from './SelectionMenu';
|
|
56
57
|
export type * from './ContextMenu';
|
|
58
|
+
export type * from './ActionSheet';
|
|
57
59
|
export type * from './Button';
|
|
58
60
|
export type * from './ButtonGroup';
|
|
59
61
|
export type * from './SplitButton';
|
|
@@ -845,9 +847,12 @@ export function SelectionMenu(props: SelectionMenuProps): React.ReactElement {
|
|
|
845
847
|
}
|
|
846
848
|
|
|
847
849
|
/**
|
|
848
|
-
* Its children in a view carrying `testID`.
|
|
849
|
-
* `
|
|
850
|
-
*
|
|
850
|
+
* Its children in a view carrying `testID`. While `visible` (or the
|
|
851
|
+
* deprecated `android.visible`) is true, each action follows them as a
|
|
852
|
+
* `Pressable` with the `menuitem` role and the action's `testID` (else
|
|
853
|
+
* `${testID}-${id}`) that calls `onPressAction(id, title)`; submenus and
|
|
854
|
+
* sections list their actions. `fireEvent(menu, 'pressAction', id, title)`,
|
|
855
|
+
* `'menuOpen'`, `'menuClose'` and `'previewPress'` reach the callbacks.
|
|
851
856
|
*/
|
|
852
857
|
export function ContextMenu(props: ContextMenuProps): React.ReactElement {
|
|
853
858
|
const {
|
|
@@ -860,18 +865,117 @@ export function ContextMenu(props: ContextMenuProps): React.ReactElement {
|
|
|
860
865
|
actions,
|
|
861
866
|
disabled,
|
|
862
867
|
trigger,
|
|
868
|
+
visible,
|
|
863
869
|
haptics,
|
|
864
870
|
ios,
|
|
865
871
|
android,
|
|
872
|
+
testID,
|
|
866
873
|
...viewProps
|
|
867
874
|
} = props;
|
|
868
875
|
|
|
876
|
+
const open = !disabled && !!(visible ?? android?.visible);
|
|
877
|
+
const leaves = (list: ContextMenuProps['actions']): ContextMenuAction[] =>
|
|
878
|
+
list.flatMap((action) =>
|
|
879
|
+
action.attributes?.hidden
|
|
880
|
+
? []
|
|
881
|
+
: action.subactions?.length
|
|
882
|
+
? leaves(action.subactions)
|
|
883
|
+
: [action]
|
|
884
|
+
);
|
|
885
|
+
|
|
869
886
|
return (
|
|
870
887
|
<View
|
|
871
888
|
{...viewProps}
|
|
889
|
+
testID={testID}
|
|
872
890
|
{...handlers({ onPressAction, onMenuOpen, onMenuClose, onPreviewPress })}
|
|
873
891
|
>
|
|
874
892
|
{children}
|
|
893
|
+
{open
|
|
894
|
+
? leaves(actions).map((action) => (
|
|
895
|
+
<Pressable
|
|
896
|
+
key={action.id}
|
|
897
|
+
testID={
|
|
898
|
+
action.testID ?? (testID ? `${testID}-${action.id}` : undefined)
|
|
899
|
+
}
|
|
900
|
+
accessibilityRole="menuitem"
|
|
901
|
+
accessibilityState={{ disabled: !!action.attributes?.disabled }}
|
|
902
|
+
disabled={action.attributes?.disabled}
|
|
903
|
+
onPress={() => onPressAction?.(action.id, action.title)}
|
|
904
|
+
>
|
|
905
|
+
<Text>{action.title}</Text>
|
|
906
|
+
</Pressable>
|
|
907
|
+
))
|
|
908
|
+
: null}
|
|
909
|
+
</View>
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Its children in a view carrying `testID`. While `visible`, the title and
|
|
915
|
+
* message show as `Text`, and each shown action is a `Pressable` with its
|
|
916
|
+
* `testID` (else `${testID}-${id}`) that calls `onSelect(id)`; a disabled
|
|
917
|
+
* one ignores presses. A cancel `Pressable` with `cancelTestID` (else
|
|
918
|
+
* `${testID}-cancel`) calls `onCancel`. `fireEvent(sheet, 'select', id)` and
|
|
919
|
+
* `fireEvent(sheet, 'cancel')` also work.
|
|
920
|
+
*/
|
|
921
|
+
export function ActionSheet(props: ActionSheetProps): React.ReactElement {
|
|
922
|
+
const {
|
|
923
|
+
visible,
|
|
924
|
+
title,
|
|
925
|
+
message,
|
|
926
|
+
actions,
|
|
927
|
+
cancelTitle,
|
|
928
|
+
cancelTestID,
|
|
929
|
+
haptics,
|
|
930
|
+
maxFontSizeMultiplier,
|
|
931
|
+
onSelect,
|
|
932
|
+
onCancel,
|
|
933
|
+
testID,
|
|
934
|
+
children,
|
|
935
|
+
...viewProps
|
|
936
|
+
} = props;
|
|
937
|
+
|
|
938
|
+
return (
|
|
939
|
+
<View
|
|
940
|
+
{...viewProps}
|
|
941
|
+
testID={testID}
|
|
942
|
+
accessibilityState={{ expanded: visible }}
|
|
943
|
+
{...handlers({ onSelect, onCancel })}
|
|
944
|
+
>
|
|
945
|
+
{children}
|
|
946
|
+
{visible ? (
|
|
947
|
+
<View accessibilityRole="menu">
|
|
948
|
+
{title ? <Text>{title}</Text> : null}
|
|
949
|
+
{message ? <Text>{message}</Text> : null}
|
|
950
|
+
{actions
|
|
951
|
+
.filter((action) => !action.attributes?.hidden)
|
|
952
|
+
.map((action) => {
|
|
953
|
+
const disabled = !!action.attributes?.disabled;
|
|
954
|
+
return (
|
|
955
|
+
<Pressable
|
|
956
|
+
key={action.id}
|
|
957
|
+
testID={
|
|
958
|
+
action.testID ??
|
|
959
|
+
(testID ? `${testID}-${action.id}` : undefined)
|
|
960
|
+
}
|
|
961
|
+
accessibilityRole="menuitem"
|
|
962
|
+
accessibilityState={{ disabled }}
|
|
963
|
+
disabled={disabled}
|
|
964
|
+
onPress={() => onSelect?.(action.id)}
|
|
965
|
+
>
|
|
966
|
+
<Text>{action.title}</Text>
|
|
967
|
+
</Pressable>
|
|
968
|
+
);
|
|
969
|
+
})}
|
|
970
|
+
<Pressable
|
|
971
|
+
testID={cancelTestID ?? (testID ? `${testID}-cancel` : undefined)}
|
|
972
|
+
accessibilityRole="button"
|
|
973
|
+
onPress={() => onCancel?.()}
|
|
974
|
+
>
|
|
975
|
+
<Text>{cancelTitle ?? 'Cancel'}</Text>
|
|
976
|
+
</Pressable>
|
|
977
|
+
</View>
|
|
978
|
+
) : null}
|
|
875
979
|
</View>
|
|
876
980
|
);
|
|
877
981
|
}
|
package/src/menuItems.ts
CHANGED
|
@@ -69,6 +69,16 @@ export interface ContextMenuAction {
|
|
|
69
69
|
* - Android: a menu group with group dividers; the title is not shown.
|
|
70
70
|
*/
|
|
71
71
|
displayInline?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Test identifier of the action's menu row, so UI tests (Detox `by.id`)
|
|
74
|
+
* find it without its title. A submenu's row takes one too; an inline
|
|
75
|
+
* section has no row of its own.
|
|
76
|
+
*
|
|
77
|
+
* - iOS: the `accessibilityIdentifier` of the `UIAction` / `UIMenu`.
|
|
78
|
+
* - Android 10+: the tag of the popup row view, which Detox and Espresso
|
|
79
|
+
* match `by.id` against. Not a resource-id.
|
|
80
|
+
*/
|
|
81
|
+
testID?: string;
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
/**
|
|
@@ -99,6 +109,8 @@ export type NativeMenuItem = {
|
|
|
99
109
|
state: string;
|
|
100
110
|
/** The action's own haptics; '' = the component's */
|
|
101
111
|
haptics: string;
|
|
112
|
+
/** Test identifier of the menu row; '' = none */
|
|
113
|
+
testID: string;
|
|
102
114
|
};
|
|
103
115
|
|
|
104
116
|
const flag = (value: boolean | undefined) => (value ? 'true' : 'false');
|
|
@@ -141,6 +153,7 @@ export function flattenMenuActions(
|
|
|
141
153
|
keepsMenuPresented: flag(attributes?.keepsMenuPresented),
|
|
142
154
|
state: action.state ?? '',
|
|
143
155
|
haptics: action.haptics ?? '',
|
|
156
|
+
testID: action.testID ?? '',
|
|
144
157
|
});
|
|
145
158
|
|
|
146
159
|
if (kind !== 'action') visit(children, index);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Diagnose identifiers reserved by the native selection protocol. */
|
|
2
2
|
export function warnSelectionIdentifiers(
|
|
3
|
-
component: 'SelectionMenu' | 'SegmentedControl',
|
|
3
|
+
component: 'SelectionMenu' | 'SegmentedControl' | 'ActionSheet',
|
|
4
4
|
values: readonly string[]
|
|
5
5
|
): void {
|
|
6
6
|
if (!__DEV__) return;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// web/ActionSheet.tsx
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { View } from 'react-native';
|
|
4
|
+
|
|
5
|
+
import type { ActionSheetProps } from '../ActionSheet';
|
|
6
|
+
import { Dialog } from './Dialog';
|
|
7
|
+
import { Icon } from './Icon';
|
|
8
|
+
|
|
9
|
+
// Material 3 baseline error color, as on Android
|
|
10
|
+
const DESTRUCTIVE_COLOR = '#B3261E';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Renders its children; while `visible`, the title, message and actions open
|
|
14
|
+
* in a `<dialog>` with a cancel button below them. Picking an action calls
|
|
15
|
+
* `onSelect`; the cancel button, Escape and a click outside call `onCancel`.
|
|
16
|
+
* Icons draw only for image sources (`{ type: 'image' }`); each button
|
|
17
|
+
* carries its action's `testID` as `data-testid`.
|
|
18
|
+
*/
|
|
19
|
+
export function ActionSheet(props: ActionSheetProps): React.ReactElement {
|
|
20
|
+
const {
|
|
21
|
+
visible,
|
|
22
|
+
title,
|
|
23
|
+
message,
|
|
24
|
+
actions,
|
|
25
|
+
cancelTitle,
|
|
26
|
+
cancelTestID,
|
|
27
|
+
haptics,
|
|
28
|
+
maxFontSizeMultiplier,
|
|
29
|
+
onSelect,
|
|
30
|
+
onCancel,
|
|
31
|
+
accessibilityLabel,
|
|
32
|
+
children,
|
|
33
|
+
...viewProps
|
|
34
|
+
} = props;
|
|
35
|
+
|
|
36
|
+
const label = accessibilityLabel ?? title ?? 'Actions';
|
|
37
|
+
const shown = actions.filter((action) => !action.attributes?.hidden);
|
|
38
|
+
const cancel = () => onCancel?.();
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<View {...viewProps}>
|
|
42
|
+
{children}
|
|
43
|
+
{visible ? (
|
|
44
|
+
<Dialog label={label} onDismiss={cancel}>
|
|
45
|
+
<div
|
|
46
|
+
style={{ display: 'flex', flexDirection: 'column', minWidth: 240 }}
|
|
47
|
+
>
|
|
48
|
+
{title || message ? (
|
|
49
|
+
<div style={{ padding: '4px 12px 12px', textAlign: 'center' }}>
|
|
50
|
+
{title ? (
|
|
51
|
+
<div style={{ fontSize: 15, fontWeight: 600 }}>{title}</div>
|
|
52
|
+
) : null}
|
|
53
|
+
{message ? (
|
|
54
|
+
<div style={{ fontSize: 13, opacity: 0.7, marginTop: 4 }}>
|
|
55
|
+
{message}
|
|
56
|
+
</div>
|
|
57
|
+
) : null}
|
|
58
|
+
</div>
|
|
59
|
+
) : null}
|
|
60
|
+
{shown.map((action, index) => {
|
|
61
|
+
const destructive = !!action.attributes?.destructive;
|
|
62
|
+
const color = destructive ? DESTRUCTIVE_COLOR : 'inherit';
|
|
63
|
+
return (
|
|
64
|
+
<button
|
|
65
|
+
key={action.id}
|
|
66
|
+
type="button"
|
|
67
|
+
data-testid={action.testID}
|
|
68
|
+
disabled={action.attributes?.disabled}
|
|
69
|
+
autoFocus={
|
|
70
|
+
index ===
|
|
71
|
+
shown.findIndex((item) => !item.attributes?.disabled)
|
|
72
|
+
}
|
|
73
|
+
onClick={() => {
|
|
74
|
+
if (!action.attributes?.disabled) onSelect?.(action.id);
|
|
75
|
+
}}
|
|
76
|
+
style={{
|
|
77
|
+
display: 'flex',
|
|
78
|
+
alignItems: 'center',
|
|
79
|
+
gap: 12,
|
|
80
|
+
minHeight: 44,
|
|
81
|
+
padding: '0 12px',
|
|
82
|
+
border: 'none',
|
|
83
|
+
borderRadius: 8,
|
|
84
|
+
fontFamily: 'inherit',
|
|
85
|
+
fontSize: 16,
|
|
86
|
+
textAlign: 'start',
|
|
87
|
+
color,
|
|
88
|
+
opacity: action.attributes?.disabled ? 0.4 : 1,
|
|
89
|
+
backgroundColor: 'transparent',
|
|
90
|
+
cursor: action.attributes?.disabled ? 'default' : 'pointer',
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
<Icon
|
|
94
|
+
icon={action.image}
|
|
95
|
+
color={
|
|
96
|
+
action.imageColor ??
|
|
97
|
+
(destructive ? DESTRUCTIVE_COLOR : 'CanvasText')
|
|
98
|
+
}
|
|
99
|
+
/>
|
|
100
|
+
<span>{action.title}</span>
|
|
101
|
+
</button>
|
|
102
|
+
);
|
|
103
|
+
})}
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
data-testid={cancelTestID}
|
|
107
|
+
onClick={cancel}
|
|
108
|
+
style={{
|
|
109
|
+
minHeight: 44,
|
|
110
|
+
marginTop: 8,
|
|
111
|
+
padding: '0 12px',
|
|
112
|
+
border: 'none',
|
|
113
|
+
borderRadius: 8,
|
|
114
|
+
fontFamily: 'inherit',
|
|
115
|
+
fontSize: 16,
|
|
116
|
+
fontWeight: 600,
|
|
117
|
+
color: 'inherit',
|
|
118
|
+
backgroundColor:
|
|
119
|
+
'color-mix(in srgb, CanvasText 8%, transparent)',
|
|
120
|
+
cursor: 'pointer',
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
{cancelTitle ?? 'Cancel'}
|
|
124
|
+
</button>
|
|
125
|
+
</div>
|
|
126
|
+
</Dialog>
|
|
127
|
+
) : null}
|
|
128
|
+
</View>
|
|
129
|
+
);
|
|
130
|
+
}
|