@@ -12,6 +12,8 @@ import type {DialogOptions} from '../NativeModules/specs/NativeDialogManagerAndr
import Platform from '../Utilities/Platform';
import {alertWithArgs} from './RCTAlertManager';
+import AlertDelegate from './delegates/AlertDelegate';
+const ALERT_DELEGATE = new AlertDelegate({});
/**
* @platform ios
@@ -66,7 +68,9 @@ class Alert {
buttons?: AlertButtons,
options?: AlertOptions,
): void {
- if (Platform.OS === 'ios') {
+ if (Platform.OS === 'harmony') {
+ ALERT_DELEGATE.alert(title, message, buttons, options);
+ } else if (Platform.OS === 'ios') {
Alert.prompt(
title,
message,
@@ -151,7 +155,9 @@ class Alert {
keyboardType?: string,
options?: AlertOptions,
): void {
- if (Platform.OS === 'ios') {
+ if (Platform.OS === 'harmony') {
+ ALERT_DELEGATE.prompt(title, message, callbackOrButtons, type, defaultValue, keyboardType, options);
+ } else if (Platform.OS === 'ios') {
let callbacks: Array<?any> = [];
const buttons = [];
let cancelButtonKey;
new file mode 100644
@@ -0,0 +1,33 @@
+/**
+ * @format
+ */
+import {
+ BaseAlertDelegate,
+ AlertDelegateButton,
+ AlertDelegateOptions,
+ AlertDelegateType,
+} from './BaseAlertDelegate';
+import {UnsupportedByPlatformError} from '../../../delegates/DelegateError';
+
+export default class AlertDelegate extends BaseAlertDelegate {
+ alert(
+ title: string,
+ message?: string,
+ buttons?: AlertDelegateButton[],
+ options?: AlertDelegateOptions,
+ ): void {
+ throw new UnsupportedByPlatformError();
+ }
+
+ prompt(
+ title: string,
+ message?: string,
+ callbackOrButtons?: ((text: string) => void) | AlertDelegateButton[],
+ type?: AlertDelegateType,
+ defaultValue?: string,
+ keyboardType?: string,
+ options?: AlertDelegateOptions,
+ ): void {
+ throw new UnsupportedByPlatformError();
+ }
+}
new file mode 100644
@@ -0,0 +1,37 @@
+/**
+ * @format
+ */
+import type {
+ AlertButton,
+ AlertOptions,
+ AlertType,
+} from '../../Alert/Alert';
+
+export type AlertDelegateContext = {};
+
+export type AlertDelegateButton = AlertButton;
+
+export type AlertDelegateOptions = AlertOptions;
+
+export type AlertDelegateType = AlertType;
+
+export abstract class BaseAlertDelegate {
+ constructor(protected ctx: AlertDelegateContext) {}
+
+ abstract alert(
+ title: string,
+ message?: string,
+ buttons?: AlertDelegateButton[],
+ options?: AlertDelegateOptions,
+ ): void;
+
+ abstract prompt(
+ title: string,
+ message?: string,
+ callbackOrButtons?: ((text: string) => void) | AlertDelegateButton[],
+ type?: AlertDelegateType,
+ defaultValue?: string,
+ keyboardType?: string,
+ options?: AlertDelegateOptions,
+ ): void;
+}
@@ -26,6 +26,8 @@ import {type ViewProps} from '../Components/View/ViewPropTypes';
import useMergeRefs from '../Utilities/useMergeRefs';
import * as React from 'react';
import {useMemo} from 'react';
+import * as DELEGATE from './delegates/createAnimatedComponentDelegate'
+import Platform from '../Utilities/Platform';
type Nullable = void | null;
type Primitive = string | number | boolean | symbol | void;
@@ -153,7 +155,7 @@ export function unstable_createAnimatedComponentWithAllowlist<
{...reducedProps}
{...passthroughAnimatedPropExplicitValues}
style={mergedStyle}
- ref={ref}
+ ref={Platform.OS === 'harmony' ? DELEGATE.processRef(ref) : ref}
/>
);
};
new file mode 100644
@@ -0,0 +1,3 @@
+export function processRef(ref) {
+ return ref;
+}
@@ -32,7 +32,7 @@ import {NativeEventSubscription} from '../EventEmitter/RCTNativeAppEventEmitter'
*
* @see https://reactnative.dev/docs/appstate#app-states
*/
-export type AppStateEvent = 'change' | 'memoryWarning' | 'blur' | 'focus';
+export type AppStateEvent = 'change' | 'memoryWarning' | 'memoryLevelChange' | 'blur' | 'focus';
export type AppStateStatus =
| 'active'
| 'background'
@@ -40,6 +40,11 @@ export type AppStateStatus =
| 'unknown'
| 'extension';
+export interface AppStateMemoryLevelInfo {
+ level: number;
+ levelName: string;
+}
+
export interface AppStateStatic {
currentState: AppStateStatus;
isAvailable: boolean;
@@ -49,9 +54,19 @@ export interface AppStateStatic {
* type and providing the handler
*/
addEventListener(
- type: AppStateEvent,
+ type: 'change',
listener: (state: AppStateStatus) => void,
): NativeEventSubscription;
+
+ addEventListener(
+ type: 'memoryWarning' | 'blur' | 'focus',
+ listener: () => void,
+ ): NativeEventSubscription;
+
+ addEventListener(
+ type: 'memoryLevelChange',
+ listener: (info: AppStateMemoryLevelInfo) => void,
+ ): NativeEventSubscription;
}
export const AppState: AppStateStatic;
@@ -29,6 +29,11 @@ export type AppStateStatus =
| 'extension'
| 'unknown';
+export type AppStateMemoryLevelInfo = {
+ level: number,
+ levelName: string,
+};
+
/**
* change - This even is received when the app state has changed.
* memoryWarning - This event is used in the need of throwing memory warning or releasing it.
@@ -38,6 +43,7 @@ export type AppStateStatus =
type AppStateEventDefinitions = {
change: [AppStateStatus],
memoryWarning: [],
+ memoryLevelChange: [AppStateMemoryLevelInfo],
blur: [],
focus: [],
};
@@ -48,6 +54,7 @@ type NativeAppStateEventDefinitions = {
appStateDidChange: [{app_state: AppStateStatus}],
appStateFocusChange: [boolean],
memoryWarning: [],
+ memoryLevelChange: [AppStateMemoryLevelInfo],
};
/**
@@ -129,6 +136,14 @@ class AppStateImpl {
// $FlowFixMe[invalid-tuple-arity] Flow cannot refine handler based on the event type
const memoryWarningHandler: () => void = handler;
return emitter.addListener('memoryWarning', memoryWarningHandler);
+ case 'memoryLevelChange':
+ // $FlowFixMe[invalid-tuple-arity] Flow cannot refine handler based on the event type
+ const memoryLevelChangeHandler: AppStateMemoryLevelInfo => void =
+ handler;
+ return emitter.addListener(
+ 'memoryLevelChange',
+ memoryLevelChangeHandler,
+ );
case 'blur':
case 'focus':
// $FlowFixMe[invalid-tuple-arity] Flow cannot refine handler based on the event type
@@ -8,12 +8,13 @@
* @format
*/
-import type {HostInstance} from '../../../src/private/types/HostInstance';
-import type {EventSubscription} from '../../vendor/emitter/EventEmitter';
+import type { HostInstance } from '../../../src/private/types/HostInstance';
+import type { EventSubscription } from '../../vendor/emitter/EventEmitter';
import RCTDeviceEventEmitter from '../../EventEmitter/RCTDeviceEventEmitter';
-import {sendAccessibilityEvent} from '../../ReactNative/RendererProxy';
+import { sendAccessibilityEvent } from '../../ReactNative/RendererProxy';
import Platform from '../../Utilities/Platform';
+import AccessibilityInfoDelegate from './delegates/AccessibilityInfoDelegate'; // RNC_patch
import legacySendAccessibilityEvent from './legacySendAccessibilityEvent';
import NativeAccessibilityInfoAndroid from './NativeAccessibilityInfo';
import NativeAccessibilityManagerIOS from './NativeAccessibilityManager';
@@ -26,7 +27,7 @@ type AccessibilityEventDefinitionsAndroid = {
// Events that are only supported on iOS.
type AccessibilityEventDefinitionsIOS = {
- announcementFinished: [{announcement: string, success: boolean}],
+ announcementFinished: [{ announcement: string, success: boolean }],
boldTextChanged: [boolean],
grayscaleChanged: [boolean],
invertColorsChanged: [boolean],
@@ -49,7 +50,7 @@ const EventNames: Map<
$Keys<AccessibilityEventDefinitions>,
string,
> = Platform.OS
- ? new Map([
+ ? new Map([
['change', 'touchExplorationDidChange'],
['reduceMotionChanged', 'reduceMotionDidChange'],
['highTextContrastChanged', 'highTextContrastDidChange'],
@@ -58,7 +59,7 @@ const EventNames: Map<
['invertColorsChanged', 'invertColorDidChange'],
['grayscaleChanged', 'grayscaleModeDidChange'],
])
- : new Map([
+ : new Map([
['announcementFinished', 'announcementFinished'],
['boldTextChanged', 'boldTextChanged'],
['change', 'screenReaderChanged'],
@@ -70,6 +71,8 @@ const EventNames: Map<
['darkerSystemColorsChanged', 'darkerSystemColorsChanged'],
]);
+const DELEGATE = new AccessibilityInfoDelegate({});
+
/**
* Sometimes it's useful to know whether or not the device has a screen reader
* that is currently active. The `AccessibilityInfo` API is designed for this
@@ -89,7 +92,9 @@ const AccessibilityInfo = {
* See https://reactnative.dev/docs/accessibilityinfo#isBoldTextEnabled
*/
isBoldTextEnabled(): Promise<boolean> {
- if (Platform.OS === 'android') {
+ if (Platform.OS === 'harmony') {
+ return DELEGATE.isBoldTextEnabled();
+ } else if (Platform.OS === 'android') {
return Promise.resolve(false);
} else {
return new Promise((resolve, reject) => {
@@ -114,9 +119,13 @@ const AccessibilityInfo = {
* See https://reactnative.dev/docs/accessibilityinfo#isGrayscaleEnabled
*/
isGrayscaleEnabled(): Promise<boolean> {
- if (Platform.OS === 'android') {
+ if (Platform.OS === 'harmony') {
+ return DELEGATE.isGrayscaleEnabled();
+ } else if (Platform.OS === 'android') {
return new Promise((resolve, reject) => {
- if (NativeAccessibilityInfoAndroid?.isGrayscaleEnabled != null) {
+ if (
+ NativeAccessibilityInfoAndroid?.isGrayscaleEnabled != null
+ ) {
NativeAccessibilityInfoAndroid.isGrayscaleEnabled(resolve);
} else {
reject(
@@ -149,10 +158,17 @@ const AccessibilityInfo = {
* See https://reactnative.dev/docs/accessibilityinfo#isInvertColorsEnabled
*/
isInvertColorsEnabled(): Promise<boolean> {
- if (Platform.OS === 'android') {
+ if (Platform.OS === 'harmony') {
+ return DELEGATE.isInvertColorsEnabled();
+ } else if (Platform.OS === 'android') {
return new Promise((resolve, reject) => {
- if (NativeAccessibilityInfoAndroid?.isInvertColorsEnabled != null) {
- NativeAccessibilityInfoAndroid.isInvertColorsEnabled(resolve);
+ if (
+ NativeAccessibilityInfoAndroid?.isInvertColorsEnabled !=
+ null
+ ) {
+ NativeAccessibilityInfoAndroid.isInvertColorsEnabled(
+ resolve,
+ );
} else {
reject(
new Error(
@@ -184,10 +200,15 @@ const AccessibilityInfo = {
* See https://reactnative.dev/docs/accessibilityinfo#isReduceMotionEnabled
*/
isReduceMotionEnabled(): Promise<boolean> {
+ if (Platform.OS === 'harmony') {
+ return DELEGATE.isReduceMotionEnabled();
+ }
return new Promise((resolve, reject) => {
if (Platform.OS
if (NativeAccessibilityInfoAndroid != null) {
- NativeAccessibilityInfoAndroid.isReduceMotionEnabled(resolve);
+ NativeAccessibilityInfoAndroid.isReduceMotionEnabled(
+ resolve,
+ );
} else {
reject(new Error('AccessibilityInfo native module is not available'));
}
@@ -213,8 +234,13 @@ const AccessibilityInfo = {
isHighTextContrastEnabled(): Promise<boolean> {
return new Promise((resolve, reject) => {
if (Platform.OS
- if (NativeAccessibilityInfoAndroid?.isHighTextContrastEnabled != null) {
- NativeAccessibilityInfoAndroid.isHighTextContrastEnabled(resolve);
+ if (
+ NativeAccessibilityInfoAndroid?.isHighTextContrastEnabled !=
+ null
+ ) {
+ NativeAccessibilityInfoAndroid.isHighTextContrastEnabled(
+ resolve,
+ );
} else {
reject(
new Error(
@@ -267,6 +293,9 @@ const AccessibilityInfo = {
* See https://reactnative.dev/docs/accessibilityinfo#prefersCrossFadeTransitions
*/
prefersCrossFadeTransitions(): Promise<boolean> {
+ if (Platform.OS === 'harmony') {
+ return DELEGATE.prefersCrossFadeTransitions();
+ }
return new Promise((resolve, reject) => {
if (Platform.OS
return Promise.resolve(false);
@@ -299,6 +328,9 @@ const AccessibilityInfo = {
* See https://reactnative.dev/docs/accessibilityinfo#isReduceTransparencyEnabled
*/
isReduceTransparencyEnabled(): Promise<boolean> {
+ if (Platform.OS === 'harmony') {
+ return DELEGATE.isReduceTransparencyEnabled();
+ }
if (Platform.OS
return Promise.resolve(false);
} else {
@@ -324,10 +356,15 @@ const AccessibilityInfo = {
* See https://reactnative.dev/docs/accessibilityinfo#isScreenReaderEnabled
*/
isScreenReaderEnabled(): Promise<boolean> {
+ if (Platform.OS === 'harmony') {
+ return DELEGATE.isScreenReaderEnabled();
+ }
return new Promise((resolve, reject) => {
if (Platform.OS
if (NativeAccessibilityInfoAndroid != null) {
- NativeAccessibilityInfoAndroid.isTouchExplorationEnabled(resolve);
+ NativeAccessibilityInfoAndroid.isTouchExplorationEnabled(
+ resolve,
+ );
} else {
reject(new Error('NativeAccessibilityInfoAndroid is not available'));
}
@@ -355,13 +392,19 @@ const AccessibilityInfo = {
* See https://reactnative.dev/docs/accessibilityinfo/#isaccessibilityserviceenabled-android
*/
isAccessibilityServiceEnabled(): Promise<boolean> {
+ if (Platform.OS === 'harmony') {
+ return DELEGATE.isAccessibilityServiceEnabled();
+ }
return new Promise((resolve, reject) => {
if (Platform.OS
if (
NativeAccessibilityInfoAndroid != null &&
- NativeAccessibilityInfoAndroid.isAccessibilityServiceEnabled != null
+ NativeAccessibilityInfoAndroid.isAccessibilityServiceEnabled !=
+ null
) {
- NativeAccessibilityInfoAndroid.isAccessibilityServiceEnabled(resolve);
+ NativeAccessibilityInfoAndroid.isAccessibilityServiceEnabled(
+ resolve,
+ );
} else {
reject(
new Error(
@@ -422,105 +465,128 @@ const AccessibilityInfo = {
*
* See https://reactnative.dev/docs/accessibilityinfo#addeventlistener
*/
- addEventListener<K: $Keys<AccessibilityEventDefinitions>>(
+ addEventListener<K: $Keys< AccessibilityEventDefinitions >> (
eventName: K,
- // $FlowFixMe[incompatible-type] - Flow bug with unions and generics (T128099423)
- handler: (...AccessibilityEventDefinitions[K]) => void,
- ): EventSubscription {
- const deviceEventName = EventNames.get(eventName);
- return deviceEventName == null
- ? {remove(): void {}}
- : // $FlowFixMe[incompatible-type]
- RCTDeviceEventEmitter.addListener(deviceEventName, handler);
- },
+ // $FlowFixMe[incompatible-type] - Flow bug with unions and generics (T128099423)
+ handler: (...AccessibilityEventDefinitions[K]) => void,
+ ): EventSubscription {
+ const deviceEventName = EventNames.get(eventName);
+ if (Platform.OS === 'harmony') {
+ return RCTDeviceEventEmitter.addListener(
+ deviceEventName ?? eventName,
+ handler,
+ );
+ }
+ return deviceEventName == null
+ ? { remove(): void { } }
+ : // $FlowFixMe[incompatible-type]
+ RCTDeviceEventEmitter.addListener(deviceEventName, handler);
+},
- /**
- * Set accessibility focus to a React component.
- *
- * See https://reactnative.dev/docs/accessibilityinfo#setaccessibilityfocus
- */
- setAccessibilityFocus(reactTag: number): void {
+/**
+ * Set accessibility focus to a React component.
+ *
+ * See https://reactnative.dev/docs/accessibilityinfo#setaccessibilityfocus
+ */
+setAccessibilityFocus(reactTag: number): void {
+ if (Platform.OS === 'harmony') {
+ DELEGATE.setAccessibilityFocus(reactTag);
+ } else {
legacySendAccessibilityEvent(reactTag, 'focus');
- },
+ }
+},
- /**
- * Send a named accessibility event to a HostComponent.
- */
- sendAccessibilityEvent(
- handle: HostInstance,
- eventType: AccessibilityEventTypes,
- ) {
- // iOS only supports 'focus' event types
- if (Platform.OS === 'ios' && eventType === 'click') {
- return;
- }
- // route through React renderer to distinguish between Fabric and non-Fabric handles
- sendAccessibilityEvent(handle, eventType);
- },
+/**
+ * Send a named accessibility event to a HostComponent.
+ */
+sendAccessibilityEvent(
+ handle: HostInstance,
+ eventType: AccessibilityEventTypes,
+) {
+ // iOS only supports 'focus' event types
+ if (Platform.OS === 'ios' && eventType === 'click') {
+ return;
+ }
+ // route through React renderer to distinguish between Fabric and non-Fabric handles
+ sendAccessibilityEvent(handle, eventType);
+},
- /**
- * Post a string to be announced by the screen reader.
- *
- * See https://reactnative.dev/docs/accessibilityinfo#announceforaccessibility
- */
- announceForAccessibility(announcement: string): void {
- if (Platform.OS === 'android') {
- NativeAccessibilityInfoAndroid?.announceForAccessibility(announcement);
+/**
+ * Post a string to be announced by the screen reader.
+ *
+ * See https://reactnative.dev/docs/accessibilityinfo#announceforaccessibility
+ */
+announceForAccessibility(announcement: string): void {
+ if (Platform.OS === 'harmony') {
+ DELEGATE.announceForAccessibility(announcement);
+ } else if (Platform.OS === 'android') {
+ NativeAccessibilityInfoAndroid?.announceForAccessibility(
+ announcement,
+ );
+ } else {
+ NativeAccessibilityManagerIOS?.announceForAccessibility(
+ announcement,
+ );
+ }
+},
+
+/**
+ * Post a string to be announced by the screen reader.
+ * - `announcement`: The string announced by the screen reader.
+ * - `options`: An object that configures the reading options.
+ * - `queue`: The announcement will be queued behind existing announcements. iOS only.
+ */
+announceForAccessibilityWithOptions(
+ announcement: string,
+ options: { queue?: boolean },
+): void {
+ if (Platform.OS === 'harmony') {
+ DELEGATE.announceForAccessibilityWithOptions(announcement, options);
+ } else if (Platform.OS === 'android') {
+ NativeAccessibilityInfoAndroid?.announceForAccessibility(
+ announcement,
+ );
+ } else {
+ if (
+ NativeAccessibilityManagerIOS?.announceForAccessibilityWithOptions
+ ) {
+ NativeAccessibilityManagerIOS?.announceForAccessibilityWithOptions(
+ announcement,
+ options,
+ );
} else {
- NativeAccessibilityManagerIOS?.announceForAccessibility(announcement);
+ NativeAccessibilityManagerIOS?.announceForAccessibility(
+ announcement,
+ );
}
- },
+ }
+},
- /**
- * Post a string to be announced by the screen reader.
- * - `announcement`: The string announced by the screen reader.
- * - `options`: An object that configures the reading options.
- * - `queue`: The announcement will be queued behind existing announcements. iOS only.
- * - `priority`: The priority of the announcement. Possible values: 'low' | 'default' | 'high'.
- * High priority announcements will interrupt any ongoing speech and cannot be interrupted.
- * Default priority announcements will interrupt any ongoing speech but can be interrupted.
- * Low priority announcements will not interrupt ongoing speech and can be interrupted.
- * (iOS only).
- */
- announceForAccessibilityWithOptions(
- announcement: string,
- options: {queue?: boolean, priority?: 'low' | 'default' | 'high'},
- ): void {
- if (Platform.OS === 'android') {
- NativeAccessibilityInfoAndroid?.announceForAccessibility(announcement);
- } else {
- if (NativeAccessibilityManagerIOS?.announceForAccessibilityWithOptions) {
- NativeAccessibilityManagerIOS?.announceForAccessibilityWithOptions(
- announcement,
- options,
+/**
+ * Get the recommended timeout for changes to the UI needed by this user.
+ *
+ * See https://reactnative.dev/docs/accessibilityinfo#getrecommendedtimeoutmillis
+ */
+getRecommendedTimeoutMillis(originalTimeout: number): Promise < number > {
+ if (Platform.OS === 'harmony') {
+ return DELEGATE.getRecommendedTimeoutMillis(originalTimeout);
+ } else if (Platform.OS === 'android') {
+ return new Promise((resolve, reject) => {
+ if (
+ NativeAccessibilityInfoAndroid?.getRecommendedTimeoutMillis
+ ) {
+ NativeAccessibilityInfoAndroid.getRecommendedTimeoutMillis(
+ originalTimeout,
+ resolve,
);
} else {
- NativeAccessibilityManagerIOS?.announceForAccessibility(announcement);
+ resolve(originalTimeout);
}
- }
- },
-
- /**
- * Get the recommended timeout for changes to the UI needed by this user.
- *
- * See https://reactnative.dev/docs/accessibilityinfo#getrecommendedtimeoutmillis
- */
- getRecommendedTimeoutMillis(originalTimeout: number): Promise<number> {
- if (Platform.OS === 'android') {
- return new Promise((resolve, reject) => {
- if (NativeAccessibilityInfoAndroid?.getRecommendedTimeoutMillis) {
- NativeAccessibilityInfoAndroid.getRecommendedTimeoutMillis(
- originalTimeout,
- resolve,
- );
- } else {
- resolve(originalTimeout);
- }
- });
- } else {
+ });
+ } else {
return Promise.resolve(originalTimeout);
}
- },
+},
};
export default AccessibilityInfo;
new file mode 100644
@@ -0,0 +1,28 @@
+/**
+ * @format
+ */
+import {BaseAccessibilityInfoDelegate} from './BaseAccessibilityInfoDelegate';
+import {UnsupportedByPlatformError} from '../../../../delegates/DelegateError';
+
+export default class AccessibilityInfoDelegate extends BaseAccessibilityInfoDelegate {
+ override setAccessibilityFocus(reactTag: number): void {
+ throw new UnsupportedByPlatformError();
+ }
+
+ override announceForAccessibility(announcement: string): void {
+ throw new UnsupportedByPlatformError();
+ }
+
+ override announceForAccessibilityWithOptions(
+ announcement: string,
+ options: {queue?: boolean},
+ ) {
+ throw new UnsupportedByPlatformError();
+ }
+
+ override getRecommendedTimeoutMillis(
+ originalTimeout: number,
+ ): Promise<number> {
+ throw new UnsupportedByPlatformError();
+ }
+}
new file mode 100644
@@ -0,0 +1,69 @@
+/**
+ * @format
+ */
+import type {AccessibilityChangeEventName} from '../AccessibilityInfo';
+
+export type BaseAccessibilityInfoDelegateContext = {};
+
+export abstract class BaseAccessibilityInfoDelegate {
+ constructor(protected ctx: BaseAccessibilityInfoDelegateContext) {}
+
+ getPlatformEventNameByAPIEventNameMap(): Map<
+ AccessibilityChangeEventName,
+ string
+ > {
+ return new Map<AccessibilityChangeEventName, string>()
+ .set('change', 'change')
+ .set('boldTextChanged', 'boldTextChanged')
+ .set('grayscaleChanged', 'grayscaleChanged')
+ .set('invertColorsChanged', 'invertColorsChanged')
+ .set('reduceMotionChanged', 'reduceMotionChanged')
+ .set('screenReaderChanged', 'screenReaderChanged')
+ .set('reduceTransparencyChanged', 'reduceTransparencyChanged');
+ }
+
+ async isBoldTextEnabled(): Promise<boolean> {
+ return false;
+ }
+
+ async isGrayscaleEnabled(): Promise<boolean> {
+ return false;
+ }
+
+ async isInvertColorsEnabled(): Promise<boolean> {
+ return false;
+ }
+
+ async isReduceMotionEnabled(): Promise<boolean> {
+ return false;
+ }
+
+ async prefersCrossFadeTransitions(): Promise<boolean> {
+ return false;
+ }
+
+ async isReduceTransparencyEnabled(): Promise<boolean> {
+ return false;
+ }
+
+ async isScreenReaderEnabled(): Promise<boolean> {
+ return false;
+ }
+
+ async isAccessibilityServiceEnabled(): Promise<boolean> {
+ return false;
+ }
+
+ abstract setAccessibilityFocus(reactTag: number): void;
+
+ abstract announceForAccessibility(announcement: string): void;
+
+ abstract announceForAccessibilityWithOptions(
+ announcement: string,
+ options: {queue?: boolean},
+ );
+
+ abstract getRecommendedTimeoutMillis(
+ originalTimeout: number,
+ ): Promise<number>;
+}
@@ -26,6 +26,7 @@ import TouchableOpacity from './Touchable/TouchableOpacity';
import View from './View/View';
import invariant from 'invariant';
import * as React from 'react';
+import ButtonDelegate from "./delegates/ButtonDelegate";
export type ButtonProps = $ReadOnly<{
/**
@@ -278,6 +279,8 @@ export type ButtonProps = $ReadOnly<{
```
*/
+const DELEGATE = new ButtonDelegate({});
+
const NativeTouchable:
| typeof TouchableNativeFeedback
| typeof TouchableOpacity =
@@ -360,6 +363,8 @@ const Button: component(
? 'no-hide-descendants'
: importantForAccessibility;
+ const innerViewFocusable = DELEGATE.getInnerViewFocusable();
+
return (
<NativeTouchable
accessible={accessible}
@@ -385,7 +390,7 @@ const Button: component(
// $FlowFixMe[prop-missing]
// $FlowFixMe[incompatible-type]
ref={ref}>
- <View style={buttonStyles}>
+ <View style={buttonStyles} focusable={innerViewFocusable}>
<Text style={textStyles} disabled={disabled}>
{formattedTitle}
</Text>
@@ -398,6 +403,7 @@ Button.displayName = 'Button';
const styles = StyleSheet.create({
button: Platform.select({
+ harmony: DELEGATE.getButtonStyle(),
ios: {},
android: {
elevation: 4,
@@ -410,6 +416,7 @@ const styles = StyleSheet.create({
textAlign: 'center',
margin: 8,
...Platform.select({
+ harmony: DELEGATE.getTextStyle(),
ios: {
// iOS blue from https://developer.apple.com/ios/human-interface-guidelines/visual-design/color/
color: '#007AFF',
@@ -422,6 +429,7 @@ const styles = StyleSheet.create({
}),
},
buttonDisabled: Platform.select({
+ harmony: DELEGATE.getButtonDisabledStyle(),
ios: {},
android: {
elevation: 0,
@@ -429,6 +437,7 @@ const styles = StyleSheet.create({
},
}),
textDisabled: Platform.select({
+ harmony: DELEGATE.getTextDisabledStyle(),
ios: {
color: '#cdcdcd',
},
@@ -26,6 +26,8 @@ import View from '../View/View';
import AndroidDrawerLayoutNativeComponent, {
Commands,
} from './AndroidDrawerLayoutNativeComponent';
+import DrawerLayoutAndroidDelegate from './delegates/DrawerLayoutAndroidDelegate'; // RNC_patch
+import Platform from '../../Utilities/Platform';
import nullthrows from 'nullthrows';
import * as React from 'react';
import {createRef} from 'react';
@@ -67,6 +69,13 @@ class DrawerLayoutAndroid
extends React.Component<DrawerLayoutAndroidProps, DrawerLayoutAndroidState>
implements DrawerLayoutAndroidMethods
{
+ _delegate: DrawerLayoutAndroidDelegate; // RNC_patch
+
+ constructor(props: Props) {
+ super(props);
+ this._delegate = new DrawerLayoutAndroidDelegate({ getProps: () => this.props }); // RNC_patch
+ }
+
static get positions(): unknown {
console.warn(
'Setting DrawerLayoutAndroid drawerPosition using `DrawerLayoutAndroid.positions` is deprecated. Instead pass the string value "left" or "right"',
@@ -84,6 +93,16 @@ class DrawerLayoutAndroid
};
render(): React.Node {
+ if (Platform.OS === 'harmony') {
+ return this._delegate.render({
+ drawerOpened: this.state.drawerOpened,
+ nativeRef: this._nativeRef,
+ onDrawerOpen: this._onDrawerOpen,
+ onDrawerClose: this._onDrawerClose,
+ onDrawerSlide: this._onDrawerSlide,
+ onDrawerStateChanged: this._onDrawerStateChanged,
+ }); // RNC_patch
+ }
const {
drawerBackgroundColor = 'white',
onDrawerStateChanged,
new file mode 100644
@@ -0,0 +1,28 @@
+/**
+ * @format
+ */
+import type { DrawerLayoutAndroidProps } from '../DrawerLayoutAndroid';
+import AndroidDrawerLayoutNativeComponent from '../AndroidDrawerLayoutNativeComponent';
+
+export type BaseDrawerLayoutAndroidDelegateContext = {
+ getProps: () => DrawerLayoutAndroidProps
+};
+
+export type DrawerLayoutAndroidDelegateInternalProps<
+ TNativeComponent extends { new(props: any): React.Component<any> },
+> = {
+ drawerOpened: boolean,
+ nativeRef: React.ElementRef<typeof AndroidDrawerLayoutNativeComponent>,
+ onDrawerOpen: () => void,
+ onDrawerClose: () => void,
+ onDrawerSlide: (event: { nativeEvent: { offset: number } }) => void,
+ onDrawerStateChanged: (newState: 'Idle' | 'Dragging' | 'Settling') => void,
+};
+
+
+export abstract class BaseDrawerLayoutAndroidDelegate {
+ constructor(protected ctx: BaseDrawerLayoutAndroidDelegateContext) { }
+ abstract render(
+ internalProps: DrawerLayoutAndroidDelegateInternalProps<any>,
+ ): React.ReactNode;
+}
new file mode 100644
@@ -0,0 +1,8 @@
+import { UnsupportedByPlatformError } from "../../../../delegates/DelegateError";
+import { BaseDrawerLayoutAndroidDelegate, DrawerLayoutAndroidDelegateInternalProps } from "./BaseDrawerLayoutAndroidDelegate";
+
+export default class DrawerLayoutAndroidDelegate extends BaseDrawerLayoutAndroidDelegate {
+ render(internalProps: DrawerLayoutAndroidDelegateInternalProps<any>): React.ReactNode {
+ throw new UnsupportedByPlatformError();
+ }
+}
\ No newline at end of file
@@ -22,6 +22,7 @@ import Platform from '../../Utilities/Platform';
import {type EventSubscription} from '../../vendor/emitter/EventEmitter';
import AccessibilityInfo from '../AccessibilityInfo/AccessibilityInfo';
import View from '../View/View';
+import KeyboardAvoidingViewDelegate from './delegates/KeyboardAvoidingViewDelegate';
import Keyboard from './Keyboard';
import * as React from 'react';
import {createRef} from 'react';
@@ -70,16 +71,22 @@ class KeyboardAvoidingView extends React.Component<
viewRef: {current: React.ElementRef<typeof View> | null, ...};
_initialFrameHeight: number = 0;
_bottom: number = 0;
+ _delegate: KeyboardAvoidingViewDelegate;
constructor(props: KeyboardAvoidingViewProps) {
super(props);
this.state = {bottom: 0};
this.viewRef = createRef();
+ this._delegate = new KeyboardAvoidingViewDelegate({ getProps: () => this.props });
}
async _relativeKeyboardHeight(
keyboardFrame: KeyboardMetrics,
): Promise<number> {
+ const result = this._delegate.getRelativeKeyboardHeight(keyboardFrame, this.props);
+ if (result !== null) {
+ return result;
+ }
const frame = this._frame;
if (!frame || !keyboardFrame) {
return 0;
@@ -233,6 +240,7 @@ class KeyboardAvoidingView extends React.Component<
...props
} = this.props;
const bottomHeight = enabled
+ const preparedChildren = this._delegate.prepareChildren(children, { bottomHeight });
switch (behavior) {
case 'height':
let heightStyle;
@@ -252,7 +260,7 @@ class KeyboardAvoidingView extends React.Component<
style={StyleSheet.compose(style, heightStyle)}
onLayout={this._onLayout}
{...props}>
- {children}
+ {Platform.OS === 'harmony' ? preparedChildren : children}
</View>
);
@@ -267,7 +275,7 @@ class KeyboardAvoidingView extends React.Component<
style={StyleSheet.compose(contentContainerStyle, {
bottom: bottomHeight,
})}>
- {children}
+ {Platform.OS === 'harmony' ? preparedChildren : children}
</View>
</View>
);
@@ -279,7 +287,7 @@ class KeyboardAvoidingView extends React.Component<
style={StyleSheet.compose(style, {paddingBottom: bottomHeight})}
onLayout={this._onLayout}
{...props}>
- {children}
+ {Platform.OS === 'harmony' ? preparedChildren : children}
</View>
);
@@ -290,7 +298,7 @@ class KeyboardAvoidingView extends React.Component<
onLayout={this._onLayout}
style={style}
{...props}>
- {children}
+ {Platform.OS === 'harmony' ? preparedChildren : children}
</View>
);
}
new file mode 100644
@@ -0,0 +1,28 @@
+/**
+ * @format
+ */
+import {ReactNode} from 'react';
+import type {KeyboardAvoidingViewProps, KeyboardMetrics} from 'react-native';
+
+export type BaseKeyboardAvoidingViewDelegateContext = {
+ getProps(): KeyboardAvoidingViewProps;
+};
+
+export type PrepareChildrenOptions = {
+ bottomHeight?: number;
+};
+
+export abstract class BaseKeyboardAvoidingViewDelegate {
+ constructor(protected ctx: BaseKeyboardAvoidingViewDelegateContext) {}
+
+ getRelativeKeyboardHeight(keyboardFrame: KeyboardMetrics): number | null {
+ return null;
+ }
+
+ prepareChildren(
+ children: ReactNode,
+ options: PrepareChildrenOptions = {},
+ ): ReactNode {
+ return children;
+ }
+}
new file mode 100644
@@ -0,0 +1,6 @@
+/**
+ * @format
+ */
+import {BaseKeyboardAvoidingViewDelegate} from './BaseKeyboardAvoidingViewDelegate';
+
+export default class KeyboardAvoidingViewDelegate extends BaseKeyboardAvoidingViewDelegate {}
@@ -18,6 +18,7 @@ import PullToRefreshViewNativeComponent, {
Commands as PullToRefreshCommands,
} from './PullToRefreshViewNativeComponent';
import * as React from 'react';
+import RefreshControlDelegate from './delegates/RefreshControlDelegate';
const Platform = require('../../Utilities/Platform').default;
@@ -131,6 +132,14 @@ class RefreshControl extends React.Component<RefreshControlProps> {
>;
_lastNativeRefreshing: boolean = false;
+ // RNC_patch
+ _delegate: RefreshControlDelegate;
+ constructor(props: RefreshControlProps) {
+ super(props);
+ this._delegate = Platform.OS === 'harmony' ? new RefreshControlDelegate({ getProps: () => this.props }) : null;
+ }
+
+
componentDidMount() {
this._lastNativeRefreshing = this.props.refreshing;
}
@@ -145,7 +154,9 @@ class RefreshControl extends React.Component<RefreshControlProps> {
this.props.refreshing !== this._lastNativeRefreshing &&
this._nativeRef
) {
- if (Platform.OS === 'android') {
+ if (Platform.OS === 'harmony') {
+ this._delegate.onSetNativeRefreshing(this._nativeRef, this.props.refreshing);
+ } else if (Platform.OS === 'android') {
AndroidSwipeRefreshLayoutCommands.setNativeRefreshing(
this._nativeRef,
this.props.refreshing,
@@ -161,7 +172,12 @@ class RefreshControl extends React.Component<RefreshControlProps> {
}
render(): React.Node {
- if (Platform.OS === 'ios') {
+ if (Platform.OS === 'harmony') {
+ return this._delegate.renderNativeComponent({
+ setNativeRef: this._setNativeRef,
+ onRefresh: this._onRefresh,
+ });
+ } else if (Platform.OS === 'ios') {
const {enabled, colors, progressBackgroundColor, size, ...props} =
this.props;
return (
new file mode 100644
@@ -0,0 +1,31 @@
+/**
+ * @format
+ */
+import React from 'react';
+import type { RefreshControlProps } from '../RefreshControl';
+
+export type RefreshControlDelegateContext = {
+ getProps: () => RefreshControlProps
+};
+
+export type RefreshControlDelegateRenderInternalProps<
+ TNativeComponent extends {new (props: any): React.Component<any>},
+> = {
+ setNativeRef: (ref: React.ElementRef<TNativeComponent>) => void;
+ onRefresh: () => void;
+};
+
+export abstract class BaseRefreshControlDelegate<
+ TNativeComponent extends {new (props: any): React.Component<any>},
+> {
+ constructor(protected ctx: RefreshControlDelegateContext) {}
+
+ abstract onSetNativeRefreshing(
+ ref: React.ElementRef<TNativeComponent>,
+ refreshing: boolean,
+ ): void;
+
+ abstract renderNativeComponent(
+ internalProps: RefreshControlDelegateRenderInternalProps<TNativeComponent>,
+ ): React.ReactNode;
+}
new file mode 100644
@@ -0,0 +1,21 @@
+/**
+ * @format
+ */
+
+import {
+ BaseRefreshControlDelegate,
+ RefreshControlDelegateRenderInternalProps,
+} from './BaseRefreshControlDelegate';
+import {UnsupportedByPlatformError} from '../../../../delegates/DelegateError';
+
+export default class RefreshControlDelegate extends BaseRefreshControlDelegate<any> {
+ renderNativeComponent(
+ internalProps: RefreshControlDelegateRenderInternalProps<any>,
+ ): React.ReactNode {
+ throw new UnsupportedByPlatformError();
+ }
+
+ onSetNativeRefreshing(ref: unknown, refreshing: boolean): void {
+ throw new UnsupportedByPlatformError();
+ }
+}
@@ -635,6 +635,13 @@ export interface ScrollViewProps
*/
decelerationRate?: 'fast' | 'normal' | number | undefined;
+ /**
+ * Limit the maximum fling speed.
+ *
+ * @platform harmony
+ */
+ flingSpeedLimit?: number | undefined;
+
/**
* When true the scroll view's children are arranged horizontally in a row
* instead of vertically in a column. The default value is false.
@@ -48,6 +48,7 @@ import View from '../View/View';
import processDecelerationRate from './processDecelerationRate';
import Commands from './ScrollViewCommands';
import ScrollViewContext, {HORIZONTAL, VERTICAL} from './ScrollViewContext';
+import ScrollViewDelegate from './delegates/ScrollViewDelegate';
import ScrollViewStickyHeader from './ScrollViewStickyHeader';
import invariant from 'invariant';
import memoize from 'memoize-one';
@@ -438,6 +439,13 @@ type ScrollViewBaseProps = $ReadOnly<{
*/
decelerationRate?: ?DecelerationRateType,
+ /**
+ * Limit the maximum fling speed.
+ *
+ * @platform harmony
+ */
+ flingSpeedLimit?: ?number,
+
/**
* *Experimental, iOS Only*. The API is experimental and will change in future releases.
*
@@ -682,6 +690,7 @@ export type ScrollViewProps = $ReadOnly<{
type ScrollViewState = {
layoutHeight: ?number,
+ showScrollIndicator: boolean, // RNC_patch
};
const IS_ANIMATING_TOUCH_START_THRESHOLD_MS = 16;
@@ -728,6 +737,8 @@ export type ScrollViewComponentStatics = $ReadOnly<{
class ScrollView extends React.Component<ScrollViewProps, ScrollViewState> {
static Context: typeof ScrollViewContext = ScrollViewContext;
+ _delegate: ScrollViewDelegate;
+
constructor(props: ScrollViewProps) {
super(props);
@@ -735,6 +746,7 @@ class ScrollView extends React.Component<ScrollViewProps, ScrollViewState> {
this.props.contentOffset?.y ?? 0,
);
this._scrollAnimatedValue.setOffset(this.props.contentInset?.top ?? 0);
+ this._delegate = new ScrollViewDelegate({});
}
_scrollAnimatedValue: AnimatedImplementation.Value;
@@ -769,6 +781,7 @@ class ScrollView extends React.Component<ScrollViewProps, ScrollViewState> {
state: ScrollViewState = {
layoutHeight: null,
+ showScrollIndicator: false, // RNC_patch
};
componentDidMount() {
@@ -930,11 +943,18 @@ class ScrollView extends React.Component<ScrollViewProps, ScrollViewState> {
*/
flashScrollIndicators: ScrollViewImperativeMethods['flashScrollIndicators'] =
() => {
- const component = this.getNativeScrollRef();
- if (component == null) {
- return;
+ if (Platform.OS === 'harmony') {
+ this.setState(prev => ({ ...prev, showScrollIndicator: true }));
+ setTimeout(() => {
+ this.setState(prev => ({ ...prev, showScrollIndicator: false }));
+ }, 500);
+ } else {
+ const component = this.getNativeScrollRef();
+ if (component == null) {
+ return;
+ }
+ Commands.flashScrollIndicators(component);
}
- Commands.flashScrollIndicators(component);
};
/**
@@ -1275,11 +1295,9 @@ class ScrollView extends React.Component<ScrollViewProps, ScrollViewState> {
*/
_handleScrollBeginDrag: (e: ScrollEvent) => void = (e: ScrollEvent) => {
FrameRateLogger.beginScroll(); // TODO: track all scrolls after implementing onScrollEndAnimation
-
- if (
- Platform.OS === 'android' &&
- this.props.keyboardDismissMode === 'on-drag'
- ) {
+ if ((Platform.OS === 'harmony' &&
+ this._delegate.shouldDismissKeyboardOnScrollBeginDrag(this.props)) || (Platform.OS === 'android' &&
+ this.props.keyboardDismissMode === 'on-drag')) {
dismissKeyboard();
}
@@ -1732,7 +1750,7 @@ class ScrollView extends React.Component<ScrollViewProps, ScrollViewState> {
removeClippedSubviews={
// Subview clipping causes issues with sticky headers on Android and
// would be hard to fix properly in a performant way.
- Platform.OS === 'android' && hasStickyHeaders
+ (Platform.OS === 'android' || Platform.OS === 'harmony') && hasStickyHeaders
? false
: this.props.removeClippedSubviews
}
@@ -1761,6 +1779,7 @@ class ScrollView extends React.Component<ScrollViewProps, ScrollViewState> {
} = this.props;
const props = {
...otherProps,
+ horizontal,
alwaysBounceHorizontal,
alwaysBounceVertical,
style: StyleSheet.compose(baseStyle, this.props.style),
@@ -1810,7 +1829,21 @@ class ScrollView extends React.Component<ScrollViewProps, ScrollViewState> {
this.props.pagingEnabled
this.props.snapToInterval != null ||
this.props.snapToOffsets != null,
+ // on harmony, pagingEnabled must be set to true to have snapToInterval / snapToOffsets work
+ harmony:
+ this.props.pagingEnabled === true &&
+ this.props.snapToInterval == null &&
+ this.props.snapToOffsets == null,
}),
+ showsVerticalScrollIndicator:
+ (this.props.showsVerticalScrollIndicator ?? true) ||
+ this.state.showScrollIndicator,
+ showsHorizontalScrollIndicator:
+ (this.props.showsHorizontalScrollIndicator ?? true) ||
+ this.state.showScrollIndicator,
+ persistentScrollbar:
+ this.props.persistentScrollbar || this.state.showScrollIndicator,
+ ...this._delegate.getCustomNativeProps(this.props),
maintainVisibleContentPosition:
ReactNativeFeatureFlags.disableMaintainVisibleContentPosition()
? undefined
@@ -1828,7 +1861,16 @@ class ScrollView extends React.Component<ScrollViewProps, ScrollViewState> {
);
if (refreshControl != null) {
- if (Platform.OS === 'ios') {
+ if (Platform.OS === 'harmony') {
+ return this._delegate.renderScrollViewWithRefreshControl({
+ NativeScrollView,
+ props,
+ scrollViewRef,
+ refreshControl,
+ contentContainer,
+ baseStyle,
+ });
+ } else if (Platform.OS === 'ios') {
// On iOS the RefreshControl is a child of the ScrollView.
return (
// $FlowFixMe[incompatible-type] - Flow only knows element refs.
@@ -15,6 +15,9 @@ import type {ScrollViewNativeProps as Props} from './ScrollViewNativeComponentTy
import * as NativeComponentRegistry from '../../NativeComponent/NativeComponentRegistry';
import {ConditionallyIgnoredEventHandlers} from '../../NativeComponent/ViewConfigIgnore';
import Platform from '../../Utilities/Platform';
+import ScrollViewNativeComponentDelegate from './delegates/ScrollViewNativeComponentDelegate';
+
+const DELEGATE = new ScrollViewNativeComponentDelegate({});
export const __INTERNAL_VIEW_CONFIG: PartialViewConfig =
Platform.OS
@@ -90,7 +93,11 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig =
isInvertedVirtualizedList: true,
},
}
- : {
+ :
+ Platform.OS === 'harmony' ?
+ DELEGATE.getInternalViewConfig()
+ :
+ {
uiViewClassName: 'RCTScrollView',
bubblingEventTypes: {},
directEventTypes: {
new file mode 100644
@@ -0,0 +1,36 @@
+/**
+ * @format
+ */
+
+import {ScrollViewProps as _ScrollViewProps} from '../ScrollView';
+
+export type ScrollViewDelegateContext = {};
+
+export type CustomNativeProps = Record<string, any>;
+
+export interface RenderScrollViewWithRefreshControlArgs {
+ NativeScrollView: React.ComponentType<any>;
+ refreshControl: React.ReactElement;
+ scrollViewRef: React.Ref<any>;
+ contentContainer: React.ReactNode;
+ props: any;
+ baseStyle: any;
+}
+
+export type ScrollViewProps = _ScrollViewProps;
+
+export abstract class BaseScrollViewDelegate {
+ constructor(protected ctx: ScrollViewDelegateContext) {}
+
+ abstract renderScrollViewWithRefreshControl(
+ args: RenderScrollViewWithRefreshControlArgs,
+ ): React.ReactNode;
+
+ shouldDismissKeyboardOnScrollBeginDrag(props: ScrollViewProps): boolean {
+ return props.keyboardDismissMode === 'on-drag';
+ }
+
+ getCustomNativeProps(props: ScrollViewProps): CustomNativeProps {
+ return {};
+ }
+}
new file mode 100644
@@ -0,0 +1,12 @@
+/**
+ * @format
+ */
+
+import type {PartialViewConfig} from '../../../Renderer/shims/ReactNativeTypes';
+export type ScrollViewNativeComponentDelegateContext = {};
+
+export abstract class BaseScrollViewNativeComponentDelegate {
+ constructor(protected ctx: ScrollViewNativeComponentDelegateContext) {}
+
+ abstract getInternalViewConfig(): PartialViewConfig;
+}
new file mode 100644
@@ -0,0 +1,23 @@
+/**
+ * @format
+ */
+import {UnsupportedByPlatformError} from '../../../../delegates/DelegateError';
+import {
+ BaseScrollViewDelegate,
+ RenderScrollViewWithRefreshControlArgs,
+ ScrollViewProps,
+} from './BaseScrollViewDelegate';
+
+export default class ScrollViewDelegate extends BaseScrollViewDelegate {
+ override renderScrollViewWithRefreshControl(
+ args: RenderScrollViewWithRefreshControlArgs,
+ ): React.ReactNode {
+ throw new UnsupportedByPlatformError();
+ }
+
+ override shouldDismissKeyboardOnScrollBeginDrag(
+ props: ScrollViewProps,
+ ): boolean {
+ throw new UnsupportedByPlatformError();
+ }
+}
new file mode 100644
@@ -0,0 +1,14 @@
+/**
+ * @format
+ */
+
+import type {PartialViewConfig} from '../../../Renderer/shims/ReactNativeTypes';
+
+import {BaseScrollViewNativeComponentDelegate} from './BaseScrollViewNativeComponentDelegate';
+import {UnsupportedByPlatformError} from '../../../../delegates/DelegateError';
+
+export default class ScrollViewNativeComponentDelegate extends BaseScrollViewNativeComponentDelegate {
+ override getInternalViewConfig(): PartialViewConfig {
+ throw new UnsupportedByPlatformError();
+ }
+}
new file mode 100644
@@ -0,0 +1 @@
+export type DecelerationRateType = 'fast' | 'normal' | number;
@@ -14,6 +14,7 @@ import processColor from '../../StyleSheet/processColor';
import Platform from '../../Utilities/Platform';
import NativeStatusBarManagerAndroid from './NativeStatusBarManagerAndroid';
import NativeStatusBarManagerIOS from './NativeStatusBarManagerIOS';
+import StatusBarDelegate from './delegates/StatusBarDelegate';
import invariant from 'invariant';
import * as React from 'react';
@@ -187,6 +188,8 @@ function createStackEntry(props: StatusBarProps): StackProps {
};
}
+const DELEGATE = new StatusBarDelegate({});
+
/**
* Component to control the app status bar.
*
@@ -234,10 +237,13 @@ class StatusBar extends React.Component<StatusBarProps> {
static _defaultProps: any = createStackEntry({
backgroundColor:
- Platform.OS === 'android'
- ? (NativeStatusBarManagerAndroid.getConstants()
+ Platform.OS === 'harmony' ?
+ DELEGATE.getDefaultBackgroundColor()
+ : Platform.OS === 'android'
+ ? (NativeStatusBarManagerAndroid.getConstants()
.DEFAULT_BACKGROUND_COLOR ?? 'black')
- : 'black',
+ :
+ 'black',
barStyle: 'default',
translucent: false,
hidden: false,
@@ -258,9 +264,12 @@ class StatusBar extends React.Component<StatusBarProps> {
* @platform android
*/
static currentHeight: ?number =
- Platform.OS === 'android'
- ? NativeStatusBarManagerAndroid.getConstants().HEIGHT
- : null;
+ Platform.OS === 'harmony' ?
+ DELEGATE.getCurrentHeight()
+ : Platform.OS === 'android'
+ ? NativeStatusBarManagerAndroid.getConstants().HEIGHT
+ :
+ null;
// Provide an imperative API as static functions of the component.
// See the corresponding prop for more detail.
@@ -274,7 +283,9 @@ class StatusBar extends React.Component<StatusBarProps> {
static setHidden(hidden: boolean, animation?: StatusBarAnimation) {
animation = animation || 'none';
StatusBar._defaultProps.hidden.value = hidden;
- if (Platform.OS === 'ios') {
+ if (Platform.OS === 'harmony') {
+ DELEGATE.setHidden(hidden, animation);
+ } if (Platform.OS === 'ios') {
NativeStatusBarManagerIOS.setHidden(hidden, animation);
} else if (Platform.OS
NativeStatusBarManagerAndroid.setHidden(hidden);
@@ -289,7 +300,9 @@ class StatusBar extends React.Component<StatusBarProps> {
static setBarStyle(style: StatusBarStyle, animated?: boolean) {
animated = animated || false;
StatusBar._defaultProps.barStyle.value = style;
- if (Platform.OS === 'ios') {
+ if (Platform.OS === 'harmony') {
+ DELEGATE.setBarStyle(style, animated);
+ } if (Platform.OS === 'ios') {
NativeStatusBarManagerIOS.setStyle(style, animated);
} else if (Platform.OS
NativeStatusBarManagerAndroid.setStyle(style);
@@ -303,15 +316,19 @@ class StatusBar extends React.Component<StatusBarProps> {
* @deprecated
*/
static setNetworkActivityIndicatorVisible(visible: boolean) {
- if (Platform.OS !== 'ios') {
+ if (Platform.OS !== 'ios' && Platform.OS !== 'harmony') {
console.warn(
- '`setNetworkActivityIndicatorVisible` is only available on iOS',
+ '`setNetworkActivityIndicatorVisible` is only available on iOS and harmony',
);
return;
}
StatusBar._defaultProps.networkActivityIndicatorVisible = visible;
- NativeStatusBarManagerIOS.setNetworkActivityIndicatorVisible(visible);
- }
+ if (Platform.OS === 'harmony') {
+ DELEGATE.setNetworkActivityIndicatorVisible(visible);
+ } else {
+ NativeStatusBarManagerIOS.setNetworkActivityIndicatorVisible(visible);
+ }
+}
/**
* Set the background color for the status bar
@@ -319,8 +336,8 @@ class StatusBar extends React.Component<StatusBarProps> {
* @param animated Animate the style change.
*/
static setBackgroundColor(color: ColorValue, animated?: boolean): void {
- if (Platform.OS !== 'android') {
- console.warn('`setBackgroundColor` is only available on Android');
+ if (Platform.OS !== 'android' && Platform.OS !== 'harmony') {
+ console.warn('`setBackgroundColor` is only available on Android and harmony');
return;
}
animated = animated || false;
@@ -337,8 +354,11 @@ class StatusBar extends React.Component<StatusBarProps> {
typeof processedColor
'Unexpected color given for StatusBar.setBackgroundColor',
);
-
- NativeStatusBarManagerAndroid.setColor(processedColor, animated);
+ if (Platform.OS === 'harmony') {
+ DELEGATE.setColor(processedColor, animated);
+ } else {
+ NativeStatusBarManagerAndroid.setColor(processedColor, animated);
+ }
}
/**
@@ -346,12 +366,16 @@ class StatusBar extends React.Component<StatusBarProps> {
* @param translucent Set as translucent.
*/
static setTranslucent(translucent: boolean) {
- if (Platform.OS !== 'android') {
- console.warn('`setTranslucent` is only available on Android');
+ if (Platform.OS !== 'android' && Platform.OS !== 'harmony') {
+ console.warn('`setTranslucent` is only available on Android and harmony');
return;
}
StatusBar._defaultProps.translucent = translucent;
- NativeStatusBarManagerAndroid.setTranslucent(translucent);
+ if (Platform.OS === 'harmony') {
+ DELEGATE.setTranslucent(translucent);
+ } else {
+ NativeStatusBarManagerAndroid.setTranslucent(translucent);
+ }
}
/**
@@ -440,7 +464,9 @@ class StatusBar extends React.Component<StatusBarProps> {
);
// Update the props that have changed using the merged values from the props stack.
- if (Platform.OS === 'ios') {
+ if (Platform.OS === 'harmony') {
+ DELEGATE.updatePropsStack(oldProps, mergedProps);
+ } else if (Platform.OS === 'ios') {
if (
!oldProps ||
oldProps.barStyle?.value !== mergedProps.barStyle.value
new file mode 100644
@@ -0,0 +1,25 @@
+/**
+ * @format
+ */
+
+export type StatusBarAnimation = 'none' | 'fade' | 'slide';
+
+export type StatusBarStyle = 'default' | 'light-content' | 'dark-content';
+
+export abstract class BaseStatusBarDelegate {
+ abstract getDefaultBackgroundColor(): string;
+
+ abstract getCurrentHeight(): number;
+
+ abstract setHidden(hidden: boolean, animation?: StatusBarAnimation): void;
+
+ abstract setBarStyle(style: StatusBarStyle, animated?: boolean): void;
+
+ abstract setNetworkActivityIndicatorVisible(visible: boolean): void;
+
+ abstract setColor(processedColor: number, animated?: boolean): void;
+
+ abstract setTranslucent(translucent: boolean): void;
+
+ abstract updatePropsStack(oldProps: any, mergedProps: any): void;
+}
new file mode 100644
@@ -0,0 +1,44 @@
+/**
+ * @format
+ */
+
+import {
+ BaseStatusBarDelegate,
+ StatusBarAnimation,
+ StatusBarStyle,
+} from './BaseStatusBarDelegate';
+import {UnsupportedByPlatformError} from '../../../../delegates/DelegateError';
+
+export default class StatusBarDelegate extends BaseStatusBarDelegate {
+ getCurrentHeight(): number {
+ throw new UnsupportedByPlatformError();
+ }
+
+ getDefaultBackgroundColor(): string {
+ throw new UnsupportedByPlatformError();
+ }
+
+ setBarStyle(style: StatusBarStyle, animated?: boolean): void {
+ throw new UnsupportedByPlatformError();
+ }
+
+ setColor(processedColor: number, animated?: boolean): void {
+ throw new UnsupportedByPlatformError();
+ }
+
+ setHidden(hidden: boolean, animation?: StatusBarAnimation): void {
+ throw new UnsupportedByPlatformError();
+ }
+
+ setNetworkActivityIndicatorVisible(visible: boolean): void {
+ throw new UnsupportedByPlatformError();
+ }
+
+ setTranslucent(translucent: boolean): void {
+ throw new UnsupportedByPlatformError();
+ }
+
+ updatePropsStack(oldProps: any, mergedProps: any): void {
+ throw new UnsupportedByPlatformError();
+ }
+}
@@ -264,6 +264,8 @@ const Switch: component(
disabled,
onTintColor: trackColorForTrue,
style: StyleSheet.compose(
+ Platform.OS === 'harmony' ?
+ {height: 31, width: 51} :
{alignSelf: 'flex-start' as const},
StyleSheet.compose(
style,
@@ -58,6 +58,7 @@ import Text from '../../Text/Text';
import TextAncestorContext from '../../Text/TextAncestorContext';
import Platform from '../../Utilities/Platform';
import useMergeRefs from '../../Utilities/useMergeRefs';
+import TextInputDelegate from './delegates/TextInputDelegate';
import TextInputState from './TextInputState';
import invariant from 'invariant';
import nullthrows from 'nullthrows';
@@ -243,6 +244,8 @@ function useTextInputStateSynchronization({
return {setLastNativeSelection, setLastNativeText};
}
+const DELEGATE = new TextInputDelegate({});
+
/**
* A foundational component for inputting text into the app via a
* keyboard. Props provide configurability for several features, such as
@@ -388,7 +391,8 @@ function InternalTextInput(props: TextInputProps): React.Node {
? props.defaultValue
: undefined;
- const viewCommands =
+ const viewCommands = Platform.OS === 'harmony' ?
+ DELEGATE.getTextInputCommands(props.multiline) :
AndroidTextInputCommands ||
(props.multiline
? RCTMultilineTextInputNativeCommands
@@ -552,6 +556,8 @@ function InternalTextInput(props: TextInputProps): React.Node {
const multiline = props.multiline ?? false;
+ let blurOnSubmit = props.blurOnSubmit; // RNC_patch
+
let submitBehavior: SubmitBehavior;
if (props.submitBehavior != null) {
// `submitBehavior` is set explicitly
@@ -570,6 +576,7 @@ function InternalTextInput(props: TextInputProps): React.Node {
} else {
// Single line
if (props.blurOnSubmit !== false) {
+ blurOnSubmit = true; // RNC_patch: The default value is true for single-line fields
submitBehavior = 'blurAndSubmit';
} else {
submitBehavior = 'submit';
@@ -590,7 +597,10 @@ function InternalTextInput(props: TextInputProps): React.Node {
const config = useMemo(
() => ({
- cancelable: Platform.OS === 'ios' ? !rejectResponderTermination : null,
+ cancelable: Platform.OS === 'harmony' ?
+ DELEGATE.shouldBeCancellable(rejectResponderTermination) :
+ Platform.OS === 'ios' ? !rejectResponderTermination :
+ null,
hitSlop,
onPress: (event: GestureResponderEvent) => {
onPress?.(event);
@@ -670,7 +680,46 @@ function InternalTextInput(props: TextInputProps): React.Node {
}
}
- if (Platform.OS === 'ios') {
+ if(Platform.OS === 'harmony') {
+ const _accessibilityLabelledBy =
+ props?.['aria-labelledby'] ?? props?.accessibilityLabelledBy;
+ const useMultilineDefaultStyle =
+ props.multiline === true &&
+ (_style == null ||
+ (_style.padding == null &&
+ _style.paddingVertical == null &&
+ _style.paddingTop == null));
+ textInput = DELEGATE.createNativeTextInput({
+ ref,
+ ...otherProps,
+ ...eventHandlers,
+ accessibilityState,
+ accessibilityLabelledBy: _accessibilityLabelledBy,
+ accessible,
+ submitBehavior,
+ caretHidden,
+ dataDetectorTypes: props.dataDetectorTypes,
+ focusable: tabIndex !== undefined ? !tabIndex : focusable,
+ mostRecentEventCount,
+ nativeID: id ?? props.nativeID,
+ onBlur: _onBlur,
+ onChange: _onChange,
+ onContentSizeChange: props.onContentSizeChange,
+ onFocus: _onFocus,
+ onScroll: _onScroll,
+ onSelectionChange: _onSelectionChange,
+ onSelectionChangeShouldSetResponder: emptyFunctionThatReturnsTrue,
+ selection,
+ selectionColor: selectionColor,
+ cursorColor: cursorColor === undefined ? selectionColor : cursorColor,
+ style: StyleSheet.compose(
+ useMultilineDefaultStyle ? styles.multilineDefault : null,
+ _style,
+ ),
+ text,
+ blurOnSubmit,
+ });
+ } else if (Platform.OS === 'ios') {
const RCTTextInputView =
props.multiline
? RCTMultilineTextInputView
@@ -16,10 +16,12 @@ import type {HostInstance} from '../../../src/private/types/HostInstance';
import {Commands as AndroidTextInputCommands} from '../../Components/TextInput/AndroidTextInputNativeComponent';
import {Commands as iOSTextInputCommands} from '../../Components/TextInput/RCTSingelineTextInputNativeComponent';
+import TextInputStateDelegate from './delegates/TextInputStateDelegate';
const {findNodeHandle} = require('../../ReactNative/RendererProxy');
const Platform = require('../../Utilities/Platform').default;
+const DELEGATE = new TextInputStateDelegate({});
let currentlyFocusedInputRef: ?HostInstance = null;
const inputs = new Set<HostInstance>();
@@ -95,7 +97,9 @@ function focusTextInput(textField: ?HostInstance) {
return;
}
focusInput(textField);
- if (Platform.OS === 'ios') {
+ if (Platform.OS === 'harmony') {
+ DELEGATE.focus(textField);
+ } else if (Platform.OS === 'ios') {
// This isn't necessarily a single line text input
// But commands don't actually care as long as the thing being passed in
// actually has a command with that name. So this should work with single
@@ -126,7 +130,9 @@ function blurTextInput(textField: ?HostInstance) {
if (currentlyFocusedInputRef
blurInput(textField);
- if (Platform.OS === 'ios') {
+ if (Platform.OS === 'harmony') {
+ DELEGATE.blur(textField);
+ } else if (Platform.OS === 'ios') {
// This isn't necessarily a single line text input
// But commands don't actually care as long as the thing being passed in
// actually has a command with that name. So this should work with single
new file mode 100644
@@ -0,0 +1,39 @@
+/**
+ * @format
+ */
+
+export type TextInputDelegateContext = {};
+
+export type GetNativeTextContentTypeOptions = {
+ autocomplete: string;
+ autocompleteWebToTextContentTypeMap: Record<string, string>;
+};
+
+export abstract class BaseTextInputDelegate {
+ constructor(protected ctx: TextInputDelegateContext) {}
+
+ abstract shouldBeCancellable(
+ rejectResponderTermination: boolean,
+ ): boolean | null;
+
+ abstract createNativeTextInput(props: any): React.ReactNode;
+
+ abstract getKeyboardTypeByInputMode(inputMode: string): string;
+
+ abstract getSupportedKeyboardTypes(): string[];
+
+ abstract getTextInputCommands(multiline: boolean): any;
+
+ getNativeAutocomplete(
+ autocomplete: string | null | undefined,
+ ): string | null | undefined {
+ return autocomplete;
+ }
+
+ getNativeTextContentType(
+ textContentType: string | null | undefined,
+ options: GetNativeTextContentTypeOptions,
+ ): string | null | undefined {
+ return textContentType;
+ }
+}
new file mode 100644
@@ -0,0 +1,12 @@
+/**
+ * @format
+ */
+export type TextInputStateContext = {};
+
+export abstract class BaseTextInputStateDelegate {
+ constructor(protected ctx: TextInputStateContext) {}
+
+ abstract focus(ref: React.ComponentRef<any>): void;
+
+ abstract blur(ref: React.ComponentRef<any>): void;
+}
new file mode 100644
@@ -0,0 +1,40 @@
+/**
+ * @format
+ */
+
+import {BaseTextInputDelegate, GetNativeTextContentTypeOptions} from './BaseTextInputDelegate';
+import {UnsupportedByPlatformError} from '../../../../delegates/DelegateError';
+
+
+export default class TextInputDelegate extends BaseTextInputDelegate {
+ override createNativeTextInput(props: any): React.ReactNode {
+ throw new UnsupportedByPlatformError();
+ }
+
+ override getKeyboardTypeByInputMode(inputMode: string): string {
+ throw new UnsupportedByPlatformError();
+ }
+
+ override getSupportedKeyboardTypes(): string[] {
+ throw new UnsupportedByPlatformError();
+ }
+
+ override getNativeAutocomplete(autocomplete: string): string {
+ throw new UnsupportedByPlatformError();
+ }
+
+ override getNativeTextContentType(
+ textContentType: string,
+ options: GetNativeTextContentTypeOptions,
+ ): string {
+ throw new UnsupportedByPlatformError();
+ }
+
+ override shouldBeCancellable(rejectResponderTermination: boolean): boolean | null {
+ throw new UnsupportedByPlatformError();
+ }
+
+ override getTextInputCommands(multiline: boolean): any {
+ throw new UnsupportedByPlatformError();
+ }
+}
new file mode 100644
@@ -0,0 +1,15 @@
+/**
+ * @format
+ */
+import {BaseTextInputStateDelegate} from './BaseTextInputStateDelegate';
+import {UnsupportedByPlatformError} from '../../../../delegates/DelegateError';
+
+export default class TextInputDelegate extends BaseTextInputStateDelegate {
+ blur(ref: React.ComponentRef<any>): void {
+ throw new UnsupportedByPlatformError();
+ }
+
+ focus(ref: React.ComponentRef<any>): void {
+ throw new UnsupportedByPlatformError();
+ }
+}
@@ -19,9 +19,12 @@ import Pressability, {
import {PressabilityDebugView} from '../../Pressability/PressabilityDebug';
import StyleSheet, {type ViewStyleProp} from '../../StyleSheet/StyleSheet';
import Platform from '../../Utilities/Platform';
+import TouchableHighlightDelegate from './delegates/TouchableHighlightDelegate';
import * as React from 'react';
import {cloneElement} from 'react';
+const DELEGATE = new TouchableHighlightDelegate({});
+
type AndroidProps = $ReadOnly<{
nextFocusDown?: ?number,
nextFocusForward?: ?number,
@@ -375,7 +378,8 @@ class TouchableHighlightImpl extends React.Component<
nativeID={this.props.id ?? this.props.nativeID}
testID={this.props.testID}
ref={this.props.hostRef}
- {...eventHandlersWithoutBlurAndFocus}>
+ {...eventHandlersWithoutBlurAndFocus}
+ {...DELEGATE.getExtraContainerProps()}>
{cloneElement(child, {
style: StyleSheet.compose(
child.props.style,
@@ -20,10 +20,13 @@ import {findHostInstance_DEPRECATED} from '../../ReactNative/RendererProxy';
import processColor from '../../StyleSheet/processColor';
import Platform from '../../Utilities/Platform';
import {Commands} from '../View/ViewNativeComponent';
+import TouchableNativeFeedbackDelegate from './delegates/TouchableNativeFeedbackDelegate';
import invariant from 'invariant';
import * as React from 'react';
import {cloneElement} from 'react';
+const DELEGATE = new TouchableNativeFeedbackDelegate({});
+
type TouchableNativeFeedbackTVProps = {
/**
* *(Apple TV only)* TV preferred focus (see documentation for the View component).
@@ -378,6 +381,7 @@ class TouchableNativeFeedback extends React.Component<
nextFocusUp: this.props.nextFocusUp,
onLayout: this.props.onLayout,
testID: this.props.testID,
+ ...DELEGATE.getExtraProps(),
},
...children,
);
@@ -391,3 +391,4 @@ const Touchable: component(
Touchable.displayName = 'TouchableOpacity';
export default Touchable;
+export {TouchableOpacity}; // RNC_PATCH
@@ -22,9 +22,12 @@ import {type AccessibilityProps} from '../../Components/View/ViewAccessibility';
import {PressabilityDebugView} from '../../Pressability/PressabilityDebug';
import usePressability from '../../Pressability/usePressability';
import {type ViewStyleProp} from '../../StyleSheet/StyleSheet';
+import TouchableWithoutFeedbackDelegate from './delegates/TouchableWithoutFeedbackDelegate';
import * as React from 'react';
import {cloneElement, useMemo} from 'react';
+const DELEGATE = new TouchableWithoutFeedbackDelegate({});
+
export type TouchableWithoutFeedbackPropsIOS = {};
export type TouchableWithoutFeedbackPropsAndroid = {
@@ -274,6 +277,7 @@ export default function TouchableWithoutFeedback(
accessibilityLiveRegion:
ariaLive
nativeID: props.id ?? props.nativeID,
+ ...DELEGATE.getExtraProps(),
};
for (const prop of PASSTHROUGH_PROPS) {
new file mode 100644
@@ -0,0 +1,13 @@
+/**
+ * @format
+ */
+
+export type BaseTouchableHighlightContext = {};
+
+export abstract class BaseTouchableHighlightDelegate {
+ constructor(protected ctx: BaseTouchableHighlightContext) {}
+
+ getExtraContainerProps(): Record<string, any> {
+ return {};
+ }
+}
new file mode 100644
@@ -0,0 +1,13 @@
+/**
+ * @format
+ */
+
+export type BaseTouchableNativeFeedbackContext = {};
+
+export abstract class BaseTouchableNativeFeedbackDelegate {
+ constructor(protected ctx: BaseTouchableNativeFeedbackContext) {}
+
+ getExtraProps(): Record<string, any> {
+ return {};
+ }
+}
new file mode 100644
@@ -0,0 +1,13 @@
+/**
+ * @format
+ */
+
+export type BaseTouchableWithoutFeedbackContext = {};
+
+export abstract class BaseTouchableWithoutFeedbackDelegate {
+ constructor(protected ctx: BaseTouchableWithoutFeedbackContext) {}
+
+ getExtraProps(): Record<string, any> {
+ return {};
+ }
+}
new file mode 100644
@@ -0,0 +1,6 @@
+/**
+ * @format
+ */
+import {BaseTouchableHighlightDelegate} from './BaseTouchableHighlightDelegate';
+
+export default class TouchableHighlightDelegate extends BaseTouchableHighlightDelegate {}
new file mode 100644
@@ -0,0 +1,6 @@
+/**
+ * @format
+ */
+import {BaseTouchableNativeFeedbackDelegate} from './BaseTouchableNativeFeedbackDelegate';
+
+export default class TouchableNativeFeedbackDelegate extends BaseTouchableNativeFeedbackDelegate {}
new file mode 100644
@@ -0,0 +1,6 @@
+/**
+ * @format
+ */
+import {BaseTouchableWithoutFeedbackDelegate} from './BaseTouchableWithoutFeedbackDelegate';
+
+export default class TouchableWithoutFeedbackDelegate extends BaseTouchableWithoutFeedbackDelegate {}
@@ -12,6 +12,8 @@ import type {ViewProps} from './ViewPropTypes';
import TextAncestorContext from '../../Text/TextAncestorContext';
import ViewNativeComponent from './ViewNativeComponent';
+import * as DELEGATE from './delegates/ViewDelegate';
+import Platform from '../../Utilities/Platform';
import * as React from 'react';
import {use} from 'react';
@@ -113,10 +115,16 @@ component View(
};
}
+ const convertedProps = DELEGATE.convertAccessibilityStates(processedProps);
+
const actualView =
ref == null ? (
+ Platform.OS === 'harmony' ?
+ <ViewNativeComponent {...convertedProps} /> :
<ViewNativeComponent {...processedProps} />
) : (
+ Platform.OS === 'harmony' ?
+ <ViewNativeComponent {...convertedProps} ref={ref} /> :
<ViewNativeComponent {...processedProps} ref={ref} />
);
new file mode 100644
@@ -0,0 +1,3 @@
+export function convertAccessibilityStates(props) {
+ return props;
+}
\ No newline at end of file
new file mode 100644
@@ -0,0 +1,15 @@
+/**
+ * @format
+ */
+import type {TextStyle, ViewStyle} from 'react-native';
+
+export type ButtonDelegateContext = {};
+
+export abstract class BaseButtonDelegate {
+ constructor(protected ctx: ButtonDelegateContext) {}
+ abstract getButtonStyle(): ViewStyle;
+ abstract getTextStyle(): TextStyle;
+ abstract getButtonDisabledStyle(): ViewStyle;
+ abstract getTextDisabledStyle(): TextStyle;
+ abstract getInnerViewFocusable(): boolean;
+}
new file mode 100644
@@ -0,0 +1,27 @@
+/**
+ * @format
+ */
+import {TextStyle, ViewStyle} from 'react-native';
+import {BaseButtonDelegate} from './BaseButtonDelegate';
+
+export default class ButtonDelegate extends BaseButtonDelegate {
+ getButtonDisabledStyle(): ViewStyle {
+ return {};
+ }
+
+ getButtonStyle(): ViewStyle {
+ return {};
+ }
+
+ getTextDisabledStyle(): TextStyle {
+ return {};
+ }
+
+ getTextStyle(): TextStyle {
+ return {};
+ }
+
+ getInnerViewFocusable(): boolean {
+ return true;
+ }
+}
@@ -12,6 +12,7 @@
import type {Domain} from '../../src/private/devsupport/rndevtools/setUpFuseboxReactDevToolsDispatcher';
import type {Spec as NativeReactDevToolsRuntimeSettingsModuleSpec} from '../../src/private/devsupport/rndevtools/specs/NativeReactDevToolsRuntimeSettingsModule';
+import Platform from '../Utilities/Platform';
if (__DEV__) {
if (typeof global.queueMicrotask !== 'function') {
@@ -140,6 +141,19 @@ if (__DEV__) {
// or the code will throw for bundles that don't have it.
const isAppActive = () => AppState.currentState !== 'background';
+ if (Platform.OS === 'harmony') {
+ // RNOHC_patch: added the following segment to fix issue in multi ReactInstance environment
+ if (!isAppActive()) {
+ const subscription = AppState.addEventListener('change', () => {
+ if (isAppActive()) {
+ connectToWSBasedReactDevToolsFrontend();
+ subscription.remove();
+ }
+ });
+ return;
+ }
+ }
+
// Get hostname from development server (packager)
const devServer = getDevServer();
const host = devServer.bundleLoadedFromServer
@@ -138,6 +138,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig =
},
},
validAttributes: {
+ ...(Platform.OS === 'harmony' ? {fadeDuration: true} : {}),
blurRadius: true,
capInsets: {
diff: require('../Utilities/differ/insetsDiffer').default,
@@ -276,7 +276,7 @@ class Modal extends React.Component<ModalProps, ModalState> {
// Helper function to encapsulate platform specific logic to show or not the Modal.
_shouldShowModal(): boolean {
- if (Platform.OS === 'ios') {
+ if (Platform.OS === 'ios' || Platform.OS === 'harmony') {
return this.props.visible
}
@@ -312,8 +312,8 @@ class Modal extends React.Component<ModalProps, ModalState> {
);
const onDismiss = () => {
- // OnDismiss is implemented on iOS only.
- if (Platform.OS === 'ios') {
+ // OnDismiss is implemented on iOS and Harmony.
+ if (Platform.OS === 'ios' || Platform.OS === 'harmony') {
this.setState({isRendered: false}, () => {
if (this.props.onDismiss) {
this.props.onDismiss();
@@ -9,6 +9,9 @@
*/
import Platform from '../Utilities/Platform';
+import Delegate from './delegates/ViewConfigIgnoreDelegate';
+
+const DELEGATE = new Delegate({});
const ignoredViewConfigProps = new WeakSet<{...}>();
@@ -35,9 +38,11 @@ export function DynamicallyInjectedByGestureHandler<T: {...}>(object: T): T {
* TODO(T110872225): Remove this logic, after achieving platform-consistency
*/
export function ConditionallyIgnoredEventHandlers<
- const T: {+[name: string]: true},
+ const T: {+[name: string]: true, T: {+[name: string]: true}},
>(value: T): T | void {
- if (Platform.OS === 'ios') {
+ if(Platform.OS === 'harmony') {
+ return DELEGATE.conditionallyIgnoredEventHandlers(value);
+ } else if (Platform.OS === 'ios') {
return value;
}
return undefined;
new file mode 100644
@@ -0,0 +1,10 @@
+/**
+ * @format
+ */
+export type ViewConfigIgnoreDelegateContext = {};
+
+export abstract class BaseViewConfigIgnoreDelegate {
+ constructor(protected ctx: ViewConfigIgnoreDelegateContext) {}
+
+ abstract conditionallyIgnoredEventHandlers<T>(value: T): T | undefined;
+}
new file mode 100644
@@ -0,0 +1,10 @@
+/**
+ * @format
+ */
+import {BaseViewConfigIgnoreDelegate} from './BaseViewConfigIgnoreDelegate';
+
+export default class ViewConfigIgnoreDelegate extends BaseViewConfigIgnoreDelegate {
+ conditionallyIgnoredEventHandlers<T>(value: T): T | undefined {
+ return undefined;
+ }
+}
@@ -16,9 +16,18 @@ import type {UIManagerJSInterface} from '../Types/UIManagerJSInterface';
import {unstable_hasComponent} from '../NativeComponent/NativeComponentRegistryUnstable';
import defineLazyObjectProperty from '../Utilities/defineLazyObjectProperty';
import Platform from '../Utilities/Platform';
+import Delegate from './delegates/BridgelessUIManagerDelegate';
import {getFabricUIManager} from './FabricUIManager';
import nullthrows from 'nullthrows';
+const DELEGATE = new Delegate({});
+
+const PatchedPlatform = {
+ select(implementationByPlatformName) {
+ return implementationByPlatformName[DELEGATE.selectAndroidOrIOSImplementation()];
+ },
+};
+
function raiseSoftError(methodName: string, details?: string): void {
console.error(
`[ReactNative Architecture][JS] '${methodName}' is not available in the new React Native architecture.` +
@@ -163,7 +172,104 @@ const UIManagerJSDeprecatedPlatformAPIs = Platform.select({
android: {},
});
-const UIManagerJSPlatformAPIs = Platform.select({
+const UIManagerJSPlatformAPIs = Platform.OS === 'harmony' ?
+PatchedPlatform.select({
+ android: {
+ getConstantsForViewManager: (viewManagerName: string): ?Object => {
+ if (getConstantsForViewManager) {
+ return getConstantsForViewManager(viewManagerName);
+ }
+
+ raiseSoftError('getConstantsForViewManager');
+ return {};
+ },
+ getDefaultEventTypes: (): Array<string> => {
+ if (getDefaultEventTypes) {
+ return getDefaultEventTypesCached();
+ }
+
+ raiseSoftError('getDefaultEventTypes');
+ return [];
+ },
+ setLayoutAnimationEnabledExperimental: (enabled: boolean): void => {
+ if (__DEV__) {
+ console.warn(
+ 'setLayoutAnimationEnabledExperimental is currently a no-op in the New Architecture.',
+ );
+ }
+ },
+ sendAccessibilityEvent: (reactTag: number, eventType: number): void => {
+ // Keep this in sync with java:FabricUIManager.sendAccessibilityEventFromJS
+ // and legacySendAccessibilityEvent.android.js
+ const AccessibilityEvent = {
+ TYPE_VIEW_FOCUSED: 0x00000008,
+ TYPE_WINDOW_STATE_CHANGED: 0x00000020,
+ TYPE_VIEW_CLICKED: 0x00000001,
+ TYPE_VIEW_HOVER_ENTER: 0x00000080,
+ };
+
+ let eventName = null;
+ if (eventType === AccessibilityEvent.TYPE_VIEW_FOCUSED) {
+ eventName = 'focus';
+ } else if (eventType === AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
+ eventName = 'windowStateChange';
+ } else if (eventType === AccessibilityEvent.TYPE_VIEW_CLICKED) {
+ eventName = 'click';
+ } else if (eventType === AccessibilityEvent.TYPE_VIEW_HOVER_ENTER) {
+ eventName = 'viewHoverEnter';
+ } else {
+ console.error(
+ `sendAccessibilityEvent() dropping event: Called with unsupported eventType: ${eventType}`,
+ );
+ return;
+ }
+
+ const FabricUIManager = nullthrows(getFabricUIManager());
+ const shadowNode =
+ FabricUIManager.findShadowNodeByTag_DEPRECATED(reactTag);
+ if (!shadowNode) {
+ console.error(
+ `sendAccessibilityEvent() dropping event: Cannot find view with tag #${reactTag}`,
+ );
+ return;
+ }
+
+ FabricUIManager.sendAccessibilityEvent(shadowNode, eventName);
+ },
+ },
+ ios: {
+ /**
+ * TODO(T174674274): Implement lazy loading of legacy view managers in the new architecture.
+ *
+ * Leave this unimplemented until we implement lazy loading of legacy modules and view managers in the new architecture.
+ */
+ lazilyLoadView: (name: string): Object => {
+ raiseSoftError('lazilyLoadView');
+ return {};
+ },
+ focus: (reactTag: number): void => {
+ const FabricUIManager = nullthrows(getFabricUIManager());
+ const shadowNode =
+ FabricUIManager.findShadowNodeByTag_DEPRECATED(reactTag);
+ if (!shadowNode) {
+ console.error(`focus() noop: Cannot find view with tag #${reactTag}`);
+ return;
+ }
+ FabricUIManager.dispatchCommand(shadowNode, 'focus', []);
+ },
+ blur: (reactTag: number): void => {
+ const FabricUIManager = nullthrows(getFabricUIManager());
+ const shadowNode =
+ FabricUIManager.findShadowNodeByTag_DEPRECATED(reactTag);
+ if (!shadowNode) {
+ console.error(`blur() noop: Cannot find view with tag #${reactTag}`);
+ return;
+ }
+ FabricUIManager.dispatchCommand(shadowNode, 'blur', []);
+ },
+ },
+}) :
+Platform.select({
android: {
getConstantsForViewManager: (viewManagerName: string): ?Object => {
if (getConstantsForViewManager) {
@@ -8,17 +8,19 @@
* @format
*/
-import type {I18nManagerConstants} from './NativeI18nManager';
+import type { I18nManagerConstants } from './NativeI18nManager';
+import Delegate from './delegates/I18nManagerDelegate';
+import Platform from '../Utilities/Platform';
import NativeI18nManager from './NativeI18nManager';
const i18nConstants: I18nManagerConstants = getI18nManagerConstants();
function getI18nManagerConstants(): I18nManagerConstants {
if (NativeI18nManager) {
- const {isRTL, doLeftAndRightSwapInRTL, localeIdentifier} =
+ const { isRTL, doLeftAndRightSwapInRTL, localeIdentifier } =
NativeI18nManager.getConstants();
- return {isRTL, doLeftAndRightSwapInRTL, localeIdentifier};
+ return { isRTL, doLeftAndRightSwapInRTL, localeIdentifier };
}
return {
@@ -27,8 +29,13 @@ function getI18nManagerConstants(): I18nManagerConstants {
};
}
+const DELEGATE = new Delegate({ constantsCallback: getI18nManagerConstants });
+
export default {
getConstants: (): I18nManagerConstants => {
+ if (Platform.OS === 'harmony') {
+ return DELEGATE.getConstants();
+ }
return i18nConstants;
},
@@ -56,7 +63,13 @@ export default {
NativeI18nManager.swapLeftAndRightInRTL(flipStyles);
},
- isRTL: i18nConstants.isRTL as I18nManagerConstants['isRTL'],
+ isRTL: (Platform.OS === 'harmony' ? DELEGATE.isRTL : i18nConstants.isRTL) as I18nManagerConstants['isRTL'],
doLeftAndRightSwapInRTL:
- i18nConstants.doLeftAndRightSwapInRTL as I18nManagerConstants['doLeftAndRightSwapInRTL'],
+ (Platform.OS === 'harmony' ? DELEGATE.doLeftAndRightSwapInRTL : i18nConstants.doLeftAndRightSwapInRTL) as I18nManagerConstants['doLeftAndRightSwapInRTL'],
+ get isRTL() {
+ return (Platform.OS === 'harmony' ? DELEGATE.isRTL : i18nConstants.isRTL) as I18nManagerConstants['isRTL'];
+ },
+ get doLeftAndRightSwapInRTL() {
+ return (Platform.OS === 'harmony' ? DELEGATE.doLeftAndRightSwapInRTL : i18nConstants.doLeftAndRightSwapInRTL) as I18nManagerConstants['doLeftAndRightSwapInRTL'];
+ }
};
new file mode 100644
@@ -0,0 +1,14 @@
+/**
+ * @format
+ */
+
+export type BridgelessUIManagerContext = {};
+
+export abstract class BaseBridgelessUIManagerDelegate {
+ constructor(protected ctx: BridgelessUIManagerContext) {}
+
+ /**
+ * NOTE: delegates should be platform independent, but doing it properly requires more changes and we want to minimize maintenance.
+ */
+ abstract selectAndroidOrIOSImplementation(): 'android' | 'ios';
+}
new file mode 100644
@@ -0,0 +1,18 @@
+/**
+ * @format
+ */
+import type {I18nManagerConstants} from '../NativeI18nManager';
+
+export type I18nManagerContext = {
+ constantsCallback: () => I18nManagerConstants;
+};
+
+export abstract class BaseI18nManagerDelegate {
+ constructor(protected ctx: I18nManagerContext) {}
+
+ abstract getConstants: () => I18nManagerConstants;
+
+ abstract isRTL: boolean;
+
+ abstract doLeftAndRightSwapInRTL: boolean;
+}
new file mode 100644
@@ -0,0 +1,11 @@
+/**
+ * @format
+ */
+import {BaseBridgelessUIManagerDelegate} from './BaseBridgelessUIManagerDelegate';
+import {UnsupportedByPlatformError} from '../../../delegates/DelegateError';
+
+export default class BridgelessUIManagerDelegate extends BaseBridgelessUIManagerDelegate {
+ override selectAndroidOrIOSImplementation(): 'android' | 'ios' {
+ throw new UnsupportedByPlatformError();
+ }
+}
new file mode 100644
@@ -0,0 +1,24 @@
+/**
+ * @format
+ */
+
+import type {I18nManagerConstants} from '../NativeI18nManager';
+import type {I18nManagerContext} from './BaseI18nManagerDelegate';
+import {BaseI18nManagerDelegate} from './BaseI18nManagerDelegate';
+
+export default class I18nManagerDelegate extends BaseI18nManagerDelegate {
+ i18nConstants: I18nManagerConstants;
+ constructor(protected ctx: I18nManagerContext) {
+ super(ctx);
+ this.i18nConstants = ctx.constantsCallback();
+ this.isRTL = this.i18nConstants.isRTL;
+ this.doLeftAndRightSwapInRTL = this.i18nConstants.doLeftAndRightSwapInRTL;
+ }
+
+ getConstants: () => I18nManagerConstants = () => {
+ return this.i18nConstants;
+ };
+
+ isRTL: boolean;
+ doLeftAndRightSwapInRTL: boolean;
+}
new file mode 100644
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) 2026 Huawei Technologies Co., Ltd.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import { TurboModuleRegistry } from "react-native";
+import type { TurboModule } from "react-native/Libraries/TurboModule/RCTExport";
+
+export interface Spec extends TurboModule {
+ sendPageName(data: string): Promise<string>;
+}
+
+export default TurboModuleRegistry.get<Spec>("RegisterPageName") as Spec | null;
@@ -8,26 +8,28 @@
* @format
*/
-import type {ColorValue} from '../StyleSheet/StyleSheet';
+import type { ColorValue } from '../StyleSheet/StyleSheet';
import NativeActionSheetManager from '../ActionSheetIOS/NativeActionSheetManager';
+import Delegate from './delegates/ShareDelegate';
import NativeShareModule from './NativeShareModule';
const processColor = require('../StyleSheet/processColor').default;
const Platform = require('../Utilities/Platform').default;
const invariant = require('invariant');
+const DELEGATE = new Delegate({});
export type ShareContent =
| {
- title?: string,
- url: string,
- message?: string,
- }
+ title?: string,
+ url: string,
+ message?: string,
+ }
| {
- title?: string,
- url?: string,
- message: string,
- };
+ title?: string,
+ url?: string,
+ message: string,
+ };
export type ShareOptions = {
dialogTitle?: string,
excludedActivityTypes?: Array<string>,
@@ -81,17 +83,13 @@ class Share {
static share(
content: ShareContent,
options?: ShareOptions = {},
- ): Promise<{action: string, activityType: ?string}> {
+ ): Promise<{ action: string, activityType: ?string }> {
invariant(
/* $FlowFixMe[invalid-compare] Error discovered during Constant Condition
* roll out. See https://fburl.com/workplace/5whu3i34. */
typeof content
'Content to share must be a valid object',
);
- invariant(
- typeof content.url === 'string' || typeof content.message === 'string',
- 'At least one of URL or message is required',
- );
invariant(
/* $FlowFixMe[invalid-compare] Error discovered during Constant Condition
* roll out. See https://fburl.com/workplace/5whu3i34. */
@@ -99,6 +97,15 @@ class Share {
'Options must be a valid object',
);
+ if (Platform.OS === 'harmony') {
+ return DELEGATE.onShare(content, options);
+ }
+
+ invariant(
+ typeof content.url === 'string' || typeof content.message === 'string',
+ 'At least one of URL or message is required',
+ );
+
if (Platform.OS
invariant(
NativeShareModule,
new file mode 100644
@@ -0,0 +1,21 @@
+/**
+ * @format
+ */
+import type {
+ ShareContent as _ShareContent,
+ ShareOptions as _ShareOptions,
+ ShareAction as _ShareAction,
+} from '../Share';
+
+export type ShareDelegateContext = {};
+export type ShareContent = _ShareContent;
+export type ShareOptions = _ShareOptions;
+export type ShareAction = _ShareAction
+export abstract class BaseShareDelegate {
+ constructor(protected ctx: ShareDelegateContext) {}
+
+ abstract onShare(
+ content: ShareContent,
+ options: ShareOptions,
+ ): Promise<ShareAction>;
+}
new file mode 100644
@@ -0,0 +1,20 @@
+/**
+ * @format
+ */
+
+import {
+ BaseShareDelegate,
+ ShareAction,
+ ShareContent,
+ ShareOptions,
+} from './BaseShareDelegate';
+import {UnsupportedByPlatformError} from '../../../delegates/DelegateError';
+
+export default class ShareDelegate extends BaseShareDelegate {
+ override onShare(
+ content: ShareContent,
+ options: ShareOptions,
+ ): Promise<ShareAction> {
+ throw new UnsupportedByPlatformError();
+ }
+}
@@ -16,6 +16,7 @@ export type PlatformOSType =
| 'macos'
| 'windows'
| 'web'
+ | 'harmony'
| 'native';
type PlatformConstants = {
isTesting: boolean;
@@ -97,11 +98,33 @@ interface PlatformWebStatic extends PlatformStatic {
Version: string;
}
+export type PlatformHarmonyConstants = PlatformConstants & {
+ deviceType:
+ | 'default'
+ | 'phone'
+ | 'wearable'
+ | 'liteWearable'
+ | 'tablet'
+ | 'tv'
+ | 'car'
+ | 'smartVision';
+ osFullName: string;
+ Model: string;
+};
+
+export interface PlatformHarmonyStatic extends PlatformStatic {
+ OS: 'harmony';
+ constants: PlatformHarmonyConstants;
+ isPad: boolean;
+ isVision: boolean;
+}
+
export type Platform =
| PlatformIOSStatic
| PlatformAndroidStatic
| PlatformWindowsOSStatic
| PlatformMacOSStatic
+ | PlatformHarmonyStatic
| PlatformWebStatic;
export const Platform: Platform;
@@ -15,10 +15,15 @@ import type {
Timespan,
} from './IPerformanceLogger';
+const Platform = require('../Utilities/Platform');
const PRINT_TO_CONSOLE: false = false; // Type as false to prevent accidentally committing `true`;
export const getCurrentTimestamp: () => number =
- global.nativeQPLTimestamp ?? (() => global.performance.now());
+ global.nativeQPLTimestamp ?? (
+ Platform.OS === 'harmony'
+ ? (global.performance ? global.performance.now.bind(global.performance) : Date.now)
+ : (() => global.performance.now())
+ );
class PerformanceLogger implements IPerformanceLogger {
_timespans: {[key: string]: ?Timespan} = {};
@@ -74,7 +74,7 @@ const Vibration = {
pattern?: number | Array<number> = _default_vibration_length,
repeat?: boolean = false,
) {
- if (Platform.OS === 'android') {
+ if (Platform.OS === 'android' || Platform.OS === 'harmony') {
if (typeof pattern
NativeVibration.vibrate(pattern);
} else if (Array.isArray(pattern)) {
@@ -32,6 +32,16 @@
#include <reactperflogger/ReactPerfettoCategories.h>
#endif
+#ifdef WITH_CUSTOM_SYSTRACE // RNC_patch
+#include <extras/TraceSection.h> // RNC_patch
+#endif
+
+// RNC_PATCH: BEGIN
+#ifndef __unused
+#define __unused __attribute__((unused))
+#endif
+// RNC_PATCH: END
+
#if defined(__APPLE__)
// This is required so that OS_LOG_TARGET_HAS_10_15_FEATURES will be set.
#include <os/trace_base.h>
@@ -93,6 +103,8 @@ struct ConcreteTraceSection {
fbsystrace::FbSystraceSection m_section;
};
using TraceSectionUnwrapped = ConcreteTraceSection;
+#elif defined(WITH_CUSTOM_SYSTRACE) // RNC_patch
+ using TraceSectionUnwrapped = CustomTraceSection; // RNC_patch
#else
struct DummyTraceSection {
public:
@@ -22,5 +22,6 @@ target_link_libraries(jserrorhandler
folly_runtime
${mapbufferjni}
react_featureflags
+ react_cxxreact # RNC_patch: JsErrorHandler imports <cxxreact/ErrorUtils.h>
)
target_compile_reactnative_options(jserrorhandler PRIVATE)
@@ -352,6 +352,23 @@ Instrumentation& Runtime::instrumentation() {
return sharedInstance;
}
+std::vector<std::pair<std::string, Value>> Runtime::getObjectProperties(
+ const Object& object) {
+ auto propertyNames = object.getPropertyNames(*this);
+ uint32_t length = propertyNames.size(*this);
+
+ std::vector<std::pair<std::string, Value>> properties;
+ properties.reserve(length);
+
+ for (size_t i = 0; i < length; i++) {
+ auto key =
+ propertyNames.getValueAtIndex(*this, i).asString(*this).utf8(*this);
+ auto value = object.getProperty(*this, key.c_str());
+ properties.emplace_back(key, std::move(value));
+ }
+ return properties;
+}
+
Value Runtime::createValueFromJsonUtf8(const uint8_t* json, size_t length) {
Function parseJson = global()
.getPropertyAsObject(*this, "JSON")
@@ -453,6 +453,9 @@ class JSI_EXPORT Runtime : public ICast {
/// data associated with the uuid, return a null pointer.
std::shared_ptr<void> getRuntimeData(const UUID& uuid);
+ virtual std::vector<std::pair<std::string, Value>> getObjectProperties(
+ const Object& object);
+
protected:
friend class Pointer;
friend class PropNameID;
@@ -6,7 +6,11 @@
cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE on)
-include(${REACT_ANDROID_DIR}/src/main/jni/first-party/jni-lib-merge/SoMerging-utils.cmake)
+# RNC_patch: BEGIN
+if(DEFINED REACT_ANDROID_DIR)
+ include(${REACT_ANDROID_DIR}/src/main/jni/first-party/jni-lib-merge/SoMerging-utils.cmake)
+endif()
+# RNC_patch: END
include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake)
file(GLOB jsinspector_SRC CONFIGURE_DEPENDS *.cpp)
@@ -14,7 +18,11 @@ file(GLOB jsinspector_SRC CONFIGURE_DEPENDS *.cpp)
# Placing it in a shared library makes the singletons safe to use from arbitrary shared libraries
# (even ones that don't depend on one another).
add_library(jsinspector OBJECT ${jsinspector_SRC})
-target_merge_so(jsinspector)
+# RNC_patch: BEGIN
+if(DEFINED REACT_ANDROID_DIR)
+ target_merge_so(jsinspector)
+endif()
+# RNC_patch: END
target_include_directories(jsinspector PUBLIC ${REACT_COMMON_DIR})
@@ -6,7 +6,11 @@
cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE on)
-include(${REACT_ANDROID_DIR}/src/main/jni/first-party/jni-lib-merge/SoMerging-utils.cmake)
+# RNC_patch: BEGIN
+if(DEFINED REACT_ANDROID_DIR)
+ include(${REACT_ANDROID_DIR}/src/main/jni/first-party/jni-lib-merge/SoMerging-utils.cmake)
+endif()
+# RNC_patch: END
add_compile_options(
-fexceptions
@@ -17,7 +21,11 @@ add_compile_options(
file(GLOB jsinspector_cdp_SRC CONFIGURE_DEPENDS *.cpp)
add_library(jsinspector_cdp OBJECT ${jsinspector_cdp_SRC})
-target_merge_so(jsinspector_cdp)
+# RNC_patch: BEGIN
+if(DEFINED REACT_ANDROID_DIR)
+ target_merge_so(jsinspector_cdp)
+endif()
+# RNC_patch: END
target_include_directories(jsinspector_cdp PUBLIC ${REACT_COMMON_DIR})
@@ -6,7 +6,11 @@
cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE on)
-include(${REACT_ANDROID_DIR}/src/main/jni/first-party/jni-lib-merge/SoMerging-utils.cmake)
+# RNC_patch: BEGIN
+if(DEFINED REACT_ANDROID_DIR)
+ include(${REACT_ANDROID_DIR}/src/main/jni/first-party/jni-lib-merge/SoMerging-utils.cmake)
+endif()
+# RNC_patch: END
add_compile_options(
-fexceptions
@@ -17,7 +21,11 @@ add_compile_options(
file(GLOB jsinspector_network_SRC CONFIGURE_DEPENDS *.cpp)
add_library(jsinspector_network OBJECT ${jsinspector_network_SRC})
-target_merge_so(jsinspector_network)
+# RNC_patch: BEGIN
+if(DEFINED REACT_ANDROID_DIR)
+ target_merge_so(jsinspector_network)
+endif()
+# RNC_patch: END
target_include_directories(jsinspector_network PUBLIC ${REACT_COMMON_DIR})
@@ -6,13 +6,21 @@
cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE on)
-include(${REACT_ANDROID_DIR}/src/main/jni/first-party/jni-lib-merge/SoMerging-utils.cmake)
+# RNC_patch: BEGIN
+if(DEFINED REACT_ANDROID_DIR)
+ include(${REACT_ANDROID_DIR}/src/main/jni/first-party/jni-lib-merge/SoMerging-utils.cmake)
+endif()
+# RNC_patch: END
include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake)
file(GLOB jsinspector_tracing_SRC CONFIGURE_DEPENDS *.cpp)
add_library(jsinspector_tracing OBJECT ${jsinspector_tracing_SRC})
-target_merge_so(jsinspector_tracing)
+# RNC_patch: BEGIN
+if(DEFINED REACT_ANDROID_DIR)
+ target_merge_so(jsinspector_tracing)
+endif()
+# RNC_patch: END
target_include_directories(jsinspector_tracing PUBLIC ${REACT_COMMON_DIR})
@@ -8,7 +8,7 @@ set(CMAKE_VERBOSE_MAKEFILE on)
include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake)
-react_native_android_selector(platform_SRC platform/android/ReactCommon/*.cpp "")
+react_native_android_selector(platform_SRC platform/android/ReactCommon/*.cpp /cxx/ReactCommon/*.cpp)
file(GLOB react_nativemodule_core_SRC CONFIGURE_DEPENDS
ReactCommon/*.cpp
${platform_SRC})
@@ -16,7 +16,7 @@ add_library(react_nativemodule_core
OBJECT
${react_nativemodule_core_SRC})
-react_native_android_selector(platform_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platform/android/ "")
+react_native_android_selector(platform_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platform/android/ ${CMAKE_CURRENT_SOURCE_DIR}/platform/cxx/)
target_include_directories(react_nativemodule_core
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
@@ -21,6 +21,8 @@ bool ParagraphAttributes::operator==(const ParagraphAttributes& rhs) const {
textBreakStrategy,
adjustsFontSizeToFit,
includeFontPadding,
+ allowFontScaling, // RNC_patch
+ writingDirection, // RNC_patch
android_hyphenationFrequency,
textAlignVertical) ==
std::tie(
@@ -29,11 +31,14 @@ bool ParagraphAttributes::operator==(const ParagraphAttributes& rhs) const {
rhs.textBreakStrategy,
rhs.adjustsFontSizeToFit,
rhs.includeFontPadding,
+ rhs.allowFontScaling, // RNC_patch
+ rhs.writingDirection, // RNC_patch
rhs.android_hyphenationFrequency,
rhs.textAlignVertical) &&
floatEquality(minimumFontSize, rhs.minimumFontSize) &&
floatEquality(maximumFontSize, rhs.maximumFontSize) &&
- floatEquality(minimumFontScale, rhs.minimumFontScale);
+ floatEquality(minimumFontScale, rhs.minimumFontScale) &&
+ floatEquality(maxFontSizeMultiplier, rhs.maxFontSizeMultiplier); // RNC_patch
}
bool ParagraphAttributes::operator!=(const ParagraphAttributes& rhs) const {
@@ -82,6 +82,12 @@ class ParagraphAttributes : public DebugStringConvertible {
*/
std::optional<TextAlignmentVertical> textAlignVertical{};
+ // RNC_patch: BEGIN
+ WritingDirection writingDirection{};
+ bool allowFontScaling{true};
+ Float maxFontSizeMultiplier{std::numeric_limits<Float>::quiet_NaN()};
+ // RNC_patch: END
+
bool operator==(const ParagraphAttributes &rhs) const;
bool operator!=(const ParagraphAttributes &rhs) const;
@@ -939,6 +939,26 @@ inline ParagraphAttributes convertRawProp(
"maximumFontSize",
sourceParagraphAttributes.maximumFontSize,
defaultParagraphAttributes.maximumFontSize);
+ // RNC_PATCH: BEGIN
+ paragraphAttributes.writingDirection = convertRawProp(
+ context,
+ rawProps,
+ "writingDirection",
+ sourceParagraphAttributes.writingDirection,
+ defaultParagraphAttributes.writingDirection);
+ paragraphAttributes.allowFontScaling = convertRawProp(
+ context,
+ rawProps,
+ "allowFontScaling",
+ sourceParagraphAttributes.allowFontScaling,
+ defaultParagraphAttributes.allowFontScaling);
+ paragraphAttributes.maxFontSizeMultiplier = convertRawProp(
+ context,
+ rawProps,
+ "maxFontSizeMultiplier",
+ sourceParagraphAttributes.maxFontSizeMultiplier,
+ defaultParagraphAttributes.maxFontSizeMultiplier);
+ // RNC_PATCH: END
paragraphAttributes.includeFontPadding = convertRawProp(
context,
rawProps,
@@ -7,6 +7,7 @@
#include "componentNameByReactViewName.h"
#include <algorithm>
+#include <glog/logging.h>
namespace facebook::react {
@@ -19,7 +20,38 @@ std::string componentNameByReactViewName(std::string viewName) {
if (std::mismatch(rctPrefix.begin(), rctPrefix.end(), viewName.begin())
.first == rctPrefix.end()) {
// If `viewName` has "RCT" prefix, remove it.
- viewName.erase(0, rctPrefix.length());
+ // RNC: patch
+ if (viewName == "RCTView") {
+ viewName = "View";
+ } else if (viewName == "RCTRawText") {
+ viewName = "RawText";
+ } else if (viewName == "RCTText") {
+ viewName = "Text";
+ } else if (viewName == "RCTScrollContentView") {
+ viewName = "ScrollContentView";
+ } else if (viewName == "RCTActivityIndicatorView") {
+ viewName = "ActivityIndicatorView";
+ } else if (viewName == "RCTScrollView") {
+ viewName = "ScrollView";
+ } else if (viewName == "RCTVirtualText") {
+ viewName = "VirtualText";
+ } else if (viewName == "RCTImageView") {
+ viewName = "ImageView";
+ } else if (viewName == "RCTSinglelineTextInputView") {
+ viewName = "SinglelineTextInputView";
+ } else if (viewName == "RCTMultilineTextInputView") {
+ viewName = "MultilineTextInputView";
+ } else if (viewName == "RCTSafeAreaView") {
+ viewName = "SafeAreaView";
+ } else if (viewName == "RCTModalHostView") {
+ viewName = "ModalHostView";
+ } else if (viewName == "RCTRefreshControl") {
+ viewName = "RefreshControl";
+ } else if (viewName == "RCTSwitch") {
+ viewName = "Switch";
+ } else {
+ DLOG(INFO) << "Skipped removing RCT prefix for component: " << viewName;
+ }
}
// Fabric uses slightly new names for Text components because of differences
@@ -19,6 +19,13 @@ jsi::Value ScrollEvent::asJSIValue(jsi::Runtime& runtime) const {
payload.setProperty(runtime, "contentOffset", contentOffsetObj);
}
+ {// RNC_patch start
+ auto velocityObj = facebook::jsi::Object(runtime);
+ velocityObj.setProperty(runtime, "x", velocity.x);
+ velocityObj.setProperty(runtime, "y", velocity.y);
+ payload.setProperty(runtime, "velocity", velocityObj);
+ }// RNC_patch end
+
{
auto contentInsetObj = jsi::Object(runtime);
contentInsetObj.setProperty(runtime, "top", contentInset.top);
@@ -44,6 +51,7 @@ jsi::Value ScrollEvent::asJSIValue(jsi::Runtime& runtime) const {
payload.setProperty(runtime, "zoomScale", zoomScale);
payload.setProperty(runtime, "timestamp", timestamp * 1000);
+ payload.setProperty(runtime, "responderIgnoreScroll", responderIgnoreScroll); // RNC_patch
return payload;
}
@@ -56,6 +64,9 @@ folly::dynamic ScrollEvent::asDynamic() const {
"left", contentInset.left)("bottom", contentInset.bottom)(
"right", contentInset.right);
+ auto velocityObj =
+ folly::dynamic::object("x", velocity.x)("height", velocity.y); // RNC_patch
+
auto contentSizeObj = folly::dynamic::object("width", contentSize.width)(
"height", contentSize.height);
@@ -66,6 +77,7 @@ folly::dynamic ScrollEvent::asDynamic() const {
folly::dynamic::object("contentOffset", std::move(contentOffsetObj))(
"contentInset", std::move(contentInsetObj))(
"contentSize", std::move(contentSizeObj))(
+ "velocity", std::move(velocityObj))( // RNC_patch
"layoutMeasurement", std::move(containerSizeObj))(
"zoomScale", zoomScale)("timestamp", timestamp * 1000);
@@ -15,12 +15,19 @@
namespace facebook::react {
+struct Velocity { // RNC_patch start
+ Float x{0};
+ Float y{0};
+};
+ // RNC_patch end
struct ScrollEvent : public EventPayload {
Size contentSize;
Point contentOffset;
EdgeInsets contentInset;
Size containerSize;
Float zoomScale{};
+ Velocity velocity; // RNC_patch
+ bool responderIgnoreScroll; // RNC_patch
/*
* The time in seconds when the touch occurred or when it was last mutated.
@@ -18,6 +18,7 @@
#include <react/renderer/telemetry/TransactionTelemetry.h>
#include <react/renderer/textlayoutmanager/TextLayoutContext.h>
#include <react/utils/FloatComparison.h>
+#include "RNOH/modalshrink/GuideLayout.h"
#include <react/renderer/components/text/ParagraphState.h>
@@ -26,11 +27,7 @@
(size).width + kDefaultEpsilon >= \
(layoutConstraints).minimumSize.width && \
(size).width - kDefaultEpsilon <= \
- (layoutConstraints).maximumSize.width && \
- (size).height + kDefaultEpsilon >= \
- (layoutConstraints).minimumSize.height && \
- (size).height - kDefaultEpsilon <= \
- (layoutConstraints).maximumSize.height)
+ (layoutConstraints).maximumSize.width)
namespace facebook::react {
using Content = ParagraphShadowNode::Content;
@@ -69,6 +66,14 @@ bool ParagraphShadowNode::shouldNewRevisionDirtyMeasurement(
const Content& ParagraphShadowNode::getContent(
const LayoutContext& layoutContext) const {
+ // Check if content cache refresh is needed (for recalculating font size
+ // during Modal scaling)
+ auto& guideLayout = GuideLayout::getInstance();
+ if (guideLayout.needsContentRefresh(getTag())) {
+ content_.reset();
+ guideLayout.markContentRefreshed(getTag());
+ }
+
if (content_.has_value()) {
return content_.value();
}
@@ -78,6 +83,27 @@ const Content& ParagraphShadowNode::getContent(
auto textAttributes = TextAttributes::defaultTextAttributes();
textAttributes.fontSizeMultiplier = layoutContext.fontSizeMultiplier;
textAttributes.apply(getConcreteProps().textAttributes);
+
+ // Modal content font scaling handling
+ if (guideLayout.isModalContentShrinkEnabled() &&
+ guideLayout.isInModalSubtree(getTag())) {
+ float scaleFactor = guideLayout.getScaleFactor();
+ // Scale font size
+ if (!std::isnan(textAttributes.fontSize) && textAttributes.fontSize > 0) {
+ textAttributes.fontSize *= scaleFactor;
+ }
+ // Scale line height
+ if (!std::isnan(textAttributes.lineHeight) &&
+ textAttributes.lineHeight > 0) {
+ textAttributes.lineHeight *= scaleFactor;
+ }
+ // Scale letter spacing
+ if (!std::isnan(textAttributes.letterSpacing) &&
+ textAttributes.letterSpacing > 0) {
+ textAttributes.letterSpacing *= scaleFactor;
+ }
+ }
+
textAttributes.layoutDirection =
YGNodeLayoutGetDirection(&yogaNode_) == YGDirectionRTL
? LayoutDirection::RightToLeft
@@ -171,7 +197,8 @@ void ParagraphShadowNode::updateStateIfNeeded(const Content& content) {
auto& state = getStateData();
react_native_assert(textLayoutManager_);
- if (state.attributedString == content.attributedString) {
+ if (state.attributedString == content.attributedString &&
+ state.paragraphAttributes == content.paragraphAttributes) {
return;
}
@@ -46,7 +46,7 @@ class ParagraphShadowNode final
traits.set(ShadowNodeTraits::Trait::MeasurableYogaNode);
traits.set(ShadowNodeTraits::Trait::BaselineYogaNode);
-#ifdef ANDROID
+#ifdef UNSET_FORMS_STACKING_CONTEXT // RNC_patch
// Unsetting `FormsStackingContext` trait is essential on Android where we
// can't mount views inside `TextView`.
// T221699219: This should be removed when PreparedLayoutTextView is rolled
@@ -52,6 +52,13 @@ class TextInputState final {
*/
ParagraphAttributes paragraphAttributes;
+ /*
+ * `TextLayoutManager` provides a connection to platform-specific
+ * text rendering infrastructure which is capable to render the
+ * `AttributedString`.
+ */
+ std::shared_ptr<TextLayoutManager const> layoutManager;
+
int64_t mostRecentEventCount{0};
};
@@ -16,13 +16,14 @@ react_native_android_selector(platform_SRC
file(GLOB rrc_view_SRC CONFIGURE_DEPENDS
*.cpp
${platform_SRC})
-
+# Add GuideLayout source file from RNOH/modalshrink
+list(APPEND rrc_view_SRC "${RNOH_CPP_DIR}/RNOH/modalshrink/GuideLayout.cpp")
add_library(rrc_view OBJECT ${rrc_view_SRC})
react_native_android_selector(platform_DIR
${CMAKE_CURRENT_SOURCE_DIR}/platform/android/
${CMAKE_CURRENT_SOURCE_DIR}/platform/cxx/)
-target_include_directories(rrc_view PUBLIC ${REACT_COMMON_DIR} ${platform_DIR})
+target_include_directories(rrc_view PUBLIC ${REACT_COMMON_DIR} ${platform_DIR} ${RNOH_CPP_DIR})
target_link_libraries(rrc_view
folly_runtime
@@ -44,11 +44,13 @@ static inline void interpolateViewProps(
// that use RawProps/folly::dynamic instead of concrete props on the
// mounting layer. Once we can remove this, we should change `rawProps` to
// be const again.
-#ifdef ANDROID
+#ifdef RAW_PROPS_ENABLED
if (!interpolatedProps->rawProps.isNull()) {
interpolatedProps->rawProps["opacity"] = interpolatedProps->opacity;
+#ifdef ANDROID
interpolatedProps->rawProps["transform"] = (folly::dynamic)interpolatedProps->transform;
+#endif
}
#endif
}
@@ -24,6 +24,7 @@
#include <algorithm>
#include <limits>
#include <memory>
+#include "RNOH/modalshrink/GuideLayout.h"
namespace facebook::react {
@@ -661,6 +662,39 @@ void YogaLayoutableShadowNode::layoutTree(
YGNodeCalculateLayout(&yogaNode_, ownerWidth, ownerHeight, direction);
}
+ // Modal content scaling handling (Two-phase commit approach)
+ if (GuideLayout::getInstance().isModalContentShrinkEnabled()) {
+ // First check if Modal node exists
+ YGNodeRef modalNode = GuideLayout::getInstance().findModalNode(&yogaNode_);
+
+ if (modalNode) {
+ bool wasScaled =
+ GuideLayout::getInstance().isModalAlreadyScaled(modalNode);
+ bool styleDirty = !wasScaled &&
+ GuideLayout::getInstance().isModalSubtreeStyleDirty(modalNode);
+
+ if (wasScaled || styleDirty) {
+ GuideLayout::getInstance().restoreModalSubtreeStyles(modalNode);
+ if (wasScaled) {
+ GuideLayout::getInstance().clearModalScalingState(modalNode);
+ }
+ GuideLayout::getInstance().resetYogaTreeState(&yogaNode_);
+ YGNodeCalculateLayout(&yogaNode_, ownerWidth, ownerHeight, direction);
+ }
+
+ bool needsRescale = GuideLayout::getInstance().checkModalNeedsScaling(&yogaNode_, ownerHeight);
+
+ if (needsRescale) {
+ GuideLayout::getInstance().resetModalSubtreeTags();
+ GuideLayout::getInstance().resetYogaTreeState(&yogaNode_);
+ GuideLayout::getInstance().scanAndScaleModalSubtrees(&yogaNode_);
+ YGNodeCalculateLayout(&yogaNode_, ownerWidth, ownerHeight, direction);
+ }
+ } else {
+ GuideLayout::getInstance().clearAllModalState();
+ }
+ }
+
// Update layout metrics for root node. Updated for children in
// YogaLayoutableShadowNode::layout
if (yogaNode_.getHasNewLayout()) {
new file mode 100644
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+
+#include "HostPlatformViewProps.h"
+
+#include <react/featureflags/ReactNativeFeatureFlags.h>
+#include <react/renderer/components/view/conversions.h>
+#include <react/renderer/components/view/propsConversions.h>
+#include <react/renderer/core/graphicsConversions.h>
+#include <react/renderer/core/propsConversions.h>
+
+namespace facebook::react {
+
+HostPlatformViewProps::HostPlatformViewProps(
+ const PropsParserContext& context,
+ const HostPlatformViewProps& sourceProps,
+ const RawProps& rawProps)
+ : BaseViewProps(context, sourceProps, rawProps),
+ needsOffscreenAlphaCompositing(
+ ReactNativeFeatureFlags::enableCppPropsIteratorSetter()
+ ? sourceProps.needsOffscreenAlphaCompositing
+ : convertRawProp(
+ context,
+ rawProps,
+ "needsOffscreenAlphaCompositing",
+ sourceProps.needsOffscreenAlphaCompositing,
+ {})) {}
+
+} // namespace facebook::react
@@ -8,7 +8,24 @@
#pragma once
#include <react/renderer/components/view/BaseViewProps.h>
+#include <react/renderer/components/view/primitives.h>
+#include <react/renderer/core/LayoutMetrics.h>
+#include <react/renderer/core/Props.h>
+#include <react/renderer/core/PropsParserContext.h>
+#include <react/renderer/graphics/Transform.h>
namespace facebook::react {
-using HostPlatformViewProps = BaseViewProps;
+
+class HostPlatformViewProps : public BaseViewProps {
+ public:
+ HostPlatformViewProps() = default;
+ HostPlatformViewProps(
+ const PropsParserContext& context,
+ const HostPlatformViewProps& sourceProps,
+ const RawProps& rawProps);
+
+#pragma mark - Props
+ bool needsOffscreenAlphaCompositing{false};
+};
+
} // namespace facebook::react
@@ -37,6 +37,10 @@ void Props::initialize(
initializeDynamicProps(sourceProps, rawProps, filterObjectKeys);
}
#endif
+// RNC_PATCH
+#ifdef RAW_PROPS_ENABLED
+ this->rawProps = folly::dynamic::merge(sourceProps.rawProps, rawProps.toDynamic());
+#endif
}
void Props::setProp(
@@ -51,7 +55,7 @@ void Props::setProp(
}
}
-#ifdef RN_SERIALIZABLE_STATE
+#ifdef RAW_PROPS_ENABLED
void Props::initializeDynamicProps(
const Props& sourceProps,
const RawProps& rawProps,
@@ -59,7 +59,8 @@ class Props : public virtual Sealable, public virtual DebugStringConvertible {
std::string nativeId;
-#ifdef RN_SERIALIZABLE_STATE
+// RNC_PATCH
+#ifdef RAW_PROPS_ENABLED
folly::dynamic rawProps = folly::dynamic::object();
void initializeDynamicProps(
@@ -15,6 +15,10 @@ namespace facebook::react {
* Exact type of float numbers which ideally should match a type behing
* platform- and chip-architecture-specific float type.
*/
+#ifdef REACT_NATIVE__RENDER__GRAPHICS__FLOAT
+using Float = REACT_NATIVE__RENDER__GRAPHICS__FLOAT;
+#else
using Float = float;
+#endif
} // namespace facebook::react
@@ -24,7 +24,7 @@ class PlatformTimerRegistry {
virtual void createRecurringTimer(uint32_t timerID, double delayMS) = 0;
- virtual ~PlatformTimerRegistry() noexcept = default;
+ virtual ~PlatformTimerRegistry() noexcept(false) = default;
virtual void quit() {}
};
new file mode 100644
@@ -0,0 +1,10 @@
+/**
+ * @format
+ */
+export abstract class DelegateError extends Error {}
+
+export class UnsupportedByPlatformError extends DelegateError {
+ constructor() {
+ super('Unsupported by the platform');
+ }
+}
@@ -34,6 +34,7 @@ export default function setUpDefaltReactNativeEnvironment(
require('../../../Libraries/Core/setUpNavigator');
require('../../../Libraries/Core/setUpBatchedBridge');
require('../../../Libraries/Core/setUpSegmentFetcher');
+ require('../../../Libraries/Core/setUpPlatform');
if (__DEV__ && enableDeveloperTools) {
require('../../../Libraries/Core/checkNativeVersion');
require('../../../Libraries/Core/setUpDeveloperTools');
@@ -6,13 +6,22 @@
*/
#include "HermesExecutorFactory.h"
+#include "RNOH/ApiVersionCheck.h"
+#include "RNOH/JSEngine/hermes/HermesMemoryWaterMark.h"
+#include <cinttypes>
+#include <cstdint>
#include <cxxreact/MessageQueueThread.h>
#include <cxxreact/TraceSection.h>
+#include <dlfcn.h>
+#include <hermes/Public/GCConfig.h>
+#include <hermes/Public/RuntimeConfig.h>
#include <hermes/hermes.h>
+#include <hilog/log.h>
#include <jsi/decorator.h>
#include <jsinspector-modern/InspectorFlags.h>
-
+#include <mutex>
+#include <string>
#include <thread>
#include <hermes/inspector-modern/chrome/HermesRuntimeTargetDelegate.h>
@@ -25,10 +34,107 @@
using namespace facebook::hermes;
using namespace facebook::jsi;
+#undef LOG_DOMAIN
+#undef LOG_TAG
+#define LOG_DOMAIN 0x0000
+#define LOG_TAG "HermesGC"
+
namespace facebook::react {
namespace {
+using ReportFrameworkMemAnomalyFn = int (*)(
+ int32_t frameworkType,
+ const char* frameworkVersion,
+ const char* description);
+
+constexpr int32_t FRAMEWORK_TYPE_REACT_NATIVE_HERMES = 1;
+
+struct HiAppEventReportApi {
+ ReportFrameworkMemAnomalyFn reportFrameworkMemAnomaly = nullptr;
+};
+
+HiAppEventReportApi& getHiAppEventReportApi() {
+ static HiAppEventReportApi api;
+ static std::once_flag onceFlag;
+
+ std::call_once(onceFlag, []() {
+ api.reportFrameworkMemAnomaly =
+ reinterpret_cast<ReportFrameworkMemAnomalyFn>(dlsym(
+ RTLD_DEFAULT, "OH_HiAppEvent_ReportFrameworkMemAnomaly"));
+ if (api.reportFrameworkMemAnomaly == nullptr) {
+ OH_LOG_WARN(
+ LOG_APP,
+ "The Hermes memory exception report is skipped because "
+ "framework memory anomaly reporting is unavailable.");
+ }
+ });
+
+ return api;
+}
+
+void reportFrameworkMemAnomaly(int64_t currentHeapBytes, int64_t waterMarkBytes) {
+ if (!rnoh::IsAtLeastApi26()) {
+ OH_LOG_WARN(
+ LOG_APP,
+ "The Hermes memory exception report is skipped because the current "
+ "API level is lower than 26, apiLevel=%{public}d",
+ OH_GetSdkApiVersion());
+ return;
+ }
+
+ auto& hiAppEventReportApi = getHiAppEventReportApi();
+ if (hiAppEventReportApi.reportFrameworkMemAnomaly == nullptr) {
+ return;
+ }
+
+ const auto description =
+ rnoh::buildHermesWaterMarkDescription(currentHeapBytes, waterMarkBytes);
+ const auto& frameworkVersion = rnoh::getHermesFrameworkVersion();
+
+ int ret = hiAppEventReportApi.reportFrameworkMemAnomaly(
+ FRAMEWORK_TYPE_REACT_NATIVE_HERMES,
+ frameworkVersion.c_str(),
+ description.c_str());
+ if (ret == 0) {
+ OH_LOG_INFO(
+ LOG_APP, "The Hermes memory exception is successfully reported.");
+ } else {
+ OH_LOG_ERROR(
+ LOG_APP,
+ "The Hermes memory exception report failed, ret=%{public}d",
+ ret);
+ }
+}
+
+void onHermesGCAnalytics(const ::hermes::vm::GCAnalyticsEvent& event) {
+ const auto heapSizeBytes = static_cast<int64_t>(event.size.after);
+ const auto hermesMemoryWaterMarkBytes = rnoh::getHermesMemoryWaterMarkBytes();
+ const auto currentHeapMB = rnoh::formatBytesToMBString(heapSizeBytes);
+ const auto waterMarkMB =
+ rnoh::formatBytesToMBString(hermesMemoryWaterMarkBytes);
+
+ if (heapSizeBytes <= hermesMemoryWaterMarkBytes) {
+ return;
+ }
+
+ if (!rnoh::tryMarkHermesWaterMarkExceededForProcessLifetime()) {
+ return;
+ }
+
+ OH_LOG_WARN(
+ LOG_APP,
+ "The Hermes Memory watermark has been exceeded. current=%{public}" PRId64
+ " bytes (%{public}s MB), watermark=%{public}" PRId64
+ " bytes (%{public}s MB)",
+ heapSizeBytes,
+ currentHeapMB.c_str(),
+ hermesMemoryWaterMarkBytes,
+ waterMarkMB.c_str());
+
+ reportFrameworkMemAnomaly(heapSizeBytes, hermesMemoryWaterMarkBytes);
+}
+
#if defined(HERMES_ENABLE_DEBUGGER) && !defined(HERMES_V1_ENABLED)
class HermesExecutorRuntimeAdapter
@@ -244,7 +350,14 @@ std::unique_ptr<JSExecutor> HermesExecutorFactory::createJSExecutor(
}
::hermes::vm::RuntimeConfig HermesExecutorFactory::defaultRuntimeConfig() {
+ auto gcConfig = ::hermes::vm::GCConfig::Builder()
+ .withName("RNOH")
+ .withAnalyticsCallback(onHermesGCAnalytics)
+ .withShouldRecordStats(true)
+ .build();
+
return ::hermes::vm::RuntimeConfig::Builder()
+ .withGCConfig(gcConfig)
.withEnableSampleProfiling(true)
.build();
}
@@ -6,11 +6,19 @@
*/
#include "HermesInstance.h"
+#include "RNOH/ApiVersionCheck.h"
+#include "RNOH/JSEngine/hermes/HermesMemoryWaterMark.h"
+#include <cinttypes>
+#include <cstdint>
+#include <dlfcn.h>
#include <hermes/inspector-modern/chrome/HermesRuntimeTargetDelegate.h>
+#include <hilog/log.h>
#include <jsi/jsilib.h>
#include <jsinspector-modern/InspectorFlags.h>
+#include <mutex>
#include <react/featureflags/ReactNativeFeatureFlags.h>
+#include <string>
#ifdef HERMES_ENABLE_DEBUGGER
#include <hermes/inspector-modern/chrome/Registration.h>
@@ -25,8 +33,109 @@
using namespace facebook::hermes;
using namespace facebook::jsi;
+#undef LOG_DOMAIN
+#undef LOG_TAG
+#define LOG_DOMAIN 0x0000
+#define LOG_TAG "HermesGC"
+
namespace facebook::react {
+namespace {
+
+using ReportFrameworkMemAnomalyFn = int (*)(
+ int32_t frameworkType,
+ const char* frameworkVersion,
+ const char* description);
+
+constexpr int32_t FRAMEWORK_TYPE_REACT_NATIVE_HERMES = 1;
+
+struct HiAppEventReportApi {
+ ReportFrameworkMemAnomalyFn reportFrameworkMemAnomaly = nullptr;
+};
+
+HiAppEventReportApi& getHiAppEventReportApi() {
+ static HiAppEventReportApi api;
+ static std::once_flag onceFlag;
+
+ std::call_once(onceFlag, []() {
+ api.reportFrameworkMemAnomaly =
+ reinterpret_cast<ReportFrameworkMemAnomalyFn>(dlsym(
+ RTLD_DEFAULT, "OH_HiAppEvent_ReportFrameworkMemAnomaly"));
+ if (api.reportFrameworkMemAnomaly == nullptr) {
+ OH_LOG_WARN(
+ LOG_APP,
+ "The Hermes memory exception report is skipped because "
+ "framework memory anomaly reporting is unavailable.");
+ }
+ });
+
+ return api;
+}
+
+void reportFrameworkMemAnomaly(int64_t currentHeapBytes, int64_t waterMarkBytes) {
+ if (!rnoh::IsAtLeastApi26()) {
+ OH_LOG_WARN(
+ LOG_APP,
+ "The Hermes memory exception report is skipped because the current "
+ "API level is lower than 26, apiLevel=%{public}d",
+ OH_GetSdkApiVersion());
+ return;
+ }
+
+ auto& hiAppEventReportApi = getHiAppEventReportApi();
+ if (hiAppEventReportApi.reportFrameworkMemAnomaly == nullptr) {
+ return;
+ }
+
+ const auto description =
+ rnoh::buildHermesWaterMarkDescription(currentHeapBytes, waterMarkBytes);
+ const auto& frameworkVersion = rnoh::getHermesFrameworkVersion();
+
+ int ret = hiAppEventReportApi.reportFrameworkMemAnomaly(
+ FRAMEWORK_TYPE_REACT_NATIVE_HERMES,
+ frameworkVersion.c_str(),
+ description.c_str());
+ if (ret == 0) {
+ OH_LOG_INFO(
+ LOG_APP, "The Hermes memory exception is successfully reported.");
+ } else {
+ OH_LOG_ERROR(
+ LOG_APP,
+ "The Hermes memory exception report failed, ret=%{public}d",
+ ret);
+ }
+}
+
+void onHermesGCAnalytics(const ::hermes::vm::GCAnalyticsEvent& event) {
+ const auto heapSizeBytes = static_cast<int64_t>(event.size.after);
+ const auto hermesMemoryWaterMarkBytes = rnoh::getHermesMemoryWaterMarkBytes();
+ const auto currentHeapMB = rnoh::formatBytesToMBString(heapSizeBytes);
+ const auto waterMarkMB =
+ rnoh::formatBytesToMBString(hermesMemoryWaterMarkBytes);
+
+ if (heapSizeBytes <= hermesMemoryWaterMarkBytes) {
+ return;
+ }
+
+ if (!rnoh::tryMarkHermesWaterMarkExceededForProcessLifetime()) {
+ return;
+ }
+
+ OH_LOG_WARN(
+ LOG_APP,
+ "The Hermes Memory watermark has been exceeded. current=%{public}" PRId64
+ " bytes (%{public}s MB), watermark=%{public}" PRId64
+ " bytes (%{public}s MB)",
+ heapSizeBytes,
+ currentHeapMB.c_str(),
+ hermesMemoryWaterMarkBytes,
+ waterMarkMB.c_str());
+
+ reportFrameworkMemAnomaly(heapSizeBytes, hermesMemoryWaterMarkBytes);
+}
+
+} // namespace
+
#if defined(HERMES_ENABLE_DEBUGGER) && !defined(HERMES_V1_ENABLED)
// Wrapper that strongly retains the HermesRuntime for on device debugging.
@@ -132,7 +241,9 @@ std::unique_ptr<JSRuntime> HermesInstance::createJSRuntime(
auto gcConfig = ::hermes::vm::GCConfig::Builder()
// Default to 3GB
.withMaxHeapSize(3072 << 20)
- .withName("RNBridgeless");
+ .withName("RNBridgeless")
+ .withAnalyticsCallback(onHermesGCAnalytics)
+ .withShouldRecordStats(true);
if (allocInOldGenBeforeTTI) {
// For the next two arguments: avoid GC before TTI