Skip to content

Commit

Permalink
fix(iOS,Fabric): fix invalid position of FullWindowOverlay in certain…
Browse files Browse the repository at this point in the history
… scenarios (#2641)

## Description

Fixes #2631

I've given extensive description of both the issue ~and potential
solution~ in #2631 issue discussion

* #2631

In particular important parts are:

*
#2631 (comment)
*
#2631 (comment)

I settled down on zeroing origin of the `FullWindowOverlay` frame in
HostTree & setting `ShadowNodeTraits::RootNodeKind` for the custom
shadow node of `FullWindowOverlay` component in the ShadowTree. This is
much cleaner than managing the state & content offset manually.

## Changes

`FullWindowOverlay` has now custom component descriptor, shadow node &
shadow node state (its empty). The shadow node has
`ShadowNodeTraits::RootNodeKind` set allowing it to be the reference
point when computing position of any descendant view in shadow tree -
this is fine, because we always expect `FullWindowOverlay` to have
origin at `(0, 0)` in window coordinate space.

In the HostTree we now ensure that `FWO` origin is at `(0, 0)` by
overriding frame received during mounting stage.

## Test code and steps to reproduce

Test2631 should now work not as in recording from issue description but
correctly.

## Checklist

- [x] Included code example that can be used to test this change
- [x] Ensured that CI passes
  • Loading branch information
kkafar authored Jan 23, 2025
1 parent 4bfc959 commit 0f411ac
Show file tree
Hide file tree
Showing 11 changed files with 242 additions and 72 deletions.
1 change: 1 addition & 0 deletions android/src/main/jni/rnscreens.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include <react/renderer/components/rnscreens/RNSModalScreenComponentDescriptor.h>
#include <react/renderer/components/rnscreens/RNSScreenStackHeaderSubviewComponentDescriptor.h>
#include <react/renderer/components/rnscreens/RNSScreenStackHeaderConfigComponentDescriptor.h>
#include <react/renderer/components/rnscreens/RNSFullWindowOverlayComponentDescriptor.h>

namespace facebook {
namespace react {
Expand Down
72 changes: 72 additions & 0 deletions apps/src/shared/PressableWithFeedback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from 'react';
import { ForwardedRef } from 'react';
import { GestureResponderEvent, Pressable, PressableProps, StyleSheet, View } from 'react-native';

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

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

const onPressInCallback = React.useCallback((e: GestureResponderEvent) => {
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?.(e);
}, [props]);

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

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

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

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>

);
});

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

export default PressableWithFeedback;

74 changes: 3 additions & 71 deletions apps/src/tests/Test2466.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Header } from '@react-navigation/elements';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator, NativeStackNavigationProp } from '@react-navigation/native-stack';
import React, { ForwardedRef } from 'react';
import { findNodeHandle, GestureResponderEvent, Pressable, PressableProps, StyleSheet, Text, View } from 'react-native';
import React from 'react';
import { findNodeHandle, Text, View } from 'react-native';
import PressableWithFeedback from '../shared/PressableWithFeedback';

type StackParamList = {
Home: undefined,
Expand All @@ -12,63 +12,8 @@ type RouteProps = {
navigation: NativeStackNavigationProp<StackParamList>;
}

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


const Stack = createNativeStackNavigator<StackParamList>();

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

const onPressInCallback = React.useCallback((e: GestureResponderEvent) => {
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?.(e);
}, [props]);

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

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

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

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
Expand Down Expand Up @@ -139,17 +84,4 @@ function App(): React.JSX.Element {
);
}

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


export default App;
63 changes: 63 additions & 0 deletions apps/src/tests/Test2631.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import * as React from 'react';

import {Button, Pressable, StyleSheet, Text, View} from 'react-native';
import {FullWindowOverlay} from 'react-native-screens';
import PressableWithFeedback from '../shared/PressableWithFeedback';

function SharedPressable() {
return (
<PressableWithFeedback>
<View style={{ width: '100%', height: 32 }}>
<Text>Pressable</Text>
</View>
</PressableWithFeedback>
);
}

function HomeScreen() {
const [overlayShown, setOverlayShown] = React.useState(false);
return (
<View style={{flex: 1, alignItems: 'center', justifyContent: 'center'}} collapsable={true}>
<Text>Home Screen</Text>
<Button title="Show Overlay" onPress={() => setOverlayShown(true)} />
<SharedPressable />
{overlayShown && (
<FullWindowOverlay>
<View style={{flex: 1}}>
<Pressable
style={{
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(130, 200, 120, 0.8)',
}}
onPress={() => setOverlayShown(false)}>
<Text>Overlay</Text>
<SharedPressable />
</Pressable>
</View>
</FullWindowOverlay>
)}
</View>
);
}

function Header() {
return (
<View pointerEvents='box-none' style={{ height: 100.6666 }}>
<View style={[StyleSheet.absoluteFill, { }]}>
<View style={{ flex: 1, backgroundColor: 'white', opacity: 0.6}} />
</View>
</View>
);
}

