Skip to content

Commit 676cb64

Browse files
committed
Merge remote-tracking branch 'upstream/main' into fix/ios-textinput-ime-composition
2 parents 7d79cdb + 540120a commit 676cb64

60 files changed

Lines changed: 2923 additions & 1406 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/react-native/Libraries/Animated/__tests__/AnimatedBackendSuspense-itest.js

Lines changed: 592 additions & 0 deletions
Large diffs are not rendered by default.

packages/react-native/Libraries/Image/Image.android.js

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ import type {HostInstance} from '../../src/private/types/HostInstance';
1212
import type {ImageStyleProp} from '../StyleSheet/StyleSheet';
1313
import type {RootTag} from '../Types/RootTagTypes';
1414
import type {ImageProps} from './ImageProps';
15+
import type {ImageSourceHeaders} from './ImageSourceUtils';
1516
import type {AbstractImageAndroid, ImageAndroid} from './ImageTypes.flow';
1617

18+
import * as ReactNativeFeatureFlags from '../../src/private/featureflags/ReactNativeFeatureFlags';
1719
import flattenStyle from '../StyleSheet/flattenStyle';
1820
import StyleSheet from '../StyleSheet/StyleSheet';
1921
import ImageAnalyticsTagContext from './ImageAnalyticsTagContext';
@@ -194,37 +196,55 @@ let BaseImage: AbstractImageAndroid = ({
194196
);
195197
}
196198

197-
const nativeProps = restProps as {
198-
...React.PropsOf<ImageViewNativeComponent>,
199-
};
200-
199+
let style_: ImageStyleProp;
200+
let sources_;
201+
let headers_: ?ImageSourceHeaders;
201202
if (Array.isArray(source_)) {
202203
const {
203204
headers: sourceHeaders,
204205
width: sourceWidth,
205206
height: sourceHeight,
206207
} = source_[0];
207-
if (sourceHeaders != null) {
208-
nativeProps.headers = sourceHeaders;
209-
}
208+
headers_ = sourceHeaders;
210209
// Default to the first source's width and height if only one is provided
211-
nativeProps.style = [
212-
source_.length === 1 && {width: sourceWidth, height: sourceHeight},
213-
styles.base,
214-
style,
215-
];
216-
nativeProps.source = source_;
210+
if (ReactNativeFeatureFlags.fixImageSrcDimensionPropagation()) {
211+
style_ = [
212+
source_.length === 1 && {width: sourceWidth, height: sourceHeight},
213+
styles.base,
214+
style,
215+
];
216+
} else {
217+
style_ = [styles.base, style];
218+
}
219+
sources_ = source_;
217220
} else {
218221
const {uri, width: sourceWidth, height: sourceHeight} = source_;
219222
if (uri === '') {
220223
console.warn('source.uri should not be an empty string');
221224
}
222-
nativeProps.style = [
225+
style_ = [
223226
{width: sourceWidth ?? width, height: sourceHeight ?? height},
224227
styles.base,
225228
style,
226229
];
227-
nativeProps.source = [source_];
230+
sources_ = [source_];
231+
}
232+
233+
const nativeProps = restProps as {
234+
...React.PropsOf<ImageViewNativeComponent>,
235+
};
236+
237+
// Both iOS and C++ sides expect to have "source" prop, whereas on Android it's "src"
238+
// (for historical reasons). So in the latter case we populate both "src" and "source",
239+
// in order to have a better alignment between platforms in the future.
240+
// TODO: `src` should be eventually removed from the API on Android.
241+
nativeProps.src = sources_;
242+
nativeProps.source = sources_;
243+
244+
nativeProps.style = style_;
245+
246+
if (headers_ != null) {
247+
nativeProps.headers = headers_;
228248
}
229249

230250
if (onLoadStart != null) {

packages/react-native/Libraries/Image/__tests__/Image-itest.js

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
* LICENSE file in the root directory of this source tree.
66
*
77
* @flow strict-local
8+
* @fantom_flags fixImageSrcDimensionPropagation:*
89
* @format
910
*/
1011

1112
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
1213

1314
import type {AccessibilityProps, HostInstance} from 'react-native';
1415

16+
import * as ReactNativeFeatureFlags from '../../../src/private/featureflags/ReactNativeFeatureFlags';
1517
import * as Fantom from '@react-native/fantom';
1618
import * as React from 'react';
1719
import {createRef} from 'react';
@@ -475,14 +477,23 @@ describe('<Image>', () => {
475477
.getRenderedOutput({props: ['source', 'width', 'height']})
476478
.toJSX(),
477479
).toEqual(
478-
<rn-image
479-
source-scale="1"
480-
source-type="remote"
481-
source-size="{40, 40}"
482-
source-uri="https://reactnative.dev/img/tiny_logo.png"
483-
width="40"
484-
height="40"
485-
/>,
480+
ReactNativeFeatureFlags.fixImageSrcDimensionPropagation() ? (
481+
<rn-image
482+
source-scale="1"
483+
source-type="remote"
484+
source-size="{40, 40}"
485+
source-uri="https://reactnative.dev/img/tiny_logo.png"
486+
width="40"
487+
height="40"
488+
/>
489+
) : (
490+
<rn-image
491+
source-scale="1"
492+
source-type="remote"
493+
source-size="{40, 40}"
494+
source-uri="https://reactnative.dev/img/tiny_logo.png"
495+
/>
496+
),
486497
);
487498
});
488499
});

packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,8 @@ const validAttributesForNonEventProps = {
372372

373373
direction: true,
374374

375+
focusable: true,
376+
375377
style: ReactNativeStyleAttributes,
376378
} as const;
377379

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
12+
13+
import type {HostInstance} from 'react-native';
14+
15+
import ensureInstance from '../../../src/private/__tests__/utilities/ensureInstance';
16+
import * as Fantom from '@react-native/fantom';
17+
import * as React from 'react';
18+
import {createRef, startTransition, useDeferredValue, useState} from 'react';
19+
import {View} from 'react-native';
20+
import {NativeEventCategory} from 'react-native/src/private/testing/fantom/specs/NativeFantom';
21+
import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement';
22+
23+
function ensureReactNativeElement(value: unknown): ReactNativeElement {
24+
return ensureInstance(value, ReactNativeElement);
25+
}
26+
27+
describe('stale event handlers from interrupted render', () => {
28+
// This test demonstrates a bug where canonical.currentProps (which stores
29+
// event handlers) is updated during completeWork (render phase), not during
30+
// commit. Since the canonical object is shared between the committed fiber
31+
// and work-in-progress fiber, this is an eager mutation. During concurrent
32+
// rendering, if a render is interrupted after a component's completeWork has
33+
// run but before commit, events dispatched at that point read stale
34+
// (never-committed) handlers instead of the last committed ones.
35+
//
36+
// The test uses sibling rendering order to exploit this:
37+
// 1. First sibling: a View with an onPointerUp handler that captures
38+
// deferredLabel. Its completeWork runs first, eagerly updating
39+
// canonical.currentProps with the in-progress handler.
40+
// 2. Second sibling: InterruptTrigger, which dispatches a discrete event on
41+
// the View during render. By this point, the View's completeWork has
42+
// already updated canonical.currentProps with the new (uncommitted) handler.
43+
it('calls stale handler from discarded render instead of committed handler', () => {
44+
const root = Fantom.createRoot();
45+
const viewRef = createRef<HostInstance>();
46+
const handlerCallLog: Array<string> = [];
47+
let shouldDispatchDuringRender = false;
48+
49+
function App({label}: {label: string}) {
50+
const deferredLabel = useDeferredValue(label);
51+
const [, setInterrupt] = useState(false);
52+
53+
return (
54+
<>
55+
<View
56+
ref={viewRef}
57+
onPointerUp={() => {
58+
handlerCallLog.push(deferredLabel);
59+
// Trigger a high-priority update to interrupt the deferred render.
60+
setInterrupt(prev => !prev);
61+
}}
62+
/>
63+
<InterruptTrigger label={label} deferredLabel={deferredLabel} />
64+
</>
65+
);
66+
}
67+
68+
// This component dispatches a discrete native event during render when
69+
// we're in the deferred re-render (deferredLabel has caught up to label).
70+
// By the time this component renders, the View sibling's completeWork has
71+
// already eagerly updated canonical.currentProps with the in-progress
72+
// (not-yet-committed) handler.
73+
function InterruptTrigger({
74+
label,
75+
deferredLabel,
76+
}: {
77+
label: string,
78+
deferredLabel: string,
79+
}) {
80+
if (shouldDispatchDuringRender && deferredLabel === label) {
81+
shouldDispatchDuringRender = false;
82+
const element = ensureReactNativeElement(viewRef.current);
83+
Fantom.dispatchNativeEvent(
84+
element,
85+
'onPointerUp',
86+
{x: 0, y: 0},
87+
{
88+
category: NativeEventCategory.Discrete,
89+
},
90+
);
91+
}
92+
return null;
93+
}
94+
95+
// Initial render: commits handler capturing deferredLabel="initial".
96+
Fantom.runTask(() => {
97+
root.render(<App label="initial" />);
98+
});
99+
100+
shouldDispatchDuringRender = true;
101+
102+
// startTransition triggers:
103+
// 1. First transition render: useDeferredValue("transition") returns
104+
// "initial" (deferred) → commits, handler still captures "initial".
105+
// 2. Deferred re-render: useDeferredValue("transition") returns
106+
// "transition" → View's completeWork eagerly updates
107+
// canonical.currentProps with handler capturing "transition" →
108+
// InterruptTrigger renders and dispatches discrete event →
109+
// The stale (uncommitted) handler is called, logging "transition" →
110+
// setState in the handler interrupts and discards the deferred render.
111+
Fantom.runTask(() => {
112+
startTransition(() => {
113+
root.render(<App label="transition" />);
114+
});
115+
});
116+
117+
// CORRECT behavior: the last committed handler (capturing "initial")
118+
// should be called, because the deferred render hasn't committed yet.
119+
// expect(handlerCallLog).toEqual(['initial']);
120+
121+
// ACTUAL (buggy) behavior: the stale handler from the interrupted
122+
// (discarded) render is called because canonical.currentProps is eagerly
123+
// updated during completeWork (render phase), before the commit.
124+
expect(handlerCallLog).toEqual(['transition']);
125+
});
126+
});

packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingProxyRootView.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ NS_ASSUME_NONNULL_BEGIN
3939
@property (nonatomic, assign) NSTimeInterval loadingViewFadeDuration;
4040
@property (nonatomic, assign) CGSize minimumSize;
4141

42+
#if TARGET_OS_TV
43+
@property (nonatomic, copy, nullable) NSArray<id<UIFocusEnvironment>> *reactPreferredFocusEnvironments;
44+
@property (nonatomic, weak, nullable) UIView *reactPreferredFocusedView;
45+
#endif
46+
4247
- (instancetype)init NS_UNAVAILABLE;
4348
+ (instancetype)new NS_UNAVAILABLE;
4449
- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;

packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingProxyRootView.mm

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,13 @@ - (void)surface:(RCTSurface *)surface didChangeStage:(RCTSurfaceStage)stage
121121
[super surface:surface didChangeStage:stage];
122122
if (RCTSurfaceStageIsRunning(stage)) {
123123
[_bridge.performanceLogger markStopForTag:RCTPLTTI];
124+
#if TARGET_OS_TV
125+
dispatch_async(dispatch_get_main_queue(), ^{
126+
self.reactPreferredFocusedView = nil;
127+
[self setNeedsFocusUpdate];
128+
[self updateFocusIfNeeded];
129+
});
130+
#endif
124131
}
125132
}
126133

@@ -154,4 +161,23 @@ - (void)cancelTouches
154161
// Not supported.
155162
}
156163

164+
#if TARGET_OS_TV
165+
#pragma mark - UIFocusEnvironment
166+
167+
- (NSArray<id<UIFocusEnvironment>> *)preferredFocusEnvironments
168+
{
169+
if (self.reactPreferredFocusEnvironments != nil && self.reactPreferredFocusedView.window != nil) {
170+
NSArray<id<UIFocusEnvironment>> *tempReactPreferredFocusEnvironments = self.reactPreferredFocusEnvironments;
171+
self.reactPreferredFocusEnvironments = nil;
172+
return tempReactPreferredFocusEnvironments;
173+
}
174+
175+
if (self.reactPreferredFocusedView && self.reactPreferredFocusedView.window != nil) {
176+
return @[ self.reactPreferredFocusedView ];
177+
}
178+
179+
return [super preferredFocusEnvironments];
180+
}
181+
#endif
182+
157183
@end

0 commit comments

Comments
 (0)