From 2959d519713fe9b6b612cfd5f275f1cd8c46866d Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Tue, 20 Oct 2026 06:55:50 -0700 Subject: [PATCH] Defer transformed TextInput edits outside UIKit delegate callbacks (#59188) Summary: Pull Request resolved: https://github.com/react/react-native/pull/59188 UIKit text change delegate callbacks now remain decision-only when JavaScript transforms an edit. The adapter rejects UIKit's original edit, records an implementation-private transaction, and applies an incremental `UITextInput` replacement on the next main-queue turn after validating the backing-view generation and base text. This preserves partial `maxLength` truncation without synchronously replacing the full attributed string or selection. Marked-text composition remains UIKit-owned; an over-limit committed value is corrected through the same deferred transaction path after composition ends. A runtime gate controls the new path. Stale edits are discarded when the backing text changes, lifecycle and JavaScript-driven updates invalidate queued transactions, and successful commits emit one change update plus the corresponding selection event. Changelog: [iOS][Fixed] - Preserve transformed `TextInput` edits without synchronously rewriting text inside UIKit delegate callbacks Reviewed By: cipolleschi Differential Revision: D126057002 --- .../Libraries/Text/React-RCTText.podspec | 1 + .../Text/TextInput/Multiline/RCTUITextView.mm | 15 + .../RCTBackedTextInputDelegateAdapter.mm | 248 +++++++++++++++- .../TextInput/Singleline/RCTUITextField.mm | 15 + .../TextInput/RCTTextInputComponentView.mm | 67 ++++- .../RCTBackedTextInputDelegateAdapterTests.mm | 273 ++++++++++++++++++ .../featureflags/ReactNativeFeatureFlags.kt | 8 +- .../ReactNativeFeatureFlagsCxxAccessor.kt | 12 +- .../ReactNativeFeatureFlagsCxxInterop.kt | 4 +- .../ReactNativeFeatureFlagsDefaults.kt | 4 +- .../ReactNativeFeatureFlagsLocalAccessor.kt | 13 +- .../ReactNativeFeatureFlagsProvider.kt | 4 +- .../JReactNativeFeatureFlagsCxxInterop.cpp | 16 +- .../JReactNativeFeatureFlagsCxxInterop.h | 5 +- .../featureflags/ReactNativeFeatureFlags.cpp | 6 +- .../featureflags/ReactNativeFeatureFlags.h | 7 +- .../ReactNativeFeatureFlagsAccessor.cpp | 200 +++++++------ .../ReactNativeFeatureFlagsAccessor.h | 6 +- .../ReactNativeFeatureFlagsDefaults.h | 6 +- .../ReactNativeFeatureFlagsDynamicProvider.h | 11 +- .../ReactNativeFeatureFlagsProvider.h | 3 +- .../NativeReactNativeFeatureFlags.cpp | 7 +- .../NativeReactNativeFeatureFlags.h | 4 +- .../ReactNativeFeatureFlags.config.js | 11 + .../featureflags/ReactNativeFeatureFlags.js | 7 +- .../specs/NativeReactNativeFeatureFlags.js | 3 +- 26 files changed, 837 insertions(+), 119 deletions(-) create mode 100644 packages/react-native/React/Tests/Text/RCTBackedTextInputDelegateAdapterTests.mm diff --git a/packages/react-native/Libraries/Text/React-RCTText.podspec b/packages/react-native/Libraries/Text/React-RCTText.podspec index 4d9bc4d8f9e7..8f714c2e83f9 100644 --- a/packages/react-native/Libraries/Text/React-RCTText.podspec +++ b/packages/react-native/Libraries/Text/React-RCTText.podspec @@ -34,6 +34,7 @@ Pod::Spec.new do |s| s.dependency "Yoga" s.dependency "React-Core/RCTTextHeaders", version + add_dependency(s, "React-featureflags") mark_as_react_native_build(s) end diff --git a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm index e64f5543b960..d0146ab6e640 100644 --- a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm +++ b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm @@ -12,6 +12,11 @@ #import +@interface RCTBackedTextViewDelegateAdapter (PendingTextInputEdit) +- (void)invalidatePendingTextInputEdit; +- (void)schedulePendingTextInputEditWithRange:(NSRange)range replacementText:(NSString *)replacementText; +@end + @implementation RCTUITextView { UILabel *_placeholderView; UITextView *_detachedTextView; @@ -207,6 +212,16 @@ - (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BO [super setSelectedTextRange:selectedTextRange]; } +- (void)rct_invalidatePendingTextInputEdit +{ + [_textInputDelegateAdapter invalidatePendingTextInputEdit]; +} + +- (void)rct_enqueueTextInputEditWithRange:(NSRange)range replacementText:(NSString *)replacementText +{ + [_textInputDelegateAdapter schedulePendingTextInputEditWithRange:range replacementText:replacementText]; +} + // After restoring the previous cursor position, we manually trigger the scroll to the new cursor position (PR 38679). - (void)scrollRangeToVisible:(NSRange)range { diff --git a/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm b/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm index 9633ab9947a5..66292be0f585 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm +++ b/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm @@ -7,6 +7,148 @@ #import +#import + +#import +#include + +namespace { + +struct PendingTextInputEdit final { + NSUInteger backingViewGeneration; + NSUInteger nativeEditRevision; + NSRange range; + __strong NSString *baseText; + __strong NSString *replacementText; + + BOOL apply(id backedTextInputView) const; +}; + +BOOL PendingTextInputEdit::apply(id backedTextInputView) const +{ + NSString *currentText = backedTextInputView.attributedText.string; + if (![currentText isEqualToString:baseText]) { + return NO; + } + + if (range.location > currentText.length || range.length > currentText.length - range.location) { + return NO; + } + + NSUInteger rangeEnd = NSMaxRange(range); + if (range.location > NSIntegerMax || rangeEnd > NSIntegerMax || + replacementText.length > NSIntegerMax - range.location) { + return NO; + } + NSUInteger caretOffset = range.location + replacementText.length; + + NSMutableString *expectedText = [currentText mutableCopy]; + [expectedText replaceCharactersInRange:range withString:replacementText]; + + UITextPosition *startPosition = [backedTextInputView positionFromPosition:backedTextInputView.beginningOfDocument + offset:static_cast(range.location)]; + UITextPosition *endPosition = [backedTextInputView positionFromPosition:backedTextInputView.beginningOfDocument + offset:static_cast(rangeEnd)]; + if (startPosition == nil || endPosition == nil) { + return NO; + } + + UITextRange *textRange = [backedTextInputView textRangeFromPosition:startPosition toPosition:endPosition]; + if (textRange == nil) { + return NO; + } + [backedTextInputView replaceRange:textRange withText:replacementText]; + + NSString *resultText = backedTextInputView.attributedText.string; + if (![resultText isEqualToString:expectedText]) { + return ![resultText isEqualToString:currentText]; + } + + UITextPosition *caretPosition = [backedTextInputView positionFromPosition:backedTextInputView.beginningOfDocument + offset:static_cast(caretOffset)]; + if (caretPosition != nil) { + UITextRange *caretRange = [backedTextInputView textRangeFromPosition:caretPosition toPosition:caretPosition]; + if (caretRange != nil) { + [backedTextInputView setSelectedTextRange:caretRange notifyDelegate:YES]; + } + } + return YES; +} + +class PendingTextInputEditCoordinator final { + public: + void schedule( + id backedTextInputView, + NSRange range, + NSString *replacementText, + dispatch_block_t drainBlock) + { + _pendingEdit = PendingTextInputEdit{ + .backingViewGeneration = _backingViewGeneration, + .nativeEditRevision = _nativeEditRevision, + .range = range, + .baseText = [backedTextInputView.attributedText.string copy], + .replacementText = replacementText, + }; + if (_drainScheduled) { + return; + } + + _drainScheduled = YES; + dispatch_async(dispatch_get_main_queue(), drainBlock); + } + + BOOL drain(id backedTextInputView, BOOL isEditable) + { + _drainScheduled = NO; + if (!_pendingEdit.has_value()) { + return NO; + } + + PendingTextInputEdit edit = std::move(*_pendingEdit); + _pendingEdit.reset(); + if (backedTextInputView == nil || !isEditable || backedTextInputView.markedTextRange != nil || + edit.backingViewGeneration != _backingViewGeneration) { + return NO; + } + if (edit.nativeEditRevision != _nativeEditRevision && + ![backedTextInputView.attributedText.string isEqualToString:edit.baseText]) { + return NO; + } + + _isApplying = YES; + BOOL didApply = edit.apply(backedTextInputView); + _isApplying = NO; + return didApply; + } + + void invalidate() + { + _pendingEdit.reset(); + _backingViewGeneration++; + _nativeEditRevision++; + } + + void nativeStateDidChange() + { + _nativeEditRevision++; + } + + BOOL isApplying() const + { + return _isApplying; + } + + private: + std::optional _pendingEdit; + NSUInteger _backingViewGeneration{1}; + NSUInteger _nativeEditRevision{0}; + BOOL _drainScheduled{NO}; + BOOL _isApplying{NO}; +}; + +} // namespace + #pragma mark - RCTBackedTextFieldDelegateAdapter (for UITextField) static void *TextFieldSelectionObservingContext = &TextFieldSelectionObservingContext; @@ -18,10 +160,15 @@ @interface RCTBackedTextFieldDelegateAdapter () < UITextDropDelegate #endif > + +- (void)schedulePendingTextInputEditWithRange:(NSRange)range replacementText:(NSString *)replacementText; +- (void)drainPendingTextInputEdit; + @end @implementation RCTBackedTextFieldDelegateAdapter { __weak UITextField *_backedTextInputView; + PendingTextInputEditCoordinator _pendingEditCoordinator; BOOL _textDidChangeIsComing; UITextRange *_previousSelectedTextRange; } @@ -71,6 +218,7 @@ - (BOOL)textFieldShouldEndEditing:(__unused UITextField *)textField - (void)textFieldDidEndEditing:(__unused UITextField *)textField { + [self invalidatePendingTextInputEdit]; if (_textDidChangeIsComing) { // iOS does't call `textViewDidChange:` delegate method if the change was happened because of autocorrection // which was triggered by losing focus. So, we call it manually. @@ -85,6 +233,10 @@ - (BOOL)textField:(__unused UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { + if (_pendingEditCoordinator.isApplying()) { + return YES; + } + NSString *newText = [_backedTextInputView.textInputDelegate textInputShouldChangeText:string inRange:range]; if (newText == nil) { @@ -101,10 +253,16 @@ - (BOOL)textField:(__unused UITextField *)textField if (range.location > textLength) { return NO; } - if (range.location + range.length > textLength) { - range = NSMakeRange(range.location, textLength - range.location); + if (range.length > textLength - range.location) { + range.length = textLength - range.location; + } + + if (facebook::react::ReactNativeFeatureFlags::deferTextInputTruncationIOS()) { + [self schedulePendingTextInputEditWithRange:range replacementText:newText]; + return NO; } + // TODO: Remove this synchronous fallback once deferred TextInput truncation is fully rolled out. NSMutableAttributedString *attributedString = [_backedTextInputView.attributedText mutableCopy]; [attributedString replaceCharactersInRange:range withString:newText]; [_backedTextInputView setAttributedText:[attributedString copy]]; @@ -120,6 +278,22 @@ - (BOOL)textField:(__unused UITextField *)textField return NO; } +- (void)schedulePendingTextInputEditWithRange:(NSRange)range replacementText:(NSString *)replacementText +{ + __weak __typeof(self) weakSelf = self; + _pendingEditCoordinator.schedule(_backedTextInputView, range, replacementText, ^{ + [weakSelf drainPendingTextInputEdit]; + }); +} + +- (void)drainPendingTextInputEdit +{ + UITextField *backedTextInputView = _backedTextInputView; + if (_pendingEditCoordinator.drain(backedTextInputView, backedTextInputView.enabled)) { + [self textFieldDidChange]; + } +} + - (BOOL)textFieldShouldReturn:(__unused UITextField *)textField { // Ignore the value of whether we submitted; just make sure the submit event is called if necessary. @@ -131,6 +305,11 @@ - (BOOL)textFieldShouldReturn:(__unused UITextField *)textField - (void)textFieldDidChange { + if (_pendingEditCoordinator.isApplying()) { + return; + } + + _pendingEditCoordinator.nativeStateDidChange(); _textDidChangeIsComing = NO; [_backedTextInputView.textInputDelegate textInputDidChange]; @@ -155,6 +334,11 @@ - (BOOL)keyboardInputShouldDelete:(__unused UITextField *)textField #pragma mark - Public Interface +- (void)invalidatePendingTextInputEdit +{ + _pendingEditCoordinator.invalidate(); +} + - (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)textRange { _previousSelectedTextRange = textRange; @@ -169,10 +353,12 @@ - (void)selectedTextRangeWasSet - (void)textFieldProbablyDidChangeSelection { - if ([_backedTextInputView.selectedTextRange isEqual:_previousSelectedTextRange]) { + if (_pendingEditCoordinator.isApplying() || + [_backedTextInputView.selectedTextRange isEqual:_previousSelectedTextRange]) { return; } + _pendingEditCoordinator.nativeStateDidChange(); _previousSelectedTextRange = _backedTextInputView.selectedTextRange; [_backedTextInputView.textInputDelegate textInputDidChangeSelection]; } @@ -228,10 +414,15 @@ @interface RCTBackedTextViewDelegateAdapter () < UITextDropDelegate #endif > + +- (void)schedulePendingTextInputEditWithRange:(NSRange)range replacementText:(NSString *)replacementText; +- (void)drainPendingTextInputEdit; + @end @implementation RCTBackedTextViewDelegateAdapter { __weak UITextView *_backedTextInputView; + PendingTextInputEditCoordinator _pendingEditCoordinator; NSAttributedString *_lastStringStateWasUpdatedWith; BOOL _ignoreNextTextInputCall; BOOL _textDidChangeIsComing; @@ -270,6 +461,7 @@ - (BOOL)textViewShouldEndEditing:(__unused UITextView *)textView - (void)textViewDidEndEditing:(__unused UITextView *)textView { + [self invalidatePendingTextInputEdit]; if (_textDidChangeIsComing) { // iOS does't call `textViewDidChange:` delegate method if the change was happened because of autocorrection // which was triggered by losing focus. So, we call it manually. @@ -282,6 +474,10 @@ - (void)textViewDidEndEditing:(__unused UITextView *)textView - (BOOL)textView:(__unused UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { + if (_pendingEditCoordinator.isApplying()) { + return YES; + } + // Custom implementation of `textInputShouldReturn` and `textInputDidReturn` pair for `UITextView`. if (!_backedTextInputView.textWasPasted && [text isEqualToString:@"\n"]) { const BOOL shouldSubmit = [_backedTextInputView.textInputDelegate textInputShouldSubmitOnReturn]; @@ -306,13 +502,19 @@ - (BOOL)textView:(__unused UITextView *)textView shouldChangeTextInRange:(NSRang if (range.location > textLength) { return NO; } - if (range.location + range.length > textLength) { - range = NSMakeRange(range.location, textLength - range.location); + if (range.length > textLength - range.location) { + range.length = textLength - range.location; } else if ([newText isEqualToString:text]) { _textDidChangeIsComing = YES; return YES; } + if (facebook::react::ReactNativeFeatureFlags::deferTextInputTruncationIOS()) { + [self schedulePendingTextInputEditWithRange:range replacementText:newText]; + return NO; + } + + // TODO: Remove this synchronous fallback once deferred TextInput truncation is fully rolled out. NSMutableAttributedString *attributedString = [_backedTextInputView.attributedText mutableCopy]; [attributedString replaceCharactersInRange:range withString:newText]; [_backedTextInputView setAttributedText:[attributedString copy]]; @@ -328,8 +530,31 @@ - (BOOL)textView:(__unused UITextView *)textView shouldChangeTextInRange:(NSRang return NO; } +- (void)schedulePendingTextInputEditWithRange:(NSRange)range replacementText:(NSString *)replacementText +{ + __weak __typeof(self) weakSelf = self; + _pendingEditCoordinator.schedule(_backedTextInputView, range, replacementText, ^{ + [weakSelf drainPendingTextInputEdit]; + }); +} + +- (void)drainPendingTextInputEdit +{ + UITextView *backedTextInputView = _backedTextInputView; + if (_pendingEditCoordinator.drain(backedTextInputView, backedTextInputView.editable)) { + [self textViewDidChange:backedTextInputView]; + _lastStringStateWasUpdatedWith = backedTextInputView.attributedText; + [self textViewProbablyDidChangeSelection]; + } +} + - (void)textViewDidChange:(__unused UITextView *)textView { + if (_pendingEditCoordinator.isApplying()) { + return; + } + + _pendingEditCoordinator.nativeStateDidChange(); if (_ignoreNextTextInputCall && [_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) { _ignoreNextTextInputCall = NO; return; @@ -340,6 +565,10 @@ - (void)textViewDidChange:(__unused UITextView *)textView - (void)textViewDidChangeSelection:(__unused UITextView *)textView { + if (_pendingEditCoordinator.isApplying()) { + return; + } + if (_lastStringStateWasUpdatedWith && ![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) { [self textViewDidChange:_backedTextInputView]; _ignoreNextTextInputCall = YES; @@ -359,6 +588,11 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView #pragma mark - Public Interface +- (void)invalidatePendingTextInputEdit +{ + _pendingEditCoordinator.invalidate(); +} + - (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)textRange { _previousSelectedTextRange = textRange; @@ -368,10 +602,12 @@ - (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)tex - (void)textViewProbablyDidChangeSelection { - if ([_backedTextInputView.selectedTextRange isEqual:_previousSelectedTextRange]) { + if (_pendingEditCoordinator.isApplying() || + [_backedTextInputView.selectedTextRange isEqual:_previousSelectedTextRange]) { return; } + _pendingEditCoordinator.nativeStateDidChange(); _previousSelectedTextRange = _backedTextInputView.selectedTextRange; [_backedTextInputView.textInputDelegate textInputDidChangeSelection]; } diff --git a/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm b/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm index 064b48bb3720..31e57d3d94d7 100644 --- a/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm +++ b/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm @@ -11,6 +11,11 @@ #import #import +@interface RCTBackedTextFieldDelegateAdapter (PendingTextInputEdit) +- (void)invalidatePendingTextInputEdit; +- (void)schedulePendingTextInputEditWithRange:(NSRange)range replacementText:(NSString *)replacementText; +@end + @implementation RCTUITextField { RCTBackedTextFieldDelegateAdapter *_textInputDelegateAdapter; NSDictionary *_defaultTextAttributes; @@ -254,6 +259,16 @@ - (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BO [super setSelectedTextRange:selectedTextRange]; } +- (void)rct_invalidatePendingTextInputEdit +{ + [_textInputDelegateAdapter invalidatePendingTextInputEdit]; +} + +- (void)rct_enqueueTextInputEditWithRange:(NSRange)range replacementText:(NSString *)replacementText +{ + [_textInputDelegateAdapter schedulePendingTextInputEditWithRange:range replacementText:replacementText]; +} + - (void)scrollRangeToVisible:(NSRange)range { // Singleline TextInput does not require scrolling after calling setSelectedTextRange (PR 38679). diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm index 1d8f73b06a55..281c5ea88b53 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm @@ -31,6 +31,17 @@ using namespace facebook::react; +@protocol RCTBackedTextInputViewPendingEdit +- (void)rct_invalidatePendingTextInputEdit; +- (void)rct_enqueueTextInputEditWithRange:(NSRange)range replacementText:(NSString *)replacementText; +@end + +@interface RCTUITextField (PendingTextInputEdit) +@end + +@interface RCTUITextView (PendingTextInputEdit) +@end + @interface RCTTextInputComponentView () < RCTBackedTextInputDelegate, RCTTextInputViewProtocol @@ -39,13 +50,16 @@ @interface RCTTextInputComponentView () < UIDropInteractionDelegate #endif > + +- (BOOL)enqueueMaxLengthCorrectionIfNeeded; + @end static NSSet *returnKeyTypesSet; @implementation RCTTextInputComponentView { TextInputShadowNode::ConcreteState::Shared _state; - UIView *_backedTextInputView; + UIView *_backedTextInputView; NSUInteger _mostRecentEventCount; NSAttributedString *_lastStringStateWasUpdatedWith; @@ -361,8 +375,12 @@ - (void)updateState:(const State::Shared &)state oldState:(const State::Shared & } if (_mostRecentEventCount == _state->getData().mostRecentEventCount) { + NSAttributedString *attributedString = RCTNSAttributedStringFromAttributedStringBox(data.attributedStringBox); + if (![attributedString.string isEqualToString:_backedTextInputView.attributedText.string]) { + [_backedTextInputView rct_invalidatePendingTextInputEdit]; + } _comingFromJS = YES; - [self _setAttributedString:RCTNSAttributedStringFromAttributedStringBox(data.attributedStringBox)]; + [self _setAttributedString:attributedString]; _comingFromJS = NO; } } @@ -386,6 +404,7 @@ - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics - (void)prepareForRecycle { [super prepareForRecycle]; + [_backedTextInputView rct_invalidatePendingTextInputEdit]; _state.reset(); _backedTextInputView.attributedText = nil; _mostRecentEventCount = 0; @@ -466,6 +485,11 @@ - (NSString *)textInputShouldChangeText:(NSString *)text inRange:(NSRange)range } } + if (facebook::react::ReactNativeFeatureFlags::deferTextInputTruncationIOS() && + _backedTextInputView.markedTextRange != nil) { + return text; + } + if (props.maxLength < std::numeric_limits::max()) { NSInteger allowedLength = props.maxLength - _backedTextInputView.attributedText.string.length + range.length; @@ -493,12 +517,43 @@ - (BOOL)textInputShouldChangeTextInRange:(NSRange)range replacementText:(NSStrin return YES; } +- (BOOL)enqueueMaxLengthCorrectionIfNeeded +{ + if (!facebook::react::ReactNativeFeatureFlags::deferTextInputTruncationIOS() || + _backedTextInputView.markedTextRange != nil) { + return NO; + } + + const auto &props = static_cast(*_props); + NSString *text = _backedTextInputView.attributedText.string; + if (props.maxLength < 0 || text.length <= static_cast(props.maxLength)) { + return NO; + } + + auto truncationLocation = static_cast(props.maxLength); + if (truncationLocation > 0) { + NSRange characterRange = [text rangeOfComposedCharacterSequenceAtIndex:truncationLocation - 1]; + if (NSMaxRange(characterRange) > truncationLocation) { + truncationLocation = characterRange.location; + } + } + + [_backedTextInputView + rct_enqueueTextInputEditWithRange:NSMakeRange(truncationLocation, text.length - truncationLocation) + replacementText:@""]; + return YES; +} + - (void)textInputDidChange { if (_comingFromJS) { return; } + if ([self enqueueMaxLengthCorrectionIfNeeded]) { + return; + } + if (_ignoreNextTextInputCall && [_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) { _ignoreNextTextInputCall = NO; return; @@ -518,6 +573,10 @@ - (void)textInputDidChangeSelection return; } + if ([self enqueueMaxLengthCorrectionIfNeeded]) { + return; + } + // T207198334: Setting a new AttributedString (_comingFromJS) will trigger a selection change before the backing // string is updated, so indicies won't point to what we want yet. Only respond to user selection change, and let // `_setAttributedString` handle updating typing attributes if content changes. @@ -595,6 +654,7 @@ - (void)setTextAndSelection:(NSInteger)eventCount if (_mostRecentEventCount != eventCount) { return; } + [_backedTextInputView rct_invalidatePendingTextInputEdit]; _comingFromJS = YES; if (value && ![value isEqualToString:_backedTextInputView.attributedText.string]) { NSAttributedString *attributedString = @@ -852,7 +912,8 @@ - (void)scrollCursorIntoView - (void)_setMultiline:(BOOL)multiline { [_backedTextInputView removeFromSuperview]; - UIView *backedTextInputView = multiline ? [RCTUITextView new] : [RCTUITextField new]; + UIView *backedTextInputView = + multiline ? [RCTUITextView new] : [RCTUITextField new]; backedTextInputView.frame = _backedTextInputView.frame; RCTCopyBackedTextInput(_backedTextInputView, backedTextInputView); _backedTextInputView = backedTextInputView; diff --git a/packages/react-native/React/Tests/Text/RCTBackedTextInputDelegateAdapterTests.mm b/packages/react-native/React/Tests/Text/RCTBackedTextInputDelegateAdapterTests.mm new file mode 100644 index 000000000000..659af6511270 --- /dev/null +++ b/packages/react-native/React/Tests/Text/RCTBackedTextInputDelegateAdapterTests.mm @@ -0,0 +1,273 @@ +/* + * 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. + */ + +#import +#import +#import +#import + +#import +#import + +namespace { + +class DeferredTextInputFeatureFlags : public facebook::react::ReactNativeFeatureFlagsDefaults { + public: + bool deferTextInputTruncationIOS() override + { + return true; + } +}; + +} // namespace + +@interface RCTTextInputDelegateStub : NSObject + +@property (nonatomic, copy) NSString *replacementText; +@property (nonatomic, assign) NSInteger shouldChangeCount; +@property (nonatomic, assign) NSInteger changeCount; +@property (nonatomic, assign) NSInteger selectionChangeCount; + +@end + +@implementation RCTTextInputDelegateStub + +- (BOOL)textInputShouldBeginEditing +{ + return YES; +} + +- (void)textInputDidBeginEditing +{ +} + +- (BOOL)textInputShouldEndEditing +{ + return YES; +} + +- (void)textInputDidEndEditing +{ +} + +- (BOOL)textInputShouldReturn +{ + return NO; +} + +- (void)textInputDidReturn +{ +} + +- (BOOL)textInputShouldSubmitOnReturn +{ + return NO; +} + +- (NSString *)textInputShouldChangeText:(NSString *)text inRange:(NSRange)range +{ + self.shouldChangeCount++; + return self.replacementText; +} + +- (void)textInputDidChange +{ + self.changeCount++; +} + +- (void)textInputDidChangeSelection +{ + self.selectionChangeCount++; +} + +@end + +@interface RCTUITextField (PendingTextInputEdit) +- (void)rct_invalidatePendingTextInputEdit; +@end + +@interface RCTBackedTextInputDelegateAdapterTests : XCTestCase +@end + +@implementation RCTBackedTextInputDelegateAdapterTests + +- (void)setUp +{ + [super setUp]; + facebook::react::ReactNativeFeatureFlags::dangerouslyReset(); + facebook::react::ReactNativeFeatureFlags::override(std::make_unique()); +} + +- (void)tearDown +{ + facebook::react::ReactNativeFeatureFlags::dangerouslyReset(); + [super tearDown]; +} + +- (void)waitForPendingEdit +{ + XCTestExpectation *expectation = [self expectationWithDescription:@"pending edit drained"]; + dispatch_async(dispatch_get_main_queue(), ^{ + [expectation fulfill]; + }); + [self waitForExpectations:@[ expectation ] timeout:1]; +} + +- (void)testTextFieldDefersTransformedEdit +{ + RCTUITextField *textField = [RCTUITextField new]; + textField.attributedText = [[NSAttributedString alloc] initWithString:@"abc"]; + + RCTTextInputDelegateStub *delegate = [RCTTextInputDelegateStub new]; + delegate.replacementText = @"x"; + textField.textInputDelegate = delegate; + + BOOL accepted = [textField.delegate textField:textField + shouldChangeCharactersInRange:NSMakeRange(3, 0) + replacementString:@"wxyz"]; + + XCTAssertFalse(accepted); + XCTAssertEqualObjects(textField.attributedText.string, @"abc"); + XCTAssertEqual(delegate.changeCount, 0); + + [self waitForPendingEdit]; + + XCTAssertEqualObjects(textField.attributedText.string, @"abcx"); + UITextRange *selectedTextRange = textField.selectedTextRange; + XCTAssertNotNil(selectedTextRange); + if (selectedTextRange != nil) { + XCTAssertEqual([textField offsetFromPosition:textField.beginningOfDocument toPosition:selectedTextRange.start], 4); + } + XCTAssertEqual(delegate.shouldChangeCount, 1); + XCTAssertEqual(delegate.changeCount, 1); + XCTAssertEqual(delegate.selectionChangeCount, 1); +} + +- (void)testTextViewDefersTransformedEdit +{ + RCTUITextView *textView = [RCTUITextView new]; + textView.attributedText = [[NSAttributedString alloc] initWithString:@"abc"]; + + RCTTextInputDelegateStub *delegate = [RCTTextInputDelegateStub new]; + delegate.replacementText = @"x"; + textView.textInputDelegate = delegate; + + BOOL accepted = [textView.delegate textView:textView + shouldChangeTextInRange:NSMakeRange(3, 0) + replacementText:@"wxyz"]; + + XCTAssertFalse(accepted); + XCTAssertEqualObjects(textView.attributedText.string, @"abc"); + XCTAssertEqual(delegate.changeCount, 0); + + [self waitForPendingEdit]; + + XCTAssertEqualObjects(textView.attributedText.string, @"abcx"); + UITextRange *selectedTextRange = textView.selectedTextRange; + XCTAssertNotNil(selectedTextRange); + if (selectedTextRange != nil) { + XCTAssertEqual([textView offsetFromPosition:textView.beginningOfDocument toPosition:selectedTextRange.start], 4); + } + XCTAssertEqual(delegate.shouldChangeCount, 1); + XCTAssertEqual(delegate.changeCount, 1); + XCTAssertEqual(delegate.selectionChangeCount, 1); +} + +- (void)testInvalidationCancelsPendingEdit +{ + RCTUITextField *textField = [RCTUITextField new]; + textField.attributedText = [[NSAttributedString alloc] initWithString:@"abc"]; + + RCTTextInputDelegateStub *delegate = [RCTTextInputDelegateStub new]; + delegate.replacementText = @"x"; + textField.textInputDelegate = delegate; + + BOOL accepted = [textField.delegate textField:textField + shouldChangeCharactersInRange:NSMakeRange(3, 0) + replacementString:@"wxyz"]; + [textField rct_invalidatePendingTextInputEdit]; + [self waitForPendingEdit]; + + XCTAssertFalse(accepted); + XCTAssertEqualObjects(textField.attributedText.string, @"abc"); + XCTAssertEqual(delegate.changeCount, 0); + XCTAssertEqual(delegate.selectionChangeCount, 0); +} + +- (void)testTextChangeCancelsPendingEdit +{ + RCTUITextField *textField = [RCTUITextField new]; + textField.attributedText = [[NSAttributedString alloc] initWithString:@"abc"]; + + RCTTextInputDelegateStub *delegate = [RCTTextInputDelegateStub new]; + delegate.replacementText = @"x"; + textField.textInputDelegate = delegate; + + [textField.delegate textField:textField shouldChangeCharactersInRange:NSMakeRange(3, 0) replacementString:@"wxyz"]; + textField.attributedText = [[NSAttributedString alloc] initWithString:@"updated"]; + [self waitForPendingEdit]; + + XCTAssertEqualObjects(textField.attributedText.string, @"updated"); + XCTAssertEqual(delegate.changeCount, 0); +} + +- (void)testLatestTransformedEditWinsWithinRunLoopTurn +{ + RCTUITextField *textField = [RCTUITextField new]; + textField.attributedText = [[NSAttributedString alloc] initWithString:@"abc"]; + + RCTTextInputDelegateStub *delegate = [RCTTextInputDelegateStub new]; + delegate.replacementText = @"x"; + textField.textInputDelegate = delegate; + + [textField.delegate textField:textField shouldChangeCharactersInRange:NSMakeRange(3, 0) replacementString:@"wxyz"]; + delegate.replacementText = @"y"; + [textField.delegate textField:textField shouldChangeCharactersInRange:NSMakeRange(3, 0) replacementString:@"wxyz"]; + [self waitForPendingEdit]; + + XCTAssertEqualObjects(textField.attributedText.string, @"abcy"); + XCTAssertEqual(delegate.shouldChangeCount, 2); + XCTAssertEqual(delegate.changeCount, 1); +} + +- (void)testBlurCancelsPendingEdit +{ + RCTUITextField *textField = [RCTUITextField new]; + textField.attributedText = [[NSAttributedString alloc] initWithString:@"abc"]; + + RCTTextInputDelegateStub *delegate = [RCTTextInputDelegateStub new]; + delegate.replacementText = @"x"; + textField.textInputDelegate = delegate; + + [textField.delegate textField:textField shouldChangeCharactersInRange:NSMakeRange(3, 0) replacementString:@"wxyz"]; + [textField.delegate textFieldDidEndEditing:textField]; + [self waitForPendingEdit]; + + XCTAssertEqualObjects(textField.attributedText.string, @"abc"); + XCTAssertEqual(delegate.changeCount, 0); +} + +- (void)testUnchangedEditRemainsUIKitOwned +{ + RCTUITextField *textField = [RCTUITextField new]; + textField.attributedText = [[NSAttributedString alloc] initWithString:@"abc"]; + + RCTTextInputDelegateStub *delegate = [RCTTextInputDelegateStub new]; + delegate.replacementText = @"x"; + textField.textInputDelegate = delegate; + + BOOL accepted = [textField.delegate textField:textField + shouldChangeCharactersInRange:NSMakeRange(3, 0) + replacementString:@"x"]; + [self waitForPendingEdit]; + + XCTAssertTrue(accepted); + XCTAssertEqualObjects(textField.attributedText.string, @"abc"); + XCTAssertEqual(delegate.changeCount, 0); +} + +@end diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt index 0c879d6415f9..b5a4e567b7e4 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt @@ -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<> + * @generated SignedSource<> */ /** @@ -54,6 +54,12 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun defaultTextToOverflowHidden(): Boolean = accessor.defaultTextToOverflowHidden() + /** + * When enabled, iOS TextInput applies edits that JS transforms (e.g. maxLength truncation) incrementally after the UIKit text change delegate call returns, avoiding synchronous text and selection rewrites that can deadlock UIKeyboardTaskQueue. + */ + @JvmStatic + public fun deferTextInputTruncationIOS(): Boolean = accessor.deferTextInputTruncationIOS() + /** * Dispatch view commands in mount item order. */ diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt index 28329721fab8..ab8d45e1f35a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt @@ -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<<71d0554dc45df7d5365f81b7ec8c3d5b>> + * @generated SignedSource<<1d46693f9d74064a63d906637b20c108>> */ /** @@ -24,6 +24,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces private var cdpInteractionMetricsEnabledCache: Boolean? = null private var cxxNativeAnimatedEnabledCache: Boolean? = null private var defaultTextToOverflowHiddenCache: Boolean? = null + private var deferTextInputTruncationIOSCache: Boolean? = null private var disableEarlyViewCommandExecutionCache: Boolean? = null private var disableIdleEventDispatchFrameCallbackRearmAndroidCache: Boolean? = null private var disableIdleMountItemFrameCallbackRearmAndroidCache: Boolean? = null @@ -151,6 +152,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun deferTextInputTruncationIOS(): Boolean { + var cached = deferTextInputTruncationIOSCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.deferTextInputTruncationIOS() + deferTextInputTruncationIOSCache = cached + } + return cached + } + override fun disableEarlyViewCommandExecution(): Boolean { var cached = disableEarlyViewCommandExecutionCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt index 6f83f1cc3dbd..9f04d2296568 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt @@ -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<<312cec99bd429d22e25f4e82d7fc4ee7>> + * @generated SignedSource<> */ /** @@ -36,6 +36,8 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun defaultTextToOverflowHidden(): Boolean + @DoNotStrip @JvmStatic public external fun deferTextInputTruncationIOS(): Boolean + @DoNotStrip @JvmStatic public external fun disableEarlyViewCommandExecution(): Boolean @DoNotStrip @JvmStatic public external fun disableIdleEventDispatchFrameCallbackRearmAndroid(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt index f4998cb8b416..58a5f61e0b1d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt @@ -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<<669f16e131d4a5ddc026ffecc6893575>> + * @generated SignedSource<> */ /** @@ -31,6 +31,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun defaultTextToOverflowHidden(): Boolean = true + override fun deferTextInputTruncationIOS(): Boolean = false + override fun disableEarlyViewCommandExecution(): Boolean = false override fun disableIdleEventDispatchFrameCallbackRearmAndroid(): Boolean = false diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt index 536ee78905d7..1cbce18ff8a7 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt @@ -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<> + * @generated SignedSource<<8ac941ef9289ab543d0adca8bf06a28d>> */ /** @@ -28,6 +28,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc private var cdpInteractionMetricsEnabledCache: Boolean? = null private var cxxNativeAnimatedEnabledCache: Boolean? = null private var defaultTextToOverflowHiddenCache: Boolean? = null + private var deferTextInputTruncationIOSCache: Boolean? = null private var disableEarlyViewCommandExecutionCache: Boolean? = null private var disableIdleEventDispatchFrameCallbackRearmAndroidCache: Boolean? = null private var disableIdleMountItemFrameCallbackRearmAndroidCache: Boolean? = null @@ -159,6 +160,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc return cached } + override fun deferTextInputTruncationIOS(): Boolean { + var cached = deferTextInputTruncationIOSCache + if (cached == null) { + cached = currentProvider.deferTextInputTruncationIOS() + accessedFeatureFlags.add("deferTextInputTruncationIOS") + deferTextInputTruncationIOSCache = cached + } + return cached + } + override fun disableEarlyViewCommandExecution(): Boolean { var cached = disableEarlyViewCommandExecutionCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt index 0143013e3ccd..6249493505e7 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt @@ -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<<18267103ef4f3eb22f23a15b808528c2>> + * @generated SignedSource<> */ /** @@ -31,6 +31,8 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun defaultTextToOverflowHidden(): Boolean + @DoNotStrip public fun deferTextInputTruncationIOS(): Boolean + @DoNotStrip public fun disableEarlyViewCommandExecution(): Boolean @DoNotStrip public fun disableIdleEventDispatchFrameCallbackRearmAndroid(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp index 526398c18c11..59bd4817a058 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp @@ -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<<568d6e4175fb51e09bc3d4441ffa9741>> + * @generated SignedSource<> */ /** @@ -63,6 +63,12 @@ class ReactNativeFeatureFlagsJavaProvider return method(javaProvider_); } + bool deferTextInputTruncationIOS() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("deferTextInputTruncationIOS"); + return method(javaProvider_); + } + bool disableEarlyViewCommandExecution() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("disableEarlyViewCommandExecution"); @@ -627,6 +633,11 @@ bool JReactNativeFeatureFlagsCxxInterop::defaultTextToOverflowHidden( return ReactNativeFeatureFlags::defaultTextToOverflowHidden(); } +bool JReactNativeFeatureFlagsCxxInterop::deferTextInputTruncationIOS( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::deferTextInputTruncationIOS(); +} + bool JReactNativeFeatureFlagsCxxInterop::disableEarlyViewCommandExecution( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::disableEarlyViewCommandExecution(); @@ -1120,6 +1131,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "defaultTextToOverflowHidden", JReactNativeFeatureFlagsCxxInterop::defaultTextToOverflowHidden), + makeNativeMethod( + "deferTextInputTruncationIOS", + JReactNativeFeatureFlagsCxxInterop::deferTextInputTruncationIOS), makeNativeMethod( "disableEarlyViewCommandExecution", JReactNativeFeatureFlagsCxxInterop::disableEarlyViewCommandExecution), diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h index 5f3fdda386cd..16ef77189754 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h @@ -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<<2e0f936c003aafc456ab3f960ca4b2f3>> + * @generated SignedSource<<997a74618b4a955bce1ac6ef14b203aa>> */ /** @@ -42,6 +42,9 @@ class JReactNativeFeatureFlagsCxxInterop static bool defaultTextToOverflowHidden( facebook::jni::alias_ref); + static bool deferTextInputTruncationIOS( + facebook::jni::alias_ref); + static bool disableEarlyViewCommandExecution( facebook::jni::alias_ref); diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp index adc3531518ef..eb91ccba86e9 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp @@ -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<> + * @generated SignedSource<> */ /** @@ -42,6 +42,10 @@ bool ReactNativeFeatureFlags::defaultTextToOverflowHidden() { return getAccessor().defaultTextToOverflowHidden(); } +bool ReactNativeFeatureFlags::deferTextInputTruncationIOS() { + return getAccessor().deferTextInputTruncationIOS(); +} + bool ReactNativeFeatureFlags::disableEarlyViewCommandExecution() { return getAccessor().disableEarlyViewCommandExecution(); } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index cd4d40533d5f..40d4982e2329 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h @@ -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<<62dbb584d6aaf595314636a81346e3e5>> + * @generated SignedSource<> */ /** @@ -61,6 +61,11 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool defaultTextToOverflowHidden(); + /** + * When enabled, iOS TextInput applies edits that JS transforms (e.g. maxLength truncation) incrementally after the UIKit text change delegate call returns, avoiding synchronous text and selection rewrites that can deadlock UIKeyboardTaskQueue. + */ + RN_EXPORT static bool deferTextInputTruncationIOS(); + /** * Dispatch view commands in mount item order. */ diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp index 93ef1cc62ace..01d45b69a439 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp @@ -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<> + * @generated SignedSource<<4b468988f7bd48db2483c8f97a29dae2>> */ /** @@ -101,6 +101,24 @@ bool ReactNativeFeatureFlagsAccessor::defaultTextToOverflowHidden() { return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::deferTextInputTruncationIOS() { + auto flagValue = deferTextInputTruncationIOS_.load(); + + if (!flagValue.has_value()) { + // This block is not exclusive but it is not necessary. + // If multiple threads try to initialize the feature flag, we would only + // be accessing the provider multiple times but the end state of this + // instance and the returned flag value would be the same. + + markFlagAsAccessed(4, "deferTextInputTruncationIOS"); + + flagValue = currentProvider_->deferTextInputTruncationIOS(); + deferTextInputTruncationIOS_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::disableEarlyViewCommandExecution() { auto flagValue = disableEarlyViewCommandExecution_.load(); @@ -110,7 +128,7 @@ bool ReactNativeFeatureFlagsAccessor::disableEarlyViewCommandExecution() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(4, "disableEarlyViewCommandExecution"); + markFlagAsAccessed(5, "disableEarlyViewCommandExecution"); flagValue = currentProvider_->disableEarlyViewCommandExecution(); disableEarlyViewCommandExecution_ = flagValue; @@ -128,7 +146,7 @@ bool ReactNativeFeatureFlagsAccessor::disableIdleEventDispatchFrameCallbackRearm // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(5, "disableIdleEventDispatchFrameCallbackRearmAndroid"); + markFlagAsAccessed(6, "disableIdleEventDispatchFrameCallbackRearmAndroid"); flagValue = currentProvider_->disableIdleEventDispatchFrameCallbackRearmAndroid(); disableIdleEventDispatchFrameCallbackRearmAndroid_ = flagValue; @@ -146,7 +164,7 @@ bool ReactNativeFeatureFlagsAccessor::disableIdleMountItemFrameCallbackRearmAndr // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(6, "disableIdleMountItemFrameCallbackRearmAndroid"); + markFlagAsAccessed(7, "disableIdleMountItemFrameCallbackRearmAndroid"); flagValue = currentProvider_->disableIdleMountItemFrameCallbackRearmAndroid(); disableIdleMountItemFrameCallbackRearmAndroid_ = flagValue; @@ -164,7 +182,7 @@ bool ReactNativeFeatureFlagsAccessor::disableImageViewPreallocationAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(7, "disableImageViewPreallocationAndroid"); + markFlagAsAccessed(8, "disableImageViewPreallocationAndroid"); flagValue = currentProvider_->disableImageViewPreallocationAndroid(); disableImageViewPreallocationAndroid_ = flagValue; @@ -182,7 +200,7 @@ bool ReactNativeFeatureFlagsAccessor::disableMountItemReorderingAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(8, "disableMountItemReorderingAndroid"); + markFlagAsAccessed(9, "disableMountItemReorderingAndroid"); flagValue = currentProvider_->disableMountItemReorderingAndroid(); disableMountItemReorderingAndroid_ = flagValue; @@ -200,7 +218,7 @@ bool ReactNativeFeatureFlagsAccessor::disableSubviewClippingAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(9, "disableSubviewClippingAndroid"); + markFlagAsAccessed(10, "disableSubviewClippingAndroid"); flagValue = currentProvider_->disableSubviewClippingAndroid(); disableSubviewClippingAndroid_ = flagValue; @@ -218,7 +236,7 @@ bool ReactNativeFeatureFlagsAccessor::disableTextLayoutManagerCacheAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(10, "disableTextLayoutManagerCacheAndroid"); + markFlagAsAccessed(11, "disableTextLayoutManagerCacheAndroid"); flagValue = currentProvider_->disableTextLayoutManagerCacheAndroid(); disableTextLayoutManagerCacheAndroid_ = flagValue; @@ -236,7 +254,7 @@ bool ReactNativeFeatureFlagsAccessor::disableViewPreallocationAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(11, "disableViewPreallocationAndroid"); + markFlagAsAccessed(12, "disableViewPreallocationAndroid"); flagValue = currentProvider_->disableViewPreallocationAndroid(); disableViewPreallocationAndroid_ = flagValue; @@ -254,7 +272,7 @@ bool ReactNativeFeatureFlagsAccessor::enableAccessibilityOrder() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(12, "enableAccessibilityOrder"); + markFlagAsAccessed(13, "enableAccessibilityOrder"); flagValue = currentProvider_->enableAccessibilityOrder(); enableAccessibilityOrder_ = flagValue; @@ -272,7 +290,7 @@ bool ReactNativeFeatureFlagsAccessor::enableAccumulatedUpdatesInRawPropsAndroid( // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(13, "enableAccumulatedUpdatesInRawPropsAndroid"); + markFlagAsAccessed(14, "enableAccumulatedUpdatesInRawPropsAndroid"); flagValue = currentProvider_->enableAccumulatedUpdatesInRawPropsAndroid(); enableAccumulatedUpdatesInRawPropsAndroid_ = flagValue; @@ -290,7 +308,7 @@ bool ReactNativeFeatureFlagsAccessor::enableAndroidAutoOffscreenCompositingForEl // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(14, "enableAndroidAutoOffscreenCompositingForElevation"); + markFlagAsAccessed(15, "enableAndroidAutoOffscreenCompositingForElevation"); flagValue = currentProvider_->enableAndroidAutoOffscreenCompositingForElevation(); enableAndroidAutoOffscreenCompositingForElevation_ = flagValue; @@ -308,7 +326,7 @@ bool ReactNativeFeatureFlagsAccessor::enableAndroidTextMeasurementOptimizations( // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(15, "enableAndroidTextMeasurementOptimizations"); + markFlagAsAccessed(16, "enableAndroidTextMeasurementOptimizations"); flagValue = currentProvider_->enableAndroidTextMeasurementOptimizations(); enableAndroidTextMeasurementOptimizations_ = flagValue; @@ -326,7 +344,7 @@ bool ReactNativeFeatureFlagsAccessor::enableAsyncDiskCacheCheckInMultiSourceImag // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(16, "enableAsyncDiskCacheCheckInMultiSourceImageAndroid"); + markFlagAsAccessed(17, "enableAsyncDiskCacheCheckInMultiSourceImageAndroid"); flagValue = currentProvider_->enableAsyncDiskCacheCheckInMultiSourceImageAndroid(); enableAsyncDiskCacheCheckInMultiSourceImageAndroid_ = flagValue; @@ -344,7 +362,7 @@ bool ReactNativeFeatureFlagsAccessor::enableBridgelessArchitecture() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(17, "enableBridgelessArchitecture"); + markFlagAsAccessed(18, "enableBridgelessArchitecture"); flagValue = currentProvider_->enableBridgelessArchitecture(); enableBridgelessArchitecture_ = flagValue; @@ -362,7 +380,7 @@ bool ReactNativeFeatureFlagsAccessor::enableCppPropsIteratorSetter() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(18, "enableCppPropsIteratorSetter"); + markFlagAsAccessed(19, "enableCppPropsIteratorSetter"); flagValue = currentProvider_->enableCppPropsIteratorSetter(); enableCppPropsIteratorSetter_ = flagValue; @@ -380,7 +398,7 @@ bool ReactNativeFeatureFlagsAccessor::enableCustomFocusSearchOnClippedElementsAn // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(19, "enableCustomFocusSearchOnClippedElementsAndroid"); + markFlagAsAccessed(20, "enableCustomFocusSearchOnClippedElementsAndroid"); flagValue = currentProvider_->enableCustomFocusSearchOnClippedElementsAndroid(); enableCustomFocusSearchOnClippedElementsAndroid_ = flagValue; @@ -398,7 +416,7 @@ bool ReactNativeFeatureFlagsAccessor::enableDestroyShadowTreeRevisionAsync() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(20, "enableDestroyShadowTreeRevisionAsync"); + markFlagAsAccessed(21, "enableDestroyShadowTreeRevisionAsync"); flagValue = currentProvider_->enableDestroyShadowTreeRevisionAsync(); enableDestroyShadowTreeRevisionAsync_ = flagValue; @@ -416,7 +434,7 @@ bool ReactNativeFeatureFlagsAccessor::enableDoubleMeasurementFixAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(21, "enableDoubleMeasurementFixAndroid"); + markFlagAsAccessed(22, "enableDoubleMeasurementFixAndroid"); flagValue = currentProvider_->enableDoubleMeasurementFixAndroid(); enableDoubleMeasurementFixAndroid_ = flagValue; @@ -434,7 +452,7 @@ bool ReactNativeFeatureFlagsAccessor::enableEagerRootViewAttachment() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(22, "enableEagerRootViewAttachment"); + markFlagAsAccessed(23, "enableEagerRootViewAttachment"); flagValue = currentProvider_->enableEagerRootViewAttachment(); enableEagerRootViewAttachment_ = flagValue; @@ -452,7 +470,7 @@ bool ReactNativeFeatureFlagsAccessor::enableExclusivePropsUpdateAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(23, "enableExclusivePropsUpdateAndroid"); + markFlagAsAccessed(24, "enableExclusivePropsUpdateAndroid"); flagValue = currentProvider_->enableExclusivePropsUpdateAndroid(); enableExclusivePropsUpdateAndroid_ = flagValue; @@ -470,7 +488,7 @@ bool ReactNativeFeatureFlagsAccessor::enableFabricCommitBranching() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(24, "enableFabricCommitBranching"); + markFlagAsAccessed(25, "enableFabricCommitBranching"); flagValue = currentProvider_->enableFabricCommitBranching(); enableFabricCommitBranching_ = flagValue; @@ -488,7 +506,7 @@ bool ReactNativeFeatureFlagsAccessor::enableFabricCommitBranchingMergeOnMainThre // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(25, "enableFabricCommitBranchingMergeOnMainThread"); + markFlagAsAccessed(26, "enableFabricCommitBranchingMergeOnMainThread"); flagValue = currentProvider_->enableFabricCommitBranchingMergeOnMainThread(); enableFabricCommitBranchingMergeOnMainThread_ = flagValue; @@ -506,7 +524,7 @@ bool ReactNativeFeatureFlagsAccessor::enableFabricLogs() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(26, "enableFabricLogs"); + markFlagAsAccessed(27, "enableFabricLogs"); flagValue = currentProvider_->enableFabricLogs(); enableFabricLogs_ = flagValue; @@ -524,7 +542,7 @@ bool ReactNativeFeatureFlagsAccessor::enableFlexboxAutoMinSizeInStrictMode() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(27, "enableFlexboxAutoMinSizeInStrictMode"); + markFlagAsAccessed(28, "enableFlexboxAutoMinSizeInStrictMode"); flagValue = currentProvider_->enableFlexboxAutoMinSizeInStrictMode(); enableFlexboxAutoMinSizeInStrictMode_ = flagValue; @@ -542,7 +560,7 @@ bool ReactNativeFeatureFlagsAccessor::enableFontScaleChangesUpdatingLayout() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(28, "enableFontScaleChangesUpdatingLayout"); + markFlagAsAccessed(29, "enableFontScaleChangesUpdatingLayout"); flagValue = currentProvider_->enableFontScaleChangesUpdatingLayout(); enableFontScaleChangesUpdatingLayout_ = flagValue; @@ -560,7 +578,7 @@ bool ReactNativeFeatureFlagsAccessor::enableIOSCompressedTextFrameAdjustment() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(29, "enableIOSCompressedTextFrameAdjustment"); + markFlagAsAccessed(30, "enableIOSCompressedTextFrameAdjustment"); flagValue = currentProvider_->enableIOSCompressedTextFrameAdjustment(); enableIOSCompressedTextFrameAdjustment_ = flagValue; @@ -578,7 +596,7 @@ bool ReactNativeFeatureFlagsAccessor::enableIOSTextBaselineOffsetPerLine() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(30, "enableIOSTextBaselineOffsetPerLine"); + markFlagAsAccessed(31, "enableIOSTextBaselineOffsetPerLine"); flagValue = currentProvider_->enableIOSTextBaselineOffsetPerLine(); enableIOSTextBaselineOffsetPerLine_ = flagValue; @@ -596,7 +614,7 @@ bool ReactNativeFeatureFlagsAccessor::enableIOSViewClipToPaddingBox() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(31, "enableIOSViewClipToPaddingBox"); + markFlagAsAccessed(32, "enableIOSViewClipToPaddingBox"); flagValue = currentProvider_->enableIOSViewClipToPaddingBox(); enableIOSViewClipToPaddingBox_ = flagValue; @@ -614,7 +632,7 @@ bool ReactNativeFeatureFlagsAccessor::enableImagePrefetchingAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(32, "enableImagePrefetchingAndroid"); + markFlagAsAccessed(33, "enableImagePrefetchingAndroid"); flagValue = currentProvider_->enableImagePrefetchingAndroid(); enableImagePrefetchingAndroid_ = flagValue; @@ -632,7 +650,7 @@ bool ReactNativeFeatureFlagsAccessor::enableImageTransparentTintColor() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(33, "enableImageTransparentTintColor"); + markFlagAsAccessed(34, "enableImageTransparentTintColor"); flagValue = currentProvider_->enableImageTransparentTintColor(); enableImageTransparentTintColor_ = flagValue; @@ -650,7 +668,7 @@ bool ReactNativeFeatureFlagsAccessor::enableImmediateUpdateModeForContentOffsetC // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(34, "enableImmediateUpdateModeForContentOffsetChanges"); + markFlagAsAccessed(35, "enableImmediateUpdateModeForContentOffsetChanges"); flagValue = currentProvider_->enableImmediateUpdateModeForContentOffsetChanges(); enableImmediateUpdateModeForContentOffsetChanges_ = flagValue; @@ -668,7 +686,7 @@ bool ReactNativeFeatureFlagsAccessor::enableImperativeEvents() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(35, "enableImperativeEvents"); + markFlagAsAccessed(36, "enableImperativeEvents"); flagValue = currentProvider_->enableImperativeEvents(); enableImperativeEvents_ = flagValue; @@ -686,7 +704,7 @@ bool ReactNativeFeatureFlagsAccessor::enableImperativeFocus() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(36, "enableImperativeFocus"); + markFlagAsAccessed(37, "enableImperativeFocus"); flagValue = currentProvider_->enableImperativeFocus(); enableImperativeFocus_ = flagValue; @@ -704,7 +722,7 @@ bool ReactNativeFeatureFlagsAccessor::enableInteropViewManagerClassLookUpOptimiz // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(37, "enableInteropViewManagerClassLookUpOptimizationIOS"); + markFlagAsAccessed(38, "enableInteropViewManagerClassLookUpOptimizationIOS"); flagValue = currentProvider_->enableInteropViewManagerClassLookUpOptimizationIOS(); enableInteropViewManagerClassLookUpOptimizationIOS_ = flagValue; @@ -722,7 +740,7 @@ bool ReactNativeFeatureFlagsAccessor::enableIntersectionObserverByDefault() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(38, "enableIntersectionObserverByDefault"); + markFlagAsAccessed(39, "enableIntersectionObserverByDefault"); flagValue = currentProvider_->enableIntersectionObserverByDefault(); enableIntersectionObserverByDefault_ = flagValue; @@ -740,7 +758,7 @@ bool ReactNativeFeatureFlagsAccessor::enableKeyEvents() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(39, "enableKeyEvents"); + markFlagAsAccessed(40, "enableKeyEvents"); flagValue = currentProvider_->enableKeyEvents(); enableKeyEvents_ = flagValue; @@ -758,7 +776,7 @@ bool ReactNativeFeatureFlagsAccessor::enableLayoutAnimationsOnAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(40, "enableLayoutAnimationsOnAndroid"); + markFlagAsAccessed(41, "enableLayoutAnimationsOnAndroid"); flagValue = currentProvider_->enableLayoutAnimationsOnAndroid(); enableLayoutAnimationsOnAndroid_ = flagValue; @@ -776,7 +794,7 @@ bool ReactNativeFeatureFlagsAccessor::enableLayoutAnimationsOnIOS() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(41, "enableLayoutAnimationsOnIOS"); + markFlagAsAccessed(42, "enableLayoutAnimationsOnIOS"); flagValue = currentProvider_->enableLayoutAnimationsOnIOS(); enableLayoutAnimationsOnIOS_ = flagValue; @@ -794,7 +812,7 @@ bool ReactNativeFeatureFlagsAccessor::enableModuleArgumentNSNullConversionIOS() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(42, "enableModuleArgumentNSNullConversionIOS"); + markFlagAsAccessed(43, "enableModuleArgumentNSNullConversionIOS"); flagValue = currentProvider_->enableModuleArgumentNSNullConversionIOS(); enableModuleArgumentNSNullConversionIOS_ = flagValue; @@ -812,7 +830,7 @@ bool ReactNativeFeatureFlagsAccessor::enableMountingCoordinatorPullModelAndroid( // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(43, "enableMountingCoordinatorPullModelAndroid"); + markFlagAsAccessed(44, "enableMountingCoordinatorPullModelAndroid"); flagValue = currentProvider_->enableMountingCoordinatorPullModelAndroid(); enableMountingCoordinatorPullModelAndroid_ = flagValue; @@ -830,7 +848,7 @@ bool ReactNativeFeatureFlagsAccessor::enableMutationObserverByDefault() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(44, "enableMutationObserverByDefault"); + markFlagAsAccessed(45, "enableMutationObserverByDefault"); flagValue = currentProvider_->enableMutationObserverByDefault(); enableMutationObserverByDefault_ = flagValue; @@ -848,7 +866,7 @@ bool ReactNativeFeatureFlagsAccessor::enableNativeCSSParsing() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(45, "enableNativeCSSParsing"); + markFlagAsAccessed(46, "enableNativeCSSParsing"); flagValue = currentProvider_->enableNativeCSSParsing(); enableNativeCSSParsing_ = flagValue; @@ -866,7 +884,7 @@ bool ReactNativeFeatureFlagsAccessor::enablePreallocatedPropsDiffOnInsertAndroid // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(46, "enablePreallocatedPropsDiffOnInsertAndroid"); + markFlagAsAccessed(47, "enablePreallocatedPropsDiffOnInsertAndroid"); flagValue = currentProvider_->enablePreallocatedPropsDiffOnInsertAndroid(); enablePreallocatedPropsDiffOnInsertAndroid_ = flagValue; @@ -884,7 +902,7 @@ bool ReactNativeFeatureFlagsAccessor::enablePreparedTextLayout() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(47, "enablePreparedTextLayout"); + markFlagAsAccessed(48, "enablePreparedTextLayout"); flagValue = currentProvider_->enablePreparedTextLayout(); enablePreparedTextLayout_ = flagValue; @@ -902,7 +920,7 @@ bool ReactNativeFeatureFlagsAccessor::enablePropsUpdateReconciliationAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(48, "enablePropsUpdateReconciliationAndroid"); + markFlagAsAccessed(49, "enablePropsUpdateReconciliationAndroid"); flagValue = currentProvider_->enablePropsUpdateReconciliationAndroid(); enablePropsUpdateReconciliationAndroid_ = flagValue; @@ -920,7 +938,7 @@ bool ReactNativeFeatureFlagsAccessor::enableResizeObserverByDefault() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(49, "enableResizeObserverByDefault"); + markFlagAsAccessed(50, "enableResizeObserverByDefault"); flagValue = currentProvider_->enableResizeObserverByDefault(); enableResizeObserverByDefault_ = flagValue; @@ -938,7 +956,7 @@ bool ReactNativeFeatureFlagsAccessor::enableSwiftUIBasedFilters() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(50, "enableSwiftUIBasedFilters"); + markFlagAsAccessed(51, "enableSwiftUIBasedFilters"); flagValue = currentProvider_->enableSwiftUIBasedFilters(); enableSwiftUIBasedFilters_ = flagValue; @@ -956,7 +974,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewCulling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(51, "enableViewCulling"); + markFlagAsAccessed(52, "enableViewCulling"); flagValue = currentProvider_->enableViewCulling(); enableViewCulling_ = flagValue; @@ -974,7 +992,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecycling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(52, "enableViewRecycling"); + markFlagAsAccessed(53, "enableViewRecycling"); flagValue = currentProvider_->enableViewRecycling(); enableViewRecycling_ = flagValue; @@ -992,7 +1010,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForImage() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(53, "enableViewRecyclingForImage"); + markFlagAsAccessed(54, "enableViewRecyclingForImage"); flagValue = currentProvider_->enableViewRecyclingForImage(); enableViewRecyclingForImage_ = flagValue; @@ -1010,7 +1028,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForScrollView() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(54, "enableViewRecyclingForScrollView"); + markFlagAsAccessed(55, "enableViewRecyclingForScrollView"); flagValue = currentProvider_->enableViewRecyclingForScrollView(); enableViewRecyclingForScrollView_ = flagValue; @@ -1028,7 +1046,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForText() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(55, "enableViewRecyclingForText"); + markFlagAsAccessed(56, "enableViewRecyclingForText"); flagValue = currentProvider_->enableViewRecyclingForText(); enableViewRecyclingForText_ = flagValue; @@ -1046,7 +1064,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForView() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(56, "enableViewRecyclingForView"); + markFlagAsAccessed(57, "enableViewRecyclingForView"); flagValue = currentProvider_->enableViewRecyclingForView(); enableViewRecyclingForView_ = flagValue; @@ -1064,7 +1082,7 @@ bool ReactNativeFeatureFlagsAccessor::enableVirtualViewContainerStateExperimenta // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(57, "enableVirtualViewContainerStateExperimental"); + markFlagAsAccessed(58, "enableVirtualViewContainerStateExperimental"); flagValue = currentProvider_->enableVirtualViewContainerStateExperimental(); enableVirtualViewContainerStateExperimental_ = flagValue; @@ -1082,7 +1100,7 @@ bool ReactNativeFeatureFlagsAccessor::fixBorderlessRippleAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(58, "fixBorderlessRippleAndroid"); + markFlagAsAccessed(59, "fixBorderlessRippleAndroid"); flagValue = currentProvider_->fixBorderlessRippleAndroid(); fixBorderlessRippleAndroid_ = flagValue; @@ -1100,7 +1118,7 @@ bool ReactNativeFeatureFlagsAccessor::fixMappingOfEventPrioritiesBetweenFabricAn // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(59, "fixMappingOfEventPrioritiesBetweenFabricAndReact"); + markFlagAsAccessed(60, "fixMappingOfEventPrioritiesBetweenFabricAndReact"); flagValue = currentProvider_->fixMappingOfEventPrioritiesBetweenFabricAndReact(); fixMappingOfEventPrioritiesBetweenFabricAndReact_ = flagValue; @@ -1118,7 +1136,7 @@ bool ReactNativeFeatureFlagsAccessor::fixTextLayoutOnPixelDensityChange() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(60, "fixTextLayoutOnPixelDensityChange"); + markFlagAsAccessed(61, "fixTextLayoutOnPixelDensityChange"); flagValue = currentProvider_->fixTextLayoutOnPixelDensityChange(); fixTextLayoutOnPixelDensityChange_ = flagValue; @@ -1136,7 +1154,7 @@ bool ReactNativeFeatureFlagsAccessor::fixYogaFlexBasisFitContentInMainAxis() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(61, "fixYogaFlexBasisFitContentInMainAxis"); + markFlagAsAccessed(62, "fixYogaFlexBasisFitContentInMainAxis"); flagValue = currentProvider_->fixYogaFlexBasisFitContentInMainAxis(); fixYogaFlexBasisFitContentInMainAxis_ = flagValue; @@ -1154,7 +1172,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxAssertSingleHostState() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(62, "fuseboxAssertSingleHostState"); + markFlagAsAccessed(63, "fuseboxAssertSingleHostState"); flagValue = currentProvider_->fuseboxAssertSingleHostState(); fuseboxAssertSingleHostState_ = flagValue; @@ -1172,7 +1190,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxEnabledRelease() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(63, "fuseboxEnabledRelease"); + markFlagAsAccessed(64, "fuseboxEnabledRelease"); flagValue = currentProvider_->fuseboxEnabledRelease(); fuseboxEnabledRelease_ = flagValue; @@ -1190,7 +1208,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxFrameRecordingEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(64, "fuseboxFrameRecordingEnabled"); + markFlagAsAccessed(65, "fuseboxFrameRecordingEnabled"); flagValue = currentProvider_->fuseboxFrameRecordingEnabled(); fuseboxFrameRecordingEnabled_ = flagValue; @@ -1208,7 +1226,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxScreenshotCaptureEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(65, "fuseboxScreenshotCaptureEnabled"); + markFlagAsAccessed(66, "fuseboxScreenshotCaptureEnabled"); flagValue = currentProvider_->fuseboxScreenshotCaptureEnabled(); fuseboxScreenshotCaptureEnabled_ = flagValue; @@ -1226,7 +1244,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxWebSocketEventsEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(66, "fuseboxWebSocketEventsEnabled"); + markFlagAsAccessed(67, "fuseboxWebSocketEventsEnabled"); flagValue = currentProvider_->fuseboxWebSocketEventsEnabled(); fuseboxWebSocketEventsEnabled_ = flagValue; @@ -1244,7 +1262,7 @@ bool ReactNativeFeatureFlagsAccessor::optimizedAnimatedPropUpdates() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(67, "optimizedAnimatedPropUpdates"); + markFlagAsAccessed(68, "optimizedAnimatedPropUpdates"); flagValue = currentProvider_->optimizedAnimatedPropUpdates(); optimizedAnimatedPropUpdates_ = flagValue; @@ -1262,7 +1280,7 @@ bool ReactNativeFeatureFlagsAccessor::overrideBySynchronousMountPropsAtMountingA // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(68, "overrideBySynchronousMountPropsAtMountingAndroid"); + markFlagAsAccessed(69, "overrideBySynchronousMountPropsAtMountingAndroid"); flagValue = currentProvider_->overrideBySynchronousMountPropsAtMountingAndroid(); overrideBySynchronousMountPropsAtMountingAndroid_ = flagValue; @@ -1280,7 +1298,7 @@ bool ReactNativeFeatureFlagsAccessor::perfIssuesEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(69, "perfIssuesEnabled"); + markFlagAsAccessed(70, "perfIssuesEnabled"); flagValue = currentProvider_->perfIssuesEnabled(); perfIssuesEnabled_ = flagValue; @@ -1298,7 +1316,7 @@ bool ReactNativeFeatureFlagsAccessor::perfMonitorV2Enabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(70, "perfMonitorV2Enabled"); + markFlagAsAccessed(71, "perfMonitorV2Enabled"); flagValue = currentProvider_->perfMonitorV2Enabled(); perfMonitorV2Enabled_ = flagValue; @@ -1316,7 +1334,7 @@ double ReactNativeFeatureFlagsAccessor::preparedTextCacheSize() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(71, "preparedTextCacheSize"); + markFlagAsAccessed(72, "preparedTextCacheSize"); flagValue = currentProvider_->preparedTextCacheSize(); preparedTextCacheSize_ = flagValue; @@ -1334,7 +1352,7 @@ bool ReactNativeFeatureFlagsAccessor::preventShadowTreeCommitExhaustion() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(72, "preventShadowTreeCommitExhaustion"); + markFlagAsAccessed(73, "preventShadowTreeCommitExhaustion"); flagValue = currentProvider_->preventShadowTreeCommitExhaustion(); preventShadowTreeCommitExhaustion_ = flagValue; @@ -1352,7 +1370,7 @@ bool ReactNativeFeatureFlagsAccessor::redBoxV2Android() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(73, "redBoxV2Android"); + markFlagAsAccessed(74, "redBoxV2Android"); flagValue = currentProvider_->redBoxV2Android(); redBoxV2Android_ = flagValue; @@ -1370,7 +1388,7 @@ bool ReactNativeFeatureFlagsAccessor::redBoxV2IOS() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(74, "redBoxV2IOS"); + markFlagAsAccessed(75, "redBoxV2IOS"); flagValue = currentProvider_->redBoxV2IOS(); redBoxV2IOS_ = flagValue; @@ -1388,7 +1406,7 @@ bool ReactNativeFeatureFlagsAccessor::shouldPressibilityUseW3CPointerEventsForHo // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(75, "shouldPressibilityUseW3CPointerEventsForHover"); + markFlagAsAccessed(76, "shouldPressibilityUseW3CPointerEventsForHover"); flagValue = currentProvider_->shouldPressibilityUseW3CPointerEventsForHover(); shouldPressibilityUseW3CPointerEventsForHover_ = flagValue; @@ -1406,7 +1424,7 @@ bool ReactNativeFeatureFlagsAccessor::shouldTriggerResponderTransferOnScrollAndr // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(76, "shouldTriggerResponderTransferOnScrollAndroid"); + markFlagAsAccessed(77, "shouldTriggerResponderTransferOnScrollAndroid"); flagValue = currentProvider_->shouldTriggerResponderTransferOnScrollAndroid(); shouldTriggerResponderTransferOnScrollAndroid_ = flagValue; @@ -1424,7 +1442,7 @@ bool ReactNativeFeatureFlagsAccessor::skipActivityIdentityAssertionOnHostPause() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(77, "skipActivityIdentityAssertionOnHostPause"); + markFlagAsAccessed(78, "skipActivityIdentityAssertionOnHostPause"); flagValue = currentProvider_->skipActivityIdentityAssertionOnHostPause(); skipActivityIdentityAssertionOnHostPause_ = flagValue; @@ -1442,7 +1460,7 @@ bool ReactNativeFeatureFlagsAccessor::syncAndroidClipBoundsWithOverflow() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(78, "syncAndroidClipBoundsWithOverflow"); + markFlagAsAccessed(79, "syncAndroidClipBoundsWithOverflow"); flagValue = currentProvider_->syncAndroidClipBoundsWithOverflow(); syncAndroidClipBoundsWithOverflow_ = flagValue; @@ -1460,7 +1478,7 @@ bool ReactNativeFeatureFlagsAccessor::traceTurboModulePromiseRejectionsOnAndroid // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(79, "traceTurboModulePromiseRejectionsOnAndroid"); + markFlagAsAccessed(80, "traceTurboModulePromiseRejectionsOnAndroid"); flagValue = currentProvider_->traceTurboModulePromiseRejectionsOnAndroid(); traceTurboModulePromiseRejectionsOnAndroid_ = flagValue; @@ -1478,7 +1496,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommit( // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(80, "updateRuntimeShadowNodeReferencesOnCommit"); + markFlagAsAccessed(81, "updateRuntimeShadowNodeReferencesOnCommit"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommit(); updateRuntimeShadowNodeReferencesOnCommit_ = flagValue; @@ -1496,7 +1514,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommitT // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(81, "updateRuntimeShadowNodeReferencesOnCommitThread"); + markFlagAsAccessed(82, "updateRuntimeShadowNodeReferencesOnCommitThread"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommitThread(); updateRuntimeShadowNodeReferencesOnCommitThread_ = flagValue; @@ -1514,7 +1532,7 @@ bool ReactNativeFeatureFlagsAccessor::useAlwaysAvailableJSErrorHandling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(82, "useAlwaysAvailableJSErrorHandling"); + markFlagAsAccessed(83, "useAlwaysAvailableJSErrorHandling"); flagValue = currentProvider_->useAlwaysAvailableJSErrorHandling(); useAlwaysAvailableJSErrorHandling_ = flagValue; @@ -1532,7 +1550,7 @@ bool ReactNativeFeatureFlagsAccessor::useFabricInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(83, "useFabricInterop"); + markFlagAsAccessed(84, "useFabricInterop"); flagValue = currentProvider_->useFabricInterop(); useFabricInterop_ = flagValue; @@ -1550,7 +1568,7 @@ bool ReactNativeFeatureFlagsAccessor::useNativeViewConfigsInBridgelessMode() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(84, "useNativeViewConfigsInBridgelessMode"); + markFlagAsAccessed(85, "useNativeViewConfigsInBridgelessMode"); flagValue = currentProvider_->useNativeViewConfigsInBridgelessMode(); useNativeViewConfigsInBridgelessMode_ = flagValue; @@ -1568,7 +1586,7 @@ bool ReactNativeFeatureFlagsAccessor::useNestedScrollViewAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(85, "useNestedScrollViewAndroid"); + markFlagAsAccessed(86, "useNestedScrollViewAndroid"); flagValue = currentProvider_->useNestedScrollViewAndroid(); useNestedScrollViewAndroid_ = flagValue; @@ -1586,7 +1604,7 @@ bool ReactNativeFeatureFlagsAccessor::useSharedAnimatedBackend() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(86, "useSharedAnimatedBackend"); + markFlagAsAccessed(87, "useSharedAnimatedBackend"); flagValue = currentProvider_->useSharedAnimatedBackend(); useSharedAnimatedBackend_ = flagValue; @@ -1604,7 +1622,7 @@ bool ReactNativeFeatureFlagsAccessor::useTraitHiddenOnAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(87, "useTraitHiddenOnAndroid"); + markFlagAsAccessed(88, "useTraitHiddenOnAndroid"); flagValue = currentProvider_->useTraitHiddenOnAndroid(); useTraitHiddenOnAndroid_ = flagValue; @@ -1622,7 +1640,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModuleInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(88, "useTurboModuleInterop"); + markFlagAsAccessed(89, "useTurboModuleInterop"); flagValue = currentProvider_->useTurboModuleInterop(); useTurboModuleInterop_ = flagValue; @@ -1640,7 +1658,7 @@ bool ReactNativeFeatureFlagsAccessor::useWindowDensityForDisplayMetricsAndroid() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(89, "useWindowDensityForDisplayMetricsAndroid"); + markFlagAsAccessed(90, "useWindowDensityForDisplayMetricsAndroid"); flagValue = currentProvider_->useWindowDensityForDisplayMetricsAndroid(); useWindowDensityForDisplayMetricsAndroid_ = flagValue; @@ -1658,7 +1676,7 @@ double ReactNativeFeatureFlagsAccessor::viewCullingOutsetRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(90, "viewCullingOutsetRatio"); + markFlagAsAccessed(91, "viewCullingOutsetRatio"); flagValue = currentProvider_->viewCullingOutsetRatio(); viewCullingOutsetRatio_ = flagValue; @@ -1676,7 +1694,7 @@ bool ReactNativeFeatureFlagsAccessor::viewTransitionEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(91, "viewTransitionEnabled"); + markFlagAsAccessed(92, "viewTransitionEnabled"); flagValue = currentProvider_->viewTransitionEnabled(); viewTransitionEnabled_ = flagValue; @@ -1694,7 +1712,7 @@ bool ReactNativeFeatureFlagsAccessor::viewTransitionUseHardwareBitmapAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(92, "viewTransitionUseHardwareBitmapAndroid"); + markFlagAsAccessed(93, "viewTransitionUseHardwareBitmapAndroid"); flagValue = currentProvider_->viewTransitionUseHardwareBitmapAndroid(); viewTransitionUseHardwareBitmapAndroid_ = flagValue; @@ -1712,7 +1730,7 @@ double ReactNativeFeatureFlagsAccessor::virtualViewPrerenderRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(93, "virtualViewPrerenderRatio"); + markFlagAsAccessed(94, "virtualViewPrerenderRatio"); flagValue = currentProvider_->virtualViewPrerenderRatio(); virtualViewPrerenderRatio_ = flagValue; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h index 5c05c3bc5427..c6e752f550c5 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h @@ -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<<074e6ad1d773f289f93ce1d16656ec6e>> + * @generated SignedSource<> */ /** @@ -38,6 +38,7 @@ class ReactNativeFeatureFlagsAccessor { bool cdpInteractionMetricsEnabled(); bool cxxNativeAnimatedEnabled(); bool defaultTextToOverflowHidden(); + bool deferTextInputTruncationIOS(); bool disableEarlyViewCommandExecution(); bool disableIdleEventDispatchFrameCallbackRearmAndroid(); bool disableIdleMountItemFrameCallbackRearmAndroid(); @@ -139,12 +140,13 @@ class ReactNativeFeatureFlagsAccessor { std::unique_ptr currentProvider_; bool wasOverridden_; - std::array, 94> accessedFeatureFlags_; + std::array, 95> accessedFeatureFlags_; std::atomic> commonTestFlag_; std::atomic> cdpInteractionMetricsEnabled_; std::atomic> cxxNativeAnimatedEnabled_; std::atomic> defaultTextToOverflowHidden_; + std::atomic> deferTextInputTruncationIOS_; std::atomic> disableEarlyViewCommandExecution_; std::atomic> disableIdleEventDispatchFrameCallbackRearmAndroid_; std::atomic> disableIdleMountItemFrameCallbackRearmAndroid_; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h index 82208e3861b9..45cda673d3d6 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h @@ -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<<52db1adfed5fd99e4ca145b50dc413a0>> + * @generated SignedSource<<0dd0a7955ed42444db34d10eb27a0df9>> */ /** @@ -45,6 +45,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { return true; } + bool deferTextInputTruncationIOS() override { + return false; + } + bool disableEarlyViewCommandExecution() override { return false; } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h index 4d40ac107fc4..f86b23729943 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h @@ -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<<8d27c5f3feb4c8000922012882701b7e>> + * @generated SignedSource<<80e61e864c897623df27cbf3782db80e>> */ /** @@ -83,6 +83,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef return ReactNativeFeatureFlagsDefaults::defaultTextToOverflowHidden(); } + bool deferTextInputTruncationIOS() override { + auto value = values_["deferTextInputTruncationIOS"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::deferTextInputTruncationIOS(); + } + bool disableEarlyViewCommandExecution() override { auto value = values_["disableEarlyViewCommandExecution"]; if (!value.isNull()) { diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h index 3ba8405c14ff..a9e469bae420 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h @@ -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<<7e5839223cfce81a42708f876c4c77ee>> + * @generated SignedSource<<8d888009df5e5575a257609f6154cc28>> */ /** @@ -31,6 +31,7 @@ class ReactNativeFeatureFlagsProvider { virtual bool cdpInteractionMetricsEnabled() = 0; virtual bool cxxNativeAnimatedEnabled() = 0; virtual bool defaultTextToOverflowHidden() = 0; + virtual bool deferTextInputTruncationIOS() = 0; virtual bool disableEarlyViewCommandExecution() = 0; virtual bool disableIdleEventDispatchFrameCallbackRearmAndroid() = 0; virtual bool disableIdleMountItemFrameCallbackRearmAndroid() = 0; diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp index f2d4210f8d23..0aabe7a8414b 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp @@ -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<> + * @generated SignedSource<<7b114ea4109d87ea3a98f1b7db00a438>> */ /** @@ -64,6 +64,11 @@ bool NativeReactNativeFeatureFlags::defaultTextToOverflowHidden( return ReactNativeFeatureFlags::defaultTextToOverflowHidden(); } +bool NativeReactNativeFeatureFlags::deferTextInputTruncationIOS( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::deferTextInputTruncationIOS(); +} + bool NativeReactNativeFeatureFlags::disableEarlyViewCommandExecution( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::disableEarlyViewCommandExecution(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h index 4aafb51f00fc..0e24bdb37301 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h @@ -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<<84b944e162fc53799326c584a5edd17d>> + * @generated SignedSource<<1defb2de7db2f4393c27075c19292c57>> */ /** @@ -46,6 +46,8 @@ class NativeReactNativeFeatureFlags bool defaultTextToOverflowHidden(jsi::Runtime& runtime); + bool deferTextInputTruncationIOS(jsi::Runtime& runtime); + bool disableEarlyViewCommandExecution(jsi::Runtime& runtime); bool disableIdleEventDispatchFrameCallbackRearmAndroid(jsi::Runtime& runtime); diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index 2c0786728b99..a1fde1a93334 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -93,6 +93,17 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'none', }, + deferTextInputTruncationIOS: { + defaultValue: false, + metadata: { + dateAdded: '2026-10-09', + description: + 'When enabled, iOS TextInput applies edits that JS transforms (e.g. maxLength truncation) incrementally after the UIKit text change delegate call returns, avoiding synchronous text and selection rewrites that can deadlock UIKeyboardTaskQueue.', + expectedReleaseValue: true, + purpose: 'experimentation', + }, + ossReleaseStage: 'none', + }, disableEarlyViewCommandExecution: { defaultValue: false, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index c190a2488b08..2fcc93b19e8f 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<> + * @generated SignedSource<<32ba8f0fb8f711fe496ca1dca67d185a>> * @flow strict * @noformat */ @@ -53,6 +53,7 @@ export type ReactNativeFeatureFlags = Readonly<{ cdpInteractionMetricsEnabled: Getter, cxxNativeAnimatedEnabled: Getter, defaultTextToOverflowHidden: Getter, + deferTextInputTruncationIOS: Getter, disableEarlyViewCommandExecution: Getter, disableIdleEventDispatchFrameCallbackRearmAndroid: Getter, disableIdleMountItemFrameCallbackRearmAndroid: Getter, @@ -235,6 +236,10 @@ export const cxxNativeAnimatedEnabled: Getter = createNativeFlagGetter( * When enabled, sets the default overflow style for Text components to hidden instead of visible. */ export const defaultTextToOverflowHidden: Getter = createNativeFlagGetter('defaultTextToOverflowHidden', true); +/** + * When enabled, iOS TextInput applies edits that JS transforms (e.g. maxLength truncation) incrementally after the UIKit text change delegate call returns, avoiding synchronous text and selection rewrites that can deadlock UIKeyboardTaskQueue. + */ +export const deferTextInputTruncationIOS: Getter = createNativeFlagGetter('deferTextInputTruncationIOS', false); /** * Dispatch view commands in mount item order. */ diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js index 673dc0ed3bf8..1e0d96dcb9d0 100644 --- a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.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<<40cc7b87d7658fb5aa3d33f468eff3fc>> + * @generated SignedSource<<0f38c0e650fa71f73ecfeae721411b7d>> * @flow strict * @noformat */ @@ -29,6 +29,7 @@ export interface Spec extends TurboModule { readonly cdpInteractionMetricsEnabled?: () => boolean; readonly cxxNativeAnimatedEnabled?: () => boolean; readonly defaultTextToOverflowHidden?: () => boolean; + readonly deferTextInputTruncationIOS?: () => boolean; readonly disableEarlyViewCommandExecution?: () => boolean; readonly disableIdleEventDispatchFrameCallbackRearmAndroid?: () => boolean; readonly disableIdleMountItemFrameCallbackRearmAndroid?: () => boolean;