Compare commits

...
Author SHA1 Message Date
Nicola CortiandFacebook GitHub Bot 5983edd21b Migrate ClipboardModuleTest to use BridgelessReactContext (#53131)
Summary:
This test was still using the old `BridgeReactContext`, I'm migrating it to `BridgelessReactContext`.

## Changelog:

[INTERNAL] -


Test Plan: CI

Reviewed By: mdvacca

Differential Revision: D79801564

Pulled By: cortinico
2025-08-07 09:35:26 -07:00
generatedunixname499836121andFacebook GitHub Bot bf13ecba7f Apply fixup patch to fbsource
Summary:
This is an automatically generated fixup patch to bring fbsource back into sync with
facebook/react on GitHub. Please land this patch as soon as possible, as the difference
reflected on here is already on GitHub and future changes may depend on these
changes!

<< DO NOT EDIT BELOW THIS LINE >>
diff-train-skip-merge
diff-train-source-id: 7da6014535999d256d2846aadbb7fc1373d76253

Generated by: https://www.internalfb.com/intern/sandcastle/job/18014400563434324/

GitHub Repo: facebook/react

Changelog: [Internal]

Reviewed By: jackpope

Differential Revision: D79754431

fbshipit-source-id: 027428142f36683b07cb66112335f75ecfb5dd12
2025-08-07 09:27:23 -07:00
Andrew DatsenkoandFacebook GitHub Bot 5f8807acc2 Add public API test (#53096)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/53096

Changelog: [Internal]
As title

Reviewed By: rshest

Differential Revision: D79726634

fbshipit-source-id: 1474cbfb635250e06f4f898338ef0874bc488ed1
2025-08-07 09:04:25 -07:00
Nicola CortiandFacebook GitHub Bot 026e22bb8d Deprecate the DefaultDevSupportManagerFactory.create() method used for Old Arch (#53137)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/53137

One of those 2 methods can be deprecated as it was used only for old architecture.
We'll be removing it at some point in the future.

Changelog:
[Android] [Deprecated] - DefaultDevSupportManagerFactory.create() method used for Old Arch

Reviewed By: rshest

Differential Revision: D79806116

fbshipit-source-id: ad2d5515f93bb85e3b7c495b369078f4c66d143b
2025-08-07 08:56:26 -07:00
Mateo GuzmánandFacebook GitHub Bot f1894393ca Initial Kotlin setup and migrate YogaConstants (#53133)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/53133

# Changelog:
[Internal] -

As part of the ongoing effort to migrate the React Native codebase to Kotlin, this PR introduces the initial setup required for Kotlin support in Yoga.

- Added initial basic Kotlin configuration to the project.
- Migrated `YogaConstants` as an initial file to try out the first migration steps.

X-link: https://github.com/facebook/yoga/pull/1829

Test Plan:
- Tested the migrated class directly against facebook/react-native, see the PR [here](https://github.com/facebook/react-native/pull/52998).
- Run: `./gradlew :yoga:assembleDebug` & `./gradlew :yoga:compileDebugSources`

I am not able to run the Java tests in this repo (even before the initial Kotlin setup) – not sure if I am missing something there but any pointers are welcome – it seems like there is some missing configuration. Currently trying with `./gradlew :yoga:test`

Reviewed By: cortinico

Differential Revision: D79545992

Pulled By: rshest

fbshipit-source-id: 8257ff53e6b6f2436980be98b6c94e1ac526b207
2025-08-07 08:17:56 -07:00
Ruslan ShestopalyukandFacebook GitHub Bot 77ee24ddce Print benchmark comparison results in a tabular format (#53097)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/53097

# Changelog:
[Internal] -

This makes the benchmark results comparison printout, added in https://github.com/facebook/react-native/pull/52925, be done in a tabular form, so it's easier to read and copy/paste around.

Reviewed By: lenaic

Differential Revision: D79728695

fbshipit-source-id: 4dfc999ad4a9a8c2d67efdfce11aff75383cf645
2025-08-07 08:11:32 -07:00
Eliot FallonandFacebook GitHub Bot 327057fad5 fix: fix a typo in the react_native_pods.rb file (#53129)
Summary:
- Switches fmt_config to fast_float_config so it matches what is used in the method

When trying to use a forked version of the Pod it is not possible to set a new repo for Fast Float. This was caused by a typo in the method used to update the configuration of where to find the Pod.

## Changelog:

[IOS] [FIXED] - Fixed variable naming error in set_fast_float_config method in react_native_pods.rb

Pull Request resolved: https://github.com/facebook/react-native/pull/53129

Test Plan: Add the line `set_fast_float_config({:git => 'some-git-repo'})` to an application's Podfile

Reviewed By: cortinico

Differential Revision: D79805939

Pulled By: cipolleschi

fbshipit-source-id: 9705e1f63e21b788362ca94b74e32bce0177a729
2025-08-07 07:48:07 -07:00
Nicola CortiandFacebook GitHub Bot cd71c9620c Do not die on collect-results after printing to Discord. (#53132)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/53132

This will prevent the Firebase script from running at all. I'm removing it.

Changelog:
[Internal] [Changed] -

Reviewed By: cipolleschi

Differential Revision: D79801691

fbshipit-source-id: 2705dff93fc9dbbcfaf97a1ba29b69d4d0a8143c
2025-08-07 07:07:10 -07:00
generatedunixname537391475639613andFacebook GitHub Bot 9bb53c02dd xplat/js/react-native-github/packages/react-native/ReactCommon/react/renderer/components/switch/androidswitch/react/renderer/components/androidswitch/AndroidSwitchShadowNode.cpp (#53126)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/53126

Reviewed By: rshest

Differential Revision: D79710434

fbshipit-source-id: da72999b76c0eff73eb62ef47dcf5ec0ed6c9e09
2025-08-07 05:47:35 -07:00
David VaccaandFacebook GitHub Bot f67078df07 Deprecate all LegacyArchitecture classes on LayoutAnimation package (#53101)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/53101

Deprecate all LegacyArchitecture classes on LayoutAnimation package

changelog: [Android][Changed] Deprecate LegacyArchitecture classes from LayoutAnimation package

Reviewed By: alanleedev

Differential Revision: D79658935

fbshipit-source-id: 34ab2f674868dbee459f2018e82a7d50d0d7333a
2025-08-07 05:11:31 -07:00
Mathieu ActhernoeneandFacebook GitHub Bot 3b185e4bce Fix Dimensions window values on Android < 15 (#52738)
Summary:
This PR (initially created for edge-to-edge opt-in support, rebased multiple times) fixes the `Dimensions` API `window` values on Android < 15, when edge-to-edge is enabled.

Currently the window height doesn't include the status and navigation bar heights (but it does on Android >= 15):

<img width="300" alt="Screenshot 2025-06-27 at 16 23 02" src="https://github.com/user-attachments/assets/c7d11334-9298-4f7f-a75c-590df8cc2d8a" />

Using `WindowMetricsCalculator` from AndroidX:

<img width="300" alt="Screenshot 2025-06-27 at 16 34 01" src="https://github.com/user-attachments/assets/7a4e3dc7-a83b-421b-8f6d-fd1344f5fe81" />

Fixes https://github.com/facebook/react-native/issues/47080

## Changelog:

[Android] [Fixed] Fix `Dimensions` `window` values on Android < 15 when edge-to-edge is enabled

Pull Request resolved: https://github.com/facebook/react-native/pull/52738

Test Plan:
Run the example app on an Android < 15 device.

Rollback Plan:

Reviewed By: cipolleschi, Abbondanzo

Differential Revision: D78738516

Pulled By: alanleedev

fbshipit-source-id: fdb22f3cc76b0bda987db426cb015124bcacdc84
2025-08-07 02:17:14 -07:00
Alan LeeandFacebook GitHub Bot 8b2e309479 replace getWindowDisplayMetrics with getScreenDisplayMetrics (#53041)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/53041

update `DisplayMetricsHolder.getWindowDisplayMetrics()` to `getScreenDisplayMetrics()`.

Where window width and height is not needed, prefer to use `screenDisplayMetrics` as with upcoming diff `windowDisplayMetrics` initialization only happen using UiContext and have potential to cause more issues if used unnecessarily.

Changelog: [Internal] Update `DisplayMetricsHolder.getWindowDisplayMetrics()` to use `.getScreenDisplayMetrics()`

Reviewed By: mlord93

Differential Revision: D79571226

fbshipit-source-id: d90fca36c119318e7a2dfa6953fc2148b35e83d4
2025-08-07 02:17:14 -07:00
David VaccaandFacebook GitHub Bot d4bf644e47 Update deprecation message for ReactNativeHost class (#53118)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/53118

Update deprecation message for ReactNativeHost class

changelog: [internal] internal

Reviewed By: shwanton

Differential Revision: D79677829

fbshipit-source-id: 5013e0988a1ffbfea49a261fd23ac71af76c3313
2025-08-06 23:58:34 -07:00
Devan BuggayandFacebook GitHub Bot 2697e8aaab Remove mode from metro client-log data (#53117)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/53117

Sending the mode is no longer needed as you can't opt out of legacy arch any longer, and was only used for printing a (NOBRIDGE) prefix.

Changelog: [Internal]

Reviewed By: shwanton

Differential Revision: D79762592

fbshipit-source-id: 845aabf2a8365c88808990ea481503b23597a8a0
2025-08-06 23:56:45 -07:00
Tim YungandFacebook GitHub Bot c861804325 VirtualView: Simplify Window Focus Detection
Summary:
Refactors the window focus detection feature flag logic in `VirtualView` (Android) to eliminate one instance property and instead utilize the existence of the focus listener to determine whether window focus detection is enabled.

Changelog:
[Internal]

Reviewed By: mdvacca

Differential Revision: D79743782

fbshipit-source-id: d12e70d8e52b72d546ff097c3b1bcfbd29fb9129
2025-08-06 20:49:05 -07:00
Alex HuntandFacebook GitHub Bot 2768c84445 Implement local connection for perf metrics in HostTarget (#52838)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/52838

**Context**

Experimental V2 Performance Monitor prototype, beginning by bringing the [Interaction to Next Paint (INP)](https://web.dev/articles/inp) metric to React Native.

**This diff**

Wires up a client/subscriber for the `"__chromium_devtools_metrics_reporter"` runtime binding (to which we emit live metrics events since D78904748). This will be used to unpack these performance updates to send to the host platform.

- Creates a new `HostRuntimeBinding` helper, which establishes a local/private CDP session.
- Conditionally installs our perf metrics runtime binding in `HostTarget` when `perfMonitorV2Enabled` is set.
- Wires up a new `onPerfMonitorUpdate` event on `HostTargetDelegate` (unimplemented until the next diff).

Changelog: [Internal]

Reviewed By: hoxyq

Differential Revision: D78904766

fbshipit-source-id: 991f17a4cc69f574917750053a5da31bbc6dc0d5
2025-08-06 16:05:28 -07:00
Zeya PengandFacebook GitHub Bot be6f3c6f77 Add NativeAnimated jest test to verify node creation behavior at update (#53115)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/53115

## Changelog:

[Internal] [Added] - Add NativeAnimated jest test to verify node creation behavior at update

Reviewed By: yungsters

Differential Revision: D79729178

fbshipit-source-id: 1c24c6a19ee994ee4ea7a916c47f227e151d65b4
2025-08-06 15:17:33 -07:00
David VaccaandFacebook GitHub Bot 54770cecc4 Mark JSInstance as InteropLegacyArchitecture (#53100)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/53100

Mark JSInstance as InteropLegacyArchitecture

changelog: [internal] internal

Reviewed By: mlord93, cortinico

Differential Revision: D79732414

fbshipit-source-id: fc66d6cfef34b4ac66af724bf4336f4f4c38f447
2025-08-06 14:13:32 -07:00
Andrew DatsenkoandFacebook GitHub Bot 457190cc4b Add test for width, style, testID and tintColor (#53088)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/53088

Changelog: [Internal]
As title

Reviewed By: rshest

Differential Revision: D79642765

fbshipit-source-id: 92862934fc72cfca2ce35c365fa7369d878d58d9
2025-08-06 09:38:29 -07:00
Maciej JastrzębskiandFacebook GitHub Bot 6965d57e75 fix(a11y): TextInput aria-label handling (#53051)
Summary:
The `aria-label` prop was ignored on `TextInput` component. Which resulted in screen reader not able to read it.

This PR forwards `aria-label` to `accessibilityLabel` in a manner similar to e.g. `View` and `Text`

## Changelog:

<!-- Help reviewers and the release process by writing your own changelog entry.

Pick one each for the category and type tags:

[ANDROID|GENERAL|IOS|INTERNAL] [BREAKING|ADDED|CHANGED|DEPRECATED|REMOVED|FIXED|SECURITY] - Message

For more details, see:
https://reactnative.dev/contributing/changelogs-in-pull-requests
-->

[GENERAL] [FIXED] - a11y: fix `aria-label` on `TextInput`

Pull Request resolved: https://github.com/facebook/react-native/pull/53051

Test Plan:
Run RNTester => TextInput => Accessibility section under Accessibility Inspector or screen reader.

iOS fixed:
https://github.com/user-attachments/assets/68c3a2ef-7dfe-479c-97fc-cbe72108c45c

iOS baseline:
https://github.com/user-attachments/assets/2e8372ba-10dc-47d2-b6b1-9f664000de7d

Reviewed By: andrewdacenko

Differential Revision: D79635413

Pulled By: rshest

fbshipit-source-id: dd2f583d67c6c6c6393e02c5fe534308e1e2f921
2025-08-06 09:36:35 -07:00
Tommy NguyenandFacebook GitHub Bot bf2c3af93b fix(codegen): fix missing dependencies (#52884)
Summary:
`react-native/codegen` uses `babel/parser` and `babel/core` but does not declare dependency on them. Depending on how packages are hoisted (and especially in pnpm setups), this causes crashes during codegen.

Resolves https://github.com/facebook/react-native/issues/52883

## Changelog:

[GENERAL] [FIXED] - Add missing Babel dependencies

Pull Request resolved: https://github.com/facebook/react-native/pull/52884

Test Plan: See https://github.com/facebook/react-native/issues/52883

Reviewed By: cortinico, christophpurrer

Differential Revision: D79103092

Pulled By: robhogan

fbshipit-source-id: ecaf690f994393a652ea7f0d4f30bbabeb23a434
2025-08-06 08:19:47 -07:00
generatedunixname537391475639613andFacebook GitHub Bot a4b958099c xplat/js/react-native-github/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp (#53093)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/53093

Reviewed By: rshest

Differential Revision: D79709943

fbshipit-source-id: b8d75b6e3b790c94dd3250654459afd782315d9f
2025-08-06 08:18:43 -07:00
Alex HuntandFacebook GitHub Bot b4164cd97c Move JInspectorNetworkReporter, add missing SoLoader call (#53055)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/53055

Moves the C++ part of `InspectorNetworkReporter.kt` into the `react_devsupportjni` JNI library, and adds missing `SoLoader.loadLibrary` call.

Replaces D79568759 / https://github.com/facebook/react-native/pull/53036.

Changelog: [Internal]

Reviewed By: robhogan

Differential Revision: D79638331

fbshipit-source-id: 5df450ad80e4532d6ada1e4dab51a1de6418a4e0
2025-08-06 08:14:31 -07:00
Andrew DatsenkoandFacebook GitHub Bot f526e91fda Add tests for src and srcSet (#53062)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/53062

Changelog: [Internal]
As title

Reviewed By: rshest

Differential Revision: D79642287

fbshipit-source-id: 59d7dbf7f2562ca550ac48eb9067469e6128e7c6
2025-08-06 08:08:44 -07:00
Andrew DatsenkoandFacebook GitHub Bot bb5d0df0e3 Add source tests (#53074)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/53074

Changelog: [Internal]
Add tests for source prop of Image.

Reviewed By: rshest

Differential Revision: D79605165

fbshipit-source-id: 400cb37f59966634a2ced3c95425be4dcb1fd1a3
2025-08-06 08:08:44 -07:00
Andrew DatsenkoandFacebook GitHub Bot a6ef65e06b Add resizeMode tests (#53061)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/53061

Changelog: [Internal]
Add tests for resizeMode prop

Reviewed By: rshest

Differential Revision: D79600137

fbshipit-source-id: d802fb375f2f57f38a51d918d2045763dd79c440
2025-08-06 08:08:44 -07:00
Andrew DatsenkoandFacebook GitHub Bot 93694e3f5d Add referrerPolicy tests (#53073)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/53073

Changelog: [Internal]
Add referrerPolicty for cross origin requests.

Reviewed By: rshest

Differential Revision: D79598735

fbshipit-source-id: 96a5b092a278993ac728a9413c1a9376bfe7a69a
2025-08-06 08:08:44 -07:00
Andrew DatsenkoandFacebook GitHub Bot 96ebf5e969 Add on<Event> props (#53060)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/53060

Changelog: [Internal]
Add tests for Image props that notify of image loading progress.

Reviewed By: rshest

Differential Revision: D79596580

fbshipit-source-id: 124ee1b4ac70710c4efa9b6e11a168b61fe29aed
2025-08-06 08:08:44 -07:00
Andrew DatsenkoandFacebook GitHub Bot 7bd1254eda Add tests for defaultSource and height (#53058)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/53058

Changelog: [Internal]
As title

Reviewed By: rshest

Differential Revision: D79566292

fbshipit-source-id: cd0d39d914d4e17ee6472be766ba18b0c3866d95
2025-08-06 08:08:44 -07:00
Sophie LandFacebook GitHub Bot 4b8dbe7642 fix: turn off build IDs for reproducibility (#53089)
Summary:
these cause issues for apps that want to be "reproducible" (i.e. the same code leads to the same output, which can be helpful for detemining if there's been any tampering or similar). see also https://gitlab.com/IzzyOnDroid/repo/-/wikis/Reproducible-Builds/RB-Hints-for-Developers#no-funny-build-time-generated-ids

I'm not exactly sure why this was set. it seems to have been introduced in https://github.com/facebook/react-native/commit/e3830ddffd9260fe071e0c9f9df40b379d54cf26 without any (public) explanation as to why it was needed?

## Changelog:

[ANDROID] [FIXED] - Turned off build IDs for native libraries, fixing issues with reproducibility

Pull Request resolved: https://github.com/facebook/react-native/pull/53089

Test Plan: I've successfully used similar patches in my own app for a while.

Reviewed By: cipolleschi

Differential Revision: D79718924

Pulled By: cortinico

fbshipit-source-id: 7c609fa0b5b305cb759586fb1c7f332589ca9cc7
2025-08-06 08:07:14 -07:00
Ruslan ShestopalyukandFacebook GitHub Bot 99f1c30409 Don't populate props with default values for Text component (#53059)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/53059

# Changelog:
[Internal] -

This is a re-submit of the partially reverted https://github.com/facebook/react-native/pull/52905, which should work fine now on top of the changes stack that support the RN feature flags being safely used on the modules' top level.

Reviewed By: rubennorte

Differential Revision: D79640965

fbshipit-source-id: d8bda0dba662930d1343ea9d5155791a686b653b
2025-08-06 07:54:26 -07:00
85 changed files with 1958 additions and 417 deletions
@@ -118,7 +118,6 @@ async function collectResults(discordWebHook) {
} else {
console.log('Discord webhook not set');
}
process.exit(1);
}
// Initialize Firebase client
@@ -29,6 +29,8 @@
"lib"
],
"dependencies": {
"@babel/core": "^7.25.2",
"@babel/parser": "^7.25.3",
"glob": "^7.1.1",
"hermes-parser": "0.30.0",
"invariant": "^2.2.4",
@@ -618,6 +618,9 @@ function InternalTextInput(props: TextInputProps): React.Node {
// so omitting onBlur and onFocus pressability handlers here.
const {onBlur, onFocus, ...eventHandlers} = usePressability(config);
const _accessibilityLabel =
props?.['aria-label'] ?? props?.accessibilityLabel;
let _accessibilityState;
if (
accessibilityState != null ||
@@ -681,6 +684,7 @@ function InternalTextInput(props: TextInputProps): React.Node {
{...otherProps}
{...eventHandlers}
acceptDragAndDropTypes={props.experimental_acceptDragAndDropTypes}
accessibilityLabel={_accessibilityLabel}
accessibilityState={_accessibilityState}
accessible={accessible}
submitBehavior={submitBehavior}
@@ -744,8 +748,9 @@ function InternalTextInput(props: TextInputProps): React.Node {
{...otherProps}
{...colorProps}
{...eventHandlers}
accessibilityState={_accessibilityState}
accessibilityLabel={_accessibilityLabel}
accessibilityLabelledBy={_accessibilityLabelledBy}
accessibilityState={_accessibilityState}
accessible={accessible}
acceptDragAndDropTypes={props.experimental_acceptDragAndDropTypes}
autoCapitalize={autoCapitalize}
@@ -432,6 +432,7 @@ jest.unmock('../TextInput');
expect(instance.toJSON()).toMatchInlineSnapshot(`
<RCTSinglelineTextInputView
accessibilityLabel="label"
accessibilityState={
Object {
"busy": true,
@@ -169,6 +169,460 @@ describe('<Image>', () => {
);
});
});
describe('defaultSource', () => {
it('can provide a default image to display', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(
<Image
defaultSource={require('./img/img1.png')}
source={LOGO_SOURCE}
/>,
);
});
expect(
root.getRenderedOutput({props: ['defaultSource']}).toJSX(),
).toEqual(
<rn-image
defaultSource-type="remote"
defaultSource-uri="file://drawable-mdpi/packages_reactnative_libraries_image___tests___img_img1.png"
/>,
);
});
});
describe('height', () => {
it('provides height for image', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(<Image height={100} source={LOGO_SOURCE} />);
});
expect(root.getRenderedOutput({props: ['height']}).toJSX()).toEqual(
<rn-image height="100.000000" />,
);
});
});
describe('width', () => {
it('provides width for image', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(<Image width={100} source={LOGO_SOURCE} />);
});
expect(root.getRenderedOutput({props: ['width']}).toJSX()).toEqual(
<rn-image width="100.000000" />,
);
});
});
describe('loading progress', () => {
(
[
['onError', 'fails to load'],
['onLoadStart', 'start loading'],
['onProgress', 'is loading'],
['onLoad', 'loads successfully'],
['onLoadEnd', 'ends loading'],
] as const
).forEach(([onProp, event]) => {
it(`${onProp} is called when image ${event}`, () => {
const onPropCallback = jest.fn();
const ref = createRef<HostInstance>();
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(
<Image
ref={ref}
source={LOGO_SOURCE}
onError={() => {
onProp === 'onError' && onPropCallback();
}}
onLoad={() => {
onProp === 'onLoad' && onPropCallback();
}}
onLoadStart={() => {
onProp === 'onLoadStart' && onPropCallback();
}}
onLoadEnd={() => {
onProp === 'onLoadEnd' && onPropCallback();
}}
onProgress={() => {
onProp === 'onProgress' && onPropCallback();
}}
/>,
);
});
expect(onPropCallback).toHaveBeenCalledTimes(0);
const image = ensureInstance(ref.current, ReactNativeElement);
Fantom.dispatchNativeEvent(image, onProp, {});
expect(onPropCallback).toHaveBeenCalledTimes(1);
});
});
});
describe('referrerPolicy', () => {
(
[
'no-referrer',
'no-referrer-when-downgrade',
'origin',
'origin-when-cross-origin',
'same-origin',
'strict-origin',
'strict-origin-when-cross-origin',
'unsafe-url',
] as const
).forEach(referrerPolicy => {
it(`${referrerPolicy} sets correct "Referrer-Policy" header`, () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(
<Image referrerPolicy={referrerPolicy} src={LOGO_SOURCE.uri} />,
);
});
expect(
root.getRenderedOutput({props: ['source-header']}).toJSX(),
).toEqual(
<rn-image source-header-Referrer-Policy={referrerPolicy} />,
);
});
});
});
describe('resizeMode', () => {
it('is set to "cover" by default', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(<Image source={LOGO_SOURCE} />);
});
expect(root.getRenderedOutput({props: ['resizeMode']}).toJSX()).toEqual(
<rn-image />,
);
Fantom.runTask(() => {
root.render(<Image resizeMode="cover" source={LOGO_SOURCE} />);
});
expect(root.getRenderedOutput({props: ['resizeMode']}).toJSX()).toEqual(
<rn-image />,
);
});
(['stretch', 'contain', 'repeat', 'center'] as const).forEach(
resizeMode => {
it(`can be set to "${resizeMode}"`, () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(
<Image resizeMode={resizeMode} source={LOGO_SOURCE} />,
);
});
expect(
root.getRenderedOutput({props: ['resizeMode']}).toJSX(),
).toEqual(<rn-image resizeMode={resizeMode} />);
});
},
);
});
describe('source', () => {
it('can be set to a local image', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(<Image source={require('./img/img1.png')} />);
});
expect(root.getRenderedOutput({props: ['source']}).toJSX()).toEqual(
<rn-image
source-scale="1"
source-size="{1, 1}"
source-type="local"
source-uri="file://drawable-mdpi/packages_reactnative_libraries_image___tests___img_img1.png"
/>,
);
});
it('can be set to a remote image', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(
<Image
source={{
uri: 'https://reactnative.dev/img/tiny_logo.png',
width: 100,
height: 100,
scale: 2,
cache: 'only-if-cached',
method: 'POST',
body: 'name=React+Native',
headers: {
Authorization: 'Basic RandomString',
},
}}
/>,
);
});
expect(root.getRenderedOutput({props: ['source']}).toJSX()).toEqual(
<rn-image
source-body="name=React+Native"
source-cache="only-if-cached"
source-header-Authorization="Basic RandomString"
source-method="POST"
source-scale="2"
source-size="{100, 100}"
source-type="remote"
source-uri="https://reactnative.dev/img/tiny_logo.png"
/>,
);
});
it('can be set to a list of remote images', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(
<Image
source={[
{
uri: 'https://reactnative.dev/img/tiny_logo.png',
scale: 1,
headers: {
Authorization: 'Basic RandomString',
},
},
{
uri: 'https://reactnative.dev/img/medium_logo.png',
scale: 2,
cache: 'only-if-cached',
},
{
uri: 'https://reactnative.dev/img/large_logo.png',
scale: 3,
method: 'POST',
},
]}
/>,
);
});
expect(root.getRenderedOutput({props: ['source']}).toJSX()).toEqual(
<rn-image
source-1x-header-Authorization="Basic RandomString"
source-1x-scale="1"
source-1x-type="remote"
source-1x-uri="https://reactnative.dev/img/tiny_logo.png"
source-2x-cache="only-if-cached"
source-2x-scale="2"
source-2x-type="remote"
source-2x-uri="https://reactnative.dev/img/medium_logo.png"
source-3x-method="POST"
source-3x-type="remote"
source-3x-uri="https://reactnative.dev/img/large_logo.png"
/>,
);
});
});
describe('src', () => {
it('can be set to a remote image', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(
<Image src="https://reactnative.dev/img/tiny_logo.png" />,
);
});
expect(root.getRenderedOutput({props: ['source']}).toJSX()).toEqual(
<rn-image
source-scale="1"
source-type="remote"
source-uri="https://reactnative.dev/img/tiny_logo.png"
/>,
);
});
it('takes precedence over `source` prop', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(
<Image
src="https://reactnative.dev/img/tiny_logo.png"
source={{uri: 'https://reactnative.dev/img/medium_logo.png'}}
/>,
);
});
expect(root.getRenderedOutput({props: ['source']}).toJSX()).toEqual(
<rn-image
source-scale="1"
source-type="remote"
source-uri="https://reactnative.dev/img/tiny_logo.png"
/>,
);
});
});
describe('srcSet', () => {
it('can be set to a list of remote images', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(
<Image
srcSet={
'https://reactnative.dev/img/tiny_logo.png 1x, https://reactnative.dev/img/header_logo.svg 2x'
}
/>,
);
});
expect(root.getRenderedOutput({props: ['source']}).toJSX()).toEqual(
<rn-image
source-1x-scale="1"
source-1x-type="remote"
source-1x-uri="https://reactnative.dev/img/tiny_logo.png"
source-2x-scale="2"
source-2x-type="remote"
source-2x-uri="https://reactnative.dev/img/header_logo.svg"
/>,
);
});
it('defaults to `1x` descriptor', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(
<Image
srcSet={
'https://reactnative.dev/img/tiny_logo.png, https://reactnative.dev/img/header_logo.svg 2x'
}
/>,
);
});
expect(root.getRenderedOutput({props: ['source']}).toJSX()).toEqual(
<rn-image
source-1x-scale="1"
source-1x-type="remote"
source-1x-uri="https://reactnative.dev/img/tiny_logo.png"
source-2x-scale="2"
source-2x-type="remote"
source-2x-uri="https://reactnative.dev/img/header_logo.svg"
/>,
);
});
it('uses `src` for `1x` descriptor when provided', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(
<Image
srcSet={
'https://reactnative.dev/img/header_logo.svg 2x, https://reactnative.dev/img/large_logo.svg 3x'
}
src="https://reactnative.dev/img/tiny_logo.png"
/>,
);
});
expect(root.getRenderedOutput({props: ['source']}).toJSX()).toEqual(
<rn-image
source-1x-scale="1"
source-1x-type="remote"
source-1x-uri="https://reactnative.dev/img/tiny_logo.png"
source-2x-scale="2"
source-2x-type="remote"
source-2x-uri="https://reactnative.dev/img/header_logo.svg"
source-3x-type="remote"
source-3x-uri="https://reactnative.dev/img/large_logo.svg"
/>,
);
});
});
describe('style', () => {
it('can be set', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(
<Image
style={{
width: 100,
height: 100,
resizeMode: 'contain',
}}
source={LOGO_SOURCE}
/>,
);
});
expect(root.getRenderedOutput().toJSX()).toEqual(
<rn-image
height="100.000000"
overflow="hidden"
resizeMode="contain"
width="100.000000"
source-scale="1"
source-type="remote"
source-uri="https://reactnative.dev/img/tiny_logo.png"
/>,
);
});
});
describe('testID', () => {
it('can be set', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(<Image testID="test" source={LOGO_SOURCE} />);
});
expect(root.getRenderedOutput({props: ['testID']}).toJSX()).toEqual(
<rn-image testID="test" />,
);
});
});
describe('tintColor', () => {
it('can be set', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(<Image tintColor="red" source={LOGO_SOURCE} />);
});
expect(root.getRenderedOutput({props: ['tintColor']}).toJSX()).toEqual(
<rn-image tintColor="rgba(255, 0, 0, 1)" />,
);
});
});
});
describe('ref', () => {
@@ -0,0 +1,128 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
import type {HostInstance} from 'react-native';
import * as Fantom from '@react-native/fantom';
import * as React from 'react';
import {createRef} from 'react';
import {ImageBackground} from 'react-native';
import ensureInstance from 'react-native/src/private/__tests__/utilities/ensureInstance';
import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement';
describe('<ImageBackground>', () => {
describe('props', () => {
describe('ImageProps', () => {
it('can have local source', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(<ImageBackground source={require('./img/img1.png')} />);
});
expect(root.getRenderedOutput({props: ['source']}).toJSX()).toEqual(
<rn-image
source-scale="1"
source-size="{1, 1}"
source-type="local"
source-uri="file://drawable-mdpi/packages_reactnative_libraries_image___tests___img_img1.png"
/>,
);
});
it('can have remote source', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(
<ImageBackground
source={{
uri: 'https://reactnative.dev/img/tiny_logo.png',
width: 100,
height: 100,
scale: 2,
cache: 'only-if-cached',
method: 'POST',
body: 'name=React+Native',
headers: {
Authorization: 'Basic RandomString',
},
}}
/>,
);
});
expect(root.getRenderedOutput({props: ['source']}).toJSX()).toEqual(
<rn-image
source-body="name=React+Native"
source-cache="only-if-cached"
source-header-Authorization="Basic RandomString"
source-method="POST"
source-scale="2"
source-size="{100, 100}"
source-type="remote"
source-uri="https://reactnative.dev/img/tiny_logo.png"
/>,
);
});
it('can have srcSet', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(
<ImageBackground srcSet="https://reactnative.dev/img/tiny_logo.png 1x, https://reactnative.dev/img/header_logo.svg 2x" />,
);
});
expect(root.getRenderedOutput({props: ['source']}).toJSX()).toEqual(
<rn-image
source-1x-scale="1"
source-1x-type="remote"
source-1x-uri="https://reactnative.dev/img/tiny_logo.png"
source-2x-scale="2"
source-2x-type="remote"
source-2x-uri="https://reactnative.dev/img/header_logo.svg"
/>,
);
});
});
describe('style', () => {
it('can be set', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(<ImageBackground style={{width: 100, height: 100}} />);
});
expect(
root.getRenderedOutput({props: ['width|height']}).toJSX(),
).toEqual(<rn-image width="100.000000" height="100.000000" />);
});
});
});
describe('ref', () => {
it('Allows to set a reference to the inner `Image` component', () => {
const elementRef = createRef<HostInstance>();
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(<ImageBackground imageRef={elementRef} />);
});
const image = ensureInstance(elementRef.current, ReactNativeElement);
expect(image.tagName).toBe('RN:Image');
});
});
});
@@ -7,9 +7,7 @@
* @noformat
* @nolint
* @flow
* @generated SignedSource<<16b364e89f43b8a47832b0dfb98af11e>>
*
* This file was sync'd from the facebook/react repository.
* @generated SignedSource<<cf323fc5ca893bab5669c7d321660412>>
*/
'use strict';
@@ -7,9 +7,7 @@
* @noformat
* @nolint
* @flow strict-local
* @generated SignedSource<<1dd9e9c3f20e37ae14e485fc6ee3d9e9>>
*
* This file was sync'd from the facebook/react repository.
* @generated SignedSource<<908f5fb85384725318e261f40e49d9a6>>
*/
'use strict';
@@ -7,9 +7,7 @@
* @noformat
* @nolint
* @flow
* @generated SignedSource<<e2c46705ed927302dbe9332dafba459d>>
*
* This file was sync'd from the facebook/react repository.
* @generated SignedSource<<8f46fdc9267fcc4fdc9e76842fe24066>>
*/
'use strict';
@@ -7,9 +7,7 @@
* @noformat
* @nolint
* @flow strict-local
* @generated SignedSource<<e8dce0e82b831c91465d04b49fb48ab2>>
*
* This file was sync'd from the facebook/react repository.
* @generated SignedSource<<83073425aa3f71ced2c8c51f25a25938>>
*/
'use strict';
@@ -7,9 +7,7 @@
* @noformat
* @nolint
* @flow strict-local
* @generated SignedSource<<556d1487de0b9e4a09cbc67dd130a884>>
*
* This file was sync'd from the facebook/react repository.
* @generated SignedSource<<52163887de05f1cff05388145cf85b3b>>
*/
'use strict';
+517 -243
View File
@@ -14,6 +14,7 @@ import type {GestureResponderEvent} from '../Types/CoreEventTypes';
import type {NativeTextProps} from './TextNativeComponent';
import type {PressRetentionOffset, TextProps} from './TextProps';
import * as ReactNativeFeatureFlags from '../../src/private/featureflags/ReactNativeFeatureFlags';
import * as PressabilityDebug from '../Pressability/PressabilityDebug';
import usePressability from '../Pressability/usePressability';
import flattenStyle from '../StyleSheet/flattenStyle';
@@ -35,156 +36,495 @@ type TextForwardRef = React.ElementRef<
*
* @see https://reactnative.dev/docs/text
*/
const TextImpl: component(
ref?: React.RefSetter<TextForwardRef>,
...props: TextProps
) = ({
ref: forwardedRef,
accessible,
accessibilityLabel,
accessibilityState,
allowFontScaling,
'aria-busy': ariaBusy,
'aria-checked': ariaChecked,
'aria-disabled': ariaDisabled,
'aria-expanded': ariaExpanded,
'aria-label': ariaLabel,
'aria-selected': ariaSelected,
children,
ellipsizeMode,
disabled,
id,
nativeID,
numberOfLines,
onLongPress,
onPress,
onPressIn,
onPressOut,
onResponderGrant,
onResponderMove,
onResponderRelease,
onResponderTerminate,
onResponderTerminationRequest,
onStartShouldSetResponder,
pressRetentionOffset,
selectable,
selectionColor,
suppressHighlighting,
style,
...restProps
}: {
ref?: React.RefSetter<TextForwardRef>,
...TextProps,
}) => {
const _accessibilityLabel = ariaLabel ?? accessibilityLabel;
let _accessibilityState: ?TextProps['accessibilityState'] =
accessibilityState;
if (
ariaBusy != null ||
ariaChecked != null ||
ariaDisabled != null ||
ariaExpanded != null ||
ariaSelected != null
) {
if (_accessibilityState != null) {
_accessibilityState = {
busy: ariaBusy ?? _accessibilityState.busy,
checked: ariaChecked ?? _accessibilityState.checked,
disabled: ariaDisabled ?? _accessibilityState.disabled,
expanded: ariaExpanded ?? _accessibilityState.expanded,
selected: ariaSelected ?? _accessibilityState.selected,
};
} else {
_accessibilityState = {
busy: ariaBusy,
checked: ariaChecked,
disabled: ariaDisabled,
expanded: ariaExpanded,
selected: ariaSelected,
};
let _TextImpl;
if (ReactNativeFeatureFlags.reduceDefaultPropsInText()) {
const TextImplNoDefaultProps: component(
ref?: React.RefSetter<TextForwardRef>,
...props: TextProps
) = ({
ref: forwardedRef,
accessible,
accessibilityLabel,
accessibilityState,
allowFontScaling,
'aria-busy': ariaBusy,
'aria-checked': ariaChecked,
'aria-disabled': ariaDisabled,
'aria-expanded': ariaExpanded,
'aria-label': ariaLabel,
'aria-selected': ariaSelected,
children,
ellipsizeMode,
disabled,
id,
nativeID,
numberOfLines,
onLongPress,
onPress,
onPressIn,
onPressOut,
onResponderGrant,
onResponderMove,
onResponderRelease,
onResponderTerminate,
onResponderTerminationRequest,
onStartShouldSetResponder,
pressRetentionOffset,
selectable,
selectionColor,
suppressHighlighting,
style,
...restProps
}: {
ref?: React.RefSetter<TextForwardRef>,
...TextProps,
}) => {
const processedProps = restProps as {
...NativeTextProps,
};
const _accessibilityLabel = ariaLabel ?? accessibilityLabel;
let _accessibilityState: ?TextProps['accessibilityState'] =
accessibilityState;
if (
ariaBusy != null ||
ariaChecked != null ||
ariaDisabled != null ||
ariaExpanded != null ||
ariaSelected != null
) {
if (_accessibilityState != null) {
_accessibilityState = {
busy: ariaBusy ?? _accessibilityState.busy,
checked: ariaChecked ?? _accessibilityState.checked,
disabled: ariaDisabled ?? _accessibilityState.disabled,
expanded: ariaExpanded ?? _accessibilityState.expanded,
selected: ariaSelected ?? _accessibilityState.selected,
};
} else {
_accessibilityState = {
busy: ariaBusy,
checked: ariaChecked,
disabled: ariaDisabled,
expanded: ariaExpanded,
selected: ariaSelected,
};
}
}
}
const _accessibilityStateDisabled = _accessibilityState?.disabled;
const _disabled = disabled ?? _accessibilityStateDisabled;
const _accessibilityStateDisabled = _accessibilityState?.disabled;
const _disabled = disabled ?? _accessibilityStateDisabled;
const isPressable =
(onPress != null ||
onLongPress != null ||
onStartShouldSetResponder != null) &&
_disabled !== true;
// TODO: Move this processing to the view configuration.
const _selectionColor =
selectionColor != null ? processColor(selectionColor) : undefined;
let _style = style;
if (__DEV__) {
if (PressabilityDebug.isEnabled() && onPress != null) {
_style = [style, {color: 'magenta'}];
// If the disabled prop and accessibilityState.disabled are out of sync but not both in
// falsy states we need to update the accessibilityState object to use the disabled prop.
if (
_accessibilityState != null &&
_disabled !== _accessibilityStateDisabled &&
((_disabled != null && _disabled !== false) ||
(_accessibilityStateDisabled != null &&
_accessibilityStateDisabled !== false))
) {
_accessibilityState.disabled = _disabled;
}
}
let _numberOfLines = numberOfLines;
if (_numberOfLines != null && !(_numberOfLines >= 0)) {
const _accessible = Platform.select({
ios: accessible !== false,
android:
accessible == null
? onPress != null || onLongPress != null
: accessible,
default: accessible,
});
const isPressable =
(onPress != null ||
onLongPress != null ||
onStartShouldSetResponder != null) &&
_disabled !== true;
// TODO: Move this processing to the view configuration.
const _selectionColor =
selectionColor != null ? processColor(selectionColor) : undefined;
let _style = style;
if (__DEV__) {
console.error(
`'numberOfLines' in <Text> must be a non-negative number, received: ${_numberOfLines}. The value will be set to 0.`,
if (PressabilityDebug.isEnabled() && onPress != null) {
_style = [style, {color: 'magenta'}];
}
}
let _numberOfLines = numberOfLines;
if (_numberOfLines != null && !(_numberOfLines >= 0)) {
if (__DEV__) {
console.error(
`'numberOfLines' in <Text> must be a non-negative number, received: ${_numberOfLines}. The value will be set to 0.`,
);
}
_numberOfLines = 0;
}
let _selectable = selectable;
let processedStyle = flattenStyle<TextStyleProp>(_style);
if (processedStyle != null) {
let overrides: ?{...TextStyleInternal} = null;
if (typeof processedStyle.fontWeight === 'number') {
overrides = overrides || ({}: {...TextStyleInternal});
overrides.fontWeight =
// $FlowFixMe[incompatible-cast]
(String(processedStyle.fontWeight): TextStyleInternal['fontWeight']);
}
if (processedStyle.userSelect != null) {
_selectable = userSelectToSelectableMap[processedStyle.userSelect];
overrides = overrides || ({}: {...TextStyleInternal});
overrides.userSelect = undefined;
}
if (processedStyle.verticalAlign != null) {
overrides = overrides || ({}: {...TextStyleInternal});
overrides.textAlignVertical =
verticalAlignToTextAlignVerticalMap[processedStyle.verticalAlign];
overrides.verticalAlign = undefined;
}
if (overrides != null) {
// $FlowFixMe[incompatible-type]
_style = [_style, overrides];
}
}
const _nativeID = id ?? nativeID;
if (_accessibilityLabel !== undefined) {
processedProps.accessibilityLabel = _accessibilityLabel;
}
if (_accessibilityState !== undefined) {
processedProps.accessibilityState = _accessibilityState;
}
if (_nativeID !== undefined) {
processedProps.nativeID = _nativeID;
}
if (_numberOfLines !== undefined) {
processedProps.numberOfLines = _numberOfLines;
}
if (_selectable !== undefined) {
processedProps.selectable = _selectable;
}
if (_style !== undefined) {
processedProps.style = _style;
}
if (_selectionColor !== undefined) {
processedProps.selectionColor = _selectionColor;
}
let textPressabilityProps: ?TextPressabilityProps;
if (isPressable) {
textPressabilityProps = {
onLongPress,
onPress,
onPressIn,
onPressOut,
onResponderGrant,
onResponderMove,
onResponderRelease,
onResponderTerminate,
onResponderTerminationRequest,
onStartShouldSetResponder,
pressRetentionOffset,
suppressHighlighting,
};
}
const hasTextAncestor = useContext(TextAncestorContext);
if (hasTextAncestor) {
processedProps.disabled = disabled;
processedProps.children = children;
if (isPressable) {
return (
<NativePressableVirtualText
ref={forwardedRef}
textProps={processedProps}
textPressabilityProps={textPressabilityProps ?? {}}
/>
);
}
return <NativeVirtualText {...processedProps} ref={forwardedRef} />;
}
let nativeText = null;
processedProps.accessible = _accessible;
processedProps.allowFontScaling = allowFontScaling !== false;
processedProps.disabled = _disabled;
processedProps.ellipsizeMode = ellipsizeMode ?? 'tail';
processedProps.children = children;
if (isPressable) {
nativeText = (
<NativePressableText
ref={forwardedRef}
textProps={processedProps}
textPressabilityProps={textPressabilityProps ?? {}}
/>
);
} else {
nativeText = <NativeText {...processedProps} ref={forwardedRef} />;
}
if (children == null) {
return nativeText;
}
// If the children do not contain a JSX element it would not be possible to have a
// nested `Text` component so we can skip adding the `TextAncestorContext` context wrapper
// which has a performance overhead. Since we do this for performance reasons we need
// to keep the check simple to avoid regressing overall perf. For this reason the
// `children.length` constant is set to `3`, this should be a reasonable tradeoff
// to capture the majority of `Text` uses but also not make this check too expensive.
if (Array.isArray(children) && children.length <= 3) {
let hasNonTextChild = false;
for (let child of children) {
if (child != null && typeof child === 'object') {
hasNonTextChild = true;
break;
}
}
if (!hasNonTextChild) {
return nativeText;
}
} else if (typeof children !== 'object') {
return nativeText;
}
return <TextAncestorContext value={true}>{nativeText}</TextAncestorContext>;
};
_TextImpl = TextImplNoDefaultProps;
} else {
const TextImplLegacy: component(
ref?: React.RefSetter<TextForwardRef>,
...props: TextProps
) = ({
ref: forwardedRef,
accessible,
accessibilityLabel,
accessibilityState,
allowFontScaling,
'aria-busy': ariaBusy,
'aria-checked': ariaChecked,
'aria-disabled': ariaDisabled,
'aria-expanded': ariaExpanded,
'aria-label': ariaLabel,
'aria-selected': ariaSelected,
children,
ellipsizeMode,
disabled,
id,
nativeID,
numberOfLines,
onLongPress,
onPress,
onPressIn,
onPressOut,
onResponderGrant,
onResponderMove,
onResponderRelease,
onResponderTerminate,
onResponderTerminationRequest,
onStartShouldSetResponder,
pressRetentionOffset,
selectable,
selectionColor,
suppressHighlighting,
style,
...restProps
}: {
ref?: React.RefSetter<TextForwardRef>,
...TextProps,
}) => {
const _accessibilityLabel = ariaLabel ?? accessibilityLabel;
let _accessibilityState: ?TextProps['accessibilityState'] =
accessibilityState;
if (
ariaBusy != null ||
ariaChecked != null ||
ariaDisabled != null ||
ariaExpanded != null ||
ariaSelected != null
) {
if (_accessibilityState != null) {
_accessibilityState = {
busy: ariaBusy ?? _accessibilityState.busy,
checked: ariaChecked ?? _accessibilityState.checked,
disabled: ariaDisabled ?? _accessibilityState.disabled,
expanded: ariaExpanded ?? _accessibilityState.expanded,
selected: ariaSelected ?? _accessibilityState.selected,
};
} else {
_accessibilityState = {
busy: ariaBusy,
checked: ariaChecked,
disabled: ariaDisabled,
expanded: ariaExpanded,
selected: ariaSelected,
};
}
}
const _accessibilityStateDisabled = _accessibilityState?.disabled;
const _disabled = disabled ?? _accessibilityStateDisabled;
const isPressable =
(onPress != null ||
onLongPress != null ||
onStartShouldSetResponder != null) &&
_disabled !== true;
// TODO: Move this processing to the view configuration.
const _selectionColor =
selectionColor != null ? processColor(selectionColor) : undefined;
let _style = style;
if (__DEV__) {
if (PressabilityDebug.isEnabled() && onPress != null) {
_style = [style, {color: 'magenta'}];
}
}
let _numberOfLines = numberOfLines;
if (_numberOfLines != null && !(_numberOfLines >= 0)) {
if (__DEV__) {
console.error(
`'numberOfLines' in <Text> must be a non-negative number, received: ${_numberOfLines}. The value will be set to 0.`,
);
}
_numberOfLines = 0;
}
let _selectable = selectable;
let processedStyle = flattenStyle<TextStyleProp>(_style);
if (processedStyle != null) {
let overrides: ?{...TextStyleInternal} = null;
if (typeof processedStyle.fontWeight === 'number') {
overrides = overrides || ({}: {...TextStyleInternal});
overrides.fontWeight =
// $FlowFixMe[incompatible-cast]
(processedStyle.fontWeight.toString(): TextStyleInternal['fontWeight']);
}
if (processedStyle.userSelect != null) {
_selectable = userSelectToSelectableMap[processedStyle.userSelect];
overrides = overrides || ({}: {...TextStyleInternal});
overrides.userSelect = undefined;
}
if (processedStyle.verticalAlign != null) {
overrides = overrides || ({}: {...TextStyleInternal});
overrides.textAlignVertical =
verticalAlignToTextAlignVerticalMap[processedStyle.verticalAlign];
overrides.verticalAlign = undefined;
}
if (overrides != null) {
// $FlowFixMe[incompatible-type]
_style = [_style, overrides];
}
}
const _nativeID = id ?? nativeID;
const hasTextAncestor = useContext(TextAncestorContext);
if (hasTextAncestor) {
if (isPressable) {
return (
<NativePressableVirtualText
ref={forwardedRef}
textProps={{
...restProps,
accessibilityLabel: _accessibilityLabel,
accessibilityState: _accessibilityState,
nativeID: _nativeID,
numberOfLines: _numberOfLines,
selectable: _selectable,
selectionColor: _selectionColor,
style: _style,
disabled: disabled,
children,
}}
textPressabilityProps={{
onLongPress,
onPress,
onPressIn,
onPressOut,
onResponderGrant,
onResponderMove,
onResponderRelease,
onResponderTerminate,
onResponderTerminationRequest,
onStartShouldSetResponder,
pressRetentionOffset,
suppressHighlighting,
}}
/>
);
}
return (
<NativeVirtualText
{...restProps}
accessibilityLabel={_accessibilityLabel}
accessibilityState={_accessibilityState}
nativeID={_nativeID}
numberOfLines={_numberOfLines}
ref={forwardedRef}
selectable={_selectable}
selectionColor={_selectionColor}
style={_style}
disabled={disabled}>
{children}
</NativeVirtualText>
);
}
_numberOfLines = 0;
}
let _selectable = selectable;
let processedStyle = flattenStyle<TextStyleProp>(_style);
if (processedStyle != null) {
let overrides: ?{...TextStyleInternal} = null;
if (typeof processedStyle.fontWeight === 'number') {
overrides = overrides || ({}: {...TextStyleInternal});
overrides.fontWeight =
// $FlowFixMe[incompatible-cast]
(processedStyle.fontWeight.toString(): TextStyleInternal['fontWeight']);
// If the disabled prop and accessibilityState.disabled are out of sync but not both in
// falsy states we need to update the accessibilityState object to use the disabled prop.
if (
_disabled !== _accessibilityStateDisabled &&
((_disabled != null && _disabled !== false) ||
(_accessibilityStateDisabled != null &&
_accessibilityStateDisabled !== false))
) {
_accessibilityState = {..._accessibilityState, disabled: _disabled};
}
if (processedStyle.userSelect != null) {
_selectable = userSelectToSelectableMap[processedStyle.userSelect];
overrides = overrides || ({}: {...TextStyleInternal});
overrides.userSelect = undefined;
}
const _accessible = Platform.select({
ios: accessible !== false,
android:
accessible == null
? onPress != null || onLongPress != null
: accessible,
default: accessible,
});
if (processedStyle.verticalAlign != null) {
overrides = overrides || ({}: {...TextStyleInternal});
overrides.textAlignVertical =
verticalAlignToTextAlignVerticalMap[processedStyle.verticalAlign];
overrides.verticalAlign = undefined;
}
if (overrides != null) {
// $FlowFixMe[incompatible-type]
_style = [_style, overrides];
}
}
const _nativeID = id ?? nativeID;
const hasTextAncestor = useContext(TextAncestorContext);
if (hasTextAncestor) {
let nativeText = null;
if (isPressable) {
return (
<NativePressableVirtualText
nativeText = (
<NativePressableText
ref={forwardedRef}
textProps={{
...restProps,
accessibilityLabel: _accessibilityLabel,
accessibilityState: _accessibilityState,
accessible: _accessible,
allowFontScaling: allowFontScaling !== false,
disabled: _disabled,
ellipsizeMode: ellipsizeMode ?? 'tail',
nativeID: _nativeID,
numberOfLines: _numberOfLines,
selectable: _selectable,
selectionColor: _selectionColor,
style: _style,
disabled: disabled,
children,
}}
textPressabilityProps={{
@@ -203,127 +543,61 @@ const TextImpl: component(
}}
/>
);
} else {
nativeText = (
<NativeText
{...restProps}
accessibilityLabel={_accessibilityLabel}
accessibilityState={_accessibilityState}
accessible={_accessible}
allowFontScaling={allowFontScaling !== false}
disabled={_disabled}
ellipsizeMode={ellipsizeMode ?? 'tail'}
nativeID={_nativeID}
numberOfLines={_numberOfLines}
ref={forwardedRef}
selectable={_selectable}
selectionColor={_selectionColor}
style={_style}>
{children}
</NativeText>
);
}
return (
<NativeVirtualText
{...restProps}
accessibilityLabel={_accessibilityLabel}
accessibilityState={_accessibilityState}
nativeID={_nativeID}
numberOfLines={_numberOfLines}
ref={forwardedRef}
selectable={_selectable}
selectionColor={_selectionColor}
style={_style}
disabled={disabled}>
{children}
</NativeVirtualText>
);
}
// If the disabled prop and accessibilityState.disabled are out of sync but not both in
// falsy states we need to update the accessibilityState object to use the disabled prop.
if (
_disabled !== _accessibilityStateDisabled &&
((_disabled != null && _disabled !== false) ||
(_accessibilityStateDisabled != null &&
_accessibilityStateDisabled !== false))
) {
_accessibilityState = {..._accessibilityState, disabled: _disabled};
}
const _accessible = Platform.select({
ios: accessible !== false,
android:
accessible == null ? onPress != null || onLongPress != null : accessible,
default: accessible,
});
let nativeText = null;
if (isPressable) {
nativeText = (
<NativePressableText
ref={forwardedRef}
textProps={{
...restProps,
accessibilityLabel: _accessibilityLabel,
accessibilityState: _accessibilityState,
accessible: _accessible,
allowFontScaling: allowFontScaling !== false,
disabled: _disabled,
ellipsizeMode: ellipsizeMode ?? 'tail',
nativeID: _nativeID,
numberOfLines: _numberOfLines,
selectable: _selectable,
selectionColor: _selectionColor,
style: _style,
children,
}}
textPressabilityProps={{
onLongPress,
onPress,
onPressIn,
onPressOut,
onResponderGrant,
onResponderMove,
onResponderRelease,
onResponderTerminate,
onResponderTerminationRequest,
onStartShouldSetResponder,
pressRetentionOffset,
suppressHighlighting,
}}
/>
);
} else {
nativeText = (
<NativeText
{...restProps}
accessibilityLabel={_accessibilityLabel}
accessibilityState={_accessibilityState}
accessible={_accessible}
allowFontScaling={allowFontScaling !== false}
disabled={_disabled}
ellipsizeMode={ellipsizeMode ?? 'tail'}
nativeID={_nativeID}
numberOfLines={_numberOfLines}
ref={forwardedRef}
selectable={_selectable}
selectionColor={_selectionColor}
style={_style}>
{children}
</NativeText>
);
}
if (children == null) {
return nativeText;
}
// If the children do not contain a JSX element it would not be possible to have a
// nested `Text` component so we can skip adding the `TextAncestorContext` context wrapper
// which has a performance overhead. Since we do this for performance reasons we need
// to keep the check simple to avoid regressing overall perf. For this reason the
// `children.length` constant is set to `3`, this should be a reasonable tradeoff
// to capture the majority of `Text` uses but also not make this check too expensive.
if (Array.isArray(children) && children.length <= 3) {
let hasNonTextChild = false;
for (let child of children) {
if (child != null && typeof child === 'object') {
hasNonTextChild = true;
break;
}
}
if (!hasNonTextChild) {
if (children == null) {
return nativeText;
}
} else if (typeof children !== 'object') {
return nativeText;
}
return <TextAncestorContext value={true}>{nativeText}</TextAncestorContext>;
};
// If the children do not contain a JSX element it would not be possible to have a
// nested `Text` component so we can skip adding the `TextAncestorContext` context wrapper
// which has a performance overhead. Since we do this for performance reasons we need
// to keep the check simple to avoid regressing overall perf. For this reason the
// `children.length` constant is set to `3`, this should be a reasonable tradeoff
// to capture the majority of `Text` uses but also not make this check too expensive.
if (Array.isArray(children) && children.length <= 3) {
let hasNonTextChild = false;
for (let child of children) {
if (child != null && typeof child === 'object') {
hasNonTextChild = true;
break;
}
}
if (!hasNonTextChild) {
return nativeText;
}
} else if (typeof children !== 'object') {
return nativeText;
}
return <TextAncestorContext value={true}>{nativeText}</TextAncestorContext>;
};
_TextImpl = TextImplLegacy;
}
const TextImpl: component(
ref?: React.RefSetter<TextForwardRef>,
...props: TextProps
) = _TextImpl;
TextImpl.displayName = 'Text';
@@ -128,7 +128,6 @@ const HMRClient: HMRClientNativeInterface = {
JSON.stringify({
type: 'log',
level,
mode: global.RN$Bridgeless === true ? 'NOBRIDGE' : 'BRIDGE',
data: data.map(item =>
typeof item === 'string'
? item
@@ -96,6 +96,8 @@ static ModalHostViewEventEmitter::OnOrientationChange onOrientationChangeStruct(
@interface RCTModalHostViewComponentView () <RCTFabricModalHostViewControllerDelegate>
@property (nonatomic, weak) UIView *accessibilityFocusedView;
@end
@implementation RCTModalHostViewComponentView {
@@ -148,6 +150,7 @@ static ModalHostViewEventEmitter::OnOrientationChange onOrientationChangeStruct(
{
BOOL shouldBePresented = !_isPresented && _shouldPresent && self.window;
if (shouldBePresented) {
[self saveAccessibilityFocusedView];
self.viewController.presentationController.delegate = self;
_isPresented = YES;
@@ -179,6 +182,8 @@ static ModalHostViewEventEmitter::OnOrientationChange onOrientationChangeStruct(
if (eventEmitter) {
eventEmitter->onDismiss(ModalHostViewEventEmitter::OnDismiss{});
}
[self restoreAccessibilityFocusedView];
}];
}
}
@@ -207,6 +212,23 @@ static ModalHostViewEventEmitter::OnOrientationChange onOrientationChangeStruct(
[self ensurePresentedOnlyIfNeeded];
}
- (void)saveAccessibilityFocusedView
{
id focusedElement = UIAccessibilityFocusedElement(nil);
if (focusedElement && [focusedElement isKindOfClass:[UIView class]]) {
self.accessibilityFocusedView = (UIView *)focusedElement;
}
}
- (void)restoreAccessibilityFocusedView
{
id viewToFocus = self.accessibilityFocusedView;
if (viewToFocus) {
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, viewToFocus);
self.accessibilityFocusedView = nil;
}
}
#pragma mark - RCTFabricModalHostViewControllerDelegate
- (void)boundsDidChange:(CGRect)newBounds
@@ -3348,8 +3348,10 @@ public final class com/facebook/react/uimanager/DisplayMetricsHolder {
public static final fun getDisplayMetricsWritableMap (D)Lcom/facebook/react/bridge/WritableMap;
public static final fun getScreenDisplayMetrics ()Landroid/util/DisplayMetrics;
public static final fun getWindowDisplayMetrics ()Landroid/util/DisplayMetrics;
public static final fun initDisplayMetrics (Landroid/content/Context;)V
public static final fun initDisplayMetricsIfNotInitialized (Landroid/content/Context;)V
public static final fun initScreenDisplayMetrics (Landroid/content/Context;)V
public static final fun initScreenDisplayMetricsIfNotInitialized (Landroid/content/Context;)V
public static final fun initWindowDisplayMetrics (Landroid/content/Context;)V
public static final fun initWindowDisplayMetricsIfNotInitialized (Landroid/content/Context;)V
public static final fun setScreenDisplayMetrics (Landroid/util/DisplayMetrics;)V
public static final fun setWindowDisplayMetrics (Landroid/util/DisplayMetrics;)V
}
@@ -630,6 +630,7 @@ dependencies {
api(libs.androidx.autofill)
api(libs.androidx.swiperefreshlayout)
api(libs.androidx.tracing)
api(libs.androidx.window)
api(libs.fbjni)
api(libs.fresco)
@@ -229,6 +229,9 @@ public class ReactInstanceManager {
return new ReactInstanceManagerBuilder();
}
/**
* @noinspection deprecation
*/
/* package */ ReactInstanceManager(
Context applicationContext,
@Nullable Activity currentActivity,
@@ -259,7 +262,11 @@ public class ReactInstanceManager {
FLog.d(TAG, "ReactInstanceManager.ctor()");
initializeSoLoaderIfNecessary(applicationContext);
DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(applicationContext);
DisplayMetricsHolder.initScreenDisplayMetricsIfNotInitialized(applicationContext);
if (currentActivity != null) {
DisplayMetricsHolder.initWindowDisplayMetricsIfNotInitialized(currentActivity);
}
// See {@code ReactInstanceManagerBuilder} for description of all flags here.
mApplicationContext = applicationContext;
@@ -924,6 +931,13 @@ public class ReactInstanceManager {
ReactContext currentReactContext = getCurrentReactContext();
if (currentReactContext != null) {
DisplayMetricsHolder.initScreenDisplayMetrics(currentReactContext);
Activity currentActivity = currentReactContext.getCurrentActivity();
if (currentActivity != null) {
DisplayMetricsHolder.initWindowDisplayMetrics(currentActivity);
}
AppearanceModule appearanceModule =
currentReactContext.getNativeModule(AppearanceModule.class);
@@ -35,7 +35,8 @@ import java.util.List;
*
* @deprecated This class will be replaced by com.facebook.react.ReactHost in the New Architecture.
*/
@Deprecated
@Deprecated(
since = "This class is part of Legacy Architecture and will be removed in a future release")
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
@Nullsafe(Nullsafe.Mode.LOCAL)
public abstract class ReactNativeHost {
@@ -136,9 +136,8 @@ public class ReactRootView extends FrameLayout implements RootView, ReactRoot {
setRootViewTag(ReactRootViewTagGenerator.getNextRootViewTag());
setClipChildren(false);
if (ReactNativeFeatureFlags.enableFontScaleChangesUpdatingLayout()) {
DisplayMetricsHolder.initDisplayMetrics(getContext().getApplicationContext());
}
DisplayMetricsHolder.initScreenDisplayMetrics(getContext());
DisplayMetricsHolder.initWindowDisplayMetrics(getContext());
}
@Override
@@ -883,7 +882,8 @@ public class ReactRootView extends FrameLayout implements RootView, ReactRoot {
private int mDeviceRotation = 0;
/* package */ CustomGlobalLayoutListener() {
DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(getContext().getApplicationContext());
DisplayMetricsHolder.initScreenDisplayMetricsIfNotInitialized(getContext());
DisplayMetricsHolder.initWindowDisplayMetricsIfNotInitialized(getContext());
mVisibleViewArea = new Rect();
mMinKeyboardHeightDetected = (int) PixelUtil.toPixelFromDIP(60);
}
@@ -1006,7 +1006,8 @@ public class ReactRootView extends FrameLayout implements RootView, ReactRoot {
return;
}
mDeviceRotation = rotation;
DisplayMetricsHolder.initDisplayMetrics(getContext().getApplicationContext());
DisplayMetricsHolder.initScreenDisplayMetrics(getContext());
DisplayMetricsHolder.initWindowDisplayMetrics(getContext());
emitOrientationChanged(rotation);
}
@@ -7,13 +7,13 @@
package com.facebook.react.bridge
import com.facebook.react.common.annotations.internal.LegacyArchitecture
import com.facebook.react.common.annotations.internal.InteropLegacyArchitecture
/**
* This interface includes the methods needed to use a running JS instance, without specifying any
* of the bridge-specific initialization or lifecycle management.
*/
@LegacyArchitecture
@InteropLegacyArchitecture
public interface JSInstance {
public fun invokeCallback(callbackID: Int, arguments: NativeArrayInterface)
@@ -25,6 +25,11 @@ import com.facebook.react.packagerconnection.RequestHandler
*/
internal class DefaultDevSupportManagerFactory : DevSupportManagerFactory {
@Deprecated(
"Use the other create() method with useDevSupport parameter for New Architecture. This method will be removed in a future release.",
replaceWith =
ReplaceWith(
"create(applicationContext, reactInstanceManagerHelper, packagerPathForJSBundleName, enableOnCreate, redBoxHandler, devBundleDownloadListener, minNumShakes, customPackagerCommandHandlers, surfaceDelegateFactory, devLoadingViewManager, pausedInDebuggerOverlayManager)"))
override fun create(
applicationContext: Context,
reactInstanceManagerHelper: ReactInstanceDevHelper,
@@ -22,6 +22,12 @@ public interface DevSupportManagerFactory {
* Factory used by the Old Architecture flow to create a [DevSupportManager] and a
* [BridgeDevSupportManager]
*/
@Deprecated(
message =
"Use the other create() method with useDevSupport parameter for New Architecture. This method will be removed in a future release.",
replaceWith =
ReplaceWith(
"create(applicationContext, reactInstanceManagerHelper, packagerPathForJSBundleName, enableOnCreate, redBoxHandler, devBundleDownloadListener, minNumShakes, customPackagerCommandHandlers, surfaceDelegateFactory, devLoadingViewManager, pausedInDebuggerOverlayManager)"))
public fun create(
applicationContext: Context,
reactInstanceManagerHelper: ReactInstanceDevHelper,
@@ -15,7 +15,8 @@ import com.facebook.react.bridge.ReactSoftExceptionLogger
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.DisplayMetricsHolder.getDisplayMetricsWritableMap
import com.facebook.react.uimanager.DisplayMetricsHolder.initDisplayMetricsIfNotInitialized
import com.facebook.react.uimanager.DisplayMetricsHolder.initScreenDisplayMetricsIfNotInitialized
import com.facebook.react.uimanager.DisplayMetricsHolder.initWindowDisplayMetricsIfNotInitialized
import com.facebook.react.views.view.isEdgeToEdgeFeatureFlagOn
/** Module that exposes Android Constants to JS. */
@@ -26,7 +27,8 @@ internal class DeviceInfoModule(reactContext: ReactApplicationContext) :
private var previousDisplayMetrics: ReadableMap? = null
init {
initDisplayMetricsIfNotInitialized(reactContext)
initScreenDisplayMetricsIfNotInitialized(reactContext)
reactContext.currentActivity?.let { initWindowDisplayMetricsIfNotInitialized(it) }
reactContext.addLifecycleEventListener(this)
}
@@ -8,6 +8,7 @@
package com.facebook.react.modules.network
import com.facebook.proguard.annotations.DoNotStripAny
import com.facebook.soloader.SoLoader
/**
* [Experimental] An interface for reporting network events to the modern debugger server and Web
@@ -19,6 +20,10 @@ import com.facebook.proguard.annotations.DoNotStripAny
*/
@DoNotStripAny
internal object InspectorNetworkReporter {
init {
SoLoader.loadLibrary("react_devsupportjni")
}
@JvmStatic external fun isDebuggingEnabled(): Boolean
/**
@@ -625,9 +625,8 @@ public class ReactHostImpl(
override fun onConfigurationChanged(context: Context) {
val currentReactContext = this.currentReactContext
if (currentReactContext != null) {
if (ReactNativeFeatureFlags.enableFontScaleChangesUpdatingLayout()) {
DisplayMetricsHolder.initDisplayMetrics(currentReactContext)
}
DisplayMetricsHolder.initScreenDisplayMetrics(currentReactContext)
currentReactContext.currentActivity?.let { DisplayMetricsHolder.initWindowDisplayMetrics(it) }
val appearanceModule = currentReactContext.getNativeModule(AppearanceModule::class.java)
appearanceModule?.onConfigurationChanged(context)
@@ -918,6 +917,7 @@ public class ReactHostImpl(
val instance =
ReactInstance(
reactContext,
currentActivity,
reactHostDelegate,
componentFactory,
devSupportManager,
@@ -7,6 +7,7 @@
package com.facebook.react.runtime
import android.app.Activity
import android.content.res.AssetManager
import android.view.View
import com.facebook.common.logging.FLog
@@ -88,6 +89,7 @@ import kotlin.jvm.JvmStatic
@UnstableReactNativeAPI
internal class ReactInstance(
private val context: BridgelessReactContext,
private val activity: Activity?,
delegate: ReactHostDelegate,
componentFactory: ComponentFactory,
devSupportManager: DevSupportManager,
@@ -240,7 +242,8 @@ internal class ReactInstance(
FabricUIManager(context, ViewManagerRegistry(viewManagerResolver), eventBeatManager)
// Misc initialization that needs to be done before Fabric init
DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(context)
DisplayMetricsHolder.initScreenDisplayMetricsIfNotInitialized(context)
activity?.let { DisplayMetricsHolder.initWindowDisplayMetricsIfNotInitialized(it) }
val binding = FabricUIManagerBinding()
binding.register(
@@ -13,16 +13,20 @@ import android.util.DisplayMetrics
import android.view.WindowManager
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.window.layout.WindowMetricsCalculator
import com.facebook.react.bridge.WritableMap
import com.facebook.react.bridge.WritableNativeMap
import com.facebook.react.views.view.isEdgeToEdgeFeatureFlagOn
/**
* Holds an instance of the current DisplayMetrics so we don't have to thread it through all the
* classes that need it.
*/
public object DisplayMetricsHolder {
private const val INITIALIZATION_MISSING_MESSAGE =
"DisplayMetricsHolder must be initialized with initDisplayMetricsIfNotInitialized or initDisplayMetrics"
private const val SCREEN_INITIALIZATION_MISSING_MESSAGE =
"DisplayMetricsHolder must be initialized with initScreenDisplayMetricsIfNotInitialized or initScreenDisplayMetrics"
private const val WINDOW_INITIALIZATION_MISSING_MESSAGE =
"DisplayMetricsHolder must be initialized with initWindowDisplayMetricsIfNotInitialized or initWindowDisplayMetrics"
@JvmStatic private var windowDisplayMetrics: DisplayMetrics? = null
@JvmStatic private var screenDisplayMetrics: DisplayMetrics? = null
@@ -30,7 +34,7 @@ public object DisplayMetricsHolder {
/** The metrics of the window associated to the Context used to initialize ReactNative */
@JvmStatic
public fun getWindowDisplayMetrics(): DisplayMetrics {
checkNotNull(windowDisplayMetrics) { INITIALIZATION_MISSING_MESSAGE }
checkNotNull(windowDisplayMetrics) { WINDOW_INITIALIZATION_MISSING_MESSAGE }
return windowDisplayMetrics as DisplayMetrics
}
@@ -42,7 +46,7 @@ public object DisplayMetricsHolder {
/** Screen metrics returns the metrics of the default screen on the device. */
@JvmStatic
public fun getScreenDisplayMetrics(): DisplayMetrics {
checkNotNull(screenDisplayMetrics) { INITIALIZATION_MISSING_MESSAGE }
checkNotNull(screenDisplayMetrics) { SCREEN_INITIALIZATION_MISSING_MESSAGE }
return screenDisplayMetrics as DisplayMetrics
}
@@ -52,33 +56,58 @@ public object DisplayMetricsHolder {
}
@JvmStatic
public fun initDisplayMetricsIfNotInitialized(context: Context) {
if (screenDisplayMetrics != null) {
return
public fun initScreenDisplayMetricsIfNotInitialized(context: Context) {
if (screenDisplayMetrics == null) {
initScreenDisplayMetrics(context)
}
initDisplayMetrics(context)
}
@JvmStatic
public fun initDisplayMetrics(context: Context) {
val displayMetrics = context.resources.displayMetrics
windowDisplayMetrics = displayMetrics
val screenDisplayMetrics = DisplayMetrics()
screenDisplayMetrics.setTo(displayMetrics)
public fun initWindowDisplayMetricsIfNotInitialized(context: Context) {
if (windowDisplayMetrics == null) {
initWindowDisplayMetrics(context)
}
}
@JvmStatic
public fun initScreenDisplayMetrics(context: Context) {
val displayMetrics = DisplayMetrics()
displayMetrics.setTo(context.resources.displayMetrics)
val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
// Get the real display metrics if we are using API level 17 or higher.
// The real metrics include system decor elements (e.g. soft menu bar).
//
// See:
// http://developer.android.com/reference/android/view/Display.html#getRealMetrics(android.util.DisplayMetrics)
@Suppress("DEPRECATION") wm.defaultDisplay.getRealMetrics(screenDisplayMetrics)
DisplayMetricsHolder.screenDisplayMetrics = screenDisplayMetrics
@Suppress("DEPRECATION") wm.defaultDisplay.getRealMetrics(displayMetrics)
screenDisplayMetrics = displayMetrics
}
/*
* NOTE: Unlike [initScreenDisplayMetrics], this method needs a UiContext (Activity of
* InputMethodService) else WindowMetircsCalculator will throw an exception.
*/
@JvmStatic
public fun initWindowDisplayMetrics(context: Context) {
val displayMetrics = DisplayMetrics()
displayMetrics.setTo(context.resources.displayMetrics)
if (isEdgeToEdgeFeatureFlagOn) {
WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(context).let { windowMetrics
->
displayMetrics.widthPixels = windowMetrics.bounds.width()
displayMetrics.heightPixels = windowMetrics.bounds.height()
}
}
windowDisplayMetrics = displayMetrics
}
@JvmStatic
public fun getDisplayMetricsWritableMap(fontScale: Double): WritableMap {
checkNotNull(windowDisplayMetrics) { INITIALIZATION_MISSING_MESSAGE }
checkNotNull(screenDisplayMetrics) { INITIALIZATION_MISSING_MESSAGE }
checkNotNull(windowDisplayMetrics) { WINDOW_INITIALIZATION_MISSING_MESSAGE }
checkNotNull(screenDisplayMetrics) { SCREEN_INITIALIZATION_MISSING_MESSAGE }
return WritableNativeMap().apply {
putMap(
@@ -20,7 +20,7 @@ public object PixelUtil {
}
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, value, DisplayMetricsHolder.getWindowDisplayMetrics())
TypedValue.COMPLEX_UNIT_DIP, value, DisplayMetricsHolder.getScreenDisplayMetrics())
}
/** Convert from DIP to PX */
@@ -37,7 +37,7 @@ public object PixelUtil {
return Float.NaN
}
val displayMetrics = DisplayMetricsHolder.getWindowDisplayMetrics()
val displayMetrics = DisplayMetricsHolder.getScreenDisplayMetrics()
val scaledValue = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, value, displayMetrics)
if (maxFontScale >= 1) {
@@ -60,13 +60,13 @@ public object PixelUtil {
return Float.NaN
}
return value / DisplayMetricsHolder.getWindowDisplayMetrics().density
return value / DisplayMetricsHolder.getScreenDisplayMetrics().density
}
/** @return [Float] that represents the density of the display metrics for device screen. */
@JvmStatic
public fun getDisplayMetricDensity(): Float =
DisplayMetricsHolder.getWindowDisplayMetrics().density
DisplayMetricsHolder.getScreenDisplayMetrics().density
/* Kotlin extensions */
public fun Int.dpToPx(): Float = toPixelFromDIP(this.toFloat())
@@ -12,6 +12,7 @@ import static com.facebook.react.bridge.ReactMarkerConstants.CREATE_UI_MANAGER_M
import static com.facebook.react.uimanager.common.UIManagerType.FABRIC;
import static com.facebook.react.uimanager.common.UIManagerType.LEGACY;
import android.app.Activity;
import android.content.ComponentCallbacks2;
import android.content.res.Configuration;
import android.view.View;
@@ -126,7 +127,11 @@ public class UIManagerModule extends ReactContextBaseJavaModule
ViewManagerResolver viewManagerResolver,
int minTimeLeftInFrameForNonBatchedOperationMs) {
super(reactContext);
DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(reactContext);
DisplayMetricsHolder.initScreenDisplayMetricsIfNotInitialized(reactContext);
Activity currentActivity = reactContext.getCurrentActivity();
if (currentActivity != null) {
DisplayMetricsHolder.initWindowDisplayMetricsIfNotInitialized(currentActivity);
}
mEventDispatcher = new EventDispatcherImpl(reactContext);
mModuleConstants = createConstants(viewManagerResolver);
mCustomDirectEvents = UIManagerModuleConstants.directEventTypeConstants;
@@ -146,7 +151,11 @@ public class UIManagerModule extends ReactContextBaseJavaModule
List<ViewManager> viewManagersList,
int minTimeLeftInFrameForNonBatchedOperationMs) {
super(reactContext);
DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(reactContext);
DisplayMetricsHolder.initScreenDisplayMetricsIfNotInitialized(reactContext);
Activity currentActivity = reactContext.getCurrentActivity();
if (currentActivity != null) {
DisplayMetricsHolder.initWindowDisplayMetricsIfNotInitialized(currentActivity);
}
mEventDispatcher = new EventDispatcherImpl(reactContext);
mCustomDirectEvents = MapBuilder.newHashMap();
mModuleConstants = createConstants(viewManagersList, null, mCustomDirectEvents);
@@ -5,6 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
@file:Suppress("DEPRECATION")
package com.facebook.react.uimanager.layoutanimation
import android.view.View
@@ -27,6 +29,9 @@ import com.facebook.react.uimanager.IllegalViewOperationException
* order to animate layout when a valid configuration has been supplied by the application.
*/
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
@Deprecated(
message = "This class is part of Legacy Architecture and will be removed in a future release",
level = DeprecationLevel.WARNING)
internal abstract class AbstractLayoutAnimation {
var interpolator: Interpolator? = null
var delayMs: Int = 0
@@ -5,6 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
@file:Suppress("DEPRECATION")
package com.facebook.react.uimanager.layoutanimation
import com.facebook.react.common.annotations.internal.LegacyArchitecture
@@ -15,6 +17,9 @@ import com.facebook.react.common.annotations.internal.LegacyArchitectureLogLevel
* creation.
*/
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
@Deprecated(
message = "This class is part of Legacy Architecture and will be removed in a future release",
level = DeprecationLevel.WARNING)
internal enum class AnimatedPropertyType {
OPACITY,
SCALE_X,
@@ -5,6 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
@file:Suppress("DEPRECATION")
package com.facebook.react.uimanager.layoutanimation
import android.view.View
@@ -17,6 +19,9 @@ import com.facebook.react.uimanager.IllegalViewOperationException
/** Class responsible for default layout animation, i.e animation of view creation and deletion. */
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
@Deprecated(
message = "This class is part of Legacy Architecture and will be removed in a future release",
level = DeprecationLevel.WARNING)
internal abstract class BaseLayoutAnimation : AbstractLayoutAnimation() {
abstract fun isReverse(): Boolean
@@ -5,6 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
@file:Suppress("DEPRECATION")
package com.facebook.react.uimanager.layoutanimation
import com.facebook.react.common.annotations.internal.LegacyArchitecture
@@ -14,6 +16,9 @@ import com.facebook.react.common.annotations.internal.LegacyArchitectureLogLevel
* Enum representing the different interpolators that can be used in layout animation configuration.
*/
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
@Deprecated(
message = "This class is part of Legacy Architecture and will be removed in a future release",
level = DeprecationLevel.WARNING)
internal enum class InterpolatorType {
LINEAR,
EASE_IN,
@@ -5,6 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
@file:Suppress("DEPRECATION")
package com.facebook.react.uimanager.layoutanimation
import android.util.SparseArray
@@ -29,6 +31,9 @@ import javax.annotation.concurrent.NotThreadSafe
*/
@NotThreadSafe
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
@Deprecated(
message = "This class is part of Legacy Architecture and will be removed in a future release",
level = DeprecationLevel.WARNING)
public open class LayoutAnimationController {
private val layoutCreateAnimation: AbstractLayoutAnimation = LayoutCreateAnimation()
private val layoutUpdateAnimation: AbstractLayoutAnimation = LayoutUpdateAnimation()
@@ -5,6 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
@file:Suppress("DEPRECATION")
package com.facebook.react.uimanager.layoutanimation
import com.facebook.react.common.annotations.internal.LegacyArchitecture
@@ -12,6 +14,9 @@ import com.facebook.react.common.annotations.internal.LegacyArchitectureLogLevel
/** Listener invoked when a layout animation has completed. */
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
@Deprecated(
message = "This class is part of Legacy Architecture and will be removed in a future release",
level = DeprecationLevel.WARNING)
public fun interface LayoutAnimationListener {
public fun onAnimationEnd()
}
@@ -5,6 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
@file:Suppress("DEPRECATION")
package com.facebook.react.uimanager.layoutanimation
import com.facebook.react.common.annotations.internal.LegacyArchitecture
@@ -15,6 +17,9 @@ import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger
* Enum representing the different animation type that can be specified in layout animation config.
*/
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
@Deprecated(
message = "This class is part of Legacy Architecture and will be removed in a future release",
level = DeprecationLevel.WARNING)
internal enum class LayoutAnimationType {
CREATE,
UPDATE,
@@ -5,6 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
@file:Suppress("DEPRECATION")
package com.facebook.react.uimanager.layoutanimation
import com.facebook.react.common.annotations.internal.LegacyArchitecture
@@ -5,6 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
@file:Suppress("DEPRECATION")
package com.facebook.react.uimanager.layoutanimation
import com.facebook.react.common.annotations.internal.LegacyArchitecture
@@ -16,6 +18,9 @@ import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger
* config was supplied for the layout animation of DELETE type.
*/
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
@Deprecated(
message = "This class is part of Legacy Architecture and will be removed in a future release",
level = DeprecationLevel.WARNING)
internal class LayoutDeleteAnimation : BaseLayoutAnimation() {
override fun isReverse(): Boolean = true
@@ -5,6 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
@file:Suppress("DEPRECATION")
package com.facebook.react.uimanager.layoutanimation
import com.facebook.react.common.annotations.internal.LegacyArchitecture
@@ -12,6 +14,9 @@ import com.facebook.react.common.annotations.internal.LegacyArchitectureLogLevel
/** Interface for an animation type that takes care of updating the view layout. */
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
@Deprecated(
message = "This class is part of Legacy Architecture and will be removed in a future release",
level = DeprecationLevel.WARNING)
internal interface LayoutHandlingAnimation {
/**
* Notifies the animation of a layout update in case one occurs during the animation. This avoids
@@ -5,6 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
@file:Suppress("DEPRECATION")
package com.facebook.react.uimanager.layoutanimation
import android.view.View
@@ -19,6 +21,9 @@ import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger
* was supplied for the layout animation of UPDATE type.
*/
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
@Deprecated(
message = "This class is part of Legacy Architecture and will be removed in a future release",
level = DeprecationLevel.WARNING)
internal class LayoutUpdateAnimation : AbstractLayoutAnimation() {
override fun isValid(): Boolean = durationMs > 0
@@ -5,6 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
@file:Suppress("DEPRECATION")
package com.facebook.react.uimanager.layoutanimation
import android.view.View
@@ -21,6 +23,9 @@ import java.lang.ref.WeakReference
* optimize rendering performances.
*/
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
@Deprecated(
message = "This class is part of Legacy Architecture and will be removed in a future release",
level = DeprecationLevel.WARNING)
internal class OpacityAnimation(view: View, private val startOpacity: Float, endOpacity: Float) :
Animation() {
private val viewRef = WeakReference(view)
@@ -5,6 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
@file:Suppress("DEPRECATION")
package com.facebook.react.uimanager.layoutanimation
import android.view.View
@@ -22,6 +24,9 @@ import java.lang.ref.WeakReference
* ScaleAnimation and TranslateAnimation.
*/
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
@Deprecated(
message = "This class is part of Legacy Architecture and will be removed in a future release",
level = DeprecationLevel.WARNING)
internal class PositionAndSizeAnimation(view: View, x: Int, y: Int, width: Int, height: Int) :
Animation(), LayoutHandlingAnimation {
private val viewRef = WeakReference(view)
@@ -5,6 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
@file:Suppress("DEPRECATION")
package com.facebook.react.uimanager.layoutanimation
import android.view.animation.Interpolator
@@ -19,6 +21,9 @@ import kotlin.math.sin
/** Simple spring interpolator */
// TODO(7613736): Improve spring interpolator with friction and damping variable support
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
@Deprecated(
message = "This class is part of Legacy Architecture and will be removed in a future release",
level = DeprecationLevel.WARNING)
internal class SimpleSpringInterpolator @JvmOverloads constructor(springDamping: Float = FACTOR) :
Interpolator {
private val _springDamping: Float = springDamping
@@ -50,28 +50,32 @@ private fun rectsOverlap(rect1: Rect, rect2: Rect): Boolean {
internal class VirtualViewContainerState {
private val prerenderRatio: Double = ReactNativeFeatureFlags.virtualViewPrerenderRatio()
private val detectWindowFocus = ReactNativeFeatureFlags.enableVirtualViewWindowFocusDetection()
private val virtualViews: MutableSet<VirtualView> = mutableSetOf()
private val emptyRect: Rect = Rect()
private val visibleRect: Rect = Rect()
private val prerenderRect: Rect = Rect()
private val onWindowFocusChangeListener =
ViewTreeObserver.OnWindowFocusChangeListener {
debugLog("onWindowFocusChanged")
updateModes()
if (ReactNativeFeatureFlags.enableVirtualViewWindowFocusDetection()) {
ViewTreeObserver.OnWindowFocusChangeListener {
debugLog("onWindowFocusChanged")
updateModes()
}
} else {
null
}
private val scrollView: ViewGroup
constructor(scrollView: ViewGroup) {
this.scrollView = scrollView
if (detectWindowFocus) {
if (onWindowFocusChangeListener != null) {
scrollView.viewTreeObserver.addOnWindowFocusChangeListener(onWindowFocusChangeListener)
}
}
public fun cleanup() {
if (detectWindowFocus) {
if (onWindowFocusChangeListener != null) {
scrollView.viewTreeObserver.removeOnWindowFocusChangeListener(onWindowFocusChangeListener)
}
}
@@ -115,7 +119,7 @@ internal class VirtualViewContainerState {
rect.isEmpty -> {}
rectsOverlap(rect, visibleRect) -> {
thresholdRect = visibleRect
if (detectWindowFocus) {
if (onWindowFocusChangeListener != null) {
if (scrollView.hasWindowFocus()) {
mode = VirtualViewMode.Visible
} else {
@@ -40,11 +40,14 @@ public class ReactVirtualView(context: Context) :
internal var modeChangeEmitter: VirtualViewModeChangeEmitter? = null
internal var prerenderRatio: Double = ReactNativeFeatureFlags.virtualViewPrerenderRatio()
internal val debugLogEnabled: Boolean = ReactNativeFeatureFlags.enableVirtualViewDebugFeatures()
internal val detectWindowFocus = ReactNativeFeatureFlags.enableVirtualViewWindowFocusDetection()
private val onWindowFocusChangeListener =
ViewTreeObserver.OnWindowFocusChangeListener {
dispatchOnModeChangeIfNeeded(checkRectChange = false)
if (ReactNativeFeatureFlags.enableVirtualViewWindowFocusDetection()) {
ViewTreeObserver.OnWindowFocusChangeListener {
dispatchOnModeChangeIfNeeded(checkRectChange = false)
}
} else {
null
}
private var parentScrollView: View? = null
@@ -90,7 +93,7 @@ public class ReactVirtualView(context: Context) :
ReactScrollViewHelper.addLayoutChangeListener(this)
}
debugLog("onAttachedToWindow")
if (detectWindowFocus) {
if (onWindowFocusChangeListener != null) {
viewTreeObserver.addOnWindowFocusChangeListener(onWindowFocusChangeListener)
}
dispatchOnModeChangeIfNeeded(checkRectChange = false)
@@ -100,7 +103,7 @@ public class ReactVirtualView(context: Context) :
super.onDetachedFromWindow()
ReactScrollViewHelper.removeScrollListener(this)
ReactScrollViewHelper.removeLayoutChangeListener(this)
if (detectWindowFocus) {
if (onWindowFocusChangeListener != null) {
viewTreeObserver.removeOnWindowFocusChangeListener(onWindowFocusChangeListener)
}
cleanupLayoutListeners()
@@ -202,7 +205,7 @@ public class ReactVirtualView(context: Context) :
val newMode: VirtualViewMode
if (rectsOverlap(targetRect, thresholdRect)) {
if (detectWindowFocus) {
if (onWindowFocusChangeListener != null) {
if (hasWindowFocus()) {
newMode = VirtualViewMode.Visible
} else {
@@ -1,25 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.yoga;
public class YogaConstants {
public static final float UNDEFINED = Float.NaN;
public static boolean isUndefined(float value) {
return Float.compare(value, UNDEFINED) == 0;
}
public static boolean isUndefined(YogaValue value) {
return value.unit == YogaUnit.UNDEFINED;
}
public static float getUndefined() {
return UNDEFINED;
}
}
@@ -0,0 +1,18 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.yoga
public object YogaConstants {
@JvmField public val UNDEFINED: Float = Float.NaN
@JvmStatic public fun isUndefined(value: Float): Boolean = value.compareTo(UNDEFINED) == 0
@JvmStatic public fun isUndefined(value: YogaValue): Boolean = value.unit == YogaUnit.UNDEFINED
@JvmStatic public fun getUndefined(): Float = UNDEFINED
}
@@ -29,9 +29,6 @@ if(CCACHE_FOUND)
set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ccache)
endif(CCACHE_FOUND)
# Make sure every shared lib includes a .note.gnu.build-id header
add_link_options(-Wl,--build-id)
function(add_react_android_subdir relative_path)
add_subdirectory(${REACT_ANDROID_DIR}/${relative_path} ReactAndroid/${relative_path})
endfunction()
@@ -19,6 +19,7 @@ target_include_directories(react_devsupportjni PUBLIC .)
target_link_libraries(react_devsupportjni
fbjni
jsinspector)
jsinspector
jsinspector_network)
target_compile_reactnative_options(react_devsupportjni PRIVATE)
@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
#include "InspectorNetworkReporter.h"
#include "JInspectorNetworkReporter.h"
#include <jsinspector-modern/network/NetworkReporter.h>
@@ -16,9 +16,8 @@
#endif
using namespace facebook::jni;
using namespace facebook::react::jsinspector_modern;
namespace facebook::react {
namespace facebook::react::jsinspector_modern {
namespace {
@@ -59,12 +58,12 @@ static std::unordered_map<int, std::string> responseBuffers;
#endif
/* static */ jboolean InspectorNetworkReporter::isDebuggingEnabled(
/* static */ jboolean JInspectorNetworkReporter::isDebuggingEnabled(
jni::alias_ref<jclass> /*unused*/) {
return NetworkReporter::getInstance().isDebuggingEnabled();
}
/* static */ void InspectorNetworkReporter::reportRequestStart(
/* static */ void JInspectorNetworkReporter::reportRequestStart(
jni::alias_ref<jclass> /*unused*/,
jint requestId,
jni::alias_ref<jstring> requestUrl,
@@ -82,7 +81,7 @@ static std::unordered_map<int, std::string> responseBuffers;
std::to_string(requestId), requestInfo, encodedDataLength, std::nullopt);
}
/* static */ void InspectorNetworkReporter::reportConnectionTiming(
/* static */ void JInspectorNetworkReporter::reportConnectionTiming(
jni::alias_ref<jclass> /*unused*/,
jint requestId,
jni::alias_ref<jni::JMap<jstring, jstring>> headers) {
@@ -90,7 +89,7 @@ static std::unordered_map<int, std::string> responseBuffers;
std::to_string(requestId), convertJavaMapToHeaders(headers));
}
/* static */ void InspectorNetworkReporter::reportResponseStart(
/* static */ void JInspectorNetworkReporter::reportResponseStart(
jni::alias_ref<jclass> /*unused*/,
jint requestId,
jni::alias_ref<jstring> requestUrl,
@@ -108,7 +107,7 @@ static std::unordered_map<int, std::string> responseBuffers;
static_cast<std::int64_t>(encodedDataLength));
}
/* static */ void InspectorNetworkReporter::reportDataReceivedImpl(
/* static */ void JInspectorNetworkReporter::reportDataReceivedImpl(
jni::alias_ref<jclass> /*unused*/,
jint requestId,
jint dataLength) {
@@ -116,7 +115,7 @@ static std::unordered_map<int, std::string> responseBuffers;
std::to_string(requestId), dataLength, std::nullopt);
}
/* static */ void InspectorNetworkReporter::reportResponseEnd(
/* static */ void JInspectorNetworkReporter::reportResponseEnd(
jni::alias_ref<jclass> /*unused*/,
jint requestId,
jlong encodedDataLength) {
@@ -134,7 +133,7 @@ static std::unordered_map<int, std::string> responseBuffers;
#endif
}
/* static */ void InspectorNetworkReporter::reportRequestFailed(
/* static */ void JInspectorNetworkReporter::reportRequestFailed(
jni::alias_ref<jclass> /*unused*/,
jint requestId,
jboolean cancelled) {
@@ -142,7 +141,7 @@ static std::unordered_map<int, std::string> responseBuffers;
std::to_string(requestId), cancelled);
}
/* static */ void InspectorNetworkReporter::maybeStoreResponseBodyImpl(
/* static */ void JInspectorNetworkReporter::maybeStoreResponseBodyImpl(
jni::alias_ref<jclass> /*unused*/,
jint requestId,
jni::alias_ref<jstring> body,
@@ -160,7 +159,7 @@ static std::unordered_map<int, std::string> responseBuffers;
}
/* static */ void
InspectorNetworkReporter::maybeStoreResponseBodyIncrementalImpl(
JInspectorNetworkReporter::maybeStoreResponseBodyIncrementalImpl(
jni::alias_ref<jclass> /*unused*/,
jint requestId,
jni::alias_ref<jstring> data) {
@@ -176,31 +175,33 @@ InspectorNetworkReporter::maybeStoreResponseBodyIncrementalImpl(
#endif
}
/* static */ void InspectorNetworkReporter::registerNatives() {
/* static */ void JInspectorNetworkReporter::registerNatives() {
javaClassLocal()->registerNatives({
makeNativeMethod(
"isDebuggingEnabled", InspectorNetworkReporter::isDebuggingEnabled),
"isDebuggingEnabled", JInspectorNetworkReporter::isDebuggingEnabled),
makeNativeMethod(
"reportRequestStart", InspectorNetworkReporter::reportRequestStart),
"reportRequestStart", JInspectorNetworkReporter::reportRequestStart),
makeNativeMethod(
"reportResponseStart", InspectorNetworkReporter::reportResponseStart),
"reportResponseStart",
JInspectorNetworkReporter::reportResponseStart),
makeNativeMethod(
"reportConnectionTiming",
InspectorNetworkReporter::reportConnectionTiming),
JInspectorNetworkReporter::reportConnectionTiming),
makeNativeMethod(
"reportDataReceivedImpl",
InspectorNetworkReporter::reportDataReceivedImpl),
JInspectorNetworkReporter::reportDataReceivedImpl),
makeNativeMethod(
"reportResponseEnd", InspectorNetworkReporter::reportResponseEnd),
"reportResponseEnd", JInspectorNetworkReporter::reportResponseEnd),
makeNativeMethod(
"reportRequestFailed", InspectorNetworkReporter::reportRequestFailed),
"reportRequestFailed",
JInspectorNetworkReporter::reportRequestFailed),
makeNativeMethod(
"maybeStoreResponseBodyImpl",
InspectorNetworkReporter::maybeStoreResponseBodyImpl),
JInspectorNetworkReporter::maybeStoreResponseBodyImpl),
makeNativeMethod(
"maybeStoreResponseBodyIncrementalImpl",
InspectorNetworkReporter::maybeStoreResponseBodyIncrementalImpl),
JInspectorNetworkReporter::maybeStoreResponseBodyIncrementalImpl),
});
}
} // namespace facebook::react
} // namespace facebook::react::jsinspector_modern
@@ -9,10 +9,10 @@
#include <fbjni/fbjni.h>
namespace facebook::react {
namespace facebook::react::jsinspector_modern {
class InspectorNetworkReporter
: public jni::HybridClass<InspectorNetworkReporter> {
class JInspectorNetworkReporter
: public jni::HybridClass<JInspectorNetworkReporter> {
public:
static constexpr auto kJavaDescriptor =
"Lcom/facebook/react/modules/network/InspectorNetworkReporter;";
@@ -70,7 +70,7 @@ class InspectorNetworkReporter
static void registerNatives();
private:
InspectorNetworkReporter() = delete;
JInspectorNetworkReporter() = delete;
};
} // namespace facebook::react
} // namespace facebook::react::jsinspector_modern
@@ -8,6 +8,7 @@
#include "JCxxInspectorPackagerConnection.h"
#include "JCxxInspectorPackagerConnectionWebSocketDelegate.h"
#include "JInspectorFlags.h"
#include "JInspectorNetworkReporter.h"
#include <fbjni/fbjni.h>
@@ -18,5 +19,7 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* /*unused*/) {
facebook::react::jsinspector_modern::
JCxxInspectorPackagerConnectionWebSocketDelegate::registerNatives();
facebook::react::jsinspector_modern::JInspectorFlags::registerNatives();
facebook::react::jsinspector_modern::JInspectorNetworkReporter::
registerNatives();
});
}
@@ -53,7 +53,6 @@ add_library(
OBJECT
CatalystInstanceImpl.cpp
InspectorNetworkRequestListener.cpp
InspectorNetworkReporter.cpp
JExecutor.cpp
JInspector.cpp
JMessageQueueThread.cpp
@@ -82,10 +81,8 @@ target_link_libraries(reactnativejni
fbjni
folly_runtime
glog_init
jsinspector_network
logger
react_cxxreact
react_featureflags
react_renderer_runtimescheduler
reactnativejni_common
runtimeexecutor
@@ -12,7 +12,6 @@
#include "CatalystInstanceImpl.h"
#include "CxxModuleWrapperBase.h"
#include "InspectorNetworkReporter.h"
#include "InspectorNetworkRequestListener.h"
#include "JInspector.h"
#include "JavaScriptExecutorHolder.h"
@@ -45,7 +44,6 @@ extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JInspector::registerNatives();
ReactInstanceManagerInspectorTarget::registerNatives();
InspectorNetworkRequestListener::registerNatives();
InspectorNetworkReporter::registerNatives();
});
}
@@ -127,6 +127,9 @@ void JReactHostInspectorTarget::onSetPausedInDebuggerMessage(
}
}
void JReactHostInspectorTarget::unstable_onPerfMonitorUpdate(
const PerfMonitorUpdateRequest& /* unused */) {}
void JReactHostInspectorTarget::loadNetworkResource(
const jsinspector_modern::LoadNetworkResourceRequest& params,
jsinspector_modern::ScopedExecutor<
@@ -84,6 +84,8 @@ class JReactHostInspectorTarget
void onReload(const PageReloadRequest& request) override;
void onSetPausedInDebuggerMessage(
const OverlaySetPausedInDebuggerMessageRequest&) override;
void unstable_onPerfMonitorUpdate(
const PerfMonitorUpdateRequest& /* unused */) override;
void loadNetworkResource(
const jsinspector_modern::LoadNetworkResourceRequest& params,
jsinspector_modern::ScopedExecutor<
@@ -71,7 +71,8 @@ class RootViewTest {
reactContext = spy(BridgeReactContext(RuntimeEnvironment.getApplication()))
reactContext.initializeWithInstance(catalystInstanceMock)
DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(reactContext)
DisplayMetricsHolder.initScreenDisplayMetricsIfNotInitialized(reactContext)
DisplayMetricsHolder.initWindowDisplayMetricsIfNotInitialized(reactContext)
val uiManagerModuleMock: UIManagerModule = mock()
whenever(catalystInstanceMock.getNativeModule(UIManagerModule::class.java))
.thenReturn(uiManagerModuleMock)
@@ -11,12 +11,18 @@
package com.facebook.react.bridge
import android.app.Application
import com.facebook.react.bridge.queue.MessageQueueThreadSpec
import com.facebook.react.bridge.queue.ReactQueueConfiguration
import com.facebook.react.bridge.queue.ReactQueueConfigurationImpl
import com.facebook.react.bridge.queue.ReactQueueConfigurationSpec
import com.facebook.react.common.annotations.UnstableReactNativeAPI
import com.facebook.react.runtime.BridgelessReactContext
import com.facebook.react.runtime.ReactHostImpl
import com.facebook.react.runtime.internal.bolts.Task
import com.facebook.react.uimanager.UIManagerModule
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.whenever
import org.robolectric.RuntimeEnvironment
@@ -49,4 +55,20 @@ object ReactTestHelper {
whenever(reactInstance.isDestroyed).thenReturn(false)
return reactInstance
}
@OptIn(UnstableReactNativeAPI::class)
fun createTestReactApplicationContext(application: Application): ReactApplicationContext {
val reactHost =
spy(
ReactHostImpl(
RuntimeEnvironment.getApplication(),
mock(),
mock(),
Task.Companion.IMMEDIATE_EXECUTOR,
Task.Companion.IMMEDIATE_EXECUTOR,
false /* allowPackagerServerAccess */,
false /* useDevSupport */,
))
return BridgelessReactContext(application, reactHost)
}
}
@@ -467,7 +467,7 @@ class TouchEventDispatchTest {
metrics.xdpi = 1f
metrics.ydpi = 1f
metrics.density = 1f
DisplayMetricsHolder.setWindowDisplayMetrics(metrics)
DisplayMetricsHolder.setScreenDisplayMetrics(metrics)
val reactContext = ReactTestHelper.createCatalystContextForTest()
@@ -5,36 +5,45 @@
* LICENSE file in the root directory of this source tree.
*/
@file:Suppress("DEPRECATION")
package com.facebook.react.modules.clipboard
import android.annotation.SuppressLint
import android.content.ClipboardManager
import android.content.Context
import com.facebook.react.bridge.BridgeReactContext
import com.facebook.react.bridge.ReactTestHelper.createTestReactApplicationContext
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
import com.facebook.testutils.shadows.ShadowSoLoader
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
@Suppress("DEPRECATION")
@SuppressLint("ClipboardManager", "DeprecatedClass")
@RunWith(RobolectricTestRunner::class)
@Config(shadows = [ShadowSoLoader::class])
class ClipboardModuleTest {
private lateinit var clipboardModule: ClipboardModule
private lateinit var clipboardManager: ClipboardManager
@Before
fun setUp() {
clipboardModule = ClipboardModule(BridgeReactContext(RuntimeEnvironment.getApplication()))
ReactNativeFeatureFlagsForTests.setUp()
clipboardModule =
ClipboardModule(createTestReactApplicationContext(RuntimeEnvironment.getApplication()))
clipboardManager =
RuntimeEnvironment.getApplication().getSystemService(Context.CLIPBOARD_SERVICE)
as ClipboardManager
}
@After
fun tearDown() {
ReactNativeFeatureFlags.dangerouslyReset()
}
@Suppress("DEPRECATION")
@Test
fun testSetString() {
clipboardModule.setString(TEST_CONTENT)
@@ -5,6 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
@file:Suppress("DEPRECATION")
package com.facebook.react.uimanager.layoutanimation
import android.view.View
@@ -5,6 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
@file:Suppress("DEPRECATION")
package com.facebook.react.uimanager.layoutanimation
import java.util.Locale
@@ -5,6 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
@file:Suppress("DEPRECATION")
package com.facebook.react.uimanager.layoutanimation
import android.view.View
@@ -25,7 +25,7 @@ class ColorStopTest {
fun setUp() {
val metrics = DisplayMetrics()
metrics.density = 1f
DisplayMetricsHolder.setWindowDisplayMetrics(metrics)
DisplayMetricsHolder.setScreenDisplayMetrics(metrics)
}
@Test
@@ -75,14 +75,14 @@ class ReactImagePropertyTest {
context.initializeWithInstance(catalystInstanceMock)
themeContext = ThemedReactContext(context, context, null, -1)
Fresco.initialize(context)
DisplayMetricsHolder.setWindowDisplayMetrics(DisplayMetrics())
DisplayMetricsHolder.setScreenDisplayMetrics(DisplayMetrics())
ReactNativeFeatureFlagsForTests.setUp()
}
@After
fun teardown() {
DisplayMetricsHolder.setWindowDisplayMetrics(null)
DisplayMetricsHolder.setScreenDisplayMetrics(null)
rnLog.close()
flogMock.close()
}
@@ -65,7 +65,7 @@ class ReactTextInputPropertyTest {
context.initializeWithInstance(catalystInstanceMock)
themedContext = ThemedReactContext(context, context.baseContext, null, ID_NULL)
manager = ReactTextInputManager()
DisplayMetricsHolder.setWindowDisplayMetrics(DisplayMetrics())
DisplayMetricsHolder.setScreenDisplayMetrics(DisplayMetrics())
view = manager.createViewInstance(themedContext)
}
@@ -47,7 +47,7 @@ class ReactVirtualViewTest {
val displayMetricsHolder = mockStatic(DisplayMetricsHolder::class.java)
displayMetricsHolder
.`when`<DisplayMetrics> { DisplayMetricsHolder.getWindowDisplayMetrics() }
.`when`<DisplayMetrics> { DisplayMetricsHolder.getScreenDisplayMetrics() }
.thenAnswer { DisplayMetrics().apply { density = 1f } }
}
@@ -146,11 +146,53 @@ class HostCommandSender {
std::unique_ptr<ILocalConnection> connection_;
};
/**
* Enables the caller to install and subscribe to a named CDP runtime binding
* on the HostTarget via a callback. Note: Per CDP spec, this does not need to
* check if the `Runtime` domain is enabled.
*/
class HostRuntimeBinding {
public:
explicit HostRuntimeBinding(
HostTarget& target,
std::string name,
std::function<void(std::string)> callback)
: connection_(target.connect(std::make_unique<CallbackRemoteConnection>(
[callback = std::move(callback)](const std::string& message) {
auto parsedMessage = folly::parseJson(message);
// Ignore initial Runtime.addBinding response
if (parsedMessage["id"] == 0 &&
parsedMessage["result"].isObject() &&
parsedMessage["result"].empty()) {
return;
}
// Assert that we only intercept bindingCalled responses
assert(
parsedMessage["method"].asString() ==
"Runtime.bindingCalled");
callback(parsedMessage["params"]["payload"].asString());
}))) {
// Install runtime binding
connection_->sendMessage(cdp::jsonRequest(
0,
"Runtime.addBinding",
folly::dynamic::object("name", std::move(name))));
}
private:
std::unique_ptr<ILocalConnection> connection_;
};
std::shared_ptr<HostTarget> HostTarget::create(
HostTargetDelegate& delegate,
VoidExecutor executor) {
std::shared_ptr<HostTarget> hostTarget{new HostTarget(delegate)};
hostTarget->setExecutor(std::move(executor));
if (InspectorFlags::getInstance().getPerfMonitorV2Enabled()) {
hostTarget->installPerfMetricsBinding();
}
return hostTarget;
}
@@ -229,6 +271,19 @@ void HostTarget::sendCommand(HostCommand command) {
});
}
void HostTarget::installPerfMetricsBinding() {
perfMetricsBinding_ = std::make_unique<HostRuntimeBinding>(
*this, // Used immediately
"__chromium_devtools_metrics_reporter",
[this](const std::string& message) {
auto payload = folly::parseJson(message);
HostTargetDelegate::PerfMonitorUpdateRequest request{
.interactionName = payload["eventName"].asString(),
.durationMs = static_cast<uint16_t>(payload["duration"].asInt())};
delegate_.unstable_onPerfMonitorUpdate(request);
});
}
HostTargetController::HostTargetController(HostTarget& target)
: target_(target) {}
@@ -38,6 +38,7 @@ class HostTargetSession;
class HostAgent;
class HostTracingAgent;
class HostCommandSender;
class HostRuntimeBinding;
class HostTarget;
class HostTargetTraceRecording;
@@ -97,6 +98,11 @@ class HostTargetDelegate : public LoadNetworkResourceDelegate {
}
};
struct PerfMonitorUpdateRequest {
std::string interactionName;
uint16_t durationMs;
};
virtual ~HostTargetDelegate() override;
/**
@@ -125,6 +131,13 @@ class HostTargetDelegate : public LoadNetworkResourceDelegate {
virtual void onSetPausedInDebuggerMessage(
const OverlaySetPausedInDebuggerMessageRequest& request) = 0;
/**
* [Experimental] Called when the runtime has new data for the V2 Perf
* Monitor overlay. This is called on the inspector thread.
*/
virtual void unstable_onPerfMonitorUpdate(
const PerfMonitorUpdateRequest& /*request*/) {}
/**
* Called by NetworkIOAgent on handling a `Network.loadNetworkResource` CDP
* request. Platform implementations should override this to perform a
@@ -296,6 +309,7 @@ class JSINSPECTOR_EXPORT HostTarget
std::shared_ptr<ExecutionContextManager> executionContextManager_;
std::shared_ptr<InstanceTarget> currentInstance_{nullptr};
std::unique_ptr<HostCommandSender> commandSender_;
std::unique_ptr<HostRuntimeBinding> perfMetricsBinding_;
/**
* Current pending trace recording, which encapsulates the configuration of
@@ -313,6 +327,13 @@ class JSINSPECTOR_EXPORT HostTarget
return currentInstance_ != nullptr;
}
/**
* Install a runtime binding subscribing to the Interaction to Next Paint
* (INP) live metric, which we broadcast to the V2 Perf Monitor overlay
* via \ref HostTargetDelegate::unstable_onPerfMonitorUpdate.
*/
void installPerfMetricsBinding();
// Necessary to allow HostAgent to access HostTarget's internals in a
// controlled way (i.e. only HostTargetController gets friend access, while
// HostAgent itself doesn't).
@@ -33,6 +33,10 @@ bool InspectorFlags::getNetworkInspectionEnabled() const {
return loadFlagsAndAssertUnchanged().networkInspectionEnabled;
}
bool InspectorFlags::getPerfMonitorV2Enabled() const {
return loadFlagsAndAssertUnchanged().perfMonitorV2Enabled;
}
void InspectorFlags::dangerouslyResetFlags() {
*this = InspectorFlags{};
}
@@ -59,6 +63,9 @@ const InspectorFlags::Values& InspectorFlags::loadFlagsAndAssertUnchanged()
.networkInspectionEnabled =
ReactNativeFeatureFlags::enableBridgelessArchitecture() &&
ReactNativeFeatureFlags::fuseboxNetworkInspectionEnabled(),
.perfMonitorV2Enabled =
ReactNativeFeatureFlags::enableBridgelessArchitecture() &&
ReactNativeFeatureFlags::perfMonitorV2Enabled(),
};
if (cachedValues_.has_value() && !inconsistentFlagsStateLogged_) {
@@ -35,6 +35,11 @@ class InspectorFlags {
*/
bool getNetworkInspectionEnabled() const;
/**
* Flag determining if the V2 in-app Performance Monitor is enabled.
*/
bool getPerfMonitorV2Enabled() const;
/**
* Forcibly disable the main `getFuseboxEnabled()` flag. This should ONLY be
* used by `ReactInstanceIntegrationTest`.
@@ -52,6 +57,7 @@ class InspectorFlags {
bool fuseboxEnabled;
bool isProfilingBuild;
bool networkInspectionEnabled;
bool perfMonitorV2Enabled;
bool operator==(const Values&) const = default;
};
@@ -24,6 +24,14 @@ void CallbackLocalConnection::disconnect() {
handler_ = nullptr;
}
CallbackRemoteConnection::CallbackRemoteConnection(
std::function<void(std::string)> handler)
: handler_(std::move(handler)) {}
void CallbackRemoteConnection::onMessage(std::string message) {
handler_(std::move(message));
}
RAIIRemoteConnection::RAIIRemoteConnection(
std::unique_ptr<IRemoteConnection> remote)
: remote_(std::move(remote)) {}
@@ -33,6 +33,25 @@ class CallbackLocalConnection : public ILocalConnection {
std::function<void(std::string)> handler_;
};
/**
* Wraps a callback function in IRemoteConnection.
*/
class CallbackRemoteConnection : public IRemoteConnection {
public:
/**
* Creates a new Connection that uses the given callback to receive messages
* from the backend.
*/
explicit CallbackRemoteConnection(std::function<void(std::string)> handler);
void onMessage(std::string message) override;
void onDisconnect() override {}
private:
std::function<void(std::string)> handler_;
};
/**
* Wraps an IRemoteConnection in a simpler interface that calls `onDisconnect`
* implicitly upon destruction.
@@ -292,7 +292,7 @@ SharedDebugStringConvertibleList ImageProps::getDebugProps() const {
sourcesList = sources[0].getDebugProps("source");
} else if (sources.size() > 1) {
for (const auto& source : sources) {
std::string sourceName = "source@" + react::toString(source.scale) + "x";
std::string sourceName = "source-" + react::toString(source.scale) + "x";
auto debugProps = source.getDebugProps(sourceName);
sourcesList.insert(
sourcesList.end(), debugProps.begin(), debugProps.end());
@@ -304,8 +304,32 @@ SharedDebugStringConvertibleList ImageProps::getDebugProps() const {
SharedDebugStringConvertibleList{
debugStringConvertibleItem(
"blurRadius", blurRadius, imageProps.blurRadius),
debugStringConvertibleItem(
"resizeMode",
toString(resizeMode),
toString(imageProps.resizeMode)),
debugStringConvertibleItem(
"tintColor", toString(tintColor), toString(imageProps.tintColor)),
};
}
inline std::string toString(ImageResizeMode resizeMode) {
switch (resizeMode) {
case ImageResizeMode::Cover:
return "cover";
case ImageResizeMode::Contain:
return "contain";
case ImageResizeMode::Stretch:
return "stretch";
case ImageResizeMode::Center:
return "center";
case ImageResizeMode::Repeat:
return "repeat";
case ImageResizeMode::None:
return "none";
}
}
#endif
} // namespace facebook::react
@@ -34,7 +34,7 @@ class ImageProps final : public ViewProps {
ImageSources sources{};
ImageSource defaultSource{};
ImageSource loadingIndicatorSource{};
ImageResizeMode resizeMode{ImageResizeMode::Stretch};
ImageResizeMode resizeMode{ImageResizeMode::Cover};
Float blurRadius{};
EdgeInsets capInsets{};
SharedColor tintColor{};
@@ -9,6 +9,7 @@
namespace facebook::react {
// NOLINTNEXTLINE(modernize-avoid-c-arrays)
extern const char AndroidSwitchComponentName[] = "AndroidSwitch";
void AndroidSwitchShadowNode::setAndroidSwitchMeasurementsManager(
@@ -15,6 +15,7 @@
namespace facebook::react {
// NOLINTNEXTLINE(modernize-avoid-c-arrays)
extern const char AndroidSwitchComponentName[];
/*
@@ -35,6 +35,7 @@
namespace facebook::react {
using Content = ParagraphShadowNode::Content;
// NOLINTNEXTLINE(facebook-hte-CArray)
const char ParagraphComponentName[] = "Paragraph";
void ParagraphShadowNode::initialize() noexcept {
@@ -16,6 +16,7 @@ androidx-swiperefreshlayout = "1.1.0"
androidx-test = "1.5.0"
androidx-test-junit = "1.2.1"
androidx-tracing = "1.1.0"
androidx-window = "1.4.0"
assertj = "3.21.0"
binary-compatibility-validator = "0.13.2"
download = "5.4.0"
@@ -64,6 +65,7 @@ androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-
androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test" }
androidx-tracing = { module = "androidx.tracing:tracing", version.ref = "androidx-tracing" }
androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" }
androidx-window = { module = "androidx.window:window", version.ref = "androidx-window" }
fbjni = { module = "com.facebook.fbjni:fbjni", version.ref = "fbjni" }
fresco = { module = "com.facebook.fresco:fresco", version.ref = "fresco" }
@@ -377,7 +377,7 @@ end
# This method can be used to set the fast_float config
# that can be used to configure libraries.
def set_fast_float_config(fmt_config)
def set_fast_float_config(fast_float_config)
Helpers::Constants.set_fast_float_config(fast_float_config)
end
@@ -466,7 +466,6 @@ describe('Native Animated', () => {
{type: 'addition', input: expect.any(Array)},
);
const additionCalls =
// $FlowFixMe[prop-missing]
// $FlowFixMe[prop-missing]
NativeAnimatedModule.createAnimatedNode.mock.calls.filter(
call => call[1].type === 'addition',
@@ -475,7 +474,6 @@ describe('Native Animated', () => {
const additionCall = additionCalls[0];
const additionNodeTag = additionCall[0];
const additionConnectionCalls =
// $FlowFixMe[prop-missing]
// $FlowFixMe[prop-missing]
NativeAnimatedModule.connectAnimatedNodes.mock.calls.filter(
call => call[1] === additionNodeTag,
@@ -1505,5 +1503,258 @@ describe('Native Animated', () => {
1,
);
});
it('creates new props, style and transform nodes at update', async () => {
const {Animated} = importModules();
const opacityA = new Animated.Value(0, {
debugID: 'opacityA',
useNativeDriver: true,
});
const opacityB = new Animated.Value(0, {
debugID: 'opacityB',
useNativeDriver: true,
});
opacityA.__makeNative();
opacityB.__makeNative();
const scaleA = new Animated.Value(0, {
debugID: 'scaleA',
useNativeDriver: true,
});
const scaleB = new Animated.Value(0, {
debugID: 'scaleB',
useNativeDriver: true,
});
scaleA.__makeNative();
scaleB.__makeNative();
expect(NativeAnimatedModule.createAnimatedNode).not.toHaveBeenCalled();
// 1. First render, both opacity and transform scale are managed by Animated
const root = await create(
<Animated.View
style={{opacity: opacityA, transform: [{scale: scaleA}]}}
/>,
);
let createAnimatedNodeCalledTimes = 0;
let dropAnimatedNodeCalledTimes = 0;
expect(
// $FlowFixMe[prop-missing]
NativeAnimatedModule.createAnimatedNode.mock.calls.slice(0, 5),
).toEqual([
[1, {debugID: 'opacityA', offset: 0, type: 'value', value: 0}],
[4, {debugID: 'scaleA', offset: 0, type: 'value', value: 0}],
[
3,
{
debugID: undefined,
transforms: [{nodeTag: 4, property: 'scale', type: 'animated'}],
type: 'transform',
},
],
[
2,
{
debugID: undefined,
style: {
opacity: 1,
transform: 3,
},
type: 'style',
},
],
[5, {debugID: undefined, props: {style: 2}, type: 'props'}],
]);
createAnimatedNodeCalledTimes += 5;
expect(NativeAnimatedModule.dropAnimatedNode).not.toHaveBeenCalled();
expect(NativeAnimatedModule.restoreDefaultValues).not.toHaveBeenCalled();
expect(
NativeAnimatedModule.disconnectAnimatedNodeFromView,
).not.toHaveBeenCalled();
expect(
NativeAnimatedModule.connectAnimatedNodeToView,
).toHaveBeenCalledTimes(1);
// 2. Update the Animated node to control opacity style
await update(
root,
<Animated.View
style={{opacity: opacityB, transform: [{scale: scaleA}]}}
/>,
);
jest.runAllTicks();
expect(
// $FlowFixMe[prop-missing]
NativeAnimatedModule.createAnimatedNode.mock.calls.slice(5, 9),
).toEqual([
[6, {debugID: 'opacityB', offset: 0, type: 'value', value: 0}],
// transform node is still recreated even though `scale` is unchanged
[
8,
{
debugID: undefined,
transforms: [{nodeTag: 4, property: 'scale', type: 'animated'}],
type: 'transform',
},
],
[
7,
{
debugID: undefined,
style: {
opacity: 6,
transform: 8,
},
type: 'style',
},
],
[9, {debugID: undefined, props: {style: 7}, type: 'props'}],
]);
createAnimatedNodeCalledTimes += 4;
expect(
// $FlowFixMe[prop-missing]
NativeAnimatedModule.dropAnimatedNode.mock.calls.slice(0, 4),
).toEqual([[1], [3], [2], [5]]);
dropAnimatedNodeCalledTimes += 4;
expect(NativeAnimatedModule.createAnimatedNode).toHaveBeenCalledTimes(
createAnimatedNodeCalledTimes,
);
expect(NativeAnimatedModule.dropAnimatedNode).toHaveBeenCalledTimes(
dropAnimatedNodeCalledTimes,
);
expect(NativeAnimatedModule.restoreDefaultValues).toHaveBeenCalledTimes(
1,
);
expect(
NativeAnimatedModule.disconnectAnimatedNodeFromView,
).toHaveBeenCalledTimes(1);
expect(
NativeAnimatedModule.connectAnimatedNodeToView,
).toHaveBeenCalledTimes(2);
// 3. Opacity is controlled by React while transform scale is still managed by Animated
await update(
root,
<Animated.View style={{opacity: 1, transform: [{scale: scaleB}]}} />,
);
jest.runAllTicks();
expect(
// $FlowFixMe[prop-missing]
NativeAnimatedModule.createAnimatedNode.mock.calls.slice(9, 13),
).toEqual([
[10, {debugID: 'scaleB', offset: 0, type: 'value', value: 0}],
[
11,
{
debugID: undefined,
transforms: [{nodeTag: 10, property: 'scale', type: 'animated'}],
type: 'transform',
},
],
[
12,
{
debugID: undefined,
style: {
transform: 11,
},
type: 'style',
},
],
[13, {debugID: undefined, props: {style: 12}, type: 'props'}],
]);
createAnimatedNodeCalledTimes += 4;
expect(
// $FlowFixMe[prop-missing]
NativeAnimatedModule.dropAnimatedNode.mock.calls.slice(4, 9),
).toEqual([[6], [4], [8], [7], [9]]);
dropAnimatedNodeCalledTimes += 5;
expect(NativeAnimatedModule.createAnimatedNode).toHaveBeenCalledTimes(
createAnimatedNodeCalledTimes,
);
expect(NativeAnimatedModule.dropAnimatedNode).toHaveBeenCalledTimes(
dropAnimatedNodeCalledTimes,
);
expect(NativeAnimatedModule.restoreDefaultValues).toHaveBeenCalledTimes(
2,
);
expect(
NativeAnimatedModule.disconnectAnimatedNodeFromView,
).toHaveBeenCalledTimes(2);
expect(
NativeAnimatedModule.connectAnimatedNodeToView,
).toHaveBeenCalledTimes(3);
// 4. Both opacity and transform scale are controlled by React instead of Animated
await update(
root,
<Animated.View style={{opacity: 1, transform: [{scale: 1}]}} />,
);
jest.runAllTicks();
{
const droppedTags = [10, 11, 12, 13];
for (let i = 0; i < droppedTags.length; i++) {
expect(NativeAnimatedModule.dropAnimatedNode).toHaveBeenNthCalledWith(
i + dropAnimatedNodeCalledTimes + 1,
droppedTags[i],
);
}
dropAnimatedNodeCalledTimes += droppedTags.length;
}
expect(NativeAnimatedModule.createAnimatedNode).toHaveBeenCalledTimes(
createAnimatedNodeCalledTimes,
);
expect(NativeAnimatedModule.dropAnimatedNode).toHaveBeenCalledTimes(
dropAnimatedNodeCalledTimes,
);
// View is no longer connected to PropsAnimatedNode
expect(
NativeAnimatedModule.disconnectAnimatedNodeFromView,
).toHaveBeenCalledTimes(3);
expect(
NativeAnimatedModule.connectAnimatedNodeToView,
).toHaveBeenCalledTimes(3);
expect(NativeAnimatedModule.restoreDefaultValues).toHaveBeenCalledTimes(
3,
);
// 5. Unmount
await unmount(root);
jest.runAllTicks();
// No change for Animated nodes on unmount.
expect(NativeAnimatedModule.createAnimatedNode).toHaveBeenCalledTimes(
createAnimatedNodeCalledTimes,
);
expect(NativeAnimatedModule.dropAnimatedNode).toHaveBeenCalledTimes(
dropAnimatedNodeCalledTimes,
);
expect(
NativeAnimatedModule.disconnectAnimatedNodeFromView,
).toHaveBeenCalledTimes(3);
expect(
NativeAnimatedModule.connectAnimatedNodeToView,
).toHaveBeenCalledTimes(3);
expect(NativeAnimatedModule.restoreDefaultValues).toHaveBeenCalledTimes(
3,
);
});
});
});
@@ -451,6 +451,19 @@ const examples: Array<RNTesterModuleExample> = [
return <ToggleDefaultPaddingExample />;
},
},
{
title: 'Accessibility',
render: function (): React.Node {
return (
<View>
<Text>accessibilityLabel prop</Text>
<ExampleTextInput accessibilityLabel="This is Accessibility Label" />
<Text>aria-label prop</Text>
<ExampleTextInput aria-label="This is Aria Label" />
</View>
);
},
},
];
module.exports = ({
@@ -1036,6 +1036,21 @@ const textInputExamples: Array<RNTesterModuleExample> = [
);
},
},
{
title: 'Accessibility',
render: function (): React.Node {
return (
<View>
<WithLabel label="accessibilityLabel">
<ExampleTextInput accessibilityLabel="This is Accessibility Label" />
</WithLabel>
<WithLabel label="aria-label">
<ExampleTextInput aria-label="This is Aria Label" />
</WithLabel>
</View>
);
},
},
];
module.exports = ({
+19 -22
View File
@@ -8,6 +8,8 @@
* @format
*/
import {markdownTable} from './utils';
type TestTaskTiming = {
name: string,
latency: {
@@ -31,7 +33,7 @@ export const printBenchmarkResultsRanking = (
testArtifact: mixed,
}>,
) => {
const testTaskTimings: {[string]: Array<[string, number]>} = {};
const testTaskTimings: {[string]: {[string]: number}} = {};
let numTestVariants = 0;
for (const testResult of testResults) {
@@ -49,12 +51,9 @@ export const printBenchmarkResultsRanking = (
for (const taskTiming of testArtifact.timings) {
const taskName = taskTiming.name;
if (testTaskTimings[taskName] === undefined) {
testTaskTimings[taskName] = [];
testTaskTimings[taskName] = {};
}
testTaskTimings[taskName].push([
testResult.title,
taskTiming.latency.p50,
]);
testTaskTimings[taskName][testResult.title] = taskTiming.latency.p50;
}
}
if (numTestVariants <= 1 || Object.keys(testTaskTimings).length === 0) {
@@ -62,25 +61,23 @@ export const printBenchmarkResultsRanking = (
return;
}
// Sort by each task's execution times
// Find relative execution times for tasks
const results: {[string]: {[string]: string}} = {};
for (const taskName in testTaskTimings) {
testTaskTimings[taskName].sort((a, b) => a[1] - b[1]);
const kv = Object.entries(testTaskTimings[taskName]);
kv.sort((a, b) => a[1] - b[1]);
const bestTiming = kv[0][1];
results[taskName] = {};
kv.forEach(([key, val]) => {
results[taskName][key] =
`${val.toFixed(3)}ms ${getTimingDelta(bestTiming, val)}`;
});
results[taskName][kv[0][0]] = `🏆 ${bestTiming.toFixed(3)}ms`;
}
// Print the rankings
console.log('### Benchmark Results Ranking ###');
for (const taskName in testTaskTimings) {
console.log(`> ${taskName}:`);
let lastTiming;
for (const [i, [testVariationName, latency]] of testTaskTimings[
taskName
].entries()) {
console.log(
` ${i + 1}. ${testVariationName}: ${latency.toFixed(2)}ms ${getTimingDelta(lastTiming, latency)}`,
);
lastTiming = latency;
}
}
console.log('### Benchmark Times Comparison (p50): ###');
console.log(markdownTable(results, 'Task name'));
console.log('');
};
function getTimingDelta(lastTiming: ?number, currentTiming: ?number): string {
+61
View File
@@ -371,3 +371,64 @@ export function printConsoleLog(log: ConsoleLogMessage): void {
break;
}
}
// Returns a markdown table corresponding to the given data, adopted from the RN console.table polyfill implementation
export function markdownTable(
data: {[string]: {[string]: string}},
indexColumnName?: string = '',
): string {
const repeat = (element: string, n: number) =>
Array.apply(null, Array(n)).map(() => element);
const rows = Object.keys(data).map((key: string) => ({
[indexColumnName]: key,
...data[key],
}));
if (rows.length === 0) {
return '';
}
const columns = Array.from(
rows.reduce((columnSet: Set<string>, row) => {
Object.keys(row).forEach(key => columnSet.add(key));
return columnSet;
}, new Set()),
);
const stringRows: Array<Array<string>> = [];
const columnWidths = [];
// Figure out max cell width for each column
columns.forEach((k, i) => {
columnWidths[i] = k.length;
for (let j = 0; j < rows.length; j++) {
const cellStr = rows[j][k];
stringRows[j] = stringRows[j] || [];
stringRows[j][i] = cellStr;
columnWidths[i] = Math.max(columnWidths[i], cellStr.length);
}
});
// Join all elements in the row into a single string with | separators
// (appends extra spaces to each cell to make separators | aligned)
const joinRow = (row: Array<string>, space?: string = ' ') => {
const cells = row.map((cell: string, i) => {
const extraSpaces = repeat(' ', columnWidths[i] - cell.length).join('');
return cell + extraSpaces;
});
return '| ' + cells.join(space + '|' + space) + ' |';
};
const separators = columnWidths.map(columnWidth =>
repeat('-', columnWidth).join(''),
);
const separatorRow = joinRow(separators);
const header = joinRow(columns);
const table = [header, separatorRow];
for (let i = 0; i < rows.length; i++) {
table.push(joinRow(stringRows[i]));
}
return '\n' + table.join('\n');
}