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
This commit is contained in:
Maximilian Krause
2026-09-04 09:38:32 -07:00
committed by meta-codesync[bot]
parent d44efb09c1
commit c300f84f2c
2 changed files with 41 additions and 20 deletions
+15 -20
View File
@@ -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) {
@@ -592,6 +592,32 @@ describe('<Text>', () => {
});
});
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(
<Text disabled accessibilityState={accessibilityState}>
{TEST_TEXT}
</Text>,
);
});
expect(accessibilityState).toEqual({disabled: false});
expect(
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
).toEqual(
<rn-paragraph accessibilityState="{disabled:true,selected:false,checked:None,busy:false,expanded:null}">
{TEST_TEXT}
</rn-paragraph>,
);
});
});
describe('aria-hidden', () => {
it('is is passed as importantForAccessibility', () => {
const root = Fantom.createRoot();