@expo/ui 56.0.7 → 56.0.8
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/CHANGELOG.md +17 -0
- package/android/build.gradle +2 -2
- package/android/src/main/java/expo/modules/ui/ExpoUIModule.kt +6 -2
- package/android/src/main/java/expo/modules/ui/HostView.kt +2 -0
- package/android/src/main/java/expo/modules/ui/ModifierRegistry.kt +19 -1
- package/android/src/main/java/expo/modules/ui/RNHostView.kt +8 -3
- package/android/src/main/java/expo/modules/ui/ShadowNodeSyncFlush.kt +28 -0
- package/build/community/menu/MenuView.android.d.ts +16 -0
- package/build/community/menu/MenuView.android.d.ts.map +1 -0
- package/build/community/menu/MenuView.d.ts +19 -0
- package/build/community/menu/MenuView.d.ts.map +1 -0
- package/build/community/menu/MenuView.ios.d.ts +10 -0
- package/build/community/menu/MenuView.ios.d.ts.map +1 -0
- package/build/community/menu/index.d.ts +5 -0
- package/build/community/menu/index.d.ts.map +1 -0
- package/build/community/menu/types.d.ts +166 -0
- package/build/community/menu/types.d.ts.map +1 -0
- package/build/jetpack-compose/modifiers/index.d.ts +15 -0
- package/build/jetpack-compose/modifiers/index.d.ts.map +1 -1
- package/build/swift-ui/Alert/index.d.ts +42 -0
- package/build/swift-ui/Alert/index.d.ts.map +1 -0
- package/build/swift-ui/SlotView.d.ts +5 -2
- package/build/swift-ui/SlotView.d.ts.map +1 -1
- package/build/swift-ui/SwipeActions/index.d.ts +38 -0
- package/build/swift-ui/SwipeActions/index.d.ts.map +1 -0
- package/build/swift-ui/index.d.ts +2 -0
- package/build/swift-ui/index.d.ts.map +1 -1
- package/build/swift-ui/modifiers/index.d.ts +3 -1
- package/build/swift-ui/modifiers/index.d.ts.map +1 -1
- package/build/swift-ui/modifiers/symbolEffect.d.ts +103 -0
- package/build/swift-ui/modifiers/symbolEffect.d.ts.map +1 -0
- package/build/universal/Host/index.d.ts +17 -6
- package/build/universal/Host/index.d.ts.map +1 -1
- package/expo-module.config.json +1 -1
- package/ios/Alert/Alert.swift +56 -0
- package/ios/Alert/AlertProps.swift +8 -0
- package/ios/ExpoUIModule.swift +2 -0
- package/ios/ExpoUITouchHandlerHelper.h +4 -1
- package/ios/ExpoUITouchHandlerHelper.mm +1 -0
- package/ios/Modifiers/SwipeActionsModifier.swift +97 -0
- package/ios/Modifiers/SymbolEffectModifier.swift +452 -0
- package/ios/Modifiers/ViewModifierRegistry.swift +4 -0
- package/ios/SlotView.swift +5 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/{56.0.7/expo.modules.ui-56.0.7-sources.jar → 56.0.8/expo.modules.ui-56.0.8-sources.jar} +0 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8-sources.jar.md5 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8-sources.jar.sha1 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8-sources.jar.sha256 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8-sources.jar.sha512 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.aar +0 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.aar.md5 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.aar.sha1 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.aar.sha256 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.aar.sha512 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/{56.0.7/expo.modules.ui-56.0.7.module → 56.0.8/expo.modules.ui-56.0.8.module} +22 -22
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.module.md5 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.module.sha1 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.module.sha256 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.module.sha512 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/{56.0.7/expo.modules.ui-56.0.7.pom → 56.0.8/expo.modules.ui-56.0.8.pom} +1 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.pom.md5 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.pom.sha1 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.pom.sha256 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.pom.sha512 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml +4 -4
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.md5 +1 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha1 +1 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha256 +1 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha512 +1 -1
- package/package.json +7 -3
- package/src/community/menu/MenuView.android.tsx +224 -0
- package/src/community/menu/MenuView.ios.tsx +149 -0
- package/src/community/menu/MenuView.tsx +36 -0
- package/src/community/menu/index.tsx +14 -0
- package/src/community/menu/types.tsx +171 -0
- package/src/jetpack-compose/modifiers/index.ts +25 -0
- package/src/swift-ui/Alert/index.tsx +87 -0
- package/src/swift-ui/SlotView.tsx +17 -4
- package/src/swift-ui/SwipeActions/index.tsx +73 -0
- package/src/swift-ui/index.tsx +2 -0
- package/src/swift-ui/modifiers/index.ts +3 -0
- package/src/swift-ui/modifiers/symbolEffect.ts +181 -0
- package/src/ts-declarations/react-native-web.d.ts +20 -0
- package/src/universal/Host/index.tsx +70 -5
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7-sources.jar.md5 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7-sources.jar.sha1 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7-sources.jar.sha256 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7-sources.jar.sha512 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.aar +0 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.aar.md5 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.aar.sha1 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.aar.sha256 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.aar.sha512 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.module.md5 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.module.sha1 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.module.sha256 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.module.sha512 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.pom.md5 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.pom.sha1 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.pom.sha256 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.pom.sha512 +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expo/ui",
|
|
3
|
-
"version": "56.0.
|
|
3
|
+
"version": "56.0.8",
|
|
4
4
|
"description": "A collection of UI components",
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"*.fx.js"
|
|
@@ -51,6 +51,10 @@
|
|
|
51
51
|
"types": "./build/community/masked-view/index.d.ts",
|
|
52
52
|
"default": "./src/community/masked-view/index.tsx"
|
|
53
53
|
},
|
|
54
|
+
"./community/menu": {
|
|
55
|
+
"types": "./build/community/menu/index.d.ts",
|
|
56
|
+
"default": "./src/community/menu/index.tsx"
|
|
57
|
+
},
|
|
54
58
|
"./babel-plugin": {
|
|
55
59
|
"types": "./plugin/babel-plugin.d.ts",
|
|
56
60
|
"default": "./plugin/babel-plugin.js"
|
|
@@ -87,7 +91,7 @@
|
|
|
87
91
|
"@types/react": "~19.2.0",
|
|
88
92
|
"react-native-reanimated": "4.3.1",
|
|
89
93
|
"react-native-worklets": "0.8.3",
|
|
90
|
-
"expo": "56.0.0-preview.
|
|
94
|
+
"expo": "56.0.0-preview.12",
|
|
91
95
|
"expo-module-scripts": "56.0.2"
|
|
92
96
|
},
|
|
93
97
|
"jest": {
|
|
@@ -116,7 +120,7 @@
|
|
|
116
120
|
"optional": true
|
|
117
121
|
}
|
|
118
122
|
},
|
|
119
|
-
"gitHead": "
|
|
123
|
+
"gitHead": "f26be3dd9396bf7c399a1d607865d0fabdbc0d64",
|
|
120
124
|
"scripts": {
|
|
121
125
|
"build": "expo-module build",
|
|
122
126
|
"clean": "expo-module clean",
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Pressable, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import type { MenuAction, MenuComponentProps, MenuComponentRef, NativeActionEvent } from './types';
|
|
5
|
+
import { HorizontalDivider } from '../../jetpack-compose/Divider';
|
|
6
|
+
import { DropdownMenu } from '../../jetpack-compose/DropdownMenu';
|
|
7
|
+
import {
|
|
8
|
+
DropdownMenuItem,
|
|
9
|
+
type DropdownMenuItemElementColors,
|
|
10
|
+
} from '../../jetpack-compose/DropdownMenu/DropdownMenuItem';
|
|
11
|
+
import { Host } from '../../jetpack-compose/Host';
|
|
12
|
+
import { Icon } from '../../jetpack-compose/Icon';
|
|
13
|
+
import { RNHostView } from '../../jetpack-compose/RNHostView';
|
|
14
|
+
import { Text as ComposeText } from '../../jetpack-compose/Text';
|
|
15
|
+
import { useMaterialColors } from '../../jetpack-compose/colors';
|
|
16
|
+
|
|
17
|
+
function actionId(action: MenuAction): string {
|
|
18
|
+
return action.id ?? action.title;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function makeEvent(action: MenuAction): NativeActionEvent {
|
|
22
|
+
return { nativeEvent: { event: actionId(action) } };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function buildElementColors(
|
|
26
|
+
action: MenuAction,
|
|
27
|
+
destructiveColor: string
|
|
28
|
+
): DropdownMenuItemElementColors | undefined {
|
|
29
|
+
const isDestructive = action.attributes?.destructive === true;
|
|
30
|
+
const textColor = action.titleColor ?? (isDestructive ? destructiveColor : undefined);
|
|
31
|
+
// The leading icon picks up `leadingIconColor` via `LocalContentColor`, but only
|
|
32
|
+
// when the `Icon` itself doesn't set `tint` — so an explicit `imageColor` from
|
|
33
|
+
// the caller still wins.
|
|
34
|
+
const leadingIconColor = isDestructive ? destructiveColor : undefined;
|
|
35
|
+
if (textColor == null && leadingIconColor == null) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
textColor,
|
|
40
|
+
disabledTextColor: textColor,
|
|
41
|
+
leadingIconColor,
|
|
42
|
+
disabledLeadingIconColor: leadingIconColor,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type ItemProps = {
|
|
47
|
+
action: MenuAction;
|
|
48
|
+
onPressAction: MenuComponentProps['onPressAction'];
|
|
49
|
+
dismissAll: () => void;
|
|
50
|
+
destructiveColor: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function MenuActionItem({ action, onPressAction, dismissAll, destructiveColor }: ItemProps) {
|
|
54
|
+
const [submenuExpanded, setSubmenuExpanded] = React.useState(false);
|
|
55
|
+
|
|
56
|
+
if (action.attributes?.hidden) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const { subactions, displayInline, state, attributes, title, image, imageColor } = action;
|
|
61
|
+
// `image` is non-string only when caller passes an ImageSourcePropType
|
|
62
|
+
// (number from `require()` or `{ uri }`). The string form is iOS-only (SF Symbol).
|
|
63
|
+
const leadingIconSource = typeof image === 'string' || image == null ? null : image;
|
|
64
|
+
const elementColors = buildElementColors(action, destructiveColor);
|
|
65
|
+
|
|
66
|
+
if (subactions && subactions.length > 0) {
|
|
67
|
+
if (displayInline) {
|
|
68
|
+
return (
|
|
69
|
+
<>
|
|
70
|
+
<HorizontalDivider />
|
|
71
|
+
{subactions.map((sub) => (
|
|
72
|
+
<MenuActionItem
|
|
73
|
+
key={actionId(sub)}
|
|
74
|
+
action={sub}
|
|
75
|
+
onPressAction={onPressAction}
|
|
76
|
+
dismissAll={dismissAll}
|
|
77
|
+
destructiveColor={destructiveColor}
|
|
78
|
+
/>
|
|
79
|
+
))}
|
|
80
|
+
<HorizontalDivider />
|
|
81
|
+
</>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return (
|
|
85
|
+
<DropdownMenu expanded={submenuExpanded} onDismissRequest={() => setSubmenuExpanded(false)}>
|
|
86
|
+
<DropdownMenu.Trigger>
|
|
87
|
+
<DropdownMenuItem
|
|
88
|
+
enabled={!attributes?.disabled}
|
|
89
|
+
elementColors={elementColors}
|
|
90
|
+
onClick={() => setSubmenuExpanded(true)}>
|
|
91
|
+
<DropdownMenuItem.Text>
|
|
92
|
+
<ComposeText>{title}</ComposeText>
|
|
93
|
+
</DropdownMenuItem.Text>
|
|
94
|
+
{leadingIconSource && (
|
|
95
|
+
<DropdownMenuItem.LeadingIcon>
|
|
96
|
+
<Icon source={leadingIconSource} size={24} tint={imageColor} />
|
|
97
|
+
</DropdownMenuItem.LeadingIcon>
|
|
98
|
+
)}
|
|
99
|
+
</DropdownMenuItem>
|
|
100
|
+
</DropdownMenu.Trigger>
|
|
101
|
+
<DropdownMenu.Items>
|
|
102
|
+
{subactions.map((sub) => (
|
|
103
|
+
<MenuActionItem
|
|
104
|
+
key={actionId(sub)}
|
|
105
|
+
action={sub}
|
|
106
|
+
onPressAction={onPressAction}
|
|
107
|
+
dismissAll={() => {
|
|
108
|
+
setSubmenuExpanded(false);
|
|
109
|
+
dismissAll();
|
|
110
|
+
}}
|
|
111
|
+
destructiveColor={destructiveColor}
|
|
112
|
+
/>
|
|
113
|
+
))}
|
|
114
|
+
</DropdownMenu.Items>
|
|
115
|
+
</DropdownMenu>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<DropdownMenuItem
|
|
121
|
+
enabled={!attributes?.disabled}
|
|
122
|
+
elementColors={elementColors}
|
|
123
|
+
onClick={() => {
|
|
124
|
+
onPressAction?.(makeEvent(action));
|
|
125
|
+
dismissAll();
|
|
126
|
+
}}>
|
|
127
|
+
<DropdownMenuItem.Text>
|
|
128
|
+
<ComposeText>{title}</ComposeText>
|
|
129
|
+
</DropdownMenuItem.Text>
|
|
130
|
+
{leadingIconSource && (
|
|
131
|
+
<DropdownMenuItem.LeadingIcon>
|
|
132
|
+
<Icon source={leadingIconSource} size={24} tint={imageColor} />
|
|
133
|
+
</DropdownMenuItem.LeadingIcon>
|
|
134
|
+
)}
|
|
135
|
+
{state === 'on' && (
|
|
136
|
+
<DropdownMenuItem.TrailingIcon>
|
|
137
|
+
<ComposeText>✓</ComposeText>
|
|
138
|
+
</DropdownMenuItem.TrailingIcon>
|
|
139
|
+
)}
|
|
140
|
+
</DropdownMenuItem>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* A drop-in replacement for `@react-native-menu/menu` on Android.
|
|
146
|
+
* Wraps the trigger in a `Pressable` (whose `onPress`/`onLongPress` opens the menu) and
|
|
147
|
+
* renders the actions tree as a controlled Material `DropdownMenu`.
|
|
148
|
+
*
|
|
149
|
+
* Note: when `action.image` is a string, it is treated as an iOS SF Symbol and ignored
|
|
150
|
+
* on Android — pass an `ImageSourcePropType` (e.g. `require('./icon.xml')`) to render
|
|
151
|
+
* a leading icon. `MenuView.title` is also unused on Android since Material
|
|
152
|
+
* `DropdownMenu` has no title slot.
|
|
153
|
+
*/
|
|
154
|
+
export function MenuView(props: MenuComponentProps & { ref?: React.Ref<MenuComponentRef> }) {
|
|
155
|
+
const {
|
|
156
|
+
ref,
|
|
157
|
+
actions,
|
|
158
|
+
onPressAction,
|
|
159
|
+
onOpenMenu,
|
|
160
|
+
onCloseMenu,
|
|
161
|
+
shouldOpenOnLongPress,
|
|
162
|
+
style,
|
|
163
|
+
children,
|
|
164
|
+
testID,
|
|
165
|
+
} = props;
|
|
166
|
+
const [expanded, setExpanded] = React.useState(false);
|
|
167
|
+
// Mirror `expanded` into a ref and flip it eagerly inside the callbacks so a
|
|
168
|
+
// second call within the same React tick is a no-op (the state update is
|
|
169
|
+
// async and wouldn't otherwise reflect back to the ref in time).
|
|
170
|
+
const expandedRef = React.useRef(false);
|
|
171
|
+
|
|
172
|
+
const open = React.useCallback(() => {
|
|
173
|
+
if (expandedRef.current) return;
|
|
174
|
+
expandedRef.current = true;
|
|
175
|
+
setExpanded(true);
|
|
176
|
+
onOpenMenu?.();
|
|
177
|
+
}, [onOpenMenu]);
|
|
178
|
+
|
|
179
|
+
const dismissAll = React.useCallback(() => {
|
|
180
|
+
if (!expandedRef.current) return;
|
|
181
|
+
expandedRef.current = false;
|
|
182
|
+
setExpanded(false);
|
|
183
|
+
onCloseMenu?.();
|
|
184
|
+
}, [onCloseMenu]);
|
|
185
|
+
|
|
186
|
+
React.useImperativeHandle(ref, () => ({ show: open }), [open]);
|
|
187
|
+
|
|
188
|
+
const destructiveColor = useMaterialColors().error;
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<View style={style} testID={testID}>
|
|
192
|
+
<Host matchContents>
|
|
193
|
+
<DropdownMenu expanded={expanded} onDismissRequest={dismissAll}>
|
|
194
|
+
<DropdownMenu.Trigger>
|
|
195
|
+
<RNHostView matchContents>
|
|
196
|
+
<Pressable
|
|
197
|
+
onPress={shouldOpenOnLongPress ? undefined : open}
|
|
198
|
+
onLongPress={shouldOpenOnLongPress ? open : undefined}
|
|
199
|
+
// Mirror upstream `@react-native-menu/menu`, which intercepts touches
|
|
200
|
+
// natively on a bare `ReactViewGroup`: no click sound, no focus
|
|
201
|
+
// highlight, no extra a11y node — children declare their own role.
|
|
202
|
+
android_disableSound
|
|
203
|
+
focusable={false}
|
|
204
|
+
accessible={false}>
|
|
205
|
+
{children}
|
|
206
|
+
</Pressable>
|
|
207
|
+
</RNHostView>
|
|
208
|
+
</DropdownMenu.Trigger>
|
|
209
|
+
<DropdownMenu.Items>
|
|
210
|
+
{actions.map((action) => (
|
|
211
|
+
<MenuActionItem
|
|
212
|
+
key={actionId(action)}
|
|
213
|
+
action={action}
|
|
214
|
+
onPressAction={onPressAction}
|
|
215
|
+
dismissAll={dismissAll}
|
|
216
|
+
destructiveColor={destructiveColor}
|
|
217
|
+
/>
|
|
218
|
+
))}
|
|
219
|
+
</DropdownMenu.Items>
|
|
220
|
+
</DropdownMenu>
|
|
221
|
+
</Host>
|
|
222
|
+
</View>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import type { MenuAction, MenuComponentProps, MenuComponentRef, NativeActionEvent } from './types';
|
|
4
|
+
import { Button } from '../../swift-ui/Button';
|
|
5
|
+
import { ContextMenu } from '../../swift-ui/ContextMenu';
|
|
6
|
+
import { Host } from '../../swift-ui/Host';
|
|
7
|
+
import { Menu } from '../../swift-ui/Menu';
|
|
8
|
+
import { RNHostView } from '../../swift-ui/RNHostView';
|
|
9
|
+
import { Section } from '../../swift-ui/Section';
|
|
10
|
+
import { Toggle } from '../../swift-ui/Toggle';
|
|
11
|
+
import {
|
|
12
|
+
disabled as disabledModifier,
|
|
13
|
+
foregroundColor as foregroundColorModifier,
|
|
14
|
+
tint as tintModifier,
|
|
15
|
+
} from '../../swift-ui/modifiers';
|
|
16
|
+
import type { ModifierConfig } from '../../types';
|
|
17
|
+
|
|
18
|
+
function actionId(action: MenuAction): string {
|
|
19
|
+
return action.id ?? action.title;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function makeEvent(action: MenuAction): NativeActionEvent {
|
|
23
|
+
return { nativeEvent: { event: actionId(action) } };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function renderAction(
|
|
27
|
+
action: MenuAction,
|
|
28
|
+
onPressAction: MenuComponentProps['onPressAction']
|
|
29
|
+
): React.ReactNode {
|
|
30
|
+
if (action.attributes?.hidden) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { subactions, displayInline, state, attributes, image, imageColor, title } = action;
|
|
35
|
+
const key = actionId(action);
|
|
36
|
+
const systemImage = typeof image === 'string' ? image : undefined;
|
|
37
|
+
// `tint` is what SwiftUI's `Menu`/`Button` honor for the leading SF Symbol —
|
|
38
|
+
// `foregroundColor` would also affect the label text.
|
|
39
|
+
const tintMod = imageColor ? tintModifier(imageColor) : null;
|
|
40
|
+
|
|
41
|
+
if (subactions && subactions.length > 0) {
|
|
42
|
+
const children = subactions.map((sub) => renderAction(sub, onPressAction));
|
|
43
|
+
if (displayInline) {
|
|
44
|
+
return (
|
|
45
|
+
<Section key={key} title={title}>
|
|
46
|
+
{children}
|
|
47
|
+
</Section>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
// SwiftUI's system menu UI ignores per-item color modifiers on submenu
|
|
51
|
+
// headers, so we don't forward `imageColor` here. Leaf `Button`s below
|
|
52
|
+
// tint via `foregroundColor`, which does take effect.
|
|
53
|
+
return (
|
|
54
|
+
<Menu key={key} label={title} systemImage={systemImage}>
|
|
55
|
+
{children}
|
|
56
|
+
</Menu>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const fire = () => onPressAction?.(makeEvent(action));
|
|
61
|
+
|
|
62
|
+
const modifiers: ModifierConfig[] = [];
|
|
63
|
+
if (attributes?.disabled) modifiers.push(disabledModifier(true));
|
|
64
|
+
|
|
65
|
+
if (state === 'on' || state === 'off') {
|
|
66
|
+
if (tintMod) modifiers.push(tintMod);
|
|
67
|
+
return (
|
|
68
|
+
<Toggle
|
|
69
|
+
key={key}
|
|
70
|
+
label={title}
|
|
71
|
+
systemImage={systemImage}
|
|
72
|
+
isOn={state === 'on'}
|
|
73
|
+
onIsOnChange={fire}
|
|
74
|
+
modifiers={modifiers.length > 0 ? modifiers : undefined}
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// For a leaf `Button`, `foregroundColor` also tints the system image —
|
|
80
|
+
// upstream uses this to color both the label and the icon. Skip when the
|
|
81
|
+
// role is destructive so SwiftUI's red tint remains in effect.
|
|
82
|
+
if (imageColor && !attributes?.destructive) {
|
|
83
|
+
modifiers.push(foregroundColorModifier(imageColor));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<Button
|
|
88
|
+
key={key}
|
|
89
|
+
label={title}
|
|
90
|
+
systemImage={systemImage}
|
|
91
|
+
role={attributes?.destructive ? 'destructive' : undefined}
|
|
92
|
+
modifiers={modifiers.length > 0 ? modifiers : undefined}
|
|
93
|
+
onPress={fire}
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let warnedShowNoop = false;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A drop-in replacement for `@react-native-menu/menu` on iOS.
|
|
102
|
+
* Uses SwiftUI `Menu` for tap triggers and `ContextMenu` for long-press triggers.
|
|
103
|
+
*/
|
|
104
|
+
export function MenuView(props: MenuComponentProps & { ref?: React.Ref<MenuComponentRef> }) {
|
|
105
|
+
const { ref, actions, onPressAction, shouldOpenOnLongPress, title, style, children, testID } =
|
|
106
|
+
props;
|
|
107
|
+
|
|
108
|
+
// SwiftUI `Menu`/`ContextMenu` expose no programmatic open API, so `show()`
|
|
109
|
+
// can't do anything on iOS. Surface that as a one-time dev warning instead of
|
|
110
|
+
// silently no-opping.
|
|
111
|
+
React.useImperativeHandle(
|
|
112
|
+
ref,
|
|
113
|
+
() => ({
|
|
114
|
+
show: () => {
|
|
115
|
+
if (__DEV__ && !warnedShowNoop) {
|
|
116
|
+
warnedShowNoop = true;
|
|
117
|
+
console.warn(
|
|
118
|
+
'[@expo/ui] MenuView.show() is a no-op on iOS. SwiftUI Menu/ContextMenu have no programmatic open API.'
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
}),
|
|
123
|
+
[]
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const items = actions.map((action) => renderAction(action, onPressAction));
|
|
127
|
+
const body = title ? <Section title={title}>{items}</Section> : items;
|
|
128
|
+
|
|
129
|
+
// RNHostView requires a single ReactElement; wrap in a fragment so callers
|
|
130
|
+
// can pass any `ReactNode` (string, array, etc.).
|
|
131
|
+
const trigger = (
|
|
132
|
+
<RNHostView matchContents>
|
|
133
|
+
<>{children}</>
|
|
134
|
+
</RNHostView>
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<Host matchContents style={style} testID={testID}>
|
|
139
|
+
{shouldOpenOnLongPress ? (
|
|
140
|
+
<ContextMenu>
|
|
141
|
+
<ContextMenu.Trigger>{trigger}</ContextMenu.Trigger>
|
|
142
|
+
<ContextMenu.Items>{body}</ContextMenu.Items>
|
|
143
|
+
</ContextMenu>
|
|
144
|
+
) : (
|
|
145
|
+
<Menu label={trigger}>{body}</Menu>
|
|
146
|
+
)}
|
|
147
|
+
</Host>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type Ref, useEffect, useImperativeHandle } from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import type { MenuComponentProps, MenuComponentRef } from './types';
|
|
5
|
+
|
|
6
|
+
let warned = false;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A drop-in replacement for `@react-native-menu/menu`'s `MenuView`. Wrap any trigger
|
|
10
|
+
* view; long-pressing or tapping (per `shouldOpenOnLongPress`) shows a popup menu
|
|
11
|
+
* built from the `actions` tree.
|
|
12
|
+
*
|
|
13
|
+
* - On Android, renders via Compose's `DropdownMenu` anchored to a `Pressable`.
|
|
14
|
+
* - On iOS, renders via SwiftUI's `Menu` (tap) or `ContextMenu` (long-press).
|
|
15
|
+
* - On web, the trigger renders the trigger but actions do not fire;
|
|
16
|
+
* a one-time `console.warn` is emitted.
|
|
17
|
+
*
|
|
18
|
+
* @platform android
|
|
19
|
+
* @platform ios
|
|
20
|
+
*/
|
|
21
|
+
export function MenuView(props: MenuComponentProps & { ref?: Ref<MenuComponentRef> }) {
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!warned) {
|
|
24
|
+
warned = true;
|
|
25
|
+
console.warn(
|
|
26
|
+
"[@expo/ui] MenuView is currently Android and iOS-only; the trigger will render but actions won't fire on this platform."
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}, []);
|
|
30
|
+
useImperativeHandle(props.ref, () => ({ show: () => {} }), []);
|
|
31
|
+
return (
|
|
32
|
+
<View style={props.style} testID={props.testID}>
|
|
33
|
+
{props.children}
|
|
34
|
+
</View>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { MenuView } from './MenuView';
|
|
2
|
+
|
|
3
|
+
export type {
|
|
4
|
+
MenuAction,
|
|
5
|
+
MenuAttributes,
|
|
6
|
+
MenuComponentProps,
|
|
7
|
+
MenuState,
|
|
8
|
+
MenuComponentRef,
|
|
9
|
+
NativeActionEvent,
|
|
10
|
+
} from './types';
|
|
11
|
+
|
|
12
|
+
export default MenuView;
|
|
13
|
+
// named export needed for docs generator
|
|
14
|
+
export { MenuView };
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ColorValue, ImageSourcePropType, StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import type { SFSymbol } from 'sf-symbols-typescript';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Visual and behavioral attributes of a menu action.
|
|
7
|
+
* Compatible with `@react-native-menu/menu`.
|
|
8
|
+
*/
|
|
9
|
+
export type MenuAttributes = {
|
|
10
|
+
/**
|
|
11
|
+
* Renders the action with a destructive style (red text/icon).
|
|
12
|
+
*/
|
|
13
|
+
destructive?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Disables the action so it can't be activated.
|
|
16
|
+
*/
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Hides the action from the menu.
|
|
20
|
+
*/
|
|
21
|
+
hidden?: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Selection state for a menu action.
|
|
26
|
+
* `'on'` renders a checkmark; `'off'` doesn't.
|
|
27
|
+
*/
|
|
28
|
+
export type MenuState = 'on' | 'off';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A single action inside a `MenuView`.
|
|
32
|
+
* Compatible with `@react-native-menu/menu`.
|
|
33
|
+
*/
|
|
34
|
+
export type MenuAction = {
|
|
35
|
+
/**
|
|
36
|
+
* Identifier passed back via `onPressAction.nativeEvent.event` when this action is selected.
|
|
37
|
+
* Defaults to `title` if omitted.
|
|
38
|
+
*/
|
|
39
|
+
id?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Action label shown in the menu.
|
|
42
|
+
*/
|
|
43
|
+
title: string;
|
|
44
|
+
/**
|
|
45
|
+
* Text color of the action label.
|
|
46
|
+
* @platform android
|
|
47
|
+
*/
|
|
48
|
+
titleColor?: ColorValue;
|
|
49
|
+
/**
|
|
50
|
+
* Icon to render beside the action label.
|
|
51
|
+
*
|
|
52
|
+
* - When an `SFSymbol` name (e.g. `'trash'`), rendered on iOS only.
|
|
53
|
+
* Not rendered on Android — pass an `ImageSourcePropType` instead to show an
|
|
54
|
+
* icon there.
|
|
55
|
+
* - When an `ImageSourcePropType` (e.g. `require('./trash.xml')` or
|
|
56
|
+
* `{ uri: '...' }`), rendered on Android via Compose `Icon`. Ignored on iOS;
|
|
57
|
+
* SwiftUI menus only accept SF Symbol names for built-in `Menu`/`Button`
|
|
58
|
+
* labels.
|
|
59
|
+
*/
|
|
60
|
+
image?: SFSymbol | ImageSourcePropType;
|
|
61
|
+
/**
|
|
62
|
+
* Tint color applied to the action's icon.
|
|
63
|
+
*
|
|
64
|
+
* Visually applied on Android via the leading `Icon`'s tint. On iOS, the
|
|
65
|
+
* value is accepted but **may not render**: SwiftUI's `Menu`/`ContextMenu`
|
|
66
|
+
* draw their items via the system menu UI, which ignores per-item color
|
|
67
|
+
* modifiers.
|
|
68
|
+
*/
|
|
69
|
+
imageColor?: ColorValue;
|
|
70
|
+
/**
|
|
71
|
+
* Selection state. When `'on'`, the action renders a checkmark.
|
|
72
|
+
*/
|
|
73
|
+
state?: MenuState;
|
|
74
|
+
/**
|
|
75
|
+
* Visual/behavioral flags.
|
|
76
|
+
*/
|
|
77
|
+
attributes?: MenuAttributes;
|
|
78
|
+
/**
|
|
79
|
+
* Nested actions. Without `displayInline`, renders as a submenu;
|
|
80
|
+
* with `displayInline: true`, renders as an inline section.
|
|
81
|
+
*/
|
|
82
|
+
subactions?: MenuAction[];
|
|
83
|
+
/**
|
|
84
|
+
* When `true` and `subactions` is present, renders the children as an inline section
|
|
85
|
+
* inside the parent menu (with this action's `title` as the section header on iOS).
|
|
86
|
+
*/
|
|
87
|
+
displayInline?: boolean;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Imperative handle exposed by `MenuView` via `ref`.
|
|
92
|
+
* Compatible with `@react-native-menu/menu`'s `ref.show()` API.
|
|
93
|
+
*/
|
|
94
|
+
export type MenuComponentRef = {
|
|
95
|
+
/**
|
|
96
|
+
* Programmatically open the menu.
|
|
97
|
+
*
|
|
98
|
+
* On Android, opens the anchored `DropdownMenu` (equivalent to the user tapping
|
|
99
|
+
* the trigger). On iOS this is a no-op — SwiftUI `Menu`/`ContextMenu` have no
|
|
100
|
+
* programmatic open API; a one-time `console.warn` is emitted in development.
|
|
101
|
+
* @platform android
|
|
102
|
+
*/
|
|
103
|
+
show: () => void;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Event payload delivered to `onPressAction` when an action is selected.
|
|
108
|
+
* Compatible with `@react-native-menu/menu`.
|
|
109
|
+
*/
|
|
110
|
+
export type NativeActionEvent = {
|
|
111
|
+
nativeEvent: {
|
|
112
|
+
/** Identifier of the pressed action: `action.id ?? action.title`. */
|
|
113
|
+
event: string;
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Props for the `MenuView` component.
|
|
119
|
+
* Drop-in compatible with `@react-native-menu/menu`.
|
|
120
|
+
*/
|
|
121
|
+
export type MenuComponentProps = {
|
|
122
|
+
/**
|
|
123
|
+
* Menu title shown at the top of the menu.
|
|
124
|
+
* @platform ios
|
|
125
|
+
*/
|
|
126
|
+
title?: string;
|
|
127
|
+
/**
|
|
128
|
+
* Callback invoked when a menu action is selected.
|
|
129
|
+
*/
|
|
130
|
+
onPressAction?: (event: NativeActionEvent) => void;
|
|
131
|
+
/**
|
|
132
|
+
* Callback invoked when the menu opens.
|
|
133
|
+
*
|
|
134
|
+
* On Android, fires when the trigger's tap/long-press flips `expanded` to `true`.
|
|
135
|
+
* On iOS, SwiftUI `Menu`/`ContextMenu` do not expose an open hook, so this is not
|
|
136
|
+
* fired there.
|
|
137
|
+
* @platform android
|
|
138
|
+
*/
|
|
139
|
+
onOpenMenu?: () => void;
|
|
140
|
+
/**
|
|
141
|
+
* Callback invoked when the menu closes (either via dismissal or after an action
|
|
142
|
+
* fires).
|
|
143
|
+
*
|
|
144
|
+
* On Android, fires from the controlled `DropdownMenu`'s dismiss path.
|
|
145
|
+
* On iOS, SwiftUI `Menu`/`ContextMenu` do not expose a close hook in a way we can
|
|
146
|
+
* forward, so this is not fired there.
|
|
147
|
+
* @platform android
|
|
148
|
+
*/
|
|
149
|
+
onCloseMenu?: () => void;
|
|
150
|
+
/**
|
|
151
|
+
* The actions to display in the menu.
|
|
152
|
+
*/
|
|
153
|
+
actions: MenuAction[];
|
|
154
|
+
/**
|
|
155
|
+
* When `true`, the menu opens on long-press of the trigger instead of a single tap.
|
|
156
|
+
* @default false
|
|
157
|
+
*/
|
|
158
|
+
shouldOpenOnLongPress?: boolean;
|
|
159
|
+
/**
|
|
160
|
+
* Style applied to the trigger wrapper.
|
|
161
|
+
*/
|
|
162
|
+
style?: StyleProp<ViewStyle>;
|
|
163
|
+
/**
|
|
164
|
+
* Test identifier passed through to the trigger view.
|
|
165
|
+
*/
|
|
166
|
+
testID?: string;
|
|
167
|
+
/**
|
|
168
|
+
* Trigger view. Long-pressing or tapping (per `shouldOpenOnLongPress`) opens the menu.
|
|
169
|
+
*/
|
|
170
|
+
children?: ReactNode;
|
|
171
|
+
};
|
|
@@ -291,6 +291,31 @@ export const clickable = (handler: () => void, options?: { indication?: boolean
|
|
|
291
291
|
indication: options?.indication ?? true,
|
|
292
292
|
});
|
|
293
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Makes the view respond to both click and long-click gestures.
|
|
296
|
+
* Wraps Compose's `Modifier.combinedClickable`. Useful for triggering a `DropdownMenu`
|
|
297
|
+
* on long-press while keeping a separate short-press action.
|
|
298
|
+
* @param handlers.onClick - Function to call on a short tap.
|
|
299
|
+
* @param handlers.onLongClick - Function to call on a long press.
|
|
300
|
+
* @param options - Optional configuration.
|
|
301
|
+
* @param options.indication - Whether to show a ripple indication. Defaults to `true`.
|
|
302
|
+
*/
|
|
303
|
+
export const combinedClickable = (
|
|
304
|
+
handlers: { onClick?: () => void; onLongClick?: () => void },
|
|
305
|
+
options?: { indication?: boolean }
|
|
306
|
+
) =>
|
|
307
|
+
createModifierWithEventListener(
|
|
308
|
+
'combinedClickable',
|
|
309
|
+
(params: { event: 'click' | 'longClick' }) => {
|
|
310
|
+
if (params.event === 'click') {
|
|
311
|
+
handlers.onClick?.();
|
|
312
|
+
} else if (params.event === 'longClick') {
|
|
313
|
+
handlers.onLongClick?.();
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
{ indication: options?.indication ?? true }
|
|
317
|
+
);
|
|
318
|
+
|
|
294
319
|
/**
|
|
295
320
|
* Makes the view selectable, like a radio button row.
|
|
296
321
|
* @param selected - Whether the item is currently selected.
|