fix: stale VoiceOver value from async UI updates to Text (#58169)

Summary:
This is a targeted fix for https://github.com/react/react-native/issues/58145, where I observed iOS VoiceOver reading stale values over `Text` elements that were updated asynchronously (from a `setTimeout` call).

This patch definitely fixes the issue, but I'm happy to iterate on the design if there are any considerations I haven't taken into account: https://github.com/coolsoftwaretyler/focusissues/pull/1

Maybe related to https://github.com/react/react-native/issues/49462? Although what I found is that when VoiceOver encountered a failure, it bounced back to the status bar, so it's possible 49462 is some *other* issue with modals that presents the same failure mode.

## Changelog:

Pick one each for the category and type tags:

[IOS] [FIXED] -VoiceOver text readout when changing elements asynchronously

Pull Request resolved: https://github.com/react/react-native/pull/58169

Test Plan:
I built a reproducer and tested this patch with it. To test:

1. Build reproducer (or any app that changes text asynchronously)
2. Install app to physical device (I found the accessibility inspector tool with simulators would paper over the issue, so you need to actually use VoiceOver on a device)
3. Turn on voiceover
4. Navigate through the interface, moving focus through your elements to ensure they work as expected
5. Then use VoiceOver to trigger an async update to some text
6. Move on to any value that was changed by the async update
7. Text will probably be stale, and moving focus no longer works

### Before

https://github.com/user-attachments/assets/05b8d308-e743-4cff-917c-c5410307937d

### After

https://github.com/user-attachments/assets/d7d8d3fc-5bd8-45b9-bab0-de9bbc9cbcda

Reviewed By: cortinico

Differential Revision: D117875865

Pulled By: christophpurrer

fbshipit-source-id: 218ca244ea99e460d687ec527d6fbcaa9e499943
This commit is contained in:
Tyler Williams
2026-09-01 12:37:45 -07:00
committed by meta-codesync[bot]
parent 671705dfb1
commit 082d787a1e
@@ -138,6 +138,17 @@ using namespace facebook::react;
_textView.state = std::static_pointer_cast<const ParagraphShadowNode::ConcreteState>(state);
[_textView setNeedsDisplay];
[self setNeedsLayout];
// If the attributed string has changed, we need to notify the accessibility system that something changed,
// otherwise it may hold on to stale values (this happens most often when an element is updated async)
// https://github.com/react/react-native/issues/58145
if (state && oldState) {
const auto &newData = std::static_pointer_cast<const ParagraphShadowNode::ConcreteState>(state)->getData();
const auto &oldData = std::static_pointer_cast<const ParagraphShadowNode::ConcreteState>(oldState)->getData();
if (!newData.attributedString.isContentEqual(oldData.attributedString)) {
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
}
}
}
- (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics