Skip to content

Commit b1e6b97

Browse files
Add undo/redo support for TextInput (#1274)
Setting `allowsUndo = YES` and having the `NSTextView` delegate return a stable `NSUndoManager` mostly makes undo/redo work, but we also needed to register an undo action whenever the input contents was changed to something unexpected by JS. This also breaks undo coalescing in a couple key places to make the input behave naturally like other apps. Co-authored-by: Scott Kyle <[email protected]>
1 parent 46da582 commit b1e6b97

File tree

3 files changed

+32
-2
lines changed

3 files changed

+32
-2
lines changed

Libraries/Text/TextInput/Multiline/RCTUITextView.m

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ - (instancetype)initWithFrame:(CGRect)frame
5151
self.insertionPointColor = [NSColor selectedControlColor];
5252
// Fix blurry text on non-retina displays.
5353
self.canDrawSubviewsIntoLayer = YES;
54+
self.allowsUndo = YES;
5455
#endif // ]TODO(macOS GH#774)
5556

5657
_textInputDelegateAdapter = [[RCTBackedTextViewDelegateAdapter alloc] initWithTextView:self];
@@ -185,6 +186,18 @@ - (BOOL)becomeFirstResponder
185186

186187
return success;
187188
}
189+
190+
- (BOOL)resignFirstResponder
191+
{
192+
BOOL success = [super resignFirstResponder];
193+
194+
if (success) {
195+
// Break undo coalescing when losing focus.
196+
[self breakUndoCoalescing];
197+
}
198+
199+
return success;
200+
}
188201
#endif // ]TODO(macOS GH#774)
189202

190203
- (void)setDefaultTextAttributes:(NSDictionary<NSAttributedStringKey, id> *)defaultTextAttributes
@@ -247,6 +260,9 @@ - (void)setAttributedText:(NSAttributedString *)attributedText
247260
}
248261
#else // [TODO(macOS GH#774)
249262
if (![self.textStorage isEqualTo:attributedText.string]) {
263+
// Break undo coalescing when the text is changed by JS (e.g. autocomplete).
264+
[self breakUndoCoalescing];
265+
250266
if (attributedText != nil) {
251267
[self.textStorage setAttributedString:attributedText];
252268
} else {

Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ @implementation RCTBackedTextViewDelegateAdapter {
263263
UITextRange *_previousSelectedTextRange;
264264
#else // [TODO(macOS GH#774)
265265
NSRange _previousSelectedTextRange;
266+
NSUndoManager *_undoManager;
266267
#endif // ]TODO(macOS GH#774)
267268
}
268269

@@ -428,6 +429,13 @@ - (BOOL)textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector
428429
return commandHandled;
429430
}
430431

432+
- (NSUndoManager *)undoManagerForTextView:(NSTextView *)textView {
433+
if (!_undoManager) {
434+
_undoManager = [NSUndoManager new];
435+
}
436+
return _undoManager;
437+
}
438+
431439
#endif // ]TODO(macOS GH#774)
432440

433441
#pragma mark - Public Interface

Libraries/Text/TextInput/RCTBaseTextInputView.m

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ - (void)setAttributedText:(NSAttributedString *)attributedText
144144
BOOL textNeedsUpdate = NO;
145145
// Remove tag attribute to ensure correct attributed string comparison.
146146
NSMutableAttributedString *const backedTextInputViewTextCopy = [self.backedTextInputView.attributedText mutableCopy];
147-
NSMutableAttributedString *const attributedTextCopy = [attributedText mutableCopy];
147+
NSMutableAttributedString *const attributedTextCopy = [attributedText mutableCopy] ?: [NSMutableAttributedString new];
148148

149149
[backedTextInputViewTextCopy removeAttribute:RCTTextAttributesTagAttributeName
150150
range:NSMakeRange(0, backedTextInputViewTextCopy.length)];
@@ -160,7 +160,13 @@ - (void)setAttributedText:(NSAttributedString *)attributedText
160160
#else // [TODO(macOS GH#774)
161161
NSRange selection = [self.backedTextInputView selectedTextRange];
162162
#endif // ]TODO(macOS GH#774)
163-
NSInteger oldTextLength = self.backedTextInputView.attributedText.string.length;
163+
NSAttributedString *oldAttributedText = [self.backedTextInputView.attributedText copy];
164+
NSInteger oldTextLength = oldAttributedText.string.length;
165+
166+
[self.backedTextInputView.undoManager registerUndoWithTarget:self handler:^(RCTBaseTextInputView *strongSelf) {
167+
strongSelf.attributedText = oldAttributedText;
168+
[strongSelf textInputDidChange];
169+
}];
164170

165171
self.backedTextInputView.attributedText = attributedText;
166172

0 commit comments

Comments
 (0)