diff --git a/packages/react-native/Libraries/Components/View/View.js b/packages/react-native/Libraries/Components/View/View.js index 77e44899ede..4bb84994919 100644 --- a/packages/react-native/Libraries/Components/View/View.js +++ b/packages/react-native/Libraries/Components/View/View.js @@ -10,12 +10,18 @@ import type {ViewProps} from './ViewPropTypes'; +import * as ReactNativeFeatureFlags from '../../../src/private/featureflags/ReactNativeFeatureFlags'; import TextAncestor from '../../Text/TextAncestor'; import ViewNativeComponent from './ViewNativeComponent'; import * as React from 'react'; export type Props = ViewProps; +type PropsWithRef = $ReadOnly<{ + ref?: React.RefSetter>, + ...ViewProps, +}>; + /** * The most fundamental component for building a UI, View is a container that * supports layout with flexbox, style, some touch handling, and accessibility @@ -23,93 +29,183 @@ export type Props = ViewProps; * * @see https://reactnative.dev/docs/view */ -function View({ - accessibilityElementsHidden, - accessibilityLabel, - accessibilityLabelledBy, - accessibilityLiveRegion, - accessibilityState, - accessibilityValue, - 'aria-busy': ariaBusy, - 'aria-checked': ariaChecked, - 'aria-disabled': ariaDisabled, - 'aria-expanded': ariaExpanded, - 'aria-hidden': ariaHidden, - 'aria-label': ariaLabel, - 'aria-labelledby': ariaLabelledBy, - 'aria-live': ariaLive, - 'aria-selected': ariaSelected, - 'aria-valuemax': ariaValueMax, - 'aria-valuemin': ariaValueMin, - 'aria-valuenow': ariaValueNow, - 'aria-valuetext': ariaValueText, - focusable, - id, - importantForAccessibility, - nativeID, - tabIndex, - ...otherProps -}: $ReadOnly<{ - ref?: React.RefSetter>, - ...ViewProps, -}>): React.Node { +function View(props: PropsWithRef): React.Node { const hasTextAncestor = React.use(TextAncestor); - const _accessibilityLabelledBy = - ariaLabelledBy?.split(/\s*,\s*/g) ?? accessibilityLabelledBy; - const _accessibilityState = - accessibilityState != null || - ariaBusy != null || - ariaChecked != null || - ariaDisabled != null || - ariaExpanded != null || - ariaSelected != null - ? { - busy: ariaBusy ?? accessibilityState?.busy, - checked: ariaChecked ?? accessibilityState?.checked, - disabled: ariaDisabled ?? accessibilityState?.disabled, - expanded: ariaExpanded ?? accessibilityState?.expanded, - selected: ariaSelected ?? accessibilityState?.selected, - } - : undefined; + let actualView; + if (ReactNativeFeatureFlags.reduceDefaultPropsInView()) { + const { + accessibilityState, + accessibilityValue, + 'aria-busy': ariaBusy, + 'aria-checked': ariaChecked, + 'aria-disabled': ariaDisabled, + 'aria-expanded': ariaExpanded, + 'aria-hidden': ariaHidden, + 'aria-label': ariaLabel, + 'aria-labelledby': ariaLabelledBy, + 'aria-live': ariaLive, + 'aria-selected': ariaSelected, + 'aria-valuemax': ariaValueMax, + 'aria-valuemin': ariaValueMin, + 'aria-valuenow': ariaValueNow, + 'aria-valuetext': ariaValueText, + id, + tabIndex, + ...otherProps + } = props; - const _accessibilityValue = - accessibilityValue != null || - ariaValueMax != null || - ariaValueMin != null || - ariaValueNow != null || - ariaValueText != null - ? { - max: ariaValueMax ?? accessibilityValue?.max, - min: ariaValueMin ?? accessibilityValue?.min, - now: ariaValueNow ?? accessibilityValue?.now, - text: ariaValueText ?? accessibilityValue?.text, - } - : undefined; + // Since we destructured props, we can now treat it as mutable + const processedProps = otherProps as {...PropsWithRef}; - const actualView = ( - - ); + } + + if (id !== undefined) { + processedProps.nativeID = id; + } + + if (tabIndex !== undefined) { + processedProps.focusable = !tabIndex; + } + + if ( + accessibilityState != null || + ariaBusy != null || + ariaChecked != null || + ariaDisabled != null || + ariaExpanded != null || + ariaSelected != null + ) { + processedProps.accessibilityState = { + busy: ariaBusy ?? accessibilityState?.busy, + checked: ariaChecked ?? accessibilityState?.checked, + disabled: ariaDisabled ?? accessibilityState?.disabled, + expanded: ariaExpanded ?? accessibilityState?.expanded, + selected: ariaSelected ?? accessibilityState?.selected, + }; + } + + if ( + accessibilityValue != null || + ariaValueMax != null || + ariaValueMin != null || + ariaValueNow != null || + ariaValueText != null + ) { + processedProps.accessibilityValue = { + max: ariaValueMax ?? accessibilityValue?.max, + min: ariaValueMin ?? accessibilityValue?.min, + now: ariaValueNow ?? accessibilityValue?.now, + text: ariaValueText ?? accessibilityValue?.text, + }; + } + + actualView = ; + } else { + const { + accessibilityElementsHidden, + accessibilityLabel, + accessibilityLabelledBy, + accessibilityLiveRegion, + accessibilityState, + accessibilityValue, + 'aria-busy': ariaBusy, + 'aria-checked': ariaChecked, + 'aria-disabled': ariaDisabled, + 'aria-expanded': ariaExpanded, + 'aria-hidden': ariaHidden, + 'aria-label': ariaLabel, + 'aria-labelledby': ariaLabelledBy, + 'aria-live': ariaLive, + 'aria-selected': ariaSelected, + 'aria-valuemax': ariaValueMax, + 'aria-valuemin': ariaValueMin, + 'aria-valuenow': ariaValueNow, + 'aria-valuetext': ariaValueText, + focusable, + id, + importantForAccessibility, + nativeID, + tabIndex, + ...otherProps + } = props; + const _accessibilityLabelledBy = + ariaLabelledBy?.split(/\s*,\s*/g) ?? accessibilityLabelledBy; + + const _accessibilityState = + accessibilityState != null || + ariaBusy != null || + ariaChecked != null || + ariaDisabled != null || + ariaExpanded != null || + ariaSelected != null + ? { + busy: ariaBusy ?? accessibilityState?.busy, + checked: ariaChecked ?? accessibilityState?.checked, + disabled: ariaDisabled ?? accessibilityState?.disabled, + expanded: ariaExpanded ?? accessibilityState?.expanded, + selected: ariaSelected ?? accessibilityState?.selected, + } + : undefined; + + const _accessibilityValue = + accessibilityValue != null || + ariaValueMax != null || + ariaValueMin != null || + ariaValueNow != null || + ariaValueText != null + ? { + max: ariaValueMax ?? accessibilityValue?.max, + min: ariaValueMin ?? accessibilityValue?.min, + now: ariaValueNow ?? accessibilityValue?.now, + text: ariaValueText ?? accessibilityValue?.text, + } + : undefined; + + actualView = ( + + ); + } if (hasTextAncestor) { return {actualView}; } - return actualView; } diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index 2afc7b97fe0..36b17fbcf43 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -653,6 +653,17 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'none', }, + reduceDefaultPropsInView: { + defaultValue: true, + metadata: { + dateAdded: '2025-5-12', + description: + 'Optimize how default (accessibility) props are processed in View to avoid unnecessary keys.', + expectedReleaseValue: true, + purpose: 'experimentation', + }, + ossReleaseStage: 'none', + }, scheduleAnimatedCleanupInMicrotask: { defaultValue: true, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index 8ffecac90b8..815d0801079 100644 --- a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<84786200dbf1e0d95605152913b73423>> + * @generated SignedSource<<2dc9ec2ea173f10586a67940720f1d37>> * @flow strict */ @@ -37,6 +37,7 @@ export type ReactNativeFeatureFlagsJsOnly = $ReadOnly<{ enableVirtualViewDoubleStateHidden: Getter, fixVirtualizeListCollapseWindowSize: Getter, isLayoutAnimationEnabled: Getter, + reduceDefaultPropsInView: Getter, scheduleAnimatedCleanupInMicrotask: Getter, shouldUseAnimatedObjectForTransform: Getter, shouldUseRemoveClippedSubviewsAsDefaultOnIOS: Getter, @@ -149,6 +150,11 @@ export const fixVirtualizeListCollapseWindowSize: Getter = createJavaSc */ export const isLayoutAnimationEnabled: Getter = createJavaScriptFlagGetter('isLayoutAnimationEnabled', true); +/** + * Optimize how default (accessibility) props are processed in View to avoid unnecessary keys. + */ +export const reduceDefaultPropsInView: Getter = createJavaScriptFlagGetter('reduceDefaultPropsInView', true); + /** * Changes the cleanup of `AnimatedProps` to occur in a microtask instead of synchronously during effect cleanup (for unmount) or subsequent mounts (for updates). */