Conversation
kkafar
left a comment
There was a problem hiding this comment.
Currently blocked by regression detected in testing
|
|
||
| auto state = std::static_pointer_cast< | ||
| const RNSScreenStackHeaderSubviewShadowNode::ConcreteState>( | ||
| shadowNode.getState()); |
There was a problem hiding this comment.
The bug I've reported in earlier comment was caused by accessing mounted state and not the most recent one.
|
@kkafar Thanks for the quick fix, unfortunately I can't install v4.7.0 on IOS now.
Android works now, but it looks like it is adding some safe area insets again (as a black bar) to the bottom of my screen, when I'm navigating (default presentation) to some other root screen.
Just to let you quickly know... |
|
@marcel-happyfloat thanks for the report, however I do need more details to investigate any of the issues you mentioned. Could you provide a recording of the bug and preferably a snippet / detailed description of navigation hierarchy where I can reproduce the issue? |
When setting subviews via `setOptions` from `useEffect` hook in a component, the first frame received might be computed by native layout & completely invalid (zero height). RN layout is the source of a subview **size** (not origin). When we send such update with zero height Yoga might (or might not, depending on exact update timing in relation to other ongoing commits / layouts) set the subview height to 0! This causes the subview to become invisible & we want to avoid that. This had not been a problem before #2696, because we would filter out this kind of frame in the `setSize` guard in the `ComponentDescriptor.adopt` method of the `HeaderSubview`. #2696 allowed for zero-sized frames for other, unrelated reason & we must allow these as long as they come from React Native layout & not native one.
…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)
When setting subviews via `setOptions` from `useEffect` hook in a component, the first frame received might be computed by native layout & completely invalid (zero height). RN layout is the source of a subview **size** (not origin). When we send such update with zero height Yoga might (or might not, depending on exact update timing in relation to other ongoing commits / layouts) set the subview height to 0! This causes the subview to become invisible & we want to avoid that. This had not been a problem before #2696, because we would filter out this kind of frame in the `setSize` guard in the `ComponentDescriptor.adopt` method of the `HeaderSubview`. #2696 allowed for zero-sized frames for other, unrelated reason & we must allow these as long as they come from React Native layout & not native one.
…`setOptions` (#2812) ## Description * [x] Should be merged after #2811 & rebased. When setting subviews via `setOptions` from `useEffect` hook in a component, the first frame received might be computed by native layout & completely invalid (zero height). RN layout is the source of a subview **size** (not origin). When we send such update with zero height Yoga might (or might not, depending on exact update timing in relation to other ongoing commits / layouts) set the subview height to 0! This causes the subview to become invisible & we want to avoid that. This had not been a problem before #2696, because we would filter out this kind of frame in the `setSize` guard in the `ComponentDescriptor.adopt` method of the `HeaderSubview`. #2696 allowed for zero-sized frames for other, unrelated reason & we must allow these as long as they come from React Native layout & not native one. ## Changes We now filter these invalid frames on the side of HostTree, by detecting whether React has measured the subview or not. ## Test code and steps to reproduce I've tested the problem on slightly modified `Test2466`: <details> <summary>Code snippet</summary> ```tsx import { NavigationContainer } 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'; type StackParamList = { Home: undefined, } type RouteProps = { navigation: NativeStackNavigationProp<StackParamList>; } const Stack = createNativeStackNavigator<StackParamList>(); 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 }: RouteProps): React.JSX.Element { React.useEffect(() => { console.log('calling setOptions in useEffect'); navigation.setOptions({ //headerTitle: HeaderTitle, headerLeft: HeaderLeft, //headerRight: HeaderLeft, }); }, [navigation]); 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, //headerLeft: HeaderLeft, //headerRight: HeaderLeft, }} /> </Stack.Navigator> </NavigationContainer> ); } export default App; ``` </details> ## Checklist - [x] Included code example that can be used to test this change - [x] Ensured that CI passes (7d3205e)
Description
Closes #2675
Error mechanism
This one was tricky to figure out.
Few concurrent facts first:
statusBarTranslucentistruewe applypaddingTopto nativeToolbarto heighten the header.Header subview that represents the search bar has no content. Therefore native layout sets it frame to exactly (0, 0), but with origin
(x, toolbar.paddingTop).This frame -
((x, toolbar.paddingTop), (0, 0))is send to shadow node, where it is ignored, but the layout is triggered by subsequent header config update.Therefore Yoga resolves height of the subview to full height of the parent (usually
154 px = 40 dip). New layout metrics are sent to HostTree, where next native toolbar layoutdetermines toolbar size to be its content height + padding == subview height + paddingTop. This gets send to ShadowTree, then back to HostTree, native layout is triggered and another
paddingTopvalue is added to the overall header height, and so on.This is a regression introduced in 4.6.0 and limited to Fabric.
The fix is simple - accept the (0, 0) size for the subview in ShadowTree. We just need to use more reasonable value to denote the uninitialized frame -
{-1, -1}seems like a better choice asit is an invalid frame.
Changes
Test code and steps to reproduce
Test2675Tested on all combinations:
statusBarTranslucent: trueandfalse,Also tested iOS on the same example, because the code changes affect shared C++ code.
Checklist