not-react-native-macos 0.87.1-rc.7 → 0.87.1-rc.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -142,9 +142,19 @@ typedef NS_ENUM(NSInteger, UIKeyboardAppearance) {
142
142
  - (void)textViewDidBeginEditing:(id)textView;
143
143
  - (void)textViewDidChange:(id)textView;
144
144
  - (void)textViewDidEndEditing:(id)textView;
145
+ - (void)textViewDidChangeSelection:(id)textView;
145
146
  @end
146
147
 
147
- @interface UITextField : NSTextField <UITextInput, UITextInputTraits>
148
+ // The runtime name is deliberately not `UITextField`.
149
+ //
150
+ // The compile-time name is what this shim is for, and @compatibility_alias
151
+ // gives that without registering the name with the ObjC runtime. That matters:
152
+ // several Apple frameworks decide whether a process is Catalyst by asking
153
+ // NSClassFromString for a UIKit class. macOS's one-time-code AutoFill does it
154
+ // for the field that holds focus, and answering yes sends it into
155
+ // UIKitMacHelper, which dlopens a UIKit.framework that does not exist on this
156
+ // platform and takes the process down with it.
157
+ @interface RCTUIKitCompatTextField : NSTextField <UITextInput, UITextInputTraits>
148
158
  @property (nonatomic, assign) UIKeyboardType keyboardType;
149
159
  @property (nonatomic, assign) UIReturnKeyType returnKeyType;
150
160
  @property (nonatomic, assign) UITextAutocapitalizationType autocapitalizationType;
@@ -213,6 +223,7 @@ typedef NS_ENUM(NSInteger, UIKeyboardAppearance) {
213
223
  - (void)removeTarget:(nullable id)target action:(nullable SEL)action forControlEvents:(UIControlEvents)controlEvents;
214
224
  @property (nonatomic, weak, nullable) id<UITextDropDelegate> textDropDelegate;
215
225
  @end
226
+ @compatibility_alias UITextField RCTUIKitCompatTextField;
216
227
 
217
228
  // UIKit's per-rect selection geometry. NSTextView exposes selection as ranges,
218
229
  // so this is a value object the text layer fills in.
@@ -78,8 +78,9 @@
78
78
 
79
79
  @end
80
80
 
81
- @implementation UITextField {
81
+ @implementation RCTUIKitCompatTextField {
82
82
  NSDictionary<NSAttributedStringKey, id> *_typingAttributes;
83
+ NSMutableArray<NSArray *> *_controlEventTargets;
83
84
  }
84
85
 
85
86
  UIKIT_COMPAT_TEXT_INPUT_GEOMETRY
@@ -143,6 +144,73 @@ UIKIT_COMPAT_TEXT_INPUT_GEOMETRY
143
144
  }
144
145
  }
145
146
 
147
+ /**
148
+ * NSTextField arrives configured as a form control: bezelled, opaque, and with
149
+ * its own focus ring. React Native draws all of that itself on the view that
150
+ * owns this one, so the field has to be stripped back to just the text.
151
+ *
152
+ * `selectable` is the one that matters for behaviour rather than looks. AppKit
153
+ * will not begin editing a field it cannot select -- `acceptsFirstResponder`
154
+ * returns NO, the click does nothing, and no field editor is ever installed.
155
+ */
156
+
157
+ /**
158
+ * A click on text input must never drag the window.
159
+ *
160
+ * React Native's view defaults `mouseDownCanMoveWindow` to YES, matching
161
+ * AppKit, and AppKit asks the view under the cursor before delivering the
162
+ * event at all -- so a text view that inherits YES swallows its own clicks and
163
+ * starts a zero-pixel window drag instead. There is no mouseDown to debug,
164
+ * which is what makes it worth a comment.
165
+ */
166
+ - (BOOL)mouseDownCanMoveWindow
167
+ {
168
+ return NO;
169
+ }
170
+
171
+ - (void)uikitCompat_configureForTextInput
172
+ {
173
+ [super setEditable:YES];
174
+ self.selectable = YES;
175
+ self.bezeled = NO;
176
+ self.bordered = NO;
177
+ self.drawsBackground = NO;
178
+ self.focusRingType = NSFocusRingTypeNone;
179
+ self.usesSingleLineMode = YES;
180
+ self.cell.scrollable = YES;
181
+ self.cell.wraps = NO;
182
+ }
183
+
184
+ - (instancetype)initWithFrame:(NSRect)frame
185
+ {
186
+ if (self = [super initWithFrame:frame]) {
187
+ [self uikitCompat_configureForTextInput];
188
+ }
189
+ return self;
190
+ }
191
+
192
+ - (instancetype)initWithCoder:(NSCoder *)coder
193
+ {
194
+ if (self = [super initWithCoder:coder]) {
195
+ [self uikitCompat_configureForTextInput];
196
+ }
197
+ return self;
198
+ }
199
+
200
+ /**
201
+ * React Native has no `editable` on UITextField, so RCTUITextField maps the
202
+ * prop onto `enabled` -- and overrides `isEditable` to answer from it. That
203
+ * leaves AppKit's own editable and selectable flags untouched, which is what
204
+ * actually decides whether a click starts editing. Keep them in step here,
205
+ * where the mapping is visible, rather than asking upstream to know about it.
206
+ */
207
+ - (void)setEnabled:(BOOL)enabled
208
+ {
209
+ [super setEnabled:enabled];
210
+ [super setEditable:enabled];
211
+ self.selectable = enabled;
212
+ }
213
+
146
214
  - (NSAttributedString *)attributedText
147
215
  {
148
216
  return self.attributedStringValue;
@@ -280,16 +348,131 @@ UIKIT_COMPAT_TEXT_INPUT_GEOMETRY
280
348
  return [self respondsToSelector:action];
281
349
  }
282
350
 
283
- - (void)addTarget:(id)target action:(SEL)action forControlEvents:(__unused UIControlEvents)controlEvents
351
+ /**
352
+ * UIKit registers a target/action pair per control event; NSControl carries
353
+ * exactly one pair, fired when editing *ends*. Collapsing the two loses the
354
+ * distinction that matters most here -- RCTBackedTextFieldDelegateAdapter
355
+ * registers for EditingChanged and EditingDidEndOnExit, and with one slot the
356
+ * second registration silently replaces the first. The result is an input that
357
+ * accepts typing and never reports it, which is how `onChangeText` came to
358
+ * never fire.
359
+ *
360
+ * So the pairs are kept per event here, and driven from the AppKit
361
+ * notifications that actually correspond to them.
362
+ */
363
+ - (void)addTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents
364
+ {
365
+ if (target == nil || action == NULL) {
366
+ return;
367
+ }
368
+ if (_controlEventTargets == nil) {
369
+ _controlEventTargets = [NSMutableArray new];
370
+ }
371
+ [_controlEventTargets addObject:@[
372
+ [NSValue valueWithNonretainedObject:target],
373
+ [NSValue valueWithPointer:action],
374
+ @(controlEvents),
375
+ ]];
376
+ }
377
+
378
+ - (void)removeTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)events
379
+ {
380
+ NSMutableArray *kept = [NSMutableArray new];
381
+ for (NSArray *entry in _controlEventTargets) {
382
+ id entryTarget = [entry[0] nonretainedObjectValue];
383
+ SEL entryAction = (SEL)[entry[1] pointerValue];
384
+ UIControlEvents entryEvents = (UIControlEvents)[entry[2] unsignedIntegerValue];
385
+ BOOL matches = (target == nil || entryTarget == target) && (action == NULL || entryAction == action) &&
386
+ (entryEvents & events) != 0;
387
+ if (!matches) {
388
+ [kept addObject:entry];
389
+ }
390
+ }
391
+ _controlEventTargets = kept;
392
+ }
393
+
394
+ - (void)uikitCompat_sendActionsForControlEvents:(UIControlEvents)controlEvents
395
+ {
396
+ // Copied first: an action is free to add or remove targets while running.
397
+ for (NSArray *entry in [_controlEventTargets copy]) {
398
+ if (((UIControlEvents)[entry[2] unsignedIntegerValue] & controlEvents) == 0) {
399
+ continue;
400
+ }
401
+ id target = [entry[0] nonretainedObjectValue];
402
+ SEL action = (SEL)[entry[1] pointerValue];
403
+ if ([target respondsToSelector:action]) {
404
+ #pragma clang diagnostic push
405
+ #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
406
+ [target performSelector:action withObject:self];
407
+ #pragma clang diagnostic pop
408
+ }
409
+ }
410
+ }
411
+
412
+ /**
413
+ * The delegate, as UIKit's protocol rather than AppKit's.
414
+ *
415
+ * NSTextField calls `controlTextDidBeginEditing:` and friends; the adapter on
416
+ * the React Native side implements `textFieldDidBeginEditing:` and friends.
417
+ * Same events, different selectors, so nothing was ever called -- which is why
418
+ * `onFocus`, `onBlur` and `onSubmitEditing` stayed silent while typing itself
419
+ * worked.
420
+ */
421
+ - (id<UITextFieldDelegate>)uikitCompat_uiDelegate
422
+ {
423
+ id delegate = self.delegate;
424
+ return [delegate conformsToProtocol:@protocol(UITextFieldDelegate)] ? delegate : nil;
425
+ }
426
+
427
+ // NSControl's own hooks, called by the field editor. Preferred over the
428
+ // matching notifications: the notifications are posted onward to the control's
429
+ // delegate, and observing them from the control itself turned out not to see
430
+ // begin and end at all.
431
+ - (void)textDidBeginEditing:(NSNotification *)notification
284
432
  {
285
- self.target = target;
286
- self.action = action;
433
+ [super textDidBeginEditing:notification];
434
+ [self uikitCompat_sendActionsForControlEvents:UIControlEventEditingDidBegin];
435
+
436
+ id<UITextFieldDelegate> delegate = [self uikitCompat_uiDelegate];
437
+ if ([delegate respondsToSelector:@selector(textFieldDidBeginEditing:)]) {
438
+ [delegate textFieldDidBeginEditing:self];
439
+ }
287
440
  }
