fix(native): let Android menus open from their trigger

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
jakeandClaude Sonnet 5.5 committed 2026-10-05 13:33:15 -04:00
1 parent ccaacfeae5
commit 7770e1a697
5 files changed
+56 -17

No files matched your search

@@ -46,6 +46,7 @@ import { SettingsRow } from "@/components/settings/settings-row";
import { SettingsSection } from "@/components/settings/settings-section"; import { SettingsSection } from "@/components/settings/settings-section";
import { TmdbLogo } from "@/components/tmdb-logo"; import { TmdbLogo } from "@/components/tmdb-logo";
import { Image } from "@/components/ui/image"; import { Image } from "@/components/ui/image";
import { MenuTrigger } from "@/components/ui/menu-trigger";
import { ScaledIcon } from "@/components/ui/scaled-icon"; import { ScaledIcon } from "@/components/ui/scaled-icon";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
@@ -285,8 +286,7 @@ export default function SettingsScreen() {
} }
}} }}
> >
<Pressable <MenuTrigger
accessibilityRole="button"
accessibilityLabel={t`Edit profile photo`} accessibilityLabel={t`Edit profile photo`}
accessibilityHint={t`Opens options to change or remove your photo`} accessibilityHint={t`Opens options to change or remove your photo`}
className="mr-3" className="mr-3"
@@ -308,7 +308,7 @@ export default function SettingsScreen() {
<View className="bg-primary absolute right-0 bottom-0 size-[18px] items-center justify-center rounded-full"> <View className="bg-primary absolute right-0 bottom-0 size-[18px] items-center justify-center rounded-full">
<IconCamera size={10} color={primaryFgColor} /> <IconCamera size={10} color={primaryFgColor} />
</View> </View>
</Pressable> </MenuTrigger>
</MenuView> </MenuView>
) : ( ) : (
<Pressable <Pressable
@@ -1,8 +1,9 @@
import { MenuView } from "@expo/ui/community/menu"; import { MenuView } from "@expo/ui/community/menu";
import { useLingui } from "@lingui/react/macro"; import { useLingui } from "@lingui/react/macro";
import type { Icon } from "@tabler/icons-react-native"; import type { Icon } from "@tabler/icons-react-native";
import { Pressable, View } from "react-native"; import { View } from "react-native";
import { MenuTrigger } from "@/components/ui/menu-trigger";
import { ScaledIcon } from "@/components/ui/scaled-icon"; import { ScaledIcon } from "@/components/ui/scaled-icon";
import { Sparkline } from "@/components/ui/sparkline"; import { Sparkline } from "@/components/ui/sparkline";
import { Text } from "@/components/ui/text"; import { Text } from "@/components/ui/text";
@@ -96,13 +97,11 @@ export function StatsCard(props: StatsCardProps) {
const nextPeriod = periods.find((value) => value === nativeEvent.event); const nextPeriod = periods.find((value) => value === nativeEvent.event);
if (nextPeriod) onPeriodChange(nextPeriod); if (nextPeriod) onPeriodChange(nextPeriod);
}} }}
onOpenMenu={() => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)}
> >
<Pressable <MenuTrigger onPress={() => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)}>
onPress={() => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)}
accessibilityRole="button"
>
<CardInner {...props} /> <CardInner {...props} />
</Pressable> </MenuTrigger>
</MenuView> </MenuView>
</View> </View>
); );
+5 -4
View File
@@ -2,9 +2,10 @@ import { Icon } from "@expo/ui";
import { MenuView } from "@expo/ui/community/menu"; import { MenuView } from "@expo/ui/community/menu";
import { useLingui } from "@lingui/react/macro"; import { useLingui } from "@lingui/react/macro";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import { Alert, Pressable, View } from "react-native"; import { Alert, View } from "react-native";
import { Image } from "@/components/ui/image"; import { Image } from "@/components/ui/image";
import { MenuTrigger } from "@/components/ui/menu-trigger";
import { Text } from "@/components/ui/text"; import { Text } from "@/components/ui/text";
import { queryClient } from "@/lib/query-client"; import { queryClient } from "@/lib/query-client";
import { authClient } from "@/lib/server"; import { authClient } from "@/lib/server";
@@ -58,10 +59,10 @@ export function HeaderAvatar() {
]); ]);
} }
}} }}
onOpenMenu={() => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)}
> >
<Pressable <MenuTrigger
onPress={() => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)} onPress={() => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)}
accessibilityRole="button"
accessibilityLabel={t`User menu`} accessibilityLabel={t`User menu`}
hitSlop={8} hitSlop={8}
> >
@@ -84,7 +85,7 @@ export function HeaderAvatar() {
</View> </View>
)} )}
</View> </View>
</Pressable> </MenuTrigger>
</MenuView> </MenuView>
); );
} }
@@ -1,9 +1,9 @@
import { MenuView } from "@expo/ui/community/menu"; import { MenuView } from "@expo/ui/community/menu";
import { useLingui } from "@lingui/react/macro"; import { useLingui } from "@lingui/react/macro";
import { IconArrowsSort } from "@tabler/icons-react-native"; import { IconArrowsSort } from "@tabler/icons-react-native";
import { Pressable } from "react-native";
import { useCSSVariable } from "uniwind"; import { useCSSVariable } from "uniwind";
import { MenuTrigger } from "@/components/ui/menu-trigger";
import { ScaledIcon } from "@/components/ui/scaled-icon"; import { ScaledIcon } from "@/components/ui/scaled-icon";
import * as Haptics from "@/utils/haptics"; import * as Haptics from "@/utils/haptics";
@@ -49,15 +49,15 @@ export function SortMenu({ sortBy, sortDirection, onSortChange }: SortMenuProps)
); );
if (option) onSortChange(option.sortBy, option.sortDirection); if (option) onSortChange(option.sortBy, option.sortDirection);
}} }}
onOpenMenu={() => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)}
> >
<Pressable <MenuTrigger
onPress={() => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)} onPress={() => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)}
accessibilityRole="button"
accessibilityLabel={t`Sort`} accessibilityLabel={t`Sort`}
hitSlop={8} hitSlop={8}
> >
<ScaledIcon icon={IconArrowsSort} size={22} color={foregroundColor} /> <ScaledIcon icon={IconArrowsSort} size={22} color={foregroundColor} />
</Pressable> </MenuTrigger>
</MenuView> </MenuView>
); );
} }
@@ -0,0 +1,39 @@
import type { ReactNode } from "react";
import { Pressable, type PressableProps, View } from "react-native";
type MenuTriggerProps = Pick<
PressableProps,
| "onPress"
| "hitSlop"
| "accessibilityLabel"
| "accessibilityHint"
| "accessibilityRole"
| "className"
> & {
children: ReactNode;
};
/**
* Child for `MenuView`. On Android, MenuView opens from its own wrapping Pressable, so the
* trigger must not be touchable (an inner Pressable would swallow the tap). On iOS the SwiftUI
* Menu handles the tap natively, so keep a Pressable for press feedback.
*/
export function MenuTrigger({ children, onPress, hitSlop, ...a11y }: MenuTriggerProps) {
if (process.env.EXPO_OS === "android") {
return (
<View accessible {...a11y} accessibilityRole={a11y.accessibilityRole ?? "button"}>
{children}
</View>
);
}
return (
<Pressable
onPress={onPress}
hitSlop={hitSlop}
{...a11y}
accessibilityRole={a11y.accessibilityRole ?? "button"}
>
{children}
</Pressable>
);
}