From c300f84f2cbcce428b6fdb4cfae252945eb02459 Mon Sep 17 00:00:00 2001 From: Maximilian Krause Date: Fri, 4 Sep 2026 09:38:32 -0700 Subject: [PATCH] fix(text): avoid mutating accessibilityState (#58318) Summary: When `accessibilityState.disabled` conflicts with an explicit `disabled` prop, the `Text` component currently updates `accessibilityState` in place. This mutates an object owned by the caller, which can cause issues when other code uses that same object. This fix creates a new object instead, while retaining existing behavior (giving priority to the explicit prop). ## Changelog: [GENERAL] [FIXED] - Prevent Text from mutating the accessibilityState prop Pull Request resolved: https://github.com/react/react-native/pull/58318 Test Plan: Unchanged tests pass + added a regression test Reviewed By: Abbondanzo Differential Revision: D118788803 Pulled By: javache fbshipit-source-id: 77f69e851d06250aa27e20912a2c2fc91394b81d --- packages/react-native/Libraries/Text/Text.js | 35 ++++++++----------- .../Libraries/Text/__tests__/Text-itest.js | 26 ++++++++++++++ 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/packages/react-native/Libraries/Text/Text.js b/packages/react-native/Libraries/Text/Text.js index 902fcaba8bd..d689ec478f1 100644 --- a/packages/react-native/Libraries/Text/Text.js +++ b/packages/react-native/Libraries/Text/Text.js @@ -92,9 +92,22 @@ const TextImpl: component( ...NativeTextProps, }; const _accessibilityLabel = ariaLabel ?? accessibilityLabel; + const _accessibilityStateDisabled = + ariaDisabled ?? accessibilityState?.disabled; + const _disabled = disabled ?? _accessibilityStateDisabled; + + // If the disabled prop and accessibilityState.disabled are out of sync but not both in + // falsy states we need to update the accessibilityState object to use the disabled prop. + const needsAccessibilityStateDisabledUpdate = + _disabled !== _accessibilityStateDisabled && + ((_disabled != null && _disabled !== false) || + (_accessibilityStateDisabled != null && + _accessibilityStateDisabled !== false)); + let _accessibilityState: ?TextProps['accessibilityState'] = accessibilityState; if ( + needsAccessibilityStateDisabledUpdate || ariaBusy != null || ariaChecked != null || ariaDisabled != null || @@ -105,7 +118,7 @@ const TextImpl: component( _accessibilityState = { busy: ariaBusy ?? _accessibilityState.busy, checked: ariaChecked ?? _accessibilityState.checked, - disabled: ariaDisabled ?? _accessibilityState.disabled, + disabled: _disabled, expanded: ariaExpanded ?? _accessibilityState.expanded, selected: ariaSelected ?? _accessibilityState.selected, }; @@ -113,31 +126,13 @@ const TextImpl: component( _accessibilityState = { busy: ariaBusy, checked: ariaChecked, - disabled: ariaDisabled, + disabled: _disabled, expanded: ariaExpanded, selected: ariaSelected, }; } } - const _accessibilityStateDisabled = _accessibilityState?.disabled; - const _disabled = disabled ?? _accessibilityStateDisabled; - - // If the disabled prop and accessibilityState.disabled are out of sync but not both in - // falsy states we need to update the accessibilityState object to use the disabled prop. - if ( - _disabled !== _accessibilityStateDisabled && - ((_disabled != null && _disabled !== false) || - (_accessibilityStateDisabled != null && - _accessibilityStateDisabled !== false)) - ) { - if (_accessibilityState == null) { - _accessibilityState = {disabled}; - } else { - _accessibilityState.disabled = _disabled; - } - } - if (ariaHidden !== undefined) { processedProps.accessibilityElementsHidden = ariaHidden; if (ariaHidden === true) { diff --git a/packages/react-native/Libraries/Text/__tests__/Text-itest.js b/packages/react-native/Libraries/Text/__tests__/Text-itest.js index 2d19756ff93..717361f3bd4 100644 --- a/packages/react-native/Libraries/Text/__tests__/Text-itest.js +++ b/packages/react-native/Libraries/Text/__tests__/Text-itest.js @@ -592,6 +592,32 @@ describe('', () => { }); }); + describe('accessibilityState', () => { + it('does not mutate the prop when disabled overrides it', () => { + const accessibilityState: AccessibilityProps['accessibilityState'] = { + disabled: false, + }; + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + + {TEST_TEXT} + , + ); + }); + + expect(accessibilityState).toEqual({disabled: false}); + expect( + root.getRenderedOutput({props: ['accessibilityState']}).toJSX(), + ).toEqual( + + {TEST_TEXT} + , + ); + }); + }); + describe('aria-hidden', () => { it('is is passed as importantForAccessibility', () => { const root = Fantom.createRoot();