288
441
 
289
- - (void)removeTarget:(__unused id)target action:(__unused SEL)action forControlEvents:(__unused UIControlEvents)events
442
+ - (void)textDidChange:(NSNotification *)notification
290
443
  {
291
- self.target = nil;
292
- self.action = NULL;
444
+ [super textDidChange:notification];
445
+ [self uikitCompat_sendActionsForControlEvents:UIControlEventEditingChanged];
446
+ }
447
+
448
+ /**
449
+ * AppKit reports *why* editing ended in the notification's text movement.
450
+ * Return is what UIKit calls EditingDidEndOnExit -- the submit -- while
451
+ * clicking away or tabbing out is a plain EditingDidEnd.
452
+ */
453
+ - (void)textDidEndEditing:(NSNotification *)notification
454
+ {
455
+ [super textDidEndEditing:notification];
456
+
457
+ NSNumber *movement = notification.userInfo[@"NSTextMovement"];
458
+ BOOL submitted = movement.integerValue == NSReturnTextMovement;
459
+ id<UITextFieldDelegate> delegate = [self uikitCompat_uiDelegate];
460
+
461
+ // Return is a submit before it is an end-of-editing, and the order matters:
462
+ // onSubmitEditing should carry the text, which onBlur may go on to clear.
463
+ if (submitted && [delegate respondsToSelector:@selector(textFieldShouldReturn:)]) {
464
+ [delegate textFieldShouldReturn:self];
465
+ }
466
+
467
+ UIControlEvents events = UIControlEventEditingDidEnd;
468
+ if (submitted) {
469
+ events |= UIControlEventEditingDidEndOnExit;
470
+ }
471
+ [self uikitCompat_sendActionsForControlEvents:events];
472
+
473
+ if ([delegate respondsToSelector:@selector(textFieldDidEndEditing:)]) {
474
+ [delegate textFieldDidEndEditing:self];
475
+ }
293
476
  }
