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

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.
@@ -48,6 +48,13 @@ const textViewConfig = {
48
48
  dataDetectorType: true,
49
49
  android_hyphenationFrequency: true,
50
50
  lineBreakStrategyIOS: true,
51
+ // [macOS] Text is a view too: ParagraphProps derives from ViewProps, which
52
+ // on macOS already carries these. Only the view config stood in the way --
53
+ // a prop missing from validAttributes never reaches C++ at all.
54
+ tooltip: true,
55
+ focusable: true,
56
+ enableFocusRing: true,
57
+ // macOS]
51
58
  },
52
59
  directEventTypes: {
53
60
  topTextLayout: {
@@ -234,9 +234,22 @@ type TextBaseProps = Readonly<{
234
234
  *
235
235
  * @build-types emit-as-interface Uniwind compatibility
236
236
  */
237
+ // [macOS] Text carries the same three view props react-native-macos gives it.
238
+ // They work because ParagraphProps derives from ViewProps, which on macOS is
239
+ // HostPlatformViewProps.
240
+ export type TextPropsMacOS = Readonly<{
241
+ /** @platform macos */
242
+ tooltip?: ?string,
243
+ /** @platform macos */
244
+ focusable?: ?boolean,
245
+ /** @platform macos */
246
+ enableFocusRing?: ?boolean,
247
+ }>;
248
+
237
249
  export type TextProps = Readonly<{
238
250
  ...TextPointerEventProps,
239
251
  ...TextPropsIOS,
252
+ ...TextPropsMacOS, // [macOS]
240
253
  ...TextPropsAndroid,
241
254
  ...TextBaseProps,
242
255
  ...AccessibilityProps,
@@ -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,11 @@
78
78
 
79
79
  @end
80
80
 
81
- @implementation UITextField {
81
+ @implementation RCTUIKitCompatTextField {
82
82
  NSDictionary<NSAttributedStringKey, id> *_typingAttributes;
83
+ NSMutableArray<NSArray *> *_controlEventTargets;
84
+ NSString *_placeholder;
85
+ BOOL _reportedEndEditing;
83
86
  }
84
87
 
85
88
  UIKIT_COMPAT_TEXT_INPUT_GEOMETRY
@@ -143,6 +146,73 @@ UIKIT_COMPAT_TEXT_INPUT_GEOMETRY
143
146
  }
144
147
  }
145
148
 
149
+ /**
150
+ * NSTextField arrives configured as a form control: bezelled, opaque, and with
151
+ * its own focus ring. React Native draws all of that itself on the view that
152
+ * owns this one, so the field has to be stripped back to just the text.
153
+ *
154
+ * `selectable` is the one that matters for behaviour rather than looks. AppKit
155
+ * will not begin editing a field it cannot select -- `acceptsFirstResponder`
156
+ * returns NO, the click does nothing, and no field editor is ever installed.
157
+ */
158
+
159
+ /**
160
+ * A click on text input must never drag the window.
161
+ *
162
+ * React Native's view defaults `mouseDownCanMoveWindow` to YES, matching
163
+ * AppKit, and AppKit asks the view under the cursor before delivering the
164
+ * event at all -- so a text view that inherits YES swallows its own clicks and
165
+ * starts a zero-pixel window drag instead. There is no mouseDown to debug,
166
+ * which is what makes it worth a comment.
167
+ */
168
+ - (BOOL)mouseDownCanMoveWindow
169
+ {
170
+ return NO;
171
+ }
172
+
173
+ - (void)uikitCompat_configureForTextInput
174
+ {
175
+ [super setEditable:YES];
176
+ self.selectable = YES;
177
+ self.bezeled = NO;
178
+ self.bordered = NO;
179
+ self.drawsBackground = NO;
180
+ self.focusRingType = NSFocusRingTypeNone;
181
+ self.usesSingleLineMode = YES;
182
+ self.cell.scrollable = YES;
183
+ self.cell.wraps = NO;
184
+ }
185
+
186
+ - (instancetype)initWithFrame:(NSRect)frame
187
+ {
188
+ if (self = [super initWithFrame:frame]) {
189
+ [self uikitCompat_configureForTextInput];
190
+ }
191
+ return self;
192
+ }
193
+
194
+ - (instancetype)initWithCoder:(NSCoder *)coder
195
+ {
196
+ if (self = [super initWithCoder:coder]) {
197
+ [self uikitCompat_configureForTextInput];
198
+ }
199
+ return self;
200
+ }
201
+
202
+ /**
203
+ * React Native has no `editable` on UITextField, so RCTUITextField maps the
204
+ * prop onto `enabled` -- and overrides `isEditable` to answer from it. That
205
+ * leaves AppKit's own editable and selectable flags untouched, which is what
206
+ * actually decides whether a click starts editing. Keep them in step here,
207
+ * where the mapping is visible, rather than asking upstream to know about it.
208
+ */
209
+ - (void)setEnabled:(BOOL)enabled
210
+ {
211
+ [super setEnabled:enabled];
212
+ [super setEditable:enabled];
213
+ self.selectable = enabled;
214
+ }
215
+
146
216
  - (NSAttributedString *)attributedText
147
217
  {
148
218
  return self.attributedStringValue;
@@ -265,13 +335,23 @@ UIKIT_COMPAT_TEXT_INPUT_GEOMETRY
265
335
  return nil;
266
336
  }
267
337
 
338
+ /**
339
+ * The plain and attributed placeholders are one value on NSTextField: setting
340
+ * `placeholderAttributedString` clears `placeholderString`, and the reverse.
341
+ * UIKit keeps both, and React Native relies on that -- it sets `placeholder`,
342
+ * builds `attributedPlaceholder` from it, and rebuilds that again whenever the
343
+ * text attributes change. Reading the plain one back through AppKit returns nil
344
+ * the second time round, so the placeholder is rebuilt as empty and silently
345
+ * disappears. Hold the string here instead.
346
+ */
268
347
  - (NSString *)placeholder
269
348
  {
270
- return self.placeholderString;
349
+ return _placeholder ?: self.placeholderString;
271
350
  }
272
351
 
273
352
  - (void)setPlaceholder:(NSString *)placeholder
274
353
  {
354
+ _placeholder = [placeholder copy];
275
355
  self.placeholderString = placeholder;
276
356
  }
277
357
 
@@ -280,16 +360,154 @@ UIKIT_COMPAT_TEXT_INPUT_GEOMETRY
280
360
  return [self respondsToSelector:action];
281
361
  }
282
362
 
283
- - (void)addTarget:(id)target action:(SEL)action forControlEvents:(__unused UIControlEvents)controlEvents
363
+ /**
364
+ * UIKit registers a target/action pair per control event; NSControl carries
365
+ * exactly one pair, fired when editing *ends*. Collapsing the two loses the
366
+ * distinction that matters most here -- RCTBackedTextFieldDelegateAdapter
367
+ * registers for EditingChanged and EditingDidEndOnExit, and with one slot the
368
+ * second registration silently replaces the first. The result is an input that
369
+ * accepts typing and never reports it, which is how `onChangeText` came to
370
+ * never fire.
371
+ *
372
+ * So the pairs are kept per event here, and driven from the AppKit
373
+ * notifications that actually correspond to them.
374
+ */
375
+ - (void)addTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents
376
+ {
377
+ if (target == nil || action == NULL) {
378
+ return;
379
+ }
380
+ if (_controlEventTargets == nil) {
381
+ _controlEventTargets = [NSMutableArray new];
382
+ }
383
+ [_controlEventTargets addObject:@[
384
+ [NSValue valueWithNonretainedObject:target],
385
+ [NSValue valueWithPointer:action],
386
+ @(controlEvents),
387
+ ]];
388
+ }
389
+
390
+ - (void)removeTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)events
391
+ {
392
+ NSMutableArray *kept = [NSMutableArray new];
393
+ for (NSArray *entry in _controlEventTargets) {
394
+ id entryTarget = [entry[0] nonretainedObjectValue];
395
+ SEL entryAction = (SEL)[entry[1] pointerValue];
396
+ UIControlEvents entryEvents = (UIControlEvents)[entry[2] unsignedIntegerValue];
397
+ BOOL matches = (target == nil || entryTarget == target) && (action == NULL || entryAction == action) &&
398
+ (entryEvents & events) != 0;
399
+ if (!matches) {
400
+ [kept addObject:entry];
401
+ }
402
+ }
403
+ _controlEventTargets = kept;
404
+ }
405
+
406
+ - (void)uikitCompat_sendActionsForControlEvents:(UIControlEvents)controlEvents
407
+ {
408
+ // Copied first: an action is free to add or remove targets while running.
409
+ for (NSArray *entry in [_controlEventTargets copy]) {
410
+ if (((UIControlEvents)[entry[2] unsignedIntegerValue] & controlEvents) == 0) {
411
+ continue;
412
+ }
413
+ id target = [entry[0] nonretainedObjectValue];
414
+ SEL action = (SEL)[entry[1] pointerValue];
415
+ if ([target respondsToSelector:action]) {
416
+ #pragma clang diagnostic push
417
+ #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
418
+ [target performSelector:action withObject:self];
419
+ #pragma clang diagnostic pop
420
+ }
421
+ }
422
+ }
423
+
424
+ /**
425
+ * The delegate, as UIKit's protocol rather than AppKit's.
426
+ *
427
+ * NSTextField calls `controlTextDidBeginEditing:` and friends; the adapter on
428
+ * the React Native side implements `textFieldDidBeginEditing:` and friends.
429
+ * Same events, different selectors, so nothing was ever called -- which is why
430
+ * `onFocus`, `onBlur` and `onSubmitEditing` stayed silent while typing itself
431
+ * worked.
432
+ */
433
+ - (id<UITextFieldDelegate>)uikitCompat_uiDelegate
284
434
  {
285
- self.target = target;
286
- self.action = action;
435
+ id delegate = self.delegate;
436
+ return [delegate conformsToProtocol:@protocol(UITextFieldDelegate)] ? delegate : nil;
287
437
  }
288
438
 
289
- - (void)removeTarget:(__unused id)target action:(__unused SEL)action forControlEvents:(__unused UIControlEvents)events
439
+ // NSControl's own hooks, called by the field editor. Preferred over the
440
+ // matching notifications: the notifications are posted onward to the control's
441
+ // delegate, and observing them from the control itself turned out not to see
442
+ // begin and end at all.
443
+ - (void)textDidBeginEditing:(NSNotification *)notification
290
444
  {
291
- self.target = nil;
292
- self.action = NULL;
445
+ [super textDidBeginEditing:notification];
446
+ _reportedEndEditing = NO;
447
+ [self uikitCompat_sendActionsForControlEvents:UIControlEventEditingDidBegin];
448
+
449
+ id<UITextFieldDelegate> delegate = [self uikitCompat_uiDelegate];
450
+ if ([delegate respondsToSelector:@selector(textFieldDidBeginEditing:)]) {
451
+ [delegate textFieldDidBeginEditing:self];
452
+ }
453
+ }
454
+
455
+ - (void)textDidChange:(NSNotification *)notification
456
+ {
457
+ [super textDidChange:notification];
458
+ [self uikitCompat_sendActionsForControlEvents:UIControlEventEditingChanged];
459
+ }
460
+
461
+ /**
462
+ * AppKit reports *why* editing ended in the notification's text movement.
463
+ * Return is what UIKit calls EditingDidEndOnExit -- the submit -- while
464
+ * clicking away or tabbing out is a plain EditingDidEnd.
465
+ */
466
+ - (void)textDidEndEditing:(NSNotification *)notification
467
+ {
468
+ [super textDidEndEditing:notification];
469
+
470
+ NSNumber *movement = notification.userInfo[@"NSTextMovement"];
471
+ BOOL submitted = movement.integerValue == NSReturnTextMovement;
472
+ id<UITextFieldDelegate> delegate = [self uikitCompat_uiDelegate];
473
+
474
+ // AppKit ends editing on Return and then re-establishes the field editor with
475
+ // the text selected, so this runs again on the next click -- a second blur
476
+ // with no focus in between. Report the end once per editing session.
477
+ if (_reportedEndEditing) {
478
+ return;
479
+ }
480
+ _reportedEndEditing = YES;
481
+
482
+ // Return is a submit before it is an end-of-editing, and the order matters:
483
+ // onSubmitEditing should carry the text, which onBlur may go on to clear.
484
+ // The answer says whether the field should also give up focus: that is
485
+ // `submitBehavior`, which distinguishes `submit` from `blurAndSubmit`.
486
+ BOOL shouldBlurOnSubmit = NO;
487
+ if (submitted && [delegate respondsToSelector:@selector(textFieldShouldReturn:)]) {
488
+ shouldBlurOnSubmit = [delegate textFieldShouldReturn:self];
489
+ }
490
+
491
+ UIControlEvents events = UIControlEventEditingDidEnd;
492
+ if (submitted) {
493
+ events |= UIControlEventEditingDidEndOnExit;
494
+ }
495
+ [self uikitCompat_sendActionsForControlEvents:events];
496
+
497
+ if ([delegate respondsToSelector:@selector(textFieldDidEndEditing:)]) {
498
+ [delegate textFieldDidEndEditing:self];
499
+ }
500
+
501
+ if (submitted && shouldBlurOnSubmit) {
502
+ // Asynchronously: AppKit is still unwinding this edit, and taking the
503
+ // responder away underneath it leaves a caret drawn in a field that no
504
+ // longer has focus.
505
+ dispatch_async(dispatch_get_main_queue(), ^{
506
+ if (self.currentEditor != nil) {
507
+ [self.window makeFirstResponder:nil];
508
+ }
509
+ });
510
+ }
293
511
  }
294
512
 
295
513
  - (id<UITextDropDelegate>)textDropDelegate
@@ -354,6 +572,123 @@ UIKIT_COMPAT_TEXT_INPUT_GEOMETRY
354
572
 
355
573
  @implementation UITextView
356
574
 
575
+ /**
576
+ * The delegate, as UIKit's protocol rather than AppKit's -- the same mismatch
577
+ * UITextField has. NSTextView tells its delegate through `textDidChange:` and
578
+ * `textViewDidChangeSelection:`; the React Native adapter listens for
579
+ * `textViewDidChange:` and the rest of the UIKit set. Without the bridge a
580
+ * multiline input accepts typing and reports none of it.
581
+ */
582
+
583
+ /**
584
+ * A click on text input must never drag the window.
585
+ *
586
+ * React Native's view defaults `mouseDownCanMoveWindow` to YES, matching
587
+ * AppKit, and AppKit asks the view under the cursor before delivering the
588
+ * event at all -- so a text view that inherits YES swallows its own clicks and
589
+ * starts a zero-pixel window drag instead. There is no mouseDown to debug,
590
+ * which is what makes it worth a comment.
591
+ */
592
+ - (BOOL)mouseDownCanMoveWindow
593
+ {
594
+ return NO;
595
+ }
596
+
597
+ /**
598
+ * NSTextView sizes itself to its text; UITextView fills the frame it is given.
599
+ * Left alone, a multiline input collapses to a single line's height -- it looks
600
+ * right, because React Native's own view draws the background behind it, but
601
+ * only that top strip is hit-testable, so clicking anywhere below the first
602
+ * line does nothing at all.
603
+ *
604
+ * Fixed height with a width-tracking container is what matches UITextView:
605
+ * the text wraps to the width, and the view keeps whatever height layout gave
606
+ * it.
607
+ */
608
+ - (void)uikitCompat_configureForTextInput
609
+ {
610
+ self.drawsBackground = NO;
611
+ self.richText = NO;
612
+ self.importsGraphics = NO;
613
+ self.allowsUndo = YES;
614
+ self.minSize = NSMakeSize(0, 0);
615
+ self.maxSize = NSMakeSize(CGFLOAT_MAX, CGFLOAT_MAX);
616
+ self.verticallyResizable = NO;
617
+ self.horizontallyResizable = NO;
618
+ self.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
619
+ self.textContainer.widthTracksTextView = YES;
620
+ self.textContainer.heightTracksTextView = YES;
621
+ self.textContainer.lineFragmentPadding = 0;
622
+ }
623
+
624
+ - (instancetype)initWithFrame:(NSRect)frame
625
+ {
626
+ if (self = [super initWithFrame:frame]) {
627
+ [self uikitCompat_configureForTextInput];
628
+ }
629
+ return self;
630
+ }
631
+
632
+ - (instancetype)initWithFrame:(NSRect)frame textContainer:(NSTextContainer *)container
633
+ {
634
+ if (self = [super initWithFrame:frame textContainer:container]) {
635
+ [self uikitCompat_configureForTextInput];
636
+ }
637
+ return self;
638
+ }
639
+
640
+ - (id<UITextViewDelegate>)uikitCompat_uiDelegate
641
+ {
642
+ id delegate = self.delegate;
643
+ return [delegate conformsToProtocol:@protocol(UITextViewDelegate)] ? delegate : nil;
644
+ }
645
+
646
+ // NSTextView funnels every edit through here, including paste, drops and
647
+ // undo -- which a keystroke-level hook would miss.
648
+ - (void)didChangeText
649
+ {
650
+ [super didChangeText];
651
+ id<UITextViewDelegate> delegate = [self uikitCompat_uiDelegate];
652
+ if ([delegate respondsToSelector:@selector(textViewDidChange:)]) {
653
+ [delegate textViewDidChange:self];
654
+ }
655
+ }
656
+
657
+ - (void)setSelectedRange:(NSRange)range affinity:(NSSelectionAffinity)affinity stillSelecting:(BOOL)stillSelecting
658
+ {
659
+ [super setSelectedRange:range affinity:affinity stillSelecting:stillSelecting];
660
+ id<UITextViewDelegate> delegate = [self uikitCompat_uiDelegate];
661
+ if ([delegate respondsToSelector:@selector(textViewDidChangeSelection:)]) {
662
+ [delegate textViewDidChangeSelection:self];
663
+ }
664
+ }
665
+
666
+ // NSTextView is its own responder rather than borrowing the window's field
667
+ // editor, so begin and end editing are the responder transitions themselves.
668
+ - (BOOL)becomeFirstResponder
669
+ {
670
+ if (![super becomeFirstResponder]) {
671
+ return NO;
672
+ }
673
+ id<UITextViewDelegate> delegate = [self uikitCompat_uiDelegate];
674
+ if ([delegate respondsToSelector:@selector(textViewDidBeginEditing:)]) {
675
+ [delegate textViewDidBeginEditing:self];
676
+ }
677
+ return YES;
678
+ }
679
+
680
+ - (BOOL)resignFirstResponder
681
+ {
682
+ if (![super resignFirstResponder]) {
683
+ return NO;
684
+ }
685
+ id<UITextViewDelegate> delegate = [self uikitCompat_uiDelegate];
686
+ if ([delegate respondsToSelector:@selector(textViewDidEndEditing:)]) {
687
+ [delegate textViewDidEndEditing:self];
688
+ }
689
+ return YES;
690
+ }
691
+
357
692
  @synthesize autocapitalizationType = _autocapitalizationType;
358
693
  @synthesize autocorrectionType = _autocorrectionType;
359
694
  @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.9",
4
4
  "description": "A framework for building native apps using React",
5
5
  "license": "MIT",
6
6
  "repository": {