From 45fd7feb9f083e5c8afc916732aed9795d344e09 Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Thu, 3 Jul 2025 02:52:01 -0700 Subject: [PATCH] Convert UIManagerModuleConstantsHelper to Kotlin (#52358) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/52358 Another class going from Java to Kotlin. This is quite involved due to the amount of Raw generics we were using so I'd appreaciate a couple of further eyes here. Changelog: [Android] [Changed] - Convert UIManagerModuleConstantsHelper to Kotlin Reviewed By: mdvacca, javache Differential Revision: D77589975 fbshipit-source-id: 477c1e2a8dfd31db60047fd1252f6d47c177f5c7 --- .../ReactAndroid/api/ReactAndroid.api | 7 - .../facebook/react/runtime/ReactInstance.kt | 12 +- .../react/uimanager/UIManagerModule.java | 6 +- .../UIManagerModuleConstantsHelper.java | 244 ------------------ .../UIManagerModuleConstantsHelper.kt | 238 +++++++++++++++++ .../UIManagerModuleConstantsHelperTest.kt | 17 +- 6 files changed, 252 insertions(+), 272 deletions(-) delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 6b3b6ce18de..9aab9261e2c 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -4428,13 +4428,6 @@ public abstract interface class com/facebook/react/uimanager/UIManagerModule$Cus public abstract fun resolveCustomEventName (Ljava/lang/String;)Ljava/lang/String; } -public class com/facebook/react/uimanager/UIManagerModuleConstantsHelper { - public fun ()V - public static fun createConstants (Ljava/util/List;Ljava/util/Map;Ljava/util/Map;)Ljava/util/Map; - public static fun createConstantsForViewManager (Lcom/facebook/react/uimanager/ViewManager;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Ljava/util/Map; - public static fun getDefaultExportableEventTypes ()Ljava/util/Map; -} - public abstract interface class com/facebook/react/uimanager/UIManagerModuleListener { public abstract fun willDispatchViewUpdates (Lcom/facebook/react/uimanager/UIManagerModule;)V } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactInstance.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactInstance.kt index f0ebca7f406..ff0cdb1d5ac 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactInstance.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactInstance.kt @@ -203,7 +203,7 @@ internal class ReactInstance( // initialized. // This happens inside getTurboModuleManagerDelegate getter. if (ReactNativeFeatureFlags.useNativeViewConfigsInBridgelessMode()) { - val customDirectEvents: Map = HashMap() + val customDirectEvents: MutableMap = HashMap() UIConstantsProviderBinding.install( // Use unbuffered RuntimeExecutor to install binding @@ -214,9 +214,7 @@ internal class ReactInstance( // 2. genericBubblingEventTypes. // 3. genericDirectEventTypes. // We want to match this beahavior. - { - Arguments.makeNativeMap(UIManagerModuleConstantsHelper.getDefaultExportableEventTypes()) - }, + { Arguments.makeNativeMap(UIManagerModuleConstantsHelper.defaultExportableEventTypes) }, ConstantsForViewManagerProvider { viewManagerName: String -> val viewManager = viewManagerResolver.getViewManager(viewManagerName) @@ -573,8 +571,8 @@ internal class ReactInstance( } private fun createConstants( - viewManagers: List>, - customDirectEvents: Map? + viewManagers: List>, + customDirectEvents: MutableMap? ): MutableMap { ReactMarker.logMarker(ReactMarkerConstants.CREATE_UI_MANAGER_MODULE_CONSTANTS_START) SystraceMessage.beginSection(Systrace.TRACE_TAG_REACT, "CreateUIManagerConstants") @@ -591,7 +589,7 @@ internal class ReactInstance( private fun getConstantsForViewManager( viewManager: ViewManager<*, *>, - customDirectEvents: Map + customDirectEvents: MutableMap ): NativeMap { SystraceMessage.beginSection( Systrace.TRACE_TAG_REACT, "ReactInstance.getConstantsForViewManager") diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java index 8d68fe7e9ec..671abca4ba1 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java @@ -248,7 +248,7 @@ public class UIManagerModule extends ReactContextBaseJavaModule .arg("Lazy", true) .flush(); try { - return UIManagerModuleConstantsHelper.createConstants(viewManagerResolver); + return UIManagerModuleConstantsHelper.internal_createConstants(viewManagerResolver); } finally { Systrace.endSection(Systrace.TRACE_TAG_REACT); ReactMarker.logMarker(CREATE_UI_MANAGER_MODULE_CONSTANTS_END); @@ -264,7 +264,7 @@ public class UIManagerModule extends ReactContextBaseJavaModule .arg("Lazy", false) .flush(); try { - return UIManagerModuleConstantsHelper.createConstants( + return UIManagerModuleConstantsHelper.internal_createConstants( viewManagers, customBubblingEvents, customDirectEvents); } finally { Systrace.endSection(Systrace.TRACE_TAG_REACT); @@ -291,7 +291,7 @@ public class UIManagerModule extends ReactContextBaseJavaModule .flush(); try { Map viewManagerConstants = - UIManagerModuleConstantsHelper.createConstantsForViewManager( + UIManagerModuleConstantsHelper.internal_createConstantsForViewManager( viewManager, null, null, null, customDirectEvents); if (viewManagerConstants != null) { return Arguments.makeNativeMap(viewManagerConstants); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java deleted file mode 100644 index 25616587264..00000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java +++ /dev/null @@ -1,244 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.uimanager; - -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import com.facebook.common.logging.FLog; -import com.facebook.infer.annotation.Nullsafe; -import com.facebook.react.common.MapBuilder; -import com.facebook.react.common.build.ReactBuildConfig; -import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlags; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Helps generate constants map for {@link UIManagerModule} by collecting and merging constants from - * registered view managers. - */ -@Nullsafe(Nullsafe.Mode.LOCAL) -public class UIManagerModuleConstantsHelper { - private static final String TAG = "UIManagerModuleConstantsHelper"; - private static final String BUBBLING_EVENTS_KEY = "bubblingEventTypes"; - private static final String DIRECT_EVENTS_KEY = "directEventTypes"; - - /** - * Generates a lazy discovery enabled version of {@link UIManagerModule} constants. It only - * contains a list of view manager names, so that JS side is aware of the managers there are. - * Actual ViewManager instantiation happens when {@code - * UIManager.getViewManagerConfig('SpecificViewManager')} call happens. The View Manager is then - * registered on the JS side with the help of {@code UIManagerModule.getConstantsForViewManager}. - */ - /* package */ static Map createConstants(ViewManagerResolver resolver) { - Map constants = UIManagerModuleConstants.constants; - constants.put("ViewManagerNames", new ArrayList<>(resolver.getViewManagerNames())); - constants.put("LazyViewManagersEnabled", true); - return constants; - } - - public static Map getDefaultExportableEventTypes() { - return MapBuilder.of( - BUBBLING_EVENTS_KEY, UIManagerModuleConstants.bubblingEventTypeConstants, - DIRECT_EVENTS_KEY, UIManagerModuleConstants.directEventTypeConstants); - } - - private static void validateDirectEventNames( - String viewManagerName, @Nullable Map directEvents) { - if (!ReactBuildConfig.DEBUG || directEvents == null) { - return; - } - - for (String key : directEvents.keySet()) { - Object value = directEvents.get(key); - if (value != null && (value instanceof Map)) { - String regName = (String) ((Map) value).get("registrationName"); - if (regName != null - && key.startsWith("top") - && regName.startsWith("on") - && !key.substring(3).equals(regName.substring(2))) { - FLog.e( - TAG, - String.format( - "Direct event name for '%s' doesn't correspond to the naming convention," - + " expected 'topEventName'->'onEventName', got '%s'->'%s'", - viewManagerName, key, regName)); - } - } - } - } - - /** - * Generates map of constants that is then exposed by {@link UIManagerModule}. Provided list of - * {@param viewManagers} is then used to populate content of those predefined fields using {@link - * ViewManager#getExportedCustomBubblingEventTypeConstants} and {@link - * ViewManager#getExportedCustomDirectEventTypeConstants} respectively. Each view manager is in - * addition allowed to expose viewmanager-specific constants that are placed under the key that - * corresponds to the view manager's name (see {@link ViewManager#getName}). Constants are merged - * into the map of {@link UIManagerModule} base constants that is stored in {@link - * UIManagerModuleConstants}. TODO(6845124): Create a test for this - */ - // NOTE: When converted to Kotlin this method should be `internal` due to - // visibility restriction for `ReactInstance` - public static Map createConstants( - List viewManagers, - @Nullable Map allBubblingEventTypes, - @Nullable Map allDirectEventTypes) { - Map constants = UIManagerModuleConstants.constants; - - // Generic/default event types: - // All view managers are capable of dispatching these events. - // They will be automatically registered with React Fiber. - Map genericBubblingEventTypes = UIManagerModuleConstants.bubblingEventTypeConstants; - Map genericDirectEventTypes = UIManagerModuleConstants.directEventTypeConstants; - - // Cumulative event types: - // View manager specific event types are collected as views are loaded. - // This information is used later when events are dispatched. - if (allBubblingEventTypes != null) { - allBubblingEventTypes.putAll(genericBubblingEventTypes); - } - if (allDirectEventTypes != null) { - allDirectEventTypes.putAll(genericDirectEventTypes); - } - - for (ViewManager viewManager : viewManagers) { - final String viewManagerName = viewManager.getName(); - - Map viewManagerConstants = - createConstantsForViewManager( - viewManager, null, null, allBubblingEventTypes, allDirectEventTypes); - if (!viewManagerConstants.isEmpty()) { - constants.put(viewManagerName, viewManagerConstants); - } - } - - constants.put("genericBubblingEventTypes", genericBubblingEventTypes); - constants.put("genericDirectEventTypes", genericDirectEventTypes); - return constants; - } - - // NOTE: When converted to Kotlin this method should be `internal` due to - // visibility restriction for `ReactInstance` - public static Map createConstantsForViewManager( - ViewManager viewManager, - @Nullable Map defaultBubblingEvents, - @Nullable Map defaultDirectEvents, - @Nullable Map cumulativeBubblingEventTypes, - @Nullable Map cumulativeDirectEventTypes) { - Map viewManagerConstants = MapBuilder.newHashMap(); - - Map viewManagerBubblingEvents = viewManager.getExportedCustomBubblingEventTypeConstants(); - if (viewManagerBubblingEvents != null) { - if (ReactNativeNewArchitectureFeatureFlags.enableFabricRenderer() - && ReactNativeNewArchitectureFeatureFlags.useFabricInterop()) { - // For Fabric, events needs to be fired with a "top" prefix. - // For the sake of Fabric Interop, here we normalize events adding "top" in their - // name if the user hasn't provided it. - viewManagerBubblingEvents = normalizeEventTypes(viewManagerBubblingEvents); - } - recursiveMerge(cumulativeBubblingEventTypes, viewManagerBubblingEvents); - recursiveMerge(viewManagerBubblingEvents, defaultBubblingEvents); - viewManagerConstants.put(BUBBLING_EVENTS_KEY, viewManagerBubblingEvents); - } else if (defaultBubblingEvents != null) { - viewManagerConstants.put(BUBBLING_EVENTS_KEY, defaultBubblingEvents); - } - - Map viewManagerDirectEvents = viewManager.getExportedCustomDirectEventTypeConstants(); - validateDirectEventNames(viewManager.getName(), viewManagerDirectEvents); - if (viewManagerDirectEvents != null) { - if (ReactNativeNewArchitectureFeatureFlags.enableFabricRenderer() - && ReactNativeNewArchitectureFeatureFlags.useFabricInterop()) { - // For Fabric, events needs to be fired with a "top" prefix. - // For the sake of Fabric Interop, here we normalize events adding "top" in their - // name if the user hasn't provided it. - viewManagerDirectEvents = normalizeEventTypes(viewManagerDirectEvents); - } - recursiveMerge(cumulativeDirectEventTypes, viewManagerDirectEvents); - recursiveMerge(viewManagerDirectEvents, defaultDirectEvents); - viewManagerConstants.put(DIRECT_EVENTS_KEY, viewManagerDirectEvents); - } else if (defaultDirectEvents != null) { - viewManagerConstants.put(DIRECT_EVENTS_KEY, defaultDirectEvents); - } - - Map customViewConstants = viewManager.getExportedViewConstants(); - if (customViewConstants != null) { - viewManagerConstants.put("Constants", customViewConstants); - } - Map viewManagerCommands = viewManager.getCommandsMap(); - if (viewManagerCommands != null) { - viewManagerConstants.put("Commands", viewManagerCommands); - } - Map viewManagerNativeProps = viewManager.getNativeProps(); - if (!viewManagerNativeProps.isEmpty()) { - viewManagerConstants.put("NativeProps", viewManagerNativeProps); - } - - return viewManagerConstants; - } - - @VisibleForTesting - /* package */ static @Nullable Map normalizeEventTypes(@Nullable Map events) { - if (events == null) { - return null; - } - Set keysToNormalize = new HashSet<>(); - for (Object key : events.keySet()) { - if (key instanceof String) { - String keyString = (String) key; - if (!keyString.startsWith("top")) { - keysToNormalize.add(keyString); - } - } - } - // When providing one event in Kotlin, it will create a SingletonMap by default - // which will throw on trying to add new element to it - if (!(events instanceof HashMap)) { - events = new HashMap(events); - } - for (String oldKey : keysToNormalize) { - Object value = events.get(oldKey); - String baseKey = ""; - if (oldKey.startsWith("on")) { - // Drop "on" prefix. - baseKey = oldKey.substring(2); - } else { - // Capitalize first letter. - baseKey = oldKey.substring(0, 1).toUpperCase() + oldKey.substring(1); - } - String newKey = "top" + baseKey; - events.put(newKey, value); - } - return events; - } - - /** Merges {@param source} map into {@param dest} map recursively */ - private static void recursiveMerge(@Nullable Map dest, @Nullable Map source) { - if (dest == null || source == null || source.isEmpty()) { - return; - } - - for (Object key : source.keySet()) { - Object sourceValue = source.get(key); - Object destValue = dest.get(key); - if (destValue != null && (sourceValue instanceof Map) && (destValue instanceof Map)) { - // Since event maps are client based Map interface, it could be immutable - if (!(destValue instanceof HashMap)) { - destValue = new HashMap((Map) destValue); - dest.replace(key, (Map) destValue); - } - recursiveMerge((Map) destValue, (Map) sourceValue); - } else { - dest.put(key, sourceValue); - } - } - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.kt new file mode 100644 index 00000000000..59eb6a51b3f --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.kt @@ -0,0 +1,238 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.uimanager + +import androidx.annotation.VisibleForTesting +import com.facebook.common.logging.FLog +import com.facebook.react.common.build.ReactBuildConfig +import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlags.enableFabricRenderer +import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlags.useFabricInterop +import java.util.Locale + +/** + * Helps generate constants map for [UIManagerModule] by collecting and merging constants from + * registered view managers. + */ +internal object UIManagerModuleConstantsHelper { + private const val TAG = "UIManagerModuleConstantsHelper" + private const val BUBBLING_EVENTS_KEY = "bubblingEventTypes" + private const val DIRECT_EVENTS_KEY = "directEventTypes" + + /** + * Generates a lazy discovery enabled version of [UIManagerModule] constants. It only contains a + * list of view manager names, so that JS side is aware of the managers there are. Actual + * ViewManager instantiation happens when `UIManager.getViewManagerConfig('SpecificViewManager')` + * call happens. The View Manager is then registered on the JS side with the help of + * `UIManagerModule.getConstantsForViewManager`. + */ + @JvmStatic + @JvmName("internal_createConstants") + internal fun createConstants(resolver: ViewManagerResolver): Map = + UIManagerModuleConstants.constants.plus( + mapOf( + "ViewManagerNames" to ArrayList(resolver.getViewManagerNames()), + "LazyViewManagersEnabled" to true)) + + @JvmStatic + val defaultExportableEventTypes: Map + get() = + mapOf( + BUBBLING_EVENTS_KEY to UIManagerModuleConstants.bubblingEventTypeConstants, + DIRECT_EVENTS_KEY to UIManagerModuleConstants.directEventTypeConstants) + + private fun validateDirectEventNames( + viewManagerName: String, + directEvents: MutableMap? + ) { + if (!ReactBuildConfig.DEBUG || directEvents == null) { + return + } + + for ((key, value) in directEvents) { + if (value is MutableMap<*, *>) { + val regName = value["registrationName"] as String? + if (regName != null && + key.startsWith("top") && + regName.startsWith("on") && + (key.substring(3) != regName.substring(2))) { + FLog.e( + TAG, + "Direct event name for '$viewManagerName' doesn't correspond to the naming convention," + + " expected 'topEventName'->'onEventName', got '$key'->'$regName'") + } + } + } + } + + /** + * Generates map of constants that is then exposed by [UIManagerModule]. Provided list of {@param + * viewManagers} is then used to populate content of those predefined fields using + * [ViewManager.getExportedCustomBubblingEventTypeConstants] and + * [ViewManager.getExportedCustomDirectEventTypeConstants] respectively. Each view manager is in + * addition allowed to expose viewmanager-specific constants that are placed under the key that + * corresponds to the view manager's name (see [ViewManager.getName]). Constants are merged into + * the map of [UIManagerModule] base constants that is stored in [UIManagerModuleConstants]. + * + * TODO(6845124): Create a test for this + */ + @JvmStatic + @JvmName("internal_createConstants") + internal fun createConstants( + viewManagers: List>, + allBubblingEventTypes: MutableMap?, + allDirectEventTypes: MutableMap? + ): MutableMap { + val constants: MutableMap = UIManagerModuleConstants.constants.toMutableMap() + + // Generic/default event types: + // All view managers are capable of dispatching these events. + // They will be automatically registered with React Fiber. + val genericBubblingEventTypes: Map = + UIManagerModuleConstants.bubblingEventTypeConstants + val genericDirectEventTypes: Map = + UIManagerModuleConstants.directEventTypeConstants + + // Cumulative event types: + // View manager specific event types are collected as views are loaded. + // This information is used later when events are dispatched. + allBubblingEventTypes?.putAll(genericBubblingEventTypes) + allDirectEventTypes?.putAll(genericDirectEventTypes) + + for (viewManager in viewManagers) { + val viewManagerName = viewManager.getName() + + val viewManagerConstants: MutableMap<*, *> = + createConstantsForViewManager( + viewManager, null, null, allBubblingEventTypes, allDirectEventTypes) + if (!viewManagerConstants.isEmpty()) { + constants[viewManagerName] = viewManagerConstants + } + } + + constants["genericBubblingEventTypes"] = genericBubblingEventTypes + constants["genericDirectEventTypes"] = genericDirectEventTypes + return constants + } + + @JvmStatic + @JvmName("internal_createConstantsForViewManager") + internal fun createConstantsForViewManager( + viewManager: ViewManager, + defaultBubblingEvents: MutableMap?, + defaultDirectEvents: MutableMap?, + cumulativeBubblingEventTypes: MutableMap?, + cumulativeDirectEventTypes: MutableMap? + ): MutableMap { + val viewManagerConstants: MutableMap = mutableMapOf() + + var viewManagerBubblingEvents: MutableMap? = + viewManager.exportedCustomBubblingEventTypeConstants + if (viewManagerBubblingEvents != null) { + if (enableFabricRenderer() && useFabricInterop()) { + // For Fabric, events needs to be fired with a "top" prefix. + // For the sake of Fabric Interop, here we normalize events adding "top" in their + // name if the user hasn't provided it. + viewManagerBubblingEvents = normalizeEventTypes(viewManagerBubblingEvents) + } + recursiveMerge(cumulativeBubblingEventTypes, viewManagerBubblingEvents) + recursiveMerge(viewManagerBubblingEvents, defaultBubblingEvents) + viewManagerConstants.put(BUBBLING_EVENTS_KEY, viewManagerBubblingEvents) + } else if (defaultBubblingEvents != null) { + viewManagerConstants.put(BUBBLING_EVENTS_KEY, defaultBubblingEvents) + } + + var viewManagerDirectEvents: MutableMap? = + viewManager.exportedCustomDirectEventTypeConstants + validateDirectEventNames(viewManager.getName(), viewManagerDirectEvents) + if (viewManagerDirectEvents != null) { + if (enableFabricRenderer() && useFabricInterop()) { + // For Fabric, events needs to be fired with a "top" prefix. + // For the sake of Fabric Interop, here we normalize events adding "top" in their + // name if the user hasn't provided it. + viewManagerDirectEvents = normalizeEventTypes(viewManagerDirectEvents) + } + recursiveMerge(cumulativeDirectEventTypes, viewManagerDirectEvents) + recursiveMerge(viewManagerDirectEvents, defaultDirectEvents) + viewManagerConstants.put(DIRECT_EVENTS_KEY, viewManagerDirectEvents) + } else if (defaultDirectEvents != null) { + viewManagerConstants.put(DIRECT_EVENTS_KEY, defaultDirectEvents) + } + + val customViewConstants: MutableMap? = viewManager.exportedViewConstants + if (customViewConstants != null) { + viewManagerConstants.put("Constants", customViewConstants) + } + val viewManagerCommands: MutableMap? = viewManager.commandsMap + if (viewManagerCommands != null) { + viewManagerConstants.put("Commands", viewManagerCommands) + } + val viewManagerNativeProps = viewManager.nativeProps + if (!viewManagerNativeProps.isEmpty()) { + viewManagerConstants.put("NativeProps", viewManagerNativeProps) + } + + return viewManagerConstants + } + + @VisibleForTesting + internal fun normalizeEventTypes( + eventsToNormalize: MutableMap + ): MutableMap { + var events = eventsToNormalize + val keysToNormalize: MutableSet = hashSetOf() + for (key in events.keys) { + val keyString = key + if (!keyString.startsWith("top")) { + keysToNormalize.add(keyString) + } + } + // When providing one event in Kotlin, it will create a SingletonMap by default + // which will throw on trying to add new element to it + if (events !is HashMap<*, *>) { + events = HashMap(events) + } + for (oldKey in keysToNormalize) { + val value = checkNotNull(events[oldKey]) + val baseKey = + if (oldKey.startsWith("on")) { + // Drop "on" prefix. + oldKey.substring(2) + } else { + // Capitalize first letter. + oldKey.substring(0, 1).uppercase(Locale.getDefault()) + oldKey.substring(1) + } + val newKey = "top$baseKey" + events.put(newKey, value) + } + return events + } + + /** Merges [source] map into [dest] map recursively */ + private fun recursiveMerge(dest: MutableMap?, source: MutableMap?) { + if (dest == null || source == null || source.isEmpty()) { + return + } + + for ((key, sourceValue) in source) { + var destValue = dest[key] + if (destValue != null && + (sourceValue is MutableMap<*, *>) && + (destValue is MutableMap<*, *>)) { + // Since event maps are client based Map interface, it could be immutable + if (destValue !is HashMap<*, *>) { + destValue = HashMap(destValue) + dest.replace(key, destValue as MutableMap<*, *>) + } + @Suppress("UNCHECKED_CAST") + recursiveMerge(destValue as MutableMap, sourceValue as MutableMap) + } else { + dest.put(key, sourceValue) + } + } + } +} diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelperTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelperTest.kt index 87712f83c1b..793cf117a75 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelperTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelperTest.kt @@ -19,27 +19,22 @@ class UIManagerModuleConstantsHelperTest { ReactNativeFeatureFlagsForTests.setUp() } - @Test - fun normalizeEventTypes_withNull_doesNothing() { - assertThat(UIManagerModuleConstantsHelper.normalizeEventTypes(null)).isNull() - } - @Test fun normalizeEventTypes_withEmptyMap_doesNothing() { - val emptyMap = mutableMapOf() + val emptyMap = mutableMapOf() assertThat(UIManagerModuleConstantsHelper.normalizeEventTypes(emptyMap)).isEmpty() } @Test fun normalizeEventTypes_withOnEvent_doesNormalize() { - val onClickMap = mutableMapOf("onClick" to "¯\\_(ツ)_/¯") + val onClickMap = mutableMapOf("onClick" to "¯\\_(ツ)_/¯") assertThat(UIManagerModuleConstantsHelper.normalizeEventTypes(onClickMap)) .containsKeys("topClick", "onClick") } @Test fun normalizeEventTypes_withTopEvent_doesNormalize() { - val onClickMap = mutableMapOf("topOnClick" to "¯\\_(ツ)_/¯") + val onClickMap = mutableMapOf("topOnClick" to "¯\\_(ツ)_/¯") assertThat(UIManagerModuleConstantsHelper.normalizeEventTypes(onClickMap)) .containsKey("topOnClick") .doesNotContainKey("onClick") @@ -49,11 +44,11 @@ class UIManagerModuleConstantsHelperTest { @Test fun normalizeEventTypes_withNestedObjects_doesNotLoseThem() { val nestedObjects = - mutableMapOf( + mutableMapOf( "onColorChanged" to - mutableMapOf( + mutableMapOf( "phasedRegistrationNames" to - mutableMapOf( + mutableMapOf( "bubbled" to "onColorChanged", "captured" to "onColorChangedCapture", )))