-
-
Notifications
You must be signed in to change notification settings - Fork 630
iOS 26 header button extends itself intermittently #3834
Copy link
Copy link
Open
Labels
missing-reproThis issue need minimum repro scenarioThis issue need minimum repro scenarioplatform:iosIssue related to iOS part of the libraryIssue related to iOS part of the library
Description
Description
Using Expo 54 and react-native-screens 4.16.0. On iOS 26, the custom header button on the right, extends itself intermittently. It's hard to reproduce consistently, but happens quite frequently.
headerButton.tsx
import { tokens } from '@/design';
import { color } from '@/design/colors';
import React from 'react';
import { AccessibilityRole, Platform, StyleProp, View, ViewStyle } from 'react-native';
import { BorderlessButton, RectButton } from 'react-native-gesture-handler';
import { Pressable } from './Pressable';
import { isLiquidGlassAvailable } from 'expo-glass-effect';
interface HeaderButtonProps {
onPress?: () => void;
children: React.ReactNode;
testID?: string;
accessibilityRole?: AccessibilityRole;
accessibilityLabel?: string;
disabled?: boolean;
rippleShape?: 'circle' | 'rect';
style?: StyleProp<ViewStyle>;
}
const InnerContent = ({ children }: { children: React.ReactNode }) => (
<View style={{ flexDirection: 'row', alignItems: 'center', gap: tokens.spacing.xs }}>
{children}
</View>
);
export function HeaderButton({
onPress,
children,
testID,
accessibilityRole = 'button',
accessibilityLabel,
disabled = false,
rippleShape = 'circle',
style,
}: HeaderButtonProps) {
if (Platform.OS === 'android') {
if (rippleShape === 'rect') {
return (
<RectButton
onPress={onPress}
testID={testID}
accessibilityRole={accessibilityRole}
accessibilityLabel={accessibilityLabel}
enabled={!disabled}
rippleColor={color.androidHighlight}
style={[
{
paddingHorizontal: 6,
opacity: disabled ? 0.3 : 1,
minWidth: 48,
minHeight: 48,
borderRadius: tokens.radius.full,
justifyContent: 'center',
alignItems: 'center',
},
style,
]}
>
<InnerContent>{children}</InnerContent>
</RectButton>
);
}
return (
<BorderlessButton
onPress={onPress}
testID={testID}
accessibilityRole={accessibilityRole}
accessibilityLabel={accessibilityLabel}
enabled={!disabled}
rippleColor={color.androidHighlight}
hitSlop={16}
rippleRadius={20}
style={[
{
paddingHorizontal: 0,
opacity: disabled ? 0.3 : 1,
justifyContent: 'center',
alignItems: 'center',
},
style,
]}
>
<InnerContent>{children}</InnerContent>
</BorderlessButton>
);
}
return (
<Pressable
onPress={onPress}
testID={testID}
accessibilityRole={accessibilityRole}
accessibilityLabel={accessibilityLabel}
disabled={disabled}
haptic
hitSlop={8}
style={[
{
paddingHorizontal: isLiquidGlassAvailable() ? 6 : undefined,
opacity: disabled ? 0.3 : 1,
justifyContent: 'center',
alignItems: 'center',
},
style,
]}
>
<InnerContent>{children}</InnerContent>
</Pressable>
);
}
HeaderMenuButton.tsx
import { MenuAction, MenuView, NativeActionEvent } from '@react-native-menu/menu';
import { isLiquidGlassAvailable } from 'expo-glass-effect';
import React from 'react';
import { type AccessibilityRole, Platform, StyleSheet, View } from 'react-native';
import { HeaderButton } from './HeaderButton';
// Liquid Glass headers render icon-only actions with a noticeably smaller circular visual than the
// pre-iOS-26 header buttons. Matching that 36pt visual keeps the menu trigger aligned with the
// existing checkmark/save buttons while still constraining the native MenuView anchor.
const LIQUID_GLASS_HEADER_MENU_BUTTON_SIZE = 36;
type AccessibleMenuViewProps = React.ComponentProps<typeof MenuView> & {
accessibilityLabel?: string;
accessibilityRole?: AccessibilityRole;
};
// TODO: Remove this once @react-native-menu/menu exposes standard accessibility props in its TS types.
const AccessibleMenuView = MenuView as React.ComponentType<AccessibleMenuViewProps>;
interface HeaderMenuButtonProps {
actions: MenuAction[];
onPressAction: ({ nativeEvent }: NativeActionEvent) => void;
children: React.ReactNode;
testID?: string;
accessibilityLabel: string;
}
export function HeaderMenuButton({
actions,
onPressAction,
children,
testID,
accessibilityLabel,
}: HeaderMenuButtonProps) {
const shouldUseConstrainedAnchor = Platform.OS === 'ios' && isLiquidGlassAvailable();
return (
<AccessibleMenuView
accessibilityLabel={accessibilityLabel}
accessibilityRole="button"
testID={testID}
actions={actions}
onPressAction={onPressAction}
shouldOpenOnLongPress={false}
style={shouldUseConstrainedAnchor ? styles.menuAnchor : undefined}
>
<View
pointerEvents="none"
accessible={false}
accessibilityElementsHidden
importantForAccessibility="no-hide-descendants"
style={shouldUseConstrainedAnchor ? styles.content : undefined}
>
<HeaderButton
accessibilityLabel={accessibilityLabel}
style={shouldUseConstrainedAnchor ? styles.visualButton : undefined}
>
{children}
</HeaderButton>
</View>
</AccessibleMenuView>
);
}
const styles = StyleSheet.create({
menuAnchor: {
width: LIQUID_GLASS_HEADER_MENU_BUTTON_SIZE,
height: LIQUID_GLASS_HEADER_MENU_BUTTON_SIZE,
minWidth: LIQUID_GLASS_HEADER_MENU_BUTTON_SIZE,
minHeight: LIQUID_GLASS_HEADER_MENU_BUTTON_SIZE,
flexGrow: 0,
flexShrink: 0,
justifyContent: 'center',
alignItems: 'center',
},
content: {
width: '100%',
height: '100%',
justifyContent: 'center',
alignItems: 'center',
},
visualButton: {
width: '100%',
height: '100%',
paddingHorizontal: 0,
},
});
Steps to reproduce
Keep viewing from one screen to another until I see the issue. It's really intermittent.
Snack or a link to a repository
N/A
Screens version
4.16.0
React Native version
0.81.5
Platforms
iOS
JavaScript runtime
None
Workflow
None
Architecture
None
Build type
Release mode
Device
Real device
Device model
No response
Acknowledgements
Yes
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
missing-reproThis issue need minimum repro scenarioThis issue need minimum repro scenarioplatform:iosIssue related to iOS part of the libraryIssue related to iOS part of the library