Skip to content

fix: clicking on Pressable located in screen header #2466

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 46 commits into from
Jan 16, 2025

Conversation

coado
Copy link
Contributor

@coado coado commented Oct 29, 2024

Description

This PR fixes clicking on Pressables that are added to the native header. Previously, Yoga had incorrect information about the location of the content in the header.

The first step was to remove top: "-100%" style from the ScreenStackHeaderConfig which made Yoga think, that the content is pushed up by the size of its parent (Screen size).

The second step involved updating layoutMetrics of the RNSScreenStackHeaderConfigShadowNode. The entire app content is pushed down by the size of the header, so it also has an impact on the header config in Yoga metrics. To mitigate this, the origin of RNSScreenStackHeaderConfigShadowNode is decreased by the size of the header which will zero out eventually leaving the header content in the desired position. On iOS this position is actually moved by the top inset size, so we also have to take it into account when setting header config layout metrics.

Fixes #2219

Changes

Updated ScreenShadowNode to decrease origin.y of the HeaderConfigShadowNode by the size of the header. Added paddingTop to HeaderConfigState and set it as origin offset on iOS.

Screenshots / GIFs

Before

iOS Android
ios-before.mov
android-before.mov

After

iOs Android
ios-after.mov
android-after.mov

Test code and steps to reproduce

Tested on this example:

code
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator, NativeStackNavigationProp } from "@react-navigation/native-stack";
import React, { ForwardedRef, forwardRef } from "react";
import { findNodeHandle, Pressable, PressableProps, StyleSheet, Text, View, } from "react-native";

type StackParamList = {
  Home: undefined,
}

type RouteProps = {
  navigation: NativeStackNavigationProp<StackParamList>;
}

type PressableState = 'pressed-in' | 'pressed' | 'pressed-out'


const Stack = createNativeStackNavigator<StackParamList>();


const PressableWithFeedback = forwardRef((props: PressableProps, ref: ForwardedRef<View>): React.JSX.Element => {
  const [pressedState, setPressedState] = React.useState<PressableState>('pressed-out');

  const onPressInCallback = React.useCallback((e) => {
    console.log('Pressable onPressIn', {
      locationX: e.nativeEvent.locationX,
      locationY: e.nativeEvent.locationY,
      pageX: e.nativeEvent.pageX,
      pageY: e.nativeEvent.pageY,
    });
    setPressedState('pressed-in');
    props.onPressIn?.();
  }, []);

  const onPressCallback = React.useCallback(() => {
    console.log('Pressable onPress');
    setPressedState('pressed');
  }, []);

  const onPressOutCallback = React.useCallback(() => {
    console.log('Pressable onPressOut');
    setPressedState('pressed-out');
  }, []);

  const onResponderMoveCallback = React.useCallback(() => {
    console.log('Pressable onResponderMove');
  }, []);

  const contentsStyle = pressedState === 'pressed-out'
    ? styles.pressablePressedOut
    : (pressedState === 'pressed'
      ? styles.pressablePressed
      : styles.pressablePressedIn);

  return (
    <View ref={ref} style={[contentsStyle, { width: "100%"}]}>
      <Pressable
        onPressIn={onPressInCallback}
        onPress={onPressCallback}
        onPressOut={onPressOutCallback}
        onResponderMove={onResponderMoveCallback}
      >
        {props.children}
      </Pressable>
    </View>

  );
})

function HeaderTitle(): React.JSX.Element {

  return (
    <PressableWithFeedback
      onPressIn={() => {
        console.log('Pressable onPressIn')
      }}
      onPress={() => console.log('Pressable onPress')}
      onPressOut={() => console.log('Pressable onPressOut')}
      onResponderMove={() => console.log('Pressable onResponderMove')}
      ref={ref => {
        console.log(findNodeHandle(ref));
        ref?.measure((x, y, width, height, pageX, pageY) => {
          console.log('header component measure', { x, y, width, height, pageX, pageY });
        })
      }}
    >
      <View style={{ height: 40, justifyContent: 'center', alignItems: 'center' }}>
        <Text style={{ alignItems: 'center' }}>Regular Pressable</Text>
      </View>
    </PressableWithFeedback>
  )
}