export default function App() {
return (
<View style={{flex: 1, backgroundColor: 'lightsalmon' }}>
<Header />
<HomeScreen />
</View>
);
}

1 change: 1 addition & 0 deletions apps/src/tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export { default as Test2379 } from './Test2379';
export { default as Test2395 } from './Test2395';
export { default as Test2466 } from './Test2466';
export { default as Test2552 } from './Test2552';
export { default as Test2631 } from './Test2631';
export { default as TestScreenAnimation } from './TestScreenAnimation';
export { default as TestScreenAnimationV5 } from './TestScreenAnimationV5';
export { default as TestHeader } from './TestHeader';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#pragma once

#ifdef ANDROID
#include <fbjni/fbjni.h>
#endif // ANDROID
#include <react/renderer/core/ConcreteComponentDescriptor.h>
#include "RNSFullWindowOverlayShadowNode.h"

namespace facebook::react {

class RNSFullWindowOverlayComponentDescriptor final
: public ConcreteComponentDescriptor<RNSFullWindowOverlayShadowNode> {
public:
using ConcreteComponentDescriptor::ConcreteComponentDescriptor;
};

} // namespace facebook::react
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#include "RNSFullWindowOverlayShadowNode.h"

namespace facebook::react {

extern const char RNSFullWindowOverlayComponentName[] = "RNSFullWindowOverlay";

} // namespace facebook::react
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#pragma once

#include <jsi/jsi.h>
#include <react/renderer/components/rnscreens/EventEmitters.h>
#include <react/renderer/components/rnscreens/Props.h>
#include <react/renderer/components/view/ConcreteViewShadowNode.h>
#include <react/renderer/core/LayoutContext.h>
#include "RNSFullWindowOverlayState.h"

namespace facebook::react {

JSI_EXPORT extern const char RNSFullWindowOverlayComponentName[];

using ConcreteViewShadowNodeSuperType = ConcreteViewShadowNode<
RNSFullWindowOverlayComponentName,
RNSFullWindowOverlayProps,
RNSFullWindowOverlayEventEmitter,
RNSFullWindowOverlayState>;

class JSI_EXPORT RNSFullWindowOverlayShadowNode final
: public ConcreteViewShadowNodeSuperType {
public:
using ConcreteViewShadowNode::ConcreteViewShadowNode;
using StateData = ConcreteViewShadowNode::ConcreteStateData;

#if !defined(ANDROID)
static ShadowNodeTraits BaseTraits() {
auto traits = ConcreteViewShadowNodeSuperType::BaseTraits();
traits.set(ShadowNodeTraits::Trait::RootNodeKind);
return traits;
}
#endif
};

} // namespace facebook::react
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#pragma once

#if defined(ANDROID)
#include <folly/dynamic.h>
#include <react/renderer/mapbuffer/MapBuffer.h>
#include <react/renderer/mapbuffer/MapBufferBuilder.h>
#endif // ANDROID

#include <react/renderer/core/graphicsConversions.h>
#include <react/renderer/graphics/Float.h>

namespace facebook::react {

class JSI_EXPORT RNSFullWindowOverlayState final {
public:
using Shared = std::shared_ptr<const RNSFullWindowOverlayState>;

RNSFullWindowOverlayState() = default;

#if defined(ANDROID)
RNSFullWindowOverlayState(
const RNSFullWindowOverlayState &previousState,
folly::dynamic data) {}
folly::dynamic getDynamic() const {
return {};
}
#endif
};

} // namespace facebook::react
10 changes: 10 additions & 0 deletions ios/RNSFullWindowOverlay.mm
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#import <react/renderer/components/rnscreens/ComponentDescriptors.h>
#import <react/renderer/components/rnscreens/Props.h>
#import <react/renderer/components/rnscreens/RCTComponentViewHelpers.h>
#import <rnscreens/RNSFullWindowOverlayComponentDescriptor.h>
#else
#import <React/RCTTouchHandler.h>
#endif // RCT_NEW_ARCH_ENABLED
Expand Down Expand Up @@ -205,9 +206,18 @@ - (void)updateLayoutMetrics:(react::LayoutMetrics const &)layoutMetrics
oldLayoutMetrics:(react::LayoutMetrics const &)oldLayoutMetrics
{
CGRect frame = RCTCGRectFromRect(layoutMetrics.frame);

// Due to view flattening on new architecture there are situations
// when we receive frames with origin different from (0, 0).
// We account for this frame manipulation in shadow node by setting
// RootNodeKind trait for the shadow node making state consistent
// between Host & Shadow Tree
frame.origin = CGPointZero;

_reactFrame = frame;
[_container setFrame:frame];
}

RNS_IGNORE_SUPER_CALL_END

#else
Expand Down
4 changes: 3 additions & 1 deletion src/fabric/FullWindowOverlayNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ import type { ViewProps } from 'react-native';

interface NativeProps extends ViewProps {}

export default codegenNativeComponent<NativeProps>('RNSFullWindowOverlay', {});
export default codegenNativeComponent<NativeProps>('RNSFullWindowOverlay', {
interfaceOnly: true,
});

0 comments on commit 0f411ac

Please sign in to comment.