REF: Update tooltip

This commit is contained in:
Marcos Rodriguez 2026-03-27 23:40:23 -05:00 committed by Overtorment
parent adc52b53c7
commit 2172d4a20b
7 changed files with 90 additions and 103 deletions

View File

@ -1,6 +1,6 @@
import React, { useCallback, useMemo, useRef } from 'react';
import { Platform, Pressable, StyleSheet, ViewStyle } from 'react-native';
import { MenuView, MenuAction, NativeActionEvent } from '@react-native-menu/menu';
import ContextMenu, { ContextMenuAction } from 'react-native-context-menu-view';
import { ToolTipMenuProps, Action } from './types';
import { useSettings } from '../hooks/context/useSettings';
@ -23,79 +23,73 @@ const ToolTipMenu = (props: ToolTipMenuProps) => {
onMenuWillShow,
onMenuWillHide,
enableAndroidRipple = true,
enableIOSPressOpacity = false,
} = props;
const { language } = useSettings();
const openedRef = useRef(false);
const menuRef = useRef<any>(null);
const normalizeMenuState = useCallback((menuState?: Action['menuState']): MenuAction['state'] | undefined => {
const handleMenuWillShow = useCallback(() => {
if (openedRef.current) {
return;
}
openedRef.current = true;
onMenuWillShow?.();
}, [onMenuWillShow]);
const normalizeMenuState = useCallback((menuState?: Action['menuState']): boolean | undefined => {
if (menuState === undefined) {
return undefined;
}
if (menuState === 'mixed') {
return 'mixed';
return true;
}
return menuState ? 'on' : 'off';
}, []);
const buildAttributes = useCallback((action: Action): MenuAction['attributes'] | undefined => {
const attributes = {
destructive: Boolean(action.destructive),
disabled: Boolean(action.disabled),
hidden: Boolean(action.hidden),
};
if (!attributes.destructive && !attributes.disabled && !attributes.hidden) {
return undefined;
}
return attributes;
return Boolean(menuState);
}, []);
const mapMenuItemForMenuView = useCallback(
(action: Action): MenuAction | null => {
if (!action?.id) return null;
(action: Action): ContextMenuAction | null => {
if (!action?.id || action.hidden) return null;
const mappedSubactions = (action.subactions || [])
.map(subaction => mapMenuItemForMenuView(subaction))
.filter((item): item is MenuAction => item !== null);
.filter((item): item is ContextMenuAction => item !== null);
const menuItem: MenuAction = {
id: action.id.toString(),
const menuItem: ContextMenuAction = {
title: action.text,
subtitle: action.subtitle,
image: action.icon?.iconValue ?? action.image,
imageColor: action.imageColor,
attributes: buildAttributes(action),
displayInline: Platform.OS === 'ios' ? action.displayInline : undefined,
systemIcon: Platform.OS === 'ios' ? action.icon?.iconValue ?? action.image : undefined,
icon: Platform.OS === 'android' ? action.icon?.iconValue ?? action.image : undefined,
iconColor: typeof action.imageColor === 'string' ? action.imageColor : undefined,
destructive: Boolean(action.destructive),
disabled: Boolean(action.disabled),
inlineChildren: Platform.OS === 'ios' ? action.displayInline : undefined,
};
const state = normalizeMenuState(action.menuState);
if (state) {
menuItem.state = state;
const selected = normalizeMenuState(action.menuState);
if (selected !== undefined) {
menuItem.selected = selected;
}
if (mappedSubactions.length > 0) {
menuItem.subactions = mappedSubactions;
menuItem.actions = mappedSubactions;
}
return menuItem;
},
[buildAttributes, normalizeMenuState],
[normalizeMenuState],
);
const menuViewItemsIOS = useMemo(() => {
return actions
.map(actionGroup => {
if (Array.isArray(actionGroup) && actionGroup.length > 0) {
const inlineActions = actionGroup.map(mapMenuItemForMenuView).filter((item): item is MenuAction => item !== null);
const inlineActions = actionGroup.map(mapMenuItemForMenuView).filter((item): item is ContextMenuAction => item !== null);
if (inlineActions.length === 0) return null;
const group: MenuAction = {
id: inlineActions[0].id,
const group: ContextMenuAction = {
title: '',
subactions: inlineActions,
displayInline: true,
actions: inlineActions,
inlineChildren: true,
};
return group;
}
@ -106,18 +100,20 @@ const ToolTipMenu = (props: ToolTipMenuProps) => {
return null;
})
.filter((item): item is MenuAction => item !== null);
.filter((item): item is ContextMenuAction => item !== null);
}, [actions, mapMenuItemForMenuView]);
const menuViewItemsAndroid = useMemo(() => {
const mergedActions = actions.flat().filter(action => action.id);
return mergedActions.map(mapMenuItemForMenuView).filter((item): item is MenuAction => item !== null);
const mergedActions = actions.flat().filter(action => action.id && !action.hidden);
return mergedActions.map(mapMenuItemForMenuView).filter((item): item is ContextMenuAction => item !== null);
}, [actions, mapMenuItemForMenuView]);
const handlePressMenuItemForMenuView = ({ nativeEvent }: NativeActionEvent) => {
if (nativeEvent?.event) {
onPressMenuItem(nativeEvent.event);
const handlePressMenuItemForMenuView = ({ nativeEvent }: { nativeEvent: { name?: string } }) => {
if (nativeEvent?.name) {
onPressMenuItem(nativeEvent.name);
}
openedRef.current = false;
onMenuWillHide?.();
};
const renderMenuView = () => {
@ -137,12 +133,18 @@ const ToolTipMenu = (props: ToolTipMenuProps) => {
base.push(buttonStyle);
}
}
if (pressed && enableAndroidRipple) base.push(styles.pressed);
// Keep visual feedback on Android by default. iOS context-menu preview
// already applies a system press effect; opt in when needed.
const shouldApplyPressedStyle =
pressed &&
((Platform.OS === 'android' && enableAndroidRipple) || (Platform.OS === 'ios' && enableIOSPressOpacity));
if (shouldApplyPressedStyle) base.push(styles.pressed);
return base;
}}
disabled={disabled}
onPress={onPress}
onLongPress={shouldOpenOnLongPress ? () => {} : undefined}
onPressIn={!shouldOpenOnLongPress ? handleMenuWillShow : undefined}
onLongPress={shouldOpenOnLongPress ? handleMenuWillShow : undefined}
accessibilityLabel={accessibilityLabel}
accessibilityHint={accessibilityHint}
accessibilityRole={accessibilityRole}
@ -151,28 +153,24 @@ const ToolTipMenu = (props: ToolTipMenuProps) => {
testID={testID}
hitSlop={8}
>
<MenuView
ref={menuRef}
<ContextMenu
title={title}
isAnchoredToRight
onOpenMenu={() => {
openedRef.current = true;
onMenuWillShow?.();
}}
onCloseMenu={() => {
previewBackgroundColor="transparent"
onPress={handlePressMenuItemForMenuView}
onCancel={() => {
if (!openedRef.current) {
return;
}
openedRef.current = false;
onMenuWillHide?.();
}}
onPressAction={handlePressMenuItemForMenuView}
actions={Platform.OS === 'ios' ? menuViewItemsIOS : menuViewItemsAndroid}
shouldOpenOnLongPress={shouldOpenOnLongPress}
dropdownMenuMode={!shouldOpenOnLongPress}
disabled={disabled}
style={buttonStyle ? styles.menuViewFlex : undefined}
>
{children}
</MenuView>
</ContextMenu>
</Pressable>
);
};

View File

@ -452,7 +452,7 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = memo(
accessibilityLabel={`${transactionTypeLabel}, ${amountWithUnit}, ${subtitle ?? title}`}
accessibilityRole="button"
>
{/* @ts-ignore - MenuView types can be overly strict about child element props */}
{/* @ts-ignore - Context menu wrapper types can be overly strict about child element props */}
<ListItem
leftAvatar={avatar}
title={listTitle}

View File

@ -22,6 +22,7 @@ export interface ToolTipMenuProps {
actions: Action[] | Action[][];
children: React.ReactNode;
enableAndroidRipple?: boolean;
enableIOSPressOpacity?: boolean;
dismissMenu?: () => void;
onPressMenuItem: (id: string) => void;
title?: string;

View File

@ -1987,6 +1987,8 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- react-native-context-menu-view (1.21.0):
- React
- react-native-document-picker (12.0.1):
- boost
- DoubleConversion
@ -2045,34 +2047,6 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- react-native-menu (2.0.0):
- boost
- DoubleConversion
- fast_float
- fmt
- glog
- hermes-engine
- RCT-Folly
- RCT-Folly/Fabric
- RCTRequired
- RCTTypeSafety
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-jsi
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- react-native-notifications (5.2.2):
- React-Core
- react-native-safe-area-context (5.7.0):
@ -3354,10 +3328,10 @@ DEPENDENCIES:
- react-native-blue-crypto (from `../node_modules/react-native-blue-crypto`)
- react-native-bw-file-access (from `../blue_modules/react-native-bw-file-access`)
- react-native-capture-protection (from `../node_modules/react-native-capture-protection`)
- react-native-context-menu-view (from `../node_modules/react-native-context-menu-view`)
- "react-native-document-picker (from `../node_modules/@react-native-documents/picker`)"
- react-native-get-random-values (from `../node_modules/react-native-get-random-values`)
- react-native-image-picker (from `../node_modules/react-native-image-picker`)
- "react-native-menu (from `../node_modules/@react-native-menu/menu`)"
- react-native-notifications (from `../node_modules/react-native-notifications`)
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- react-native-secure-key-store (from `../node_modules/react-native-secure-key-store`)
@ -3534,14 +3508,14 @@ EXTERNAL SOURCES:
:path: "../blue_modules/react-native-bw-file-access"
react-native-capture-protection:
:path: "../node_modules/react-native-capture-protection"
react-native-context-menu-view:
:path: "../node_modules/react-native-context-menu-view"
react-native-document-picker:
:path: "../node_modules/@react-native-documents/picker"
react-native-get-random-values:
:path: "../node_modules/react-native-get-random-values"
react-native-image-picker:
:path: "../node_modules/react-native-image-picker"
react-native-menu:
:path: "../node_modules/@react-native-menu/menu"
react-native-notifications:
:path: "../node_modules/react-native-notifications"
react-native-safe-area-context:
@ -3683,7 +3657,7 @@ SPEC CHECKSUMS:
FBLazyVector: 309703e71d3f2f1ed7dc7889d58309c9d77a95a4
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
hermes-engine: 1195b00d0f49a367b01a38c87b43227542fc3532
hermes-engine: b6ee973c4fd6366874d9aabcaf396d53475038eb
lottie-ios: 8f959969761e9c45d70353667d00af0e5b9cadb3
lottie-react-native: 983fd0489530e8d40f173de7f04e2f88b9317a15
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
@ -3726,10 +3700,10 @@ SPEC CHECKSUMS:
react-native-blue-crypto: de5babd59b17fbf3fc31d2e1e5d59ec859093fbc
react-native-bw-file-access: fe925b77dbf48500df0b294c6851f8c84607a203
react-native-capture-protection: 6d8d6a714114decb938d029f88e8df3da636e996
react-native-context-menu-view: da39a4612c1294d651907162529cfba8421e9b89
react-native-document-picker: dc2d83366e47e89e7c51e8a41eab99c1d54e941c
react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba
react-native-image-picker: 0314366753615115fa55c3cc937ac44cb7e75702
react-native-menu: 8d2c831a735c9e6528c28b26ca14e57591d87e14
react-native-notifications: e2d3c022d6077de7e420ba5c01b4bd9464f3941d
react-native-safe-area-context: befb5404eb8a16fdc07fa2bebab3568ecabcbb8a
react-native-secure-key-store: eb45b44bdec3f48e9be5cdfca0f49ddf64892ea6

21
package-lock.json generated
View File

@ -25,7 +25,6 @@
"@react-native-community/cli-platform-android": "20.0.2",
"@react-native-community/cli-platform-ios": "20.0.2",
"@react-native-documents/picker": "12.0.1",
"@react-native-menu/menu": "2.0.0",
"@react-native-vector-icons/entypo": "12.4.2",
"@react-native-vector-icons/fontawesome": "12.4.2",
"@react-native-vector-icons/fontawesome6": "12.3.2",
@ -76,6 +75,7 @@
"react-native-blue-crypto": "github:BlueWallet/react-native-blue-crypto#3cb5442",
"react-native-camera-kit-no-google": "16.2.0",
"react-native-capture-protection": "github:BlueWallet/react-native-capture-protection#bb78a40",
"react-native-context-menu-view": "github:BlueWallet/react-native-context-menu-view#main",
"react-native-default-preference": "https://github.com/BlueWallet/react-native-default-preference.git#6338a1f1235e4130b8cfc2dd3b53015eeff2870c",
"react-native-device-info": "14.1.1",
"react-native-draggable-flatlist": "4.0.3",
@ -3624,16 +3624,6 @@
"react-native": ">=0.79.0"
}
},
"node_modules/@react-native-menu/menu": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@react-native-menu/menu/-/menu-2.0.0.tgz",
"integrity": "sha512-hb8Mirw6aKPGONhgo52IiNpwHtISVrgCT3rMdFX1qS7eOFNzOcQh8d2UDnaH5zVpxN+QuvWtaaiRMGFpIjzdtA==",
"license": "MIT",
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/@react-native-vector-icons/common": {
"version": "12.4.1",
"resolved": "https://registry.npmjs.org/@react-native-vector-icons/common/-/common-12.4.1.tgz",
@ -15989,6 +15979,15 @@
}
}
},
"node_modules/react-native-context-menu-view": {
"version": "1.21.0",
"resolved": "git+ssh://git@github.com/BlueWallet/react-native-context-menu-view.git#144110b02afdb11b431741aef5da95e91b942a9b",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.1 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-native": ">=0.60.0-rc.0 <1.0.x"
}
},
"node_modules/react-native-default-preference": {
"version": "1.5.1",
"resolved": "git+ssh://git@github.com/BlueWallet/react-native-default-preference.git#6338a1f1235e4130b8cfc2dd3b53015eeff2870c",

View File

@ -107,7 +107,7 @@
"@react-native-community/cli-platform-android": "20.0.2",
"@react-native-community/cli-platform-ios": "20.0.2",
"@react-native-documents/picker": "12.0.1",
"@react-native-menu/menu": "2.0.0",
"react-native-context-menu-view": "github:BlueWallet/react-native-context-menu-view#main",
"@react-native-vector-icons/entypo": "12.4.2",
"@react-native-vector-icons/fontawesome": "12.4.2",
"@react-native-vector-icons/fontawesome6": "12.3.2",

View File

@ -1,8 +1,23 @@
const path = require('path');
module.exports = {
project: {
android: {},
ios: {},
},
dependencies: {
'react-native-context-menu-view': {
root: path.resolve(__dirname, 'node_modules/react-native-context-menu-view'),
platforms: {
ios: {
podspecPath: path.resolve(__dirname, 'node_modules/react-native-context-menu-view/react-native-context-menu-view.podspec'),
},
android: {
sourceDir: path.resolve(__dirname, 'node_modules/react-native-context-menu-view/android'),
},
},
},
},
codegenConfig: {
name: 'BlueWalletSpec',
type: 'all',