function Home(_: RouteProps): React.JSX.Element {
  return (
    <View style={{ flex: 1, backgroundColor: 'rgba(0, 0, 0, .8)' }}
    >
      <View style={{ flex: 1, alignItems: 'center', marginTop: 48 }}>
        <PressableWithFeedback
          onPressIn={() => console.log('Pressable onPressIn')}
          onPress={() => console.log('Pressable onPress')}
          onPressOut={() => console.log('Pressable onPressOut')}
        >
          <View style={{ height: 40, width: 200, justifyContent: 'center', alignItems: 'center' }}>
            <Text style={{ alignItems: 'center' }}>Regular Pressable</Text>
          </View>
        </PressableWithFeedback>
      </View>
    </View>
  );
}

function App(): React.JSX.Element {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen
          name="Home"
          component={Home}
          options={{
            headerTitle: HeaderTitle,
          }}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

const styles = StyleSheet.create({
  pressablePressedIn: {
    backgroundColor: 'lightsalmon',
  },
  pressablePressed: {
    backgroundColor: 'crimson',
  },
  pressablePressedOut: {
    backgroundColor: 'lightseagreen',
  }
});


export default App;

@alduzy
Copy link
Contributor

alduzy commented Oct 30, 2024

Hi @coado I like this approach. However, I'm afraid in current form it may not respect the placement of the elements in regards to flex layout of the header. Have you tested it with smaller elements, headerLeft and/or headerRight for example?

You should be able to use the Element Inspector from the dev menu to inspect the actual placement of the pressables laid out by yoga. I remember using it in #2292

@kkafar kkafar self-requested a review October 30, 2024 11:51
@coado
Copy link
Contributor Author

coado commented Oct 31, 2024

Hi @coado I like this approach. However, I'm afraid in current form it may not respect the placement of the elements in regards to flex layout of the header. Have you tested it with smaller elements, headerLeft and/or headerRight for example?

You should be able to use the Element Inspector from the dev menu to inspect the actual placement of the pressables laid out by yoga. I remember using it in #2292

Hey @alduzy, thanks for the reply! This is how it looks when I set Pressable on headerRight only or on both:

right both

Please let me know if this is desired behaviour.

Also I've checked a placement using inspector as you proposed and it seems like Pressable boundary in Yoga is not perfectly aligned with what is displayed.

with-inspector

This is something that I will have a closer look at!

code
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator, NativeStackNavigationProp } from "@react-navigation/native-stack";
import React, { ForwardedRef, forwardRef } from "react";
import { findNodeHandle, Pressable, PressableProps, StyleSheet, Text, View, Button } from "react-native";

type StackParamList = {
  Home: undefined,
}

type RouteProps = {
  navigation: NativeStackNavigationProp<StackParamList>;
}

type PressableState = 'pressed-in' | 'pressed' | 'pressed-out'


const Stack = createNativeStackNavigator<StackParamList>();


const PressableWithFeedback = forwardRef((props: PressableProps, ref: ForwardedRef<View>): React.JSX.Element => {
  const [pressedState, setPressedState] = React.useState<PressableState>('pressed-out');

  const onPressInCallback = React.useCallback((e) => {
    console.log('Pressable onPressIn', {
      locationX: e.nativeEvent.locationX,
      locationY: e.nativeEvent.locationY,
      pageX: e.nativeEvent.pageX,
      pageY: e.nativeEvent.pageY,
    });
    setPressedState('pressed-in');
    props.onPressIn?.();
  }, []);

  const onPressCallback = React.useCallback(() => {
    console.log('Pressable onPress');
    setPressedState('pressed');
  }, []);

  const onPressOutCallback = React.useCallback(() => {
    console.log('Pressable onPressOut');
    setPressedState('pressed-out');
  }, []);

  const onResponderMoveCallback = React.useCallback(() => {
    console.log('Pressable onResponderMove');
  }, []);

  const contentsStyle = pressedState === 'pressed-out'
    ? styles.pressablePressedOut
    : (pressedState === 'pressed'
      ? styles.pressablePressed
      : styles.pressablePressedIn);

  return (
    <View ref={ref} style={[contentsStyle]}>
      <Pressable
        onPressIn={onPressInCallback}
        onPress={onPressCallback}
        onPressOut={onPressOutCallback}
        onResponderMove={onResponderMoveCallback}
      >
        {props.children}
      </Pressable>
    </View>

  );
})

