(props: ToolTipMenuProps)
| 6 | import { buildMenu, lookupId } from './TooltipMenu.helpers'; |
| 7 | |
| 8 | const ToolTipMenu = (props: ToolTipMenuProps) => { |
| 9 | const { |
| 10 | title = '', |
| 11 | shouldOpenOnLongPress = true, |
| 12 | disabled = false, |
| 13 | onPress, |
| 14 | isButton = false, |
| 15 | buttonStyle, |
| 16 | onPressMenuItem, |
| 17 | children, |
| 18 | actions, |
| 19 | accessibilityLabel, |
| 20 | accessibilityHint, |
| 21 | accessibilityRole, |
| 22 | accessibilityState, |
| 23 | testID, |
| 24 | style, |
| 25 | enableAndroidRipple = true, |
| 26 | } = props; |
| 27 | |
| 28 | const { language } = useSettings(); |
| 29 | |
| 30 | const { items, ids } = useMemo(() => buildMenu(actions, Platform.OS as 'ios' | 'android'), [actions]); |
| 31 | |
| 32 | const handlePressMenuItem = useCallback( |
| 33 | (e: NativeSyntheticEvent<ContextMenuOnPressNativeEvent>) => { |
| 34 | const { name, indexPath, index } = e.nativeEvent; |
| 35 | const path = indexPath?.length ? indexPath : typeof index === 'number' ? [index] : []; |
| 36 | const id = lookupId(ids, path); |
| 37 | if (id !== undefined) onPressMenuItem(id); |
| 38 | else if (name) onPressMenuItem(name); // last-resort fallback |
| 39 | }, |
| 40 | [ids, onPressMenuItem], |
| 41 | ); |
| 42 | |
| 43 | if (disabled || actions.length === 0) return null; |
| 44 | |
| 45 | // The native ContextMenu is the single source of truth for opening the menu: |
| 46 | // - Android: ContextMenuView's GestureDetector handles tap (dropdown mode) |
| 47 | // and long-press, then opens the popup itself. |
| 48 | // - iOS: UIContextMenuInteraction is attached to the first React child, with |
| 49 | // `showsMenuAsPrimaryAction` for tap-to-open in dropdown mode. |
| 50 | // |
| 51 | // We wrap in a Pressable ONLY when the caller wants a separate `onPress` |
| 52 | // (a short-tap action that does something OTHER than open the menu). Adding |
| 53 | // any extra Pressable handler is unnecessary and on Android races with the |
| 54 | // native gesture detector — usePressability always returns true from |
| 55 | // onStartShouldSetResponder, so the JS responder system claims the touch |
| 56 | // and dispatches ACTION_CANCEL to the child native view, leaving the menu |
| 57 | // unopened. There is no escape hatch for that — Pressable cannot be |
| 58 | // configured to skip responder claiming. |
| 59 | // |
| 60 | // Trade-off: dropdown buttons without onPress (HeaderMenuButton et al.) |
| 61 | // get no Android ripple. The menu opening (≈100ms) is the feedback. We |
| 62 | // accept this rather than reintroduce the gesture-cancel race. |
| 63 | const wrapInPressable = Boolean(onPress); |
| 64 | |
| 65 | const buttonShellStyle: StyleProp<ViewStyle> = isButton ? styles.button : undefined; |
nothing calls this directly
no test coverage detected