294
477
 
295
478
  - (id<UITextDropDelegate>)textDropDelegate
@@ -354,6 +537,123 @@ UIKIT_COMPAT_TEXT_INPUT_GEOMETRY
354
537
 
355
538
  @implementation UITextView
356
539
 
540
+ /**
541
+ * The delegate, as UIKit's protocol rather than AppKit's -- the same mismatch
542
+ * UITextField has. NSTextView tells its delegate through `textDidChange:` and
543
+ * `textViewDidChangeSelection:`; the React Native adapter listens for
544
+ * `textViewDidChange:` and the rest of the UIKit set. Without the bridge a
545
+ * multiline input accepts typing and reports none of it.
546
+ */
547
+
548
+ /**
549
+ * A click on text input must never drag the window.
550
+ *
551
+ * React Native's view defaults `mouseDownCanMoveWindow` to YES, matching
552
+ * AppKit, and AppKit asks the view under the cursor before delivering the
553
+ * event at all -- so a text view that inherits YES swallows its own clicks and
554
+ * starts a zero-pixel window drag instead. There is no mouseDown to debug,
555
+ * which is what makes it worth a comment.
556
+ */
557
+ - (BOOL)mouseDownCanMoveWindow
558
+ {
559
+ return NO;
560
+ }
561
+
562
+ /**
563
+ * NSTextView sizes itself to its text; UITextView fills the frame it is given.
564
+ * Left alone, a multiline input collapses to a single line's height -- it looks
565
+ * right, because React Native's own view draws the background behind it, but
566
+ * only that top strip is hit-testable, so clicking anywhere below the first
567
+ * line does nothing at all.
568
+ *
569
+ * Fixed height with a width-tracking container is what matches UITextView:
570
+ * the text wraps to the width, and the view keeps whatever height layout gave
571
+ * it.
572
+ */
573
+ - (void)uikitCompat_configureForTextInput
574
+ {
575
+ self.drawsBackground = NO;
576
+ self.richText = NO;
577
+ self.importsGraphics = NO;
578
+ self.allowsUndo = YES;
579
+ self.minSize = NSMakeSize(0, 0);
580
+ self.maxSize = NSMakeSize(CGFLOAT_MAX, CGFLOAT_MAX);
581
+ self.verticallyResizable = NO;
582
+ self.horizontallyResizable = NO;
583
+ self.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
584
+ self.textContainer.widthTracksTextView = YES;
585
+ self.textContainer.heightTracksTextView = YES;
586
+ self.textContainer.lineFragmentPadding = 0;
587
+ }
588
+
589
+ - (instancetype)initWithFrame:(NSRect)frame
590
+ {
591
+ if (self = [super initWithFrame:frame]) {
592
+ [self uikitCompat_configureForTextInput];
593
+ }
594
+ return self;
595
+ }
596
+
597
+ - (instancetype)initWithFrame:(NSRect)frame textContainer:(NSTextContainer *)container
598
+ {
599
+ if (self = [super initWithFrame:frame textContainer:container]) {
600
+ [self uikitCompat_configureForTextInput];
601
+ }
602
+ return self;
603
+ }
604
+
605
+ - (id<UITextViewDelegate>)uikitCompat_uiDelegate
606
+ {
607
+ id delegate = self.delegate;
608
+ return [delegate conformsToProtocol:@protocol(UITextViewDelegate)] ? delegate : nil;
609
+ }
610
+
611
+ // NSTextView funnels every edit through here, including paste, drops and
612
+ // undo -- which a keystroke-level hook would miss.
613
+ - (void)didChangeText
614
+ {
615
+ [super didChangeText];
616
+ id<UITextViewDelegate> delegate = [self uikitCompat_uiDelegate];
617
+ if ([delegate respondsToSelector:@selector(textViewDidChange:)]) {
618
+ [delegate textViewDidChange:self];
619
+ }
620
+ }
621
+
622
+ - (void)setSelectedRange:(NSRange)range affinity:(NSSelectionAffinity)affinity stillSelecting:(BOOL)stillSelecting
623
+ {
624
+ [super setSelectedRange:range affinity:affinity stillSelecting:stillSelecting];
625
+ id<UITextViewDelegate> delegate = [self uikitCompat_uiDelegate];
626
+ if ([delegate respondsToSelector:@selector(textViewDidChangeSelection:)]) {
627
+ [delegate textViewDidChangeSelection:self];
628
+ }
629
+ }
630
+
631
+ // NSTextView is its own responder rather than borrowing the window's field
632
+ // editor, so begin and end editing are the responder transitions themselves.
633
+ - (BOOL)becomeFirstResponder
634
+ {
635
+ if (![super becomeFirstResponder]) {
636
+ return NO;
637
+ }
638
+ id<UITextViewDelegate> delegate = [self uikitCompat_uiDelegate];
639
+ if ([delegate respondsToSelector:@selector(textViewDidBeginEditing:)]) {
640
+ [delegate textViewDidBeginEditing:self];
641
+ }
642
+ return YES;
643
+ }
644
+
645
+ - (BOOL)resignFirstResponder
646
+ {
647
+ if (![super resignFirstResponder]) {
648
+ return NO;
649
+ }
650
+ id<UITextViewDelegate> delegate = [self uikitCompat_uiDelegate];
651
+ if ([delegate respondsToSelector:@selector(textViewDidEndEditing:)]) {
652
+ [delegate textViewDidEndEditing:self];
653
+ }
654
+ return YES;
655
+ }
656
+
357
657
  @synthesize autocapitalizationType = _autocapitalizationType;
358
658
  @synthesize autocorrectionType = _autocorrectionType;
359
659
  @synthesize spellCheckingType = _spellCheckingType;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "not-react-native-macos",
3
- "version": "0.87.1-rc.7",
3
+ "version": "0.87.1-rc.8",
4
4
  "description": "A framework for building native apps using React",
5
5
  "license": "MIT",
6
6
  "repository": {