function HeaderTitle(): React.JSX.Element {

  return (
    <PressableWithFeedback
      onPressIn={() => {
        console.log('Pressable onPressIn')
      }}
      onPress={() => console.log('Pressable onPress')}
      onPressOut={() => console.log('Pressable onPressOut')}
      onResponderMove={() => console.log('Pressable onResponderMove')}
      ref={ref => {
        console.log(findNodeHandle(ref));
        ref?.measure((x, y, width, height, pageX, pageY) => {
          console.log('header component measure', { x, y, width, height, pageX, pageY });
        })
      }}
    >
      <View style={{ height: 40, justifyContent: 'center', alignItems: 'center' }}>
        <Text style={{ alignItems: 'center' }}>Regular Pressable</Text>
      </View>
    </PressableWithFeedback>
  )
}

function Home(_: RouteProps): React.JSX.Element {
  return (
    <View style={{ flex: 1, backgroundColor: 'rgba(0, 0, 0, .8)' }}
    >
      <View style={{ flex: 1, alignItems: 'center', marginTop: 48 }}>
        <PressableWithFeedback
          onPressIn={() => console.log('Pressable onPressIn')}
          onPress={() => console.log('Pressable onPress')}
          onPressOut={() => console.log('Pressable onPressOut')}
        >
          <View style={{ height: 40, width: 200, justifyContent: 'center', alignItems: 'center' }}>
            <Text style={{ alignItems: 'center' }}>Regular Pressable</Text>
          </View>
        </PressableWithFeedback>
      </View>
    </View>
  );
}

function App(): React.JSX.Element {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen
          name="Home"
          component={Home}
          options={{
            // headerTitle: HeaderTitle,
            headerRight: HeaderTitle,
            // headerLeft: HeaderTitle
          }}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

const styles = StyleSheet.create({
  pressablePressedIn: {
    backgroundColor: 'lightsalmon',
  },
  pressablePressed: {
    backgroundColor: 'crimson',
  },
  pressablePressedOut: {
    backgroundColor: 'lightseagreen',
  }
});


export default App;

@coado
Copy link
Contributor Author

coado commented Oct 31, 2024

Actually, when the Pressable is set as the headerRight only it doesn't work 🤔

@jiroscripts
Copy link

Hello @coado

Any news on this issue?

I started upgrading my project to RN 0.76 and this issue is by far the most problematic

@coado
Copy link
Contributor Author

coado commented Nov 13, 2024

Hey @thibaultcapelli
Sorry for the delay. I will get back to this issue shortly.

@kkafar
Copy link
Member

kkafar commented Nov 13, 2024

I was just looking through the code and determined that the only reliable solution would be to update position of header elements in ShadowTree (ST) based on their position in HostTree (HT), i. e. you do send additional information on topinset now - maybe let's send whole frame instead and update headersubviews layout metrics in shadow node? I think this is only way to get this at least partially consistent.

@coado coado marked this pull request as draft November 14, 2024 14:31
@acrabb
Copy link

acrabb commented Nov 18, 2024

+1 for this issue 🙏

kkafar added a commit that referenced this pull request Jan 21, 2025
…g header subviews (#2623)

## Description

In #2466 I've introduced `RCTMountingTransactionObserving` for
`RNSScreenStackHeaderConfig` component.
Now we can use this to reduce amount of calls to
`updateViewControllerIfNeeded` (causing layout passes) when adding
subviews.

I'm not changing the `unmount` path of the code as we have some more
additional logic there which requires some more
careful consideration. 

This also aligns us with Paper implementation. Note that it has been
only implemented this way, because at the implementation time there was
no way
to run this code on transaction completion.

## Changes

`- [RNSScreenStackHeaderConfig updateViewControllerIfNeeded]` is now
called only once per transaction when adding subviews to header config.

## Test code and steps to reproduce

Test432, TestHeaderTitle, Test2552 - see that there are no regressions. 

> [!note]
During testing I've found that there is a bug with subview layout when
modifying subviews after first render. This is not a regression however.
Notice the wrong position of header right on video below 👇🏻 (Test432)



https://github.com/user-attachments/assets/7f8653e8-a7d9-4fb8-a875-182e7deb0495



## Checklist

- [x] Included code example that can be used to test this change
- [x] Ensured that CI passes
@JcbPrn
Copy link

JcbPrn commented Jan 22, 2025

@kkafar Thank you very much for the effort! Seems like header right still does not work on iPhone XS. I am using TouchableOpacity and 4.6.0-beta.0

@kkafar
Copy link
Member

kkafar commented Jan 22, 2025

@JcbPrn thanks for the report, I'll look into this. If you had capacity to provide me with snack/repo/snippet where I can reproduce the issue directly, that would be awesome.

@mikeswann
Copy link

mikeswann commented Feb 6, 2025

Now, while the changes with 4.6.0. seem to have fixed the pressable issue, I am discovering a new issue, when setting the header right button dynamically from within a component:

useFocusEffect(
    React.useCallback(() => {
      const stackNav = navigation.getParent();

      stackNav?.setOptions({
        title: ScreenName,
        headerRight: () => (
          <View
            style={{
              width: 20,
              height: 20,
              display: 'none',
            }}></View>
...

When using any kind of sized view, it works as expected on initial render, but after navigating (in my case to a different tab) and back, the header ~tripples in height, regardless of what the headerRight element is, or how its sized.

What could help identify the issue is, when setting the headerRight element to a view with display: none (see snippet) the header increases in height slowly, without stopping as far as i can see. I assume this has to do with some of the size/position refactoring i saw mentioned for this issue.

Using: Android, react-native 0.77.0, "react-native-screens": "^4.6.0", "@react-navigation/native": "^7.0.14", "@react-navigation/native-stack": "^7.2.0",

before nav:
image

after nav:
image

@kkafar
Copy link
Member

kkafar commented Feb 6, 2025

Thanks @mikeswann for the detailed report. I'll look into it.

@robertying
Copy link

robertying commented Feb 9, 2025

I'm on v4.6.0 and found an issue on Android only with ScreenStackHeaderConfig interfering with clicks. Essentially an invisible ScreenStackHeaderConfig is drawn on top of the screen content blocking clicks.

I was able to pin down the issue caused by this diff - top: '-100%',: https://github.com/software-mansion/react-native-screens/pull/2466/files#diff-bd1164595b04f44490738b8183f84a625c0e7552a4ae70bfefcdf3bca4d37fc7

This is what it looks like with the current v4.6.0 version from the layout inspector:

image

Clicking on the top half of the card doesn't register because of it.

Happy to provide more details in a separate issue if needed, but at first glance the cause is clear.

@kkafar kkafar self-assigned this Feb 9, 2025
@kkafar
Copy link
Member

kkafar commented Feb 10, 2025

Hey @robertying, I've tried to reproduce your setup and tested on both architectures (I see you're using Paper in the screenshot, though) but I do not confirm the issue.

image

Here's the snippet I've tested on:

Snippet
import { NavigationContainer, RouteProp } from '@react-navigation/native';
import { createNativeStackNavigator, NativeStackNavigationProp } from '@react-navigation/native-stack';
import React from 'react';
import { findNodeHandle, Text, View } from 'react-native';
import PressableWithFeedback from '../shared/PressableWithFeedback';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';

type StackParamList = {
  Home: { marginTop?: number },
  NestedTabsHost: undefined;
}

type RouteProps = {
  navigation: NativeStackNavigationProp<StackParamList>;
  route: RouteProp<StackParamList>;
}

const Stack = createNativeStackNavigator<StackParamList>();

const Tabs = createBottomTabNavigator();
const NestedStack = createNativeStackNavigator();

function NestedStackHost() {
  return (
    <NestedStack.Navigator>
      <NestedStack.Screen name="NestedHome" component={Home} initialParams={{ marginTop: 0 }} />
    </NestedStack.Navigator>
  );
}


function NestedTabsHost() {
  return (
    <Tabs.Navigator>
      <Tabs.Screen name="NestedStackHost" component={NestedStackHost} options={{ headerShown: false }} />
    </Tabs.Navigator>
  );
}


function HeaderTitle(): React.JSX.Element {
  return (
    <PressableWithFeedback
      onLayout={event => {
        const { x, y, width, height } = event.nativeEvent.layout;
        console.log('Title onLayout', { x, y, width, height });
      }}
      onPressIn={() => {
        console.log('Pressable onPressIn');
      }}
      onPress={() => console.log('Pressable onPress')}
      onPressOut={() => console.log('Pressable onPressOut')}
      onResponderMove={() => console.log('Pressable onResponderMove')}
      ref={node => {
        console.log(findNodeHandle(node));
        node?.measure((x, y, width, height, pageX, pageY) => {
          console.log('header component measure', { x, y, width, height, pageX, pageY });
        });
      }}
    >
      <View style={{ height: 40, justifyContent: 'center', alignItems: 'center' }}>
        <Text style={{ alignItems: 'center' }}>Regular Pressable</Text>
      </View>
    </PressableWithFeedback>
  );
}

function HeaderLeft(): React.JSX.Element {
  return (
    <HeaderTitle />
  );
}

function Home({ navigation, route }: RouteProps): React.JSX.Element {
  return (
    <View style={{ flex: 1, backgroundColor: 'rgba(0, 0, 0, .8)' }}
    >
      <View style={{ flex: 1, alignItems: 'center', marginTop: route.params?.marginTop ?? 48 }}>
        <PressableWithFeedback
          onPressIn={() => console.log('Pressable onPressIn')}
          onPress={() => console.log('Pressable onPress')}
          onPressOut={() => console.log('Pressable onPressOut')}
        >
          <View style={{ height: 40, width: 200, justifyContent: 'center', alignItems: 'center' }}>
            <Text style={{ alignItems: 'center' }}>Regular Pressable</Text>
          </View>
        </PressableWithFeedback>
        <PressableWithFeedback onPress={() => navigation.navigate('NestedTabsHost')}>
          <View style={{ height: 40, width: 200, justifyContent: 'center', alignItems: 'center' }}>
            <Text style={{ alignItems: 'center' }}>Show tabs</Text>
          </View>
        </PressableWithFeedback>
      </View>
    </View>
  );
}

function App(): React.JSX.Element {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen
          name="Home"
          component={Home}
          options={{
            headerTitle: HeaderTitle,
            headerLeft: HeaderLeft,
            headerRight: HeaderLeft,
          }}
        />
        <Stack.Screen name="NestedTabsHost" component={NestedTabsHost} options={{ headerShown: false }} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

export default App;

If you could modify it in such way that I can reproduce this issue & let me know that would be great. Preferably open a new ticket, but here is also fine.

@robertying
Copy link

robertying commented Feb 15, 2025

@kkafar sorry for the late response.

Can you try using this App.tsx? I think I pinned down the issue to the custom headerTitle component:

App.tsx
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
import {NavigationContainer} from '@react-navigation/native';
import {createNativeStackNavigator} from '@react-navigation/native-stack';
import {Text, View} from 'react-native';

const NoticeStackNavigator = createNativeStackNavigator();
const MainNavigator = createBottomTabNavigator();
const RootNavigator = createNativeStackNavigator();

const NoticeStack = () => (
  <NoticeStackNavigator.Navigator>
    <NoticeStackNavigator.Screen
      name="Notices"
      component={() => <View></View>}
      options={{
        headerTitle: () => <Text>Title</Text>,
      }}
    />
  </NoticeStackNavigator.Navigator>
);

const MainTab = () => (
  <MainNavigator.Navigator>
    <MainNavigator.Screen
      name="NoticeStack"
      component={NoticeStack}
      options={{
        title: 'Notices',
      }}
    />
  </MainNavigator.Navigator>
);

const App = () => {
  return (
    <NavigationContainer>
      <RootNavigator.Navigator>
        <RootNavigator.Screen name="MainTab" component={MainTab} />
      </RootNavigator.Navigator>
    </NavigationContainer>
  );
};

export default App;
image

@kamui545
Copy link

I believe this PR might have introduced regressions.

The ripple effect from my buttons inside of the header are now truncated (<HeaderButton> from @react-navigation/elements). It used to work on 4.4.0 and 4.5.0.

Screen.Recording.2025-02-19.at.4.20.19.PM.mov

kligarski pushed a commit that referenced this pull request Feb 27, 2025
Motivation:

working on #2466 right now & need these changes + this is also required
to fix the failing CI

Bumped versions of reanimated, gesture handler and safe-area-context in
both lib and the examples

:fingers_crossed: CI?

Okay, seems that Android part of the CI is fixed. iOS has some other,
yet undetermined problems. I do not see a regression on iOS part,
however.

- [x] (kinda ☝🏻) Ensured that CI passes

(cherry picked from commit 3555d23)
kkafar added a commit that referenced this pull request Mar 14, 2025
… the screen (#2781)

## Description

Fixes #2758

#2466 removed old workaround for header config blocking gestures - we
just set top: `-100%`
to place the headerconfig in the top of the screen effectively
preventing blocking.

#2466 removed these styles & frame correction is now applied directly in
shadow node. This is done fine
on Fabric, however the solution was not replicated on Paper.

## Changes

Beside padding information we now send native toolbar height to header
config shadow node on Paper.
This information is used there to offset the header config position by
this value.

Should solve the problem.

### Screenshots

| before | after |
| - | - |
| <img
src="https://github.com/user-attachments/assets/640cad09-584d-4983-af92-11ae68d49f9f"
alt="before" /> | <img
src="https://github.com/user-attachments/assets/de15190e-e999-495f-8a6d-abf7f375d468"
alt="after" /> |


## Test code and steps to reproduce

`Example` -> `Header options` -> click on the very top button (very top
of it). Previously the button was not effectively
clicked. Now it works.

## Checklist

- [ ] Included code example that can be used to test this change
- [ ] Updated TS types
- [ ] Updated documentation: <!-- For adding new props to native-stack
-->
- [ ]
https://github.com/software-mansion/react-native-screens/blob/main/guides/GUIDE_FOR_LIBRARY_AUTHORS.md
- [ ]
https://github.com/software-mansion/react-native-screens/blob/main/native-stack/README.md
- [ ]
https://github.com/software-mansion/react-native-screens/blob/main/src/types.tsx
- [ ]
https://github.com/software-mansion/react-native-screens/blob/main/src/native-stack/types.tsx
- [ ] Ensured that CI passes
kkafar added a commit that referenced this pull request Mar 18, 2025
…et` presentation (#2788)

## Description

This PR relates to new architecture only.

Currently, the pressables on form sheet lose focus on `move` action.
This is the same problem we had with `Pressables` in screen & header on
new architecture.
See: #2466

Basically the information about sheet position is different between
`ShadowTree` (ST) and `HostTree` (HT), which leads to losing focus due
to how pressables
are now handled on new architecture.

**Simplified description** of basically what happens when you click a
pressable on new arch is as follows (Android):

1. The gesture is detected after the host platform dispatches touch
event (touch events on Android are dispatched in top-down manner)
[(link)](https://github.com/facebook/react-native/blob/d6ca25b0c1a0aeed3507ec3c2f65e453e2700dc2/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSTouchDispatcher.java#L81-L105)
2. The JS responder is set & touch event dispatched - therefore
pressable is always clickable,
3. Moreover, the JS responder is [measured **IN THE
ST**](https://github.com/facebook/react-native/blob/d6ca25b0c1a0aeed3507ec3c2f65e453e2700dc2/packages/react-native/Libraries/Pressability/Pressability.js#L805-L810)
3. When you start moving your finger, the platform is still the source
of motion events, and target [coordinates are **read from HOST
TREE**](#2466)
4. Motion event (with platform measurement (HT)!!!) is then [compared in
JS with responder measurements based on
ST](https://github.com/facebook/react-native/blob/d6ca25b0c1a0aeed3507ec3c2f65e453e2700dc2/packages/react-native/Libraries/Pressability/Pressability.js#L831-L8750)

Therefore, any inconsistency here leads to responder losing focus.

Reference:

1.
[`TouchesHelper.createPointersArray`](https://github.com/facebook/react-native/blob/ac97177651cf369783ca93fac50c2824b484abef/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.kt#L37)
2. [Responder
measurement](https://github.com/facebook/react-native/blob/d6ca25b0c1a0aeed3507ec3c2f65e453e2700dc2/packages/react-native/Libraries/Pressability/Pressability.js#L805-L810)
3. [Gesture start detection
(Android)](https://github.com/facebook/react-native/blob/d6ca25b0c1a0aeed3507ec3c2f65e453e2700dc2/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSTouchDispatcher.java#L81-L105)
4. [Gesture move handling
(Android)](https://github.com/facebook/react-native/blob/d6ca25b0c1a0aeed3507ec3c2f65e453e2700dc2/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSTouchDispatcher.java#L137-L147)
5. [Checking whether native touch is in responder
region](https://github.com/facebook/react-native/blob/d6ca25b0c1a0aeed3507ec3c2f65e453e2700dc2/packages/react-native/Libraries/Pressability/Pressability.js#L831-L875)

What is bewildering is that it works on iOS, despite the fact, that e.g.
when using React inspector the views are completely out of place in
ShadowTree.
I haven't debugged the iOS part to the very bottom, but my suspicion is
as follows:

1. The form sheet on Android is mounted in subtree under react root
view, while on iOS it is mounted under separate `UITransitionView`
(different subtree of window),
2. additionally we use `RNSModalScreen` component on iOS, which has
shadow node with `RootNodeKind` (measurements in subtree of its shadow
node are done relative to it),
3. There is no root view / any react view above it -> native measurement
(done by react-native) might also think that its positioned at (0, 0)
(same as in shadow tree).

This is something to verify in the future. Opened ticked on the board
for it.

## Changes

The sheet now sends updates to the shadow tree at following moments:

1. layout (including initial one),
2. sheet detent change to any stable state (could consider here not
sending the update when the sheet is hidden, added to project board),

I'm not sending updates on every sheet move (`View.top` change), to
avoid clogging the JS thread (and later UI), whole advantage of our
sheet is that it feels smooth (given sensible Android device).
However, it could be considered in case of any further issues.

Please note, that between the event syncs or in case the JS thread is
blocked / clogged, this still will be a problem. However, any JS would
lag, not only pressables and it's not down to us.

Commits:

- **Prevent modal content from disappearing**
- **Add comment that createShadowNode is used only on old arch**
- **Add comment on threading on NativeProxy mechanisms**
- **Simplify code in RNSModalScreenShadowNode.cpp**
- **Make modal screen component selection more clear in Screen.tsx**
- **Add `headerHeight` to diff prop list when determining need for
shadow state update**
- **Add pressables to form sheet example!**
- **Send updates to shadow tree on form sheet layout changes in
appropriate moments**

### Recordings

| before | after |
| -- | -- |
| <video
src="https://github.com/user-attachments/assets/b5bbb149-61da-41d1-b6a4-73dd89eb1f92"
alt="before" /> | <video
src="https://github.com/user-attachments/assets/ce5c89c0-7215-4ebb-a347-fa50946308f4"
alt="after" /> |






## Test code and steps to reproduce

`TestFormSheet`, open the sheet, play with it & see that pressables
work!

## Checklist

- [x] Included code example that can be used to test this change
- [ ] Ensured that CI passes
kkafar added a commit that referenced this pull request Mar 27, 2025
…er subview (#2811)

## Description

When setting header subviews from the rendered component via
`setOptions` the native header enlarges (see the "before" video below
:point_down:).

### Bug mechanism

When `HeaderSubview` is set from `setOptions` its mounted after first
render has happened.
It means that `HeaderConfig` has already been laid out and possibly its
shadow state got
updated with its size. Now, when first layout for the `HeaderSubview` is
computed Yoga
will stretch-fit `HeaderSubview` height to fill available space - the
`HeaderSubview` will
receive height equal to the height of the `HeaderConfig`. Such frame
will be then send
to HostTree, triggering native header layout, which will expand to make
enough space for
such high `HeaderSubview` & additional padding. Thanks to #2696 the
update cycle will be broken
& the issue described in #2675 won't happen.

Note that there is no such buggy behaviour in case the `HeaderSubviews`
is passed directly as option
when defining a `Screen`. This is because Yoga resolves the
`childHeight` (`HeaderSubview` height)
differently depending on whether `containingNode`'s height
(`HeaderConfig`'s height) is defined upfront or not.
When the `containingNode` height is not known (case of initial render
with `HeaderSubview` present) the Yoga will
use `FitContent` or `MaxContent` (not sure here)
[`SizingMode`](https://github.com/facebook/yoga/blob/51e6095005fd713dbfcbaf2c6296009de782d966/yoga/algorithm/SizingMode.h#L21-L45).
In cases, it is known (`HeaderConfig` has received state from HT, case
of `HeaderSubview` rendered via `setOptions`) - `StretchFit` will be
used for some reason (taking into consideration all layout options,
including flex
direction which is `row` for both `HeaderConfig` and `HeaderSubview`).

I believe this regression has been introduced in #2466, where we added
state updates for `HeaderConfig`
and `HeaderSubviews`. We need these state updates though, however it
seems that we do not need to inform Yoga
with `HeaderConfig` height & therefore avoid this layout problem.

### Debugging trail 

It seems that the `SizingMode` for laying out `HeaderSubview` is
determined
[here.](https://github.com/facebook/yoga/blob/51e6095005fd713dbfcbaf2c6296009de782d966/yoga/algorithm/CalculateLayout.cpp#L208-L214),
which is being called from
[`computeFlexBasisForChild`](https://github.com/facebook/yoga/blob/main/yoga/algorithm/CalculateLayout.cpp#L66).
The `resolveChildAlignment` method returns there `Align::Strech` and
this leads to `SizingMode::StretchFit` being used later on when
measuring/laying out `HeaderSubview`.


### Recordings

| before | after |
| -- | -- |
| <video
src="https://github.com/user-attachments/assets/f74039f5-919f-4e28-a56d-ebb360b6ce3a"
alt="before" /> | <video
src="https://github.com/user-attachments/assets/a9bab5f8-8176-4d6d-a93e-cdd5f8e5cca3"
alt="after" /> |

## Changes

Now, we do set only width & horizontal padding of the `HeaderConfig`.
`YGUndefined` is passed as height argument to `setSize` call.

## Test code and steps to reproduce

Added `Test2811` that allows to test these changes directly. We need to
also check following test cases for regressions:

* [x] #2675 Infinite state update loop
* [x] #2466 Pressables in header
* [x] `TestHeaderTitle` Header title truncation
* [x] `TestHeaderTitle` Header spacing when changing orientation

> [!warning]
> There is a issue when header elements set in `setOptions` disappear /
become invisible. [link to internal
board](https://github.com/orgs/software-mansion/projects/3/views/1?pane=issue&itemId=103681490).
It seems that it happens also on 4.9.2 and therefore is not a
regression. However it is bad & must be fixed before stable release.

## Checklist

- [x] Included code example that can be used to test this change
- [x] Ensured that CI passes
(db55977)
@Michota
Copy link

Michota commented Apr 19, 2025

this issue has came back to me, on both 4.6 and 4.10.0, once I placed MaterialTopTabs navigator as parent of layout that used stack navigator to display buttons in header.

kkafar added a commit that referenced this pull request May 7, 2025
…es (#2905)

## Description

Regression most likely introduced in 4.5.0 by #2466.
Fixes #2714
Fixes #2815
Supersedes #2845


This is a ugly hack to workaround issue with dynamic content change.
When the size of this shadow node contents (children) change due to JS
update, e.g. new icon is added, if the size is set for the yogaNode
corresponding to this shadow node, the enforced size will be used
and the size won't be updated by Yoga to reflect the contents size
change -> host tree won't get layout metrics update -> we won't trigger
native
layout -> the views in header will be positioned incorrectly.

> [!important]
> This PR handles iOS only, however there is **similar** issue on
Android. The issue can be reproduced on the same test example. Android
will be handled in separate PR.

## Changes

## Test code and steps to reproduce

In this approach I've settled with:

1. not calling set size on iOS for
`RNSScreenStackHeaderSubviewShadowNode`,
2. updating header config padding & sending it as state to shadow tree.

Added `Test2714`

Most of the fragile header interactions must be tested:

* [x] Header title truncation - `TestHeaderTitle` ~❌ This PR introduces
regression here on iOS (Android not tested yet)~ ✅ Works
* [x] Pressables in header - `Test2466` (iOS works, Android code is
unmodified here)
* [x] #2807
(this PR does not touch Android)
* [x] #2811
(this PR does not touch Android)
* [x] #2812
(this PR does not touch Android)
* [x] Header behaviour on orientation changes -
#2756 (this
PR does not touch Android)
* [x] New test `Test2714` handling header item resizing.
## Checklist

- [x] Included code example that can be used to test this change
- [ ] Ensured that CI passes
kkafar added a commit that referenced this pull request May 8, 2025
…hanges (#2910)

## Description

Fixes
#2714 on
Android
Fixes
#2815 on
Android

See #2905 for detailed description.

## Changes

Removed call to `RNSScreenStaceaderSubviewShadowNode.setSize` in
corresponding component descriptor.

It seems that we do not need to enforce node size from HostTree. Setting
appropriate content offset is enough for pressables to function
correctly (assuming that native layout **does not change size of any
subview**). I currently can't come up with any scenario where this would
happen.

## Test code and steps to reproduce

I've tested:

* [x] `Test2714` introduced in PR with iOS fixes -
#2905
* [x] Pressables in header -
#2466,
* [x] Header title truncation -
#2325 (only
few cases, as the list is long there) & noticed a regression (not
related to this PR, described in comment below the PR description),
* [x] Insets with orientation change (Android) -
#2756
* [x] #2807
(on `TestHeaderTitle` with call to `setOptions` in `useLayoutEffect`)
* [x] `Test2811` -
#2811
* [x] #2812
(with snippet provided in that PR description)



## Checklist

- [x] Included code example that can be used to test this change
- [x] Ensured that CI passes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Pressable elements not working in native-stack on Android devices with new architecture