diff --git a/packages/react-native/React/CoreModules/RCTRedBox.mm b/packages/react-native/React/CoreModules/RCTRedBox.mm index 0531ab50eb7a..28b945778da6 100644 --- a/packages/react-native/React/CoreModules/RCTRedBox.mm +++ b/packages/react-native/React/CoreModules/RCTRedBox.mm @@ -15,72 +15,15 @@ #import #import #import +#import // [macOS] #import -#import - #import "CoreModulesPlugins.h" #if RCT_DEV_MENU @class RCTRedBoxController; -#if !TARGET_OS_OSX // [macOS] -@interface UIButton (RCTRedBox) -#else // [macOS -@interface NSButton (RCTRedBox) -#endif // macOS] - -@property (nonatomic) RCTRedBoxButtonPressHandler rct_handler; - -#if !TARGET_OS_OSX // [macOS] -- (void)rct_addBlock:(RCTRedBoxButtonPressHandler)handler forControlEvents:(UIControlEvents)controlEvents; -#else // [macOS -- (void)rct_addBlock:(RCTRedBoxButtonPressHandler)handler; -#endif // macOS] - -@end - -#if !TARGET_OS_OSX // [macOS] -@implementation UIButton (RCTRedBox) -#else // [macOS -@implementation NSButton (RCTRedBox) -#endif // macOS] - -- (RCTRedBoxButtonPressHandler)rct_handler -{ - return objc_getAssociatedObject(self, @selector(rct_handler)); -} - -- (void)setRct_handler:(RCTRedBoxButtonPressHandler)rct_handler -{ - objc_setAssociatedObject(self, @selector(rct_handler), rct_handler, OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} - -- (void)rct_callBlock -{ - if (self.rct_handler) { - self.rct_handler(); - } -} - -#if !TARGET_OS_OSX // [macOS] -- (void)rct_addBlock:(RCTRedBoxButtonPressHandler)handler forControlEvents:(UIControlEvents)controlEvents; -#else // [macOS -- (void)rct_addBlock:(RCTRedBoxButtonPressHandler)handler -#endif // macOS] -{ - self.rct_handler = handler; -#if !TARGET_OS_OSX // [macOS] - [self addTarget:self action:@selector(rct_callBlock) forControlEvents:controlEvents]; -#else // [macOS - [self setTarget:self]; - [self setAction:@selector(rct_callBlock)]; -#endif // macOS] -} - -@end - @protocol RCTRedBoxControllerActionDelegate - (void)redBoxController:(RCTRedBoxController *)redBoxController openStackFrameInEditor:(RCTJSStackFrame *)stackFrame; @@ -89,20 +32,13 @@ - (void)loadExtraDataViewController; @end -#if !TARGET_OS_OSX // [macOS] -@interface RCTRedBoxController : UIViewController -#else // [macOS -@interface RCTRedBoxController : NSViewController -#endif // macOS] +@interface RCTRedBoxController + : RCTPlatformViewController // [macOS] @property (nonatomic, weak) id actionDelegate; @end @implementation RCTRedBoxController { -#if !TARGET_OS_OSX // [macOS] - UITableView *_stackTraceTableView; -#else // [macOS - NSTableView *_stackTraceTableView; -#endif // macOS] + RCTUITableView *_stackTraceTableView; // [macOS] NSString *_lastErrorMessage; NSArray *_lastStackTrace; NSArray *_customButtonTitles; @@ -138,38 +74,19 @@ - (void)viewDidLoad detailsFrame.size.height -= buttonHeight + (double)[self bottomSafeViewHeight]; #if !TARGET_OS_OSX // [macOS] - _stackTraceTableView = [[UITableView alloc] initWithFrame:detailsFrame style:UITableViewStylePlain]; + _stackTraceTableView = [[RCTUITableView alloc] initWithFrame:detailsFrame style:UITableViewStylePlain]; // [macOS] _stackTraceTableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - _stackTraceTableView.delegate = self; - _stackTraceTableView.dataSource = self; _stackTraceTableView.backgroundColor = [UIColor clearColor]; _stackTraceTableView.separatorColor = [UIColor colorWithWhite:1 alpha:0.3]; _stackTraceTableView.separatorStyle = UITableViewCellSeparatorStyleNone; _stackTraceTableView.indicatorStyle = UIScrollViewIndicatorStyleWhite; - [self.view addSubview:_stackTraceTableView]; #else // [macOS - NSScrollView *scrollView = [[NSScrollView alloc] initWithFrame:NSZeroRect]; - scrollView.translatesAutoresizingMaskIntoConstraints = NO; - scrollView.autoresizesSubviews = YES; - scrollView.drawsBackground = NO; - - _stackTraceTableView = [[NSTableView alloc] initWithFrame:NSZeroRect]; + _stackTraceTableView = [[RCTUITableView alloc] initWithFrame:NSZeroRect]; _stackTraceTableView.translatesAutoresizingMaskIntoConstraints = NO; +#endif // macOS] _stackTraceTableView.dataSource = self; _stackTraceTableView.delegate = self; - _stackTraceTableView.headerView = nil; - _stackTraceTableView.allowsColumnReordering = NO; - _stackTraceTableView.allowsColumnResizing = NO; - _stackTraceTableView.columnAutoresizingStyle = NSTableViewFirstColumnOnlyAutoresizingStyle; - _stackTraceTableView.backgroundColor = [NSColor clearColor]; - _stackTraceTableView.allowsTypeSelect = NO; - - NSTableColumn *tableColumn = [[NSTableColumn alloc] initWithIdentifier:@"info"]; - [_stackTraceTableView addTableColumn:tableColumn]; - - scrollView.documentView = _stackTraceTableView; - [self.view addSubview:scrollView]; -#endif // macOS] + [self.view addSubview:_stackTraceTableView]; #if TARGET_OS_SIMULATOR || TARGET_OS_MACCATALYST || TARGET_OS_OSX // [macOS] NSString *reloadText = @"Reload\n(\u2318R)"; @@ -183,45 +100,28 @@ - (void)viewDidLoad NSString *extraText = @"Extra Info"; #endif -#if !TARGET_OS_OSX // [macOS] - UIButton *dismissButton = [self redBoxButton:dismissText + RCTUIButton *dismissButton = [self redBoxButton:dismissText // [macOS] accessibilityIdentifier:@"redbox-dismiss" selector:@selector(dismiss) block:nil]; - UIButton *reloadButton = [self redBoxButton:reloadText + RCTUIButton *reloadButton = [self redBoxButton:reloadText // [macOS] accessibilityIdentifier:@"redbox-reload" selector:@selector(reload) block:nil]; - UIButton *copyButton = [self redBoxButton:copyText + RCTUIButton *copyButton = [self redBoxButton:copyText // [macOS] accessibilityIdentifier:@"redbox-copy" selector:@selector(copyStack) block:nil]; - UIButton *extraButton = [self redBoxButton:extraText + RCTUIButton *extraButton = [self redBoxButton:extraText // [macOS] accessibilityIdentifier:@"redbox-extra" selector:@selector(showExtraDataViewController) block:nil]; -#else // [macOS - NSButton *dismissButton = [self redBoxButton:dismissText - accessibilityIdentifier:@"redbox-dismiss" - selector:@selector(dismiss) - block:nil]; +#if TARGET_OS_OSX // [macOS [dismissButton setKeyEquivalent:@"\e"]; - NSButton *reloadButton = [self redBoxButton:reloadText - accessibilityIdentifier:@"redbox-reload" - selector:@selector(reload) - block:nil]; [reloadButton setKeyEquivalent:@"r"]; [reloadButton setKeyEquivalentModifierMask:NSEventModifierFlagCommand]; - NSButton *copyButton = [self redBoxButton:copyText - accessibilityIdentifier:@"redbox-copy" - selector:@selector(copyStack) - block:nil]; [copyButton setKeyEquivalent:@"c"]; [copyButton setKeyEquivalentModifierMask:NSEventModifierFlagOption | NSEventModifierFlagCommand]; - NSButton *extraButton = [self redBoxButton:extraText - accessibilityIdentifier:@"redbox-extra" - selector:@selector(showExtraDataViewController) - block:nil]; #endif // macOS] [NSLayoutConstraint activateConstraints:@[ @@ -264,17 +164,10 @@ - (void)viewDidLoad for (NSUInteger i = 0; i < [_customButtonTitles count]; i++) { -#if !TARGET_OS_OSX // [macOS] - UIButton *button = [self redBoxButton:_customButtonTitles[i] - accessibilityIdentifier:@"" - selector:nil - block:_customButtonHandlers[i]]; -#else // [macOS - NSButton *button = [self redBoxButton:_customButtonTitles[i] + RCTUIButton *button = [self redBoxButton:_customButtonTitles[i] // [macOS] accessibilityIdentifier:@"" selector:nil block:_customButtonHandlers[i]]; -#endif // macOS] [button.heightAnchor constraintEqualToConstant:buttonHeight].active = YES; [buttonStackView addArrangedSubview:button]; } @@ -299,24 +192,23 @@ - (void)viewDidLoad #if TARGET_OS_OSX // [macOS [NSLayoutConstraint activateConstraints:@[ - [[scrollView leadingAnchor] constraintEqualToAnchor:[[self view] leadingAnchor]], - [[scrollView topAnchor] constraintEqualToAnchor:[[self view] topAnchor]], - [[scrollView trailingAnchor] constraintEqualToAnchor:[[self view] trailingAnchor]], - [[scrollView bottomAnchor] constraintEqualToAnchor:[topBorder topAnchor]], + [[_stackTraceTableView leadingAnchor] constraintEqualToAnchor:[[self view] leadingAnchor]], + [[_stackTraceTableView topAnchor] constraintEqualToAnchor:[[self view] topAnchor]], + [[_stackTraceTableView trailingAnchor] constraintEqualToAnchor:[[self view] trailingAnchor]], + [[_stackTraceTableView bottomAnchor] constraintEqualToAnchor:[topBorder topAnchor]], ]]; #endif // macOS] } -#if !TARGET_OS_OSX // [macOS] -- (UIButton *)redBoxButton:(NSString *)title +- (RCTUIButton *)redBoxButton:(NSString *)title // [macOS] accessibilityIdentifier:(NSString *)accessibilityIdentifier selector:(SEL)selector block:(RCTRedBoxButtonPressHandler)block { - UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; +#if !TARGET_OS_OSX // [macOS] + RCTUIButton *button = [RCTUIButton buttonWithType:UIButtonTypeCustom]; // [macOS] button.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleRightMargin; - button.accessibilityIdentifier = accessibilityIdentifier; button.titleLabel.font = [UIFont systemFontOfSize:13]; button.titleLabel.lineBreakMode = NSLineBreakByWordWrapping; button.titleLabel.textAlignment = NSTextAlignmentCenter; @@ -324,37 +216,30 @@ - (UIButton *)redBoxButton:(NSString *)title [button setTitle:title forState:UIControlStateNormal]; [button setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; [button setTitleColor:[UIColor colorWithWhite:1 alpha:0.5] forState:UIControlStateHighlighted]; - if (selector) { - [button addTarget:self action:selector forControlEvents:UIControlEventTouchUpInside]; - } else if (block) { - [button rct_addBlock:block forControlEvents:UIControlEventTouchUpInside]; - } - return button; -} #else // [macOS -- (NSButton *)redBoxButton:(NSString *)title - accessibilityIdentifier:(NSString *)accessibilityIdentifier - selector:(SEL)selector - block:(RCTRedBoxButtonPressHandler)block -{ - NSButton *button = [[NSButton alloc] initWithFrame:NSZeroRect]; + RCTUIButton *button = [[RCTUIButton alloc] initWithFrame:NSZeroRect]; button.translatesAutoresizingMaskIntoConstraints = NO; - button.accessibilityIdentifier = @"accessibilityIdentifier"; button.bordered = NO; NSAttributedString *attributedTitle = [[NSAttributedString alloc] initWithString:title attributes:@{NSForegroundColorAttributeName : [ NSColor whiteColor] }]; button.attributedTitle = attributedTitle; [button setButtonType:NSButtonTypeMomentaryPushIn]; +#endif // macOS] + button.accessibilityIdentifier = accessibilityIdentifier; + if (selector) { +#if !TARGET_OS_OSX // [macOS] + [button addTarget:self action:selector forControlEvents:UIControlEventTouchUpInside]; +#else // [macOS button.target = self; button.action = selector; +#endif // macOS] } else if (block) { - [button rct_addBlock:block]; + [button rct_setPrimaryAction:[RCTUIAction actionWithHandler:block]]; // [macOS] } return button; } -#endif // macOS] - (NSInteger)bottomSafeViewHeight { @@ -401,14 +286,15 @@ - (void)showErrorMessage:(NSString *)message [_stackTraceTableView reloadData]; if (!isRootViewControllerPresented) { + if (_lastErrorMessage != nil) { // [macOS + [_stackTraceTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] + atScrollPosition:RCTUITableViewScrollPositionTop + animated:NO]; + } // macOS] #if !TARGET_OS_OSX // [macOS] - [_stackTraceTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] - atScrollPosition:UITableViewScrollPositionTop - animated:NO]; [RCTKeyWindow().rootViewController presentViewController:self animated:YES completion:nil]; #else // [macOS - [_stackTraceTableView scrollRowToVisible:0]; - [[RCTKeyWindow() contentViewController] presentViewControllerAsSheet:self]; + [[RCTKeyWindow() contentViewController] presentViewControllerAsSheet:self]; #endif // macOS] } } @@ -481,200 +367,120 @@ - (NSString *)formatFrameSource:(RCTJSStackFrame *)stackFrame #pragma mark - TableView -#if !TARGET_OS_OSX // [macOS] -- (NSInteger)numberOfSectionsInTableView:(__unused UITableView *)tableView +- (NSInteger)numberOfSectionsInTableView:(__unused RCTUITableView *)tableView // [macOS] { return 2; } -- (NSInteger)tableView:(__unused UITableView *)tableView numberOfRowsInSection:(NSInteger)section +- (NSInteger)tableView:(__unused RCTUITableView *)tableView numberOfRowsInSection:(NSInteger)section // [macOS] { - return section == 0 ? 1 : _lastStackTrace.count; + return section == 0 ? (_lastErrorMessage != nil) : _lastStackTrace.count; } -#else // [macOS -- (NSInteger)numberOfRowsInTableView:(__unused NSTableView *)tableView -{ - return (_lastErrorMessage != nil) + _lastStackTrace.count; -} -#endif // macOS] -#if !TARGET_OS_OSX // [macOS] -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +- (RCTUITableViewCell *)tableView:(RCTUITableView *)tableView + cellForRowAtIndexPath:(NSIndexPath *)indexPath // [macOS] { if (indexPath.section == 0) { - UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"msg-cell"]; + RCTUITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"msg-cell"]; return [self reuseCell:cell forErrorMessage:_lastErrorMessage]; } - UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"]; + RCTUITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"]; NSUInteger index = indexPath.row; RCTJSStackFrame *stackFrame = _lastStackTrace[index]; return [self reuseCell:cell forStackFrame:stackFrame]; } -#else // [macOS -- (nullable NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row -{ - if (row == 0) { - NSTableCellView *cell = [tableView makeViewWithIdentifier:@"msg-cell" owner:nil]; - return [self reuseCell:cell forErrorMessage:_lastErrorMessage]; - } - NSTableCellView *cell = [tableView makeViewWithIdentifier:@"cell" owner:nil]; - NSUInteger index = row - 1; - RCTJSStackFrame *stackFrame = _lastStackTrace[index]; - return [self reuseCell:cell forStackFrame:stackFrame]; - -} -#endif // macOS] -#if !TARGET_OS_OSX -- (UITableViewCell *)reuseCell:(UITableViewCell *)cell forErrorMessage:(NSString *)message +- (RCTUITableViewCell *)reuseCell:(RCTUITableViewCell *)cell forErrorMessage:(NSString *)message // [macOS] { if (!cell) { - cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"msg-cell"]; - cell.textLabel.accessibilityIdentifier = @"redbox-error"; - cell.textLabel.textColor = [UIColor whiteColor]; + cell = [[RCTUITableViewCell alloc] initWithStyle:RCTUITableViewCellStyleDefault reuseIdentifier:@"msg-cell"]; + cell.textLabel.textColor = [RCTUIColor whiteColor]; // [macOS] // Prefer a monofont for formatting messages that were designed // to be displayed in a terminal. cell.textLabel.font = [UIFont monospacedSystemFontOfSize:14 weight:UIFontWeightBold]; - cell.textLabel.lineBreakMode = NSLineBreakByWordWrapping; - cell.textLabel.numberOfLines = 0; - cell.detailTextLabel.textColor = [UIColor whiteColor]; + cell.detailTextLabel.textColor = [RCTUIColor whiteColor]; // [macOS] +#if !TARGET_OS_OSX // [macOS] cell.backgroundColor = [UIColor colorWithRed:0.82 green:0.10 blue:0.15 alpha:1.0]; cell.selectionStyle = UITableViewCellSelectionStyleNone; - } - - cell.textLabel.text = message; - - return cell; -} #else // [macOS -- (NSTableCellView *)reuseCell:(NSTableCellView *)cell forErrorMessage:(NSString *)message -{ - if (!cell) { - cell = [[NSTableCellView alloc] initWithFrame:NSZeroRect]; - cell.rowSizeStyle = NSTableViewRowSizeStyleCustom; - cell.textField.accessibilityIdentifier = @"red box-error"; - - NSTextField *label = [[NSTextField alloc] initWithFrame:NSZeroRect]; - label.translatesAutoresizingMaskIntoConstraints = NO; - label.drawsBackground = NO; - label.bezeled = NO; - label.editable = NO; - - [cell addSubview:label]; - cell.textField = label; - - [NSLayoutConstraint activateConstraints:@[ - [[label leadingAnchor] constraintEqualToAnchor:[cell leadingAnchor] constant:5], - [[label topAnchor] constraintEqualToAnchor:[cell topAnchor] constant:5], - [[label trailingAnchor] constraintEqualToAnchor:[cell trailingAnchor] constant:-5], - [[label bottomAnchor] constraintEqualToAnchor:[cell bottomAnchor] constant:-5], - ]]; - - // Prefer a monofont for formatting messages that were designed - // to be displayed in a terminal. - cell.textField.font = [NSFont monospacedSystemFontOfSize:14 weight:NSFontWeightBold]; - - cell.textField.lineBreakMode = NSLineBreakByWordWrapping; - cell.textField.maximumNumberOfLines = 0; - cell.wantsLayer = true; + cell.wantsLayer = YES; + cell.layer.backgroundColor = [NSColor colorWithRed:0.82 green:0.10 blue:0.15 alpha:1.0].CGColor; cell.layer.cornerRadius = 8.0; cell.layer.cornerCurve = kCACornerCurveContinuous; - - cell.layer.backgroundColor = [NSColor colorWithRed:0.82 green:0.10 blue:0.15 alpha:1.0].CGColor; + cell.textLabel.selectable = YES; +#endif // macOS] } + cell.textLabel.accessibilityIdentifier = @"redbox-error"; + cell.textLabel.lineBreakMode = NSLineBreakByWordWrapping; + cell.textLabel.numberOfLines = 0; +#if !TARGET_OS_OSX // [macOS] + cell.textLabel.text = message; +#else // [macOS NSDictionary *attributes = @{ NSForegroundColorAttributeName : [NSColor whiteColor], NSFontAttributeName : [NSFont systemFontOfSize:16], }; NSAttributedString *title = [[NSAttributedString alloc] initWithString:message attributes:attributes]; - - cell.textField.attributedStringValue = title; + cell.textLabel.attributedStringValue = title; +#endif // macOS] return cell; } -#endif // [macOS] -#if !TARGET_OS_OSX // [macOS] -- (UITableViewCell *)reuseCell:(UITableViewCell *)cell forStackFrame:(RCTJSStackFrame *)stackFrame +- (RCTUITableViewCell *)reuseCell:(RCTUITableViewCell *)cell forStackFrame:(RCTJSStackFrame *)stackFrame // [macOS] { if (!cell) { - cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"cell"]; + cell = [[RCTUITableViewCell alloc] initWithStyle:RCTUITableViewCellStyleSubtitle reuseIdentifier:@"cell"]; cell.textLabel.font = [UIFont fontWithName:@"Menlo-Regular" size:14]; - cell.textLabel.lineBreakMode = NSLineBreakByCharWrapping; - cell.textLabel.numberOfLines = 2; - cell.detailTextLabel.textColor = [UIColor colorWithRed:0.70 green:0.70 blue:0.70 alpha:1.0]; + cell.detailTextLabel.textColor = [RCTUIColor colorWithRed:0.70 green:0.70 blue:0.70 alpha:1.0]; // [macOS] cell.detailTextLabel.font = [UIFont fontWithName:@"Menlo-Regular" size:11]; cell.detailTextLabel.lineBreakMode = NSLineBreakByTruncatingMiddle; - cell.backgroundColor = [UIColor clearColor]; + cell.backgroundColor = [RCTUIColor clearColor]; // [macOS] +#if !TARGET_OS_OSX // [macOS] cell.selectedBackgroundView = [UIView new]; cell.selectedBackgroundView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.2]; +#else // [macOS + [cell.detailTextLabel removeFromSuperview]; + [cell.textLabel.bottomAnchor constraintEqualToAnchor:cell.contentView.bottomAnchor constant:-5].active = YES; + cell.textLabel.selectable = YES; +#endif // macOS] } - cell.textLabel.text = stackFrame.methodName ?: @"(unnamed method)"; - if (stackFrame.file) { - cell.detailTextLabel.text = [self formatFrameSource:stackFrame]; - } else { - cell.detailTextLabel.text = @""; - } + NSString *text = stackFrame.methodName ?: @"(unnamed method)"; + cell.textLabel.lineBreakMode = NSLineBreakByCharWrapping; + cell.textLabel.numberOfLines = 2; + +#if !TARGET_OS_OSX // [macOS] + cell.textLabel.text = text; + cell.detailTextLabel.text = stackFrame.file ? [self formatFrameSource:stackFrame] : @""; cell.textLabel.textColor = stackFrame.collapse ? [UIColor lightGrayColor] : [UIColor whiteColor]; cell.detailTextLabel.textColor = stackFrame.collapse ? [UIColor colorWithRed:0.50 green:0.50 blue:0.50 alpha:1.0] : [UIColor colorWithRed:0.70 green:0.70 blue:0.70 alpha:1.0]; - return cell; -} #else // [macOS -- (NSTableCellView *)reuseCell:(NSTableCellView *)cell forStackFrame:(RCTJSStackFrame *)stackFrame -{ - if (!cell) { - cell = [[NSTableCellView alloc] initWithFrame:NSZeroRect]; - - NSTextField *label = [[NSTextField alloc] initWithFrame:NSZeroRect]; - label.translatesAutoresizingMaskIntoConstraints = NO; - label.backgroundColor = [NSColor clearColor]; - label.bezeled = NO; - label.editable = NO; - - label.maximumNumberOfLines = 2; - - [cell addSubview:label]; - cell.textField = label; - - [NSLayoutConstraint activateConstraints:@[ - [[label leadingAnchor] constraintEqualToAnchor:[cell leadingAnchor] constant:5], - [[label topAnchor] constraintEqualToAnchor:[cell topAnchor]], - [[label trailingAnchor] constraintEqualToAnchor:[cell trailingAnchor] constant:-5], - [[label bottomAnchor] constraintEqualToAnchor:[cell bottomAnchor]], - ]]; - } - - NSString *text = stackFrame.methodName ?: @"(unnamed method)"; - NSMutableParagraphStyle *textParagraphStyle = [NSMutableParagraphStyle new]; textParagraphStyle.lineBreakMode = NSLineBreakByCharWrapping; - + NSDictionary *textAttributes = @{ NSForegroundColorAttributeName : stackFrame.collapse ? [NSColor lightGrayColor] : [NSColor whiteColor], NSFontAttributeName : [NSFont fontWithName:@"Menlo-Regular" size:14], NSParagraphStyleAttributeName : textParagraphStyle, }; - + NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:text attributes:textAttributes]; - - NSMutableAttributedString *title = [attributedText mutableCopy]; // NSTableCellView doesn't contain a subtitle text field. Rather than define our own custom row view, // let's append the detail text with a new line if it is needed. + cell.textLabel.maximumNumberOfLines = stackFrame.file ? 3 : 2; if (stackFrame.file) { - cell.textField.maximumNumberOfLines = 3; - NSString *detailText = [self formatFrameSource:stackFrame]; - + NSMutableParagraphStyle *detailTextParagraphStyle = [NSMutableParagraphStyle new]; detailTextParagraphStyle.lineBreakMode = NSLineBreakByTruncatingMiddle; - + NSDictionary *detailTextAttributes = @{ NSForegroundColorAttributeName : stackFrame.collapse ? [NSColor colorWithRed:0.50 green:0.50 blue:0.50 alpha:1.0] : @@ -683,39 +489,32 @@ - (NSTableCellView *)reuseCell:(NSTableCellView *)cell forStackFrame:(RCTJSStack NSParagraphStyleAttributeName : detailTextParagraphStyle, }; NSAttributedString *attributedDetailText = [[NSAttributedString alloc] initWithString:detailText attributes:detailTextAttributes]; - + [title appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n"]]; [title appendAttributedString:attributedDetailText]; } - - cell.textField.attributedStringValue = title; + cell.textLabel.attributedStringValue = title; +#endif // macOS] return cell; } -#endif // macOS] -#if !TARGET_OS_OSX // [macOS] -- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath -#else // [macOS -- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row -#endif // macOS] +- (CGFloat)tableView:(RCTUITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath // [macOS] { -#if !TARGET_OS_OSX // [macOS] if (indexPath.section == 0) { -#else // [macOS - if (row == 0) { -#endif // macOS] NSMutableParagraphStyle *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping; - NSDictionary *attributes = -#if !TARGET_OS_OSX // [macOS] - @{NSFontAttributeName : [UIFont boldSystemFontOfSize:16], NSParagraphStyleAttributeName : paragraphStyle}; -#else // [macOS - @{NSFontAttributeName : [NSFont boldSystemFontOfSize:16], NSParagraphStyleAttributeName : paragraphStyle}; + NSDictionary *attributes = @{ + NSFontAttributeName : [UIFont boldSystemFontOfSize:16], + NSParagraphStyleAttributeName : paragraphStyle + }; // [macOS] + CGFloat tableWidth = tableView.frame.size.width; +#if TARGET_OS_OSX // [macOS + tableWidth = tableView.contentSize.width; #endif // macOS] CGRect boundingRect = - [_lastErrorMessage boundingRectWithSize:CGSizeMake(tableView.frame.size.width - 30, CGFLOAT_MAX) + [_lastErrorMessage boundingRectWithSize:CGSizeMake(tableWidth - 30, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:nil]; @@ -725,8 +524,7 @@ - (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row } } -#if !TARGET_OS_OSX // [macOS -- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +- (void)tableView:(RCTUITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath // [macOS] { if (indexPath.section == 1) { NSUInteger row = indexPath.row; @@ -735,17 +533,6 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath } [tableView deselectRowAtIndexPath:indexPath animated:YES]; } -#else // [macOS -- (BOOL)tableView:(__unused NSTableView *)tableView shouldSelectRow:(__unused NSInteger)row -{ - if (row != 0) { - NSUInteger index = row - 1; - RCTJSStackFrame *stackFrame = _lastStackTrace[index]; - [_actionDelegate redBoxController:self openStackFrameInEditor:stackFrame]; - } - return NO; -} -#endif // macOS] #pragma mark - Key commands diff --git a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIButton.h b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIButton.h new file mode 100644 index 000000000000..4f97eacf1078 --- /dev/null +++ b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIButton.h @@ -0,0 +1,73 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// [macOS] + +#pragma once + +#include + +#import "RCTUIKitCompat.h" + +#if !TARGET_OS_OSX +#import +#else +#import +#endif + +NS_ASSUME_NONNULL_BEGIN + +#if !TARGET_OS_OSX + +@compatibility_alias RCTUIButton UIButton; +#define RCTUIControlStateNormal UIControlStateNormal +#define RCTUIControlStateHighlighted UIControlStateHighlighted +typedef UIControlState RCTUIControlState; + +#else // TARGET_OS_OSX [ + +typedef NS_OPTIONS(NSUInteger, RCTUIControlState) { + RCTUIControlStateNormal = 0, + RCTUIControlStateHighlighted = 1 << 0, +}; + +@interface RCTUIButtonTitleProxy : NSObject + +@property (nonatomic, strong, nullable) UIFont *font; +@property (nonatomic, assign) NSLineBreakMode lineBreakMode; +@property (nonatomic, assign) NSTextAlignment textAlignment; + +@end + +@interface RCTUIButton : NSButton + +@property (nonatomic, readonly) RCTUIButtonTitleProxy *titleLabel; +@property (nonatomic, copy, nullable) RCTUIColor *backgroundColor; + +- (void)setTitle:(nullable NSString *)title forState:(RCTUIControlState)state; +- (void)setTitleColor:(nullable RCTUIColor *)color forState:(RCTUIControlState)state; + +@end + +#endif // ] TARGET_OS_OSX + +typedef void (^RCTUIActionHandler)(void); + +@interface RCTUIAction : NSObject + ++ (instancetype)actionWithHandler:(RCTUIActionHandler)handler; +@property (nonatomic, readonly, copy) RCTUIActionHandler handler; + +@end + +@interface RCTUIButton (RCTUIAction) + +- (void)rct_setPrimaryAction:(RCTUIAction *)action; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIButton.m b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIButton.m new file mode 100644 index 000000000000..d01c39f7fb46 --- /dev/null +++ b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIButton.m @@ -0,0 +1,197 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// [macOS] + +#import "RCTUIButton.h" + +#import + +@interface RCTUIAction () + +- (instancetype)initWithHandler:(RCTUIActionHandler)handler; +- (void)invoke; + +@end + +@implementation RCTUIAction + ++ (instancetype)actionWithHandler:(RCTUIActionHandler)handler +{ + return [[self alloc] initWithHandler:handler]; +} + +- (instancetype)initWithHandler:(RCTUIActionHandler)handler +{ + if (self = [super init]) { + _handler = [handler copy]; + } + return self; +} + +- (void)invoke +{ + self.handler(); +} + +@end + +#if TARGET_OS_OSX + +@class RCTUIButton; + +@interface RCTUIButtonTitleProxy () + +@property (nonatomic, weak) RCTUIButton *button; + +- (instancetype)initWithButton:(RCTUIButton *)button; + +@end + +@interface RCTUIButton () + +- (void)updateAttributedTitles; + +@end + +@implementation RCTUIButtonTitleProxy + +- (instancetype)initWithButton:(RCTUIButton *)button +{ + if (self = [super init]) { + _button = button; + _font = button.font; + _lineBreakMode = NSLineBreakByClipping; + _textAlignment = NSTextAlignmentNatural; + } + return self; +} + +- (void)setFont:(UIFont *)font +{ + _font = font; + [self.button updateAttributedTitles]; +} + +- (void)setLineBreakMode:(NSLineBreakMode)lineBreakMode +{ + _lineBreakMode = lineBreakMode; + [self.button updateAttributedTitles]; +} + +- (void)setTextAlignment:(NSTextAlignment)textAlignment +{ + _textAlignment = textAlignment; + [self.button updateAttributedTitles]; +} + +@end + +@implementation RCTUIButton { + NSString *_normalTitle; + NSString *_highlightedTitle; + RCTUIColor *_normalTitleColor; + RCTUIColor *_highlightedTitleColor; +} + +- (instancetype)initWithFrame:(NSRect)frameRect +{ + if (self = [super initWithFrame:frameRect]) { + _titleLabel = [[RCTUIButtonTitleProxy alloc] initWithButton:self]; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)coder +{ + if (self = [super initWithCoder:coder]) { + _titleLabel = [[RCTUIButtonTitleProxy alloc] initWithButton:self]; + } + return self; +} + +- (void)setTitle:(NSString *)title forState:(RCTUIControlState)state +{ + if (state == RCTUIControlStateHighlighted) { + _highlightedTitle = [title copy]; + } else { + _normalTitle = [title copy]; + } + [self updateAttributedTitles]; +} + +- (void)setTitleColor:(RCTUIColor *)color forState:(RCTUIControlState)state +{ + if (state == RCTUIControlStateHighlighted) { + _highlightedTitleColor = [color copy]; + } else { + _normalTitleColor = [color copy]; + } + [self updateAttributedTitles]; +} + +- (void)setBackgroundColor:(RCTUIColor *)backgroundColor +{ + _backgroundColor = [backgroundColor copy]; + self.wantsLayer = YES; + self.layer.backgroundColor = backgroundColor.CGColor; + self.bordered = NO; +} + +- (void)updateAttributedTitles +{ + self.attributedTitle = [self attributedTitleForTitle:_normalTitle ?: @"" + color:_normalTitleColor]; + self.attributedAlternateTitle = [self attributedTitleForTitle:_highlightedTitle ?: _normalTitle ?: @"" + color:_highlightedTitleColor ?: _normalTitleColor]; +} + +- (NSAttributedString *)attributedTitleForTitle:(NSString *)title color:(RCTUIColor *)color +{ + NSMutableDictionary *attributes = [NSMutableDictionary new]; + if (color != nil) { + attributes[NSForegroundColorAttributeName] = color; + } + if (self.titleLabel.font != nil) { + attributes[NSFontAttributeName] = self.titleLabel.font; + } + + NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; + paragraphStyle.lineBreakMode = self.titleLabel.lineBreakMode; + paragraphStyle.alignment = self.titleLabel.textAlignment; + attributes[NSParagraphStyleAttributeName] = paragraphStyle; + + return [[NSAttributedString alloc] initWithString:title attributes:attributes]; +} + +@end + +#endif + +@implementation RCTUIButton (RCTUIAction) + +- (void)rct_setPrimaryAction:(RCTUIAction *)action +{ +#if !TARGET_OS_OSX + RCTUIAction *previousAction = objc_getAssociatedObject(self, @selector(rct_setPrimaryAction:)); + if (previousAction != nil) { + [self removeTarget:previousAction action:@selector(invoke) forControlEvents:UIControlEventTouchUpInside]; + } +#endif + + objc_setAssociatedObject( + self, @selector(rct_setPrimaryAction:), action, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + +#if !TARGET_OS_OSX + [self addTarget:action action:@selector(invoke) forControlEvents:UIControlEventTouchUpInside]; +#else + self.target = action; + self.action = @selector(invoke); +#endif +} + +@end diff --git a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIKit.h b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIKit.h index eccebc648400..7d51cf2dfe76 100644 --- a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIKit.h +++ b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIKit.h @@ -16,6 +16,8 @@ #import "RCTUIView.h" #import "RCTUIScrollView.h" #import "RCTUISlider.h" +#import "RCTUITableView.h" +#import "RCTUIButton.h" #import "RCTUILabel.h" #import "RCTUISwitch.h" #import "RCTUIActivityIndicatorView.h" diff --git a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUILabel.m b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUILabel.m index 919eb5641868..76d873d22349 100644 --- a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUILabel.m +++ b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUILabel.m @@ -31,6 +31,31 @@ - (void)setText:(NSString *)text [self setStringValue:text]; } +- (NSString *)text +{ + return self.stringValue; +} + +- (void)setNumberOfLines:(NSInteger)numberOfLines +{ + self.maximumNumberOfLines = numberOfLines; +} + +- (NSInteger)numberOfLines +{ + return self.maximumNumberOfLines; +} + +- (void)setTextAlignment:(NSTextAlignment)textAlignment +{ + self.alignment = textAlignment; +} + +- (NSTextAlignment)textAlignment +{ + return self.alignment; +} + @end #endif diff --git a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.h b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.h new file mode 100644 index 000000000000..1abaf797f52b --- /dev/null +++ b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.h @@ -0,0 +1,111 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// [macOS] + +#pragma once + +#include + +#import "RCTUILabel.h" +#import "RCTUIView.h" + +#if !TARGET_OS_OSX + +#import + +@compatibility_alias RCTUITableView UITableView; +@compatibility_alias RCTUITableViewCell UITableViewCell; +#define RCTUITableViewDataSource UITableViewDataSource +#define RCTUITableViewDelegate UITableViewDelegate +#define RCTUITableViewCellStyleDefault UITableViewCellStyleDefault +#define RCTUITableViewCellStyleSubtitle UITableViewCellStyleSubtitle +#define RCTUITableViewScrollPositionTop UITableViewScrollPositionTop +#define RCTUITableViewAutomaticDimension UITableViewAutomaticDimension + +#else // TARGET_OS_OSX [ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class RCTUITableView; + +typedef NS_ENUM(NSInteger, RCTUITableViewCellStyle) { + RCTUITableViewCellStyleDefault, + RCTUITableViewCellStyleSubtitle, +}; + +typedef NS_ENUM(NSInteger, RCTUITableViewScrollPosition) { + RCTUITableViewScrollPositionTop, +}; + +extern const CGFloat RCTUITableViewAutomaticDimension; + +@interface NSIndexPath (RCTUITableView) + +@property (nonatomic, readonly) NSInteger row; ++ (instancetype)indexPathForRow:(NSInteger)row inSection:(NSInteger)section; + +@end + +@interface RCTUITableViewCell : NSTableCellView + +- (instancetype)initWithStyle:(RCTUITableViewCellStyle)style + reuseIdentifier:(nullable NSString *)reuseIdentifier NS_DESIGNATED_INITIALIZER; +- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE; + +@property (nonatomic, readonly, nullable, copy) NSString *reuseIdentifier; +@property (nonatomic, readonly, strong) RCTPlatformView *contentView; +@property (nonatomic, readonly, strong) RCTUILabel *textLabel; +@property (nonatomic, readonly, nullable, strong) RCTUILabel *detailTextLabel; +@property (nonatomic, nullable, strong) RCTUIColor *backgroundColor; + +- (void)prepareForReuse; + +@end + +@protocol RCTUITableViewDataSource + +@required +- (NSInteger)tableView:(RCTUITableView *)tableView numberOfRowsInSection:(NSInteger)section; +- (RCTUITableViewCell *)tableView:(RCTUITableView *)tableView + cellForRowAtIndexPath:(NSIndexPath *)indexPath; + +@optional +- (NSInteger)numberOfSectionsInTableView:(RCTUITableView *)tableView; + +@end + +@protocol RCTUITableViewDelegate + +@optional +- (CGFloat)tableView:(RCTUITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath; +- (nullable RCTPlatformView *)tableView:(RCTUITableView *)tableView viewForHeaderInSection:(NSInteger)section; +- (CGFloat)tableView:(RCTUITableView *)tableView heightForHeaderInSection:(NSInteger)section; +- (void)tableView:(RCTUITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath; + +@end + +@interface RCTUITableView : NSScrollView + +@property (nonatomic, weak, nullable) id dataSource; +@property (nonatomic, weak, nullable) id delegate; +@property (nonatomic, nullable, copy) RCTUIColor *separatorColor; + +- (void)reloadData; +- (nullable __kindof RCTUITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier; +- (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath + atScrollPosition:(RCTUITableViewScrollPosition)position + animated:(BOOL)animated; +- (void)deselectRowAtIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated; + +@end + +NS_ASSUME_NONNULL_END + +#endif // ] TARGET_OS_OSX diff --git a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m new file mode 100644 index 000000000000..808668e26979 --- /dev/null +++ b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m @@ -0,0 +1,457 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// [macOS] + +#import "RCTUITableView.h" + +#if TARGET_OS_OSX + +#import + +const CGFloat RCTUITableViewAutomaticDimension = -1.0; +static NSString *const RCTUITableViewHeaderHeightConstraintIdentifier = @"RCTUITableViewHeaderHeight"; +static char RCTUITableViewHeaderHeightConstraintKey; + +typedef NS_ENUM(NSInteger, RCTUITableViewSlotKind) { + RCTUITableViewSlotKindHeader, + RCTUITableViewSlotKindRow, +}; + +@interface RCTUITableViewSlot : NSObject + +@property (nonatomic, readonly) RCTUITableViewSlotKind kind; +@property (nonatomic, readonly) NSInteger section; +@property (nonatomic, readonly) NSInteger row; +@property (nonatomic, readonly) CGFloat headerHeight; + +- (instancetype)initWithKind:(RCTUITableViewSlotKind)kind + section:(NSInteger)section + row:(NSInteger)row + headerHeight:(CGFloat)headerHeight; + +@end + +@implementation RCTUITableViewSlot + +- (instancetype)initWithKind:(RCTUITableViewSlotKind)kind + section:(NSInteger)section + row:(NSInteger)row + headerHeight:(CGFloat)headerHeight +{ + if (self = [super init]) { + _kind = kind; + _section = section; + _row = row; + _headerHeight = headerHeight; + } + + return self; +} + +@end + +@implementation NSIndexPath (RCTUITableView) + +- (NSInteger)row +{ + return self.item; +} + ++ (instancetype)indexPathForRow:(NSInteger)row inSection:(NSInteger)section +{ + return [self indexPathForItem:row inSection:section]; +} + +@end + +@interface RCTUITableViewCell () + +@property (nonatomic, readwrite, nullable, copy) NSString *reuseIdentifier; +@property (nonatomic, readwrite, strong) RCTPlatformView *contentView; +@property (nonatomic, readwrite, strong) RCTUILabel *textLabel; +@property (nonatomic, readwrite, nullable, strong) RCTUILabel *detailTextLabel; + +- (void)setFixedHeight:(nullable NSNumber *)fixedHeight; + +@end + +@implementation RCTUITableViewCell { + NSLayoutConstraint *_fixedHeightConstraint; + RCTUITableViewCellStyle _style; +} + +- (instancetype)initWithFrame:(NSRect)frameRect +{ + self = [self initWithStyle:RCTUITableViewCellStyleDefault reuseIdentifier:nil]; + self.frame = frameRect; + return self; +} + +- (instancetype)initWithStyle:(RCTUITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier +{ + if (self = [super initWithFrame:NSZeroRect]) { + [self initializeWithStyle:style reuseIdentifier:reuseIdentifier]; + } + + return self; +} + +- (void)initializeWithStyle:(RCTUITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier +{ + _style = style; + _reuseIdentifier = [reuseIdentifier copy]; + self.identifier = reuseIdentifier; + self.rowSizeStyle = NSTableViewRowSizeStyleCustom; + + _contentView = [[RCTPlatformView alloc] initWithFrame:NSZeroRect]; + _contentView.translatesAutoresizingMaskIntoConstraints = NO; + [self addSubview:_contentView]; + [NSLayoutConstraint activateConstraints:@[ + [_contentView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor], + [_contentView.topAnchor constraintEqualToAnchor:self.topAnchor], + [_contentView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor], + [_contentView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor], + ]]; + + _textLabel = [[RCTUILabel alloc] initWithFrame:NSZeroRect]; + _textLabel.translatesAutoresizingMaskIntoConstraints = NO; + [_contentView addSubview:_textLabel]; + self.textField = _textLabel; + + if (style == RCTUITableViewCellStyleSubtitle) { + _detailTextLabel = [[RCTUILabel alloc] initWithFrame:NSZeroRect]; + _detailTextLabel.translatesAutoresizingMaskIntoConstraints = NO; + [_contentView addSubview:_detailTextLabel]; + + [NSLayoutConstraint activateConstraints:@[ + [_textLabel.leadingAnchor constraintEqualToAnchor:_contentView.leadingAnchor constant:5], + [_textLabel.topAnchor constraintEqualToAnchor:_contentView.topAnchor constant:5], + [_textLabel.trailingAnchor constraintEqualToAnchor:_contentView.trailingAnchor constant:-5], + [_detailTextLabel.leadingAnchor constraintEqualToAnchor:_contentView.leadingAnchor constant:5], + [_detailTextLabel.topAnchor constraintEqualToAnchor:_textLabel.bottomAnchor], + [_detailTextLabel.trailingAnchor constraintEqualToAnchor:_contentView.trailingAnchor constant:-5], + [_detailTextLabel.bottomAnchor constraintEqualToAnchor:_contentView.bottomAnchor constant:-5], + ]]; + } else { + [NSLayoutConstraint activateConstraints:@[ + [_textLabel.leadingAnchor constraintEqualToAnchor:_contentView.leadingAnchor constant:5], + [_textLabel.topAnchor constraintEqualToAnchor:_contentView.topAnchor constant:5], + [_textLabel.trailingAnchor constraintEqualToAnchor:_contentView.trailingAnchor constant:-5], + [_textLabel.bottomAnchor constraintEqualToAnchor:_contentView.bottomAnchor constant:-5], + ]]; + } +} + +- (void)setBackgroundColor:(RCTUIColor *)backgroundColor +{ + _backgroundColor = [backgroundColor copy]; + self.contentView.wantsLayer = YES; + self.contentView.layer.backgroundColor = backgroundColor.CGColor; +} + +- (void)setAccessibilityIdentifier:(NSString *)accessibilityIdentifier +{ + [super setAccessibilityIdentifier:accessibilityIdentifier]; + self.textLabel.accessibilityIdentifier = accessibilityIdentifier; +} + +- (void)setAccessibilityLabel:(NSString *)accessibilityLabel +{ + [super setAccessibilityLabel:accessibilityLabel]; + self.textLabel.accessibilityLabel = accessibilityLabel; +} + +- (void)prepareForReuse +{ + [super prepareForReuse]; + self.textLabel.text = @""; + self.textLabel.maximumNumberOfLines = _style == RCTUITableViewCellStyleDefault ? 1 : 2; + self.detailTextLabel.text = @""; + self.detailTextLabel.maximumNumberOfLines = 1; +} + +- (void)setFrameSize:(NSSize)newSize +{ + [super setFrameSize:newSize]; + + CGFloat preferredWidth = MAX(newSize.width - 10, 0); + self.textLabel.preferredMaxLayoutWidth = preferredWidth; + self.detailTextLabel.preferredMaxLayoutWidth = preferredWidth; +} + +- (void)setFixedHeight:(NSNumber *)fixedHeight +{ + if (fixedHeight == nil) { + _fixedHeightConstraint.active = NO; + _fixedHeightConstraint = nil; + return; + } + + if (_fixedHeightConstraint == nil) { + _fixedHeightConstraint = [self.heightAnchor constraintEqualToConstant:fixedHeight.doubleValue]; + _fixedHeightConstraint.active = YES; + } else { + _fixedHeightConstraint.constant = fixedHeight.doubleValue; + } +} + +@end + +@interface RCTUITableView () +@end + +@implementation RCTUITableView { + NSTableView *_tableView; + NSArray *_slots; + NSMutableDictionary *_headerViews; + NSMutableIndexSet *_automaticRows; + CGFloat _lastContentWidth; +} + +- (instancetype)initWithFrame:(NSRect)frameRect +{ + if (self = [super initWithFrame:frameRect]) { + self.drawsBackground = NO; + + _slots = @[]; + _headerViews = [NSMutableDictionary new]; + _automaticRows = [NSMutableIndexSet new]; + + _tableView = [[NSTableView alloc] initWithFrame:self.contentView.bounds]; + _tableView.autoresizingMask = NSViewWidthSizable; + _tableView.dataSource = self; + _tableView.delegate = self; + _tableView.headerView = nil; + _tableView.allowsColumnReordering = NO; + _tableView.allowsColumnResizing = NO; + _tableView.allowsTypeSelect = NO; + _tableView.columnAutoresizingStyle = NSTableViewFirstColumnOnlyAutoresizingStyle; + _tableView.backgroundColor = NSColor.clearColor; + _tableView.style = NSTableViewStyleInset; + _tableView.usesAutomaticRowHeights = YES; + _tableView.accessibilityRole = NSAccessibilityTableRole; + + NSTableColumn *column = [[NSTableColumn alloc] initWithIdentifier:@"RCTUITableViewColumn"]; + [_tableView addTableColumn:column]; + + self.documentView = _tableView; + _lastContentWidth = self.contentSize.width; + self.separatorColor = nil; + } + + return self; +} + +- (void)setAccessibilityIdentifier:(NSString *)accessibilityIdentifier +{ + [super setAccessibilityIdentifier:accessibilityIdentifier]; + _tableView.accessibilityIdentifier = accessibilityIdentifier; +} + +- (void)setSeparatorColor:(RCTUIColor *)separatorColor +{ + _separatorColor = [separatorColor copy]; + if (separatorColor == nil) { + _tableView.gridStyleMask = NSTableViewGridNone; + } else { + _tableView.gridColor = separatorColor; + _tableView.gridStyleMask = NSTableViewSolidHorizontalGridLineMask; + } +} + +- (void)setFrameSize:(NSSize)newSize +{ + [super setFrameSize:newSize]; + + CGFloat contentWidth = self.contentSize.width; + if (_lastContentWidth != contentWidth) { + _lastContentWidth = contentWidth; + if (_automaticRows.count > 0) { + [_tableView noteHeightOfRowsWithIndexesChanged:_automaticRows]; + } + } +} + +- (void)reloadData +{ + for (RCTPlatformView *headerView in _headerViews.allValues) { + NSLayoutConstraint *heightConstraint = + objc_getAssociatedObject(headerView, &RCTUITableViewHeaderHeightConstraintKey); + heightConstraint.active = NO; + } + [_headerViews removeAllObjects]; + [_automaticRows removeAllIndexes]; + + NSInteger sectionCount = 1; + if ([self.dataSource respondsToSelector:@selector(numberOfSectionsInTableView:)]) { + sectionCount = [self.dataSource numberOfSectionsInTableView:self]; + } + sectionCount = MAX(sectionCount, 0); + + NSMutableArray *slots = [NSMutableArray new]; + for (NSInteger section = 0; section < sectionCount; section++) { + if ([self.delegate respondsToSelector:@selector(tableView:heightForHeaderInSection:)]) { + CGFloat headerHeight = [self.delegate tableView:self heightForHeaderInSection:section]; + if (headerHeight > 0) { + [slots addObject:[[RCTUITableViewSlot alloc] initWithKind:RCTUITableViewSlotKindHeader + section:section + row:NSNotFound + headerHeight:headerHeight]]; + } + } + + NSInteger rowCount = MAX([self.dataSource tableView:self numberOfRowsInSection:section], 0); + for (NSInteger row = 0; row < rowCount; row++) { + [slots addObject:[[RCTUITableViewSlot alloc] initWithKind:RCTUITableViewSlotKindRow + section:section + row:row + headerHeight:0]]; + } + } + + _slots = [slots copy]; + [_tableView reloadData]; +} + +- (__kindof RCTUITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier +{ + return [_tableView makeViewWithIdentifier:identifier owner:self]; +} + +- (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath + atScrollPosition:(RCTUITableViewScrollPosition)position + animated:(BOOL)animated +{ + NSInteger backingRow = [self backingRowForIndexPath:indexPath]; + if (backingRow == NSNotFound || position != RCTUITableViewScrollPositionTop) { + return; + } + + NSRect rowRect = [_tableView rectOfRow:backingRow]; + NSClipView *clipView = self.contentView; + NSRect proposedBounds = clipView.bounds; + proposedBounds.origin.y = NSMinY(rowRect); + NSRect constrainedBounds = [clipView constrainBoundsRect:proposedBounds]; + [clipView scrollToPoint:constrainedBounds.origin]; + [self reflectScrolledClipView:clipView]; +} + +- (void)deselectRowAtIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated +{ + NSInteger backingRow = [self backingRowForIndexPath:indexPath]; + if (backingRow != NSNotFound && [_tableView.selectedRowIndexes containsIndex:backingRow]) { + [_tableView deselectRow:backingRow]; + } +} + +- (NSInteger)backingRowForIndexPath:(NSIndexPath *)indexPath +{ + for (NSInteger backingRow = 0; backingRow < (NSInteger)_slots.count; backingRow++) { + RCTUITableViewSlot *slot = _slots[backingRow]; + if (slot.kind == RCTUITableViewSlotKindRow && slot.section == indexPath.section && slot.row == indexPath.row) { + return backingRow; + } + } + return NSNotFound; +} + +- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView +{ + return _slots.count; +} + +- (NSView *)tableView:(NSTableView *)tableView + viewForTableColumn:(NSTableColumn *)tableColumn + row:(NSInteger)row +{ + return [self viewForBackingRow:row]; +} + +- (NSView *)viewForBackingRow:(NSInteger)backingRow +{ + RCTUITableViewSlot *slot = _slots[backingRow]; + if (slot.kind == RCTUITableViewSlotKindRow) { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:slot.row inSection:slot.section]; + RCTUITableViewCell *cell = [self.dataSource tableView:self cellForRowAtIndexPath:indexPath]; + CGFloat height = [self requestedHeightForSlot:slot]; + [cell setFixedHeight:height == RCTUITableViewAutomaticDimension ? nil : @(height)]; + return cell; + } + + NSNumber *sectionKey = @(slot.section); + RCTPlatformView *headerView = _headerViews[sectionKey]; + if (headerView == nil) { + if ([self.delegate respondsToSelector:@selector(tableView:viewForHeaderInSection:)]) { + headerView = [self.delegate tableView:self viewForHeaderInSection:slot.section]; + } + if (headerView == nil) { + headerView = [RCTPlatformView new]; + } + headerView.translatesAutoresizingMaskIntoConstraints = NO; + NSLayoutConstraint *heightConstraint = + objc_getAssociatedObject(headerView, &RCTUITableViewHeaderHeightConstraintKey); + if (heightConstraint == nil) { + heightConstraint = [headerView.heightAnchor constraintEqualToConstant:slot.headerHeight]; + heightConstraint.identifier = RCTUITableViewHeaderHeightConstraintIdentifier; + objc_setAssociatedObject( + headerView, + &RCTUITableViewHeaderHeightConstraintKey, + heightConstraint, + OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } else { + heightConstraint.constant = slot.headerHeight; + } + heightConstraint.active = YES; + _headerViews[sectionKey] = headerView; + } + return headerView; +} + +- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row +{ + RCTUITableViewSlot *slot = _slots[row]; + if (slot.kind == RCTUITableViewSlotKindHeader) { + return slot.headerHeight; + } + + CGFloat height = [self requestedHeightForSlot:slot]; + if (height != RCTUITableViewAutomaticDimension) { + return height; + } + + [_automaticRows addIndex:row]; + return tableView.rowHeight; +} + +- (CGFloat)requestedHeightForSlot:(RCTUITableViewSlot *)slot +{ + if (![self.delegate respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]) { + return _tableView.rowHeight; + } + + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:slot.row inSection:slot.section]; + return [self.delegate tableView:self heightForRowAtIndexPath:indexPath]; +} + +- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)row +{ + RCTUITableViewSlot *slot = _slots[row]; + if (slot.kind == RCTUITableViewSlotKindHeader) { + return NO; + } + + if ([self.delegate respondsToSelector:@selector(tableView:didSelectRowAtIndexPath:)]) { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:slot.row inSection:slot.section]; + [self.delegate tableView:self didSelectRowAtIndexPath:indexPath]; + } + return NO; +} + +@end + +#endif diff --git a/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj b/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj index 90e6c295ce0e..a442c35151ae 100644 --- a/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj +++ b/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj @@ -23,6 +23,8 @@ 832F45BB2A8A6E1F0097B4E6 /* SwiftTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832F45BA2A8A6E1F0097B4E6 /* SwiftTest.swift */; }; 918A215FD4EF5A828705D765 /* libPods-RNTester-macOSIntegrationTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 474EC5CD967949D41527EECF /* libPods-RNTester-macOSIntegrationTests.a */; }; A975CA6C2C05EADF0043F72A /* RCTNetworkTaskTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A975CA6B2C05EADE0043F72A /* RCTNetworkTaskTests.m */; }; + B54A00012F27800000C0FFEE /* RCTUITableViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B54A00032F27800000C0FFEE /* RCTUITableViewTests.m */; }; + B54A00022F27800000C0FFEE /* RCTUITableViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B54A00032F27800000C0FFEE /* RCTUITableViewTests.m */; }; AC30658829B14F38007A839A /* RCTComponentPropsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E7DB20CC22B2BAA5005AC45F /* RCTComponentPropsTests.m */; }; AC73FCE829B1316D0003586F /* RNTesterIntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E7C1241922BEC44B00DA25C0 /* RNTesterIntegrationTests.m */; }; AC73FCE929B131700003586F /* RCTLoggingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E7DB215E22B2F3EC005AC45F /* RCTLoggingTests.m */; }; @@ -227,6 +229,7 @@ E7DB20CF22B2BAA5005AC45F /* RCTMultipartStreamReaderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMultipartStreamReaderTests.m; sourceTree = ""; }; E7DB20D022B2BAA5005AC45F /* RCTURLUtilsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTURLUtilsTests.m; sourceTree = ""; }; E7DB20E022B2BAA5005AC45F /* RCTViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTViewTests.m; sourceTree = ""; }; + B54A00032F27800000C0FFEE /* RCTUITableViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTUITableViewTests.m; sourceTree = ""; }; E7DB20F022B2BD53005AC45F /* libDoubleConversion.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libDoubleConversion.a; sourceTree = BUILT_PRODUCTS_DIR; }; E7DB20F222B2BD53005AC45F /* libFolly.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libFolly.a; sourceTree = BUILT_PRODUCTS_DIR; }; E7DB20F422B2BD53005AC45F /* libglog.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libglog.a; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -531,6 +534,7 @@ E7DB20C122B2BAA4005AC45F /* RCTUnicodeDecodeTests.m */, E7DB20D022B2BAA5005AC45F /* RCTURLUtilsTests.m */, E7DB20E022B2BAA5005AC45F /* RCTViewTests.m */, + B54A00032F27800000C0FFEE /* RCTUITableViewTests.m */, E7DB20B322B2BAA4005AC45F /* RNTesterUnitTestsBundle.js */, ); path = RNTesterUnitTests; @@ -1301,6 +1305,7 @@ AC73FD0929B131E10003586F /* RCTURLUtilsTests.m in Sources */, AC73FD0029B131C30003586F /* RCTModuleInitNotificationRaceTests.m in Sources */, AC73FD0A29B131E50003586F /* RCTViewTests.m in Sources */, + B54A00012F27800000C0FFEE /* RCTUITableViewTests.m in Sources */, AC73FCFE29B131BE0003586F /* RCTJSONTests.m in Sources */, AC73FCF629B1319F0003586F /* RCTDevMenuTests.m in Sources */, AC73FD0329B131CD0003586F /* RCTMultipartStreamReaderTests.m in Sources */, @@ -1349,6 +1354,7 @@ E7DB20E222B2BAA6005AC45F /* RCTGzipTests.m in Sources */, E7DB20ED22B2BAA6005AC45F /* RCTURLUtilsTests.m in Sources */, E7DB20EE22B2BAA6005AC45F /* RCTViewTests.m in Sources */, + B54A00022F27800000C0FFEE /* RCTUITableViewTests.m in Sources */, E7DB20D322B2BAA6005AC45F /* RCTBlobManagerTests.m in Sources */, E7DB20DC22B2BAA6005AC45F /* RCTUIManagerTests.m in Sources */, E7DB20E322B2BAA6005AC45F /* RCTAllocationTests.m in Sources */, diff --git a/packages/rn-tester/RNTesterUnitTests/RCTUITableViewTests.m b/packages/rn-tester/RNTesterUnitTests/RCTUITableViewTests.m new file mode 100644 index 000000000000..8e21d378050a --- /dev/null +++ b/packages/rn-tester/RNTesterUnitTests/RCTUITableViewTests.m @@ -0,0 +1,801 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import + +#if TARGET_OS_OSX // [macOS + +#import + +typedef RCTUITableViewCell * (^RCTUITableViewTestCellProvider)( + RCTUITableView *tableView, + NSIndexPath *indexPath); +typedef CGFloat (^RCTUITableViewTestHeightProvider)(NSIndexPath *indexPath); +typedef RCTPlatformView * (^RCTUITableViewTestHeaderProvider)(NSInteger section); + +static void RCTUIPumpRunLoop(NSTimeInterval duration) +{ + NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:duration]; + while (deadline.timeIntervalSinceNow > 0) { + NSDate *slice = [NSDate dateWithTimeIntervalSinceNow:0.002]; + NSEvent *event = [NSApp nextEventMatchingMask:NSEventMaskAny + untilDate:slice + inMode:NSDefaultRunLoopMode + dequeue:YES]; + if (event != nil) { + [NSApp sendEvent:event]; + } + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:slice]; + } +} + +static NSTableView *RCTUIBackingTable(RCTUITableView *tableView) +{ + NSTableView *backingTable = [tableView valueForKey:@"tableView"]; + XCTAssertTrue([backingTable isKindOfClass:NSTableView.class]); + return backingTable; +} + +static BOOL RCTUIAccessibilityTreeContainsIdentifier(id element, NSString *identifier, NSUInteger depth) +{ + if (depth == 0 || element == nil) { + return NO; + } + if ([element respondsToSelector:@selector(accessibilityIdentifier)] && + [[element accessibilityIdentifier] isEqualToString:identifier]) { + return YES; + } + if (![element respondsToSelector:@selector(accessibilityChildren)]) { + return NO; + } + + for (id child in [element accessibilityChildren]) { + if (RCTUIAccessibilityTreeContainsIdentifier(child, identifier, depth - 1)) { + return YES; + } + } + return NO; +} + +@interface RCTUITableViewTestDataSource : NSObject + +@property (nonatomic, copy) NSArray *rowCounts; +@property (nonatomic, copy) NSArray *headerHeights; +@property (nonatomic, copy) RCTUITableViewTestCellProvider cellProvider; +@property (nonatomic, copy) RCTUITableViewTestHeightProvider heightProvider; +@property (nonatomic, copy) RCTUITableViewTestHeaderProvider headerProvider; +@property (nonatomic, strong) NSMutableDictionary *headerViewCallCounts; +@property (nonatomic, strong) NSIndexPath *selectedIndexPath; +@property (nonatomic, assign) NSInteger selectionCallCount; +@property (nonatomic, assign) BOOL deselectDuringSelection; + +@end + +@implementation RCTUITableViewTestDataSource + +- (instancetype)init +{ + if (self = [super init]) { + _rowCounts = @[]; + _headerHeights = @[]; + _headerViewCallCounts = [NSMutableDictionary new]; + } + return self; +} + +- (NSInteger)numberOfSectionsInTableView:(RCTUITableView *)tableView +{ + return self.rowCounts.count; +} + +- (NSInteger)tableView:(RCTUITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + return self.rowCounts[section].integerValue; +} + +- (RCTUITableViewCell *)tableView:(RCTUITableView *)tableView + cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (self.cellProvider != nil) { + return self.cellProvider(tableView, indexPath); + } + + RCTUITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"]; + if (cell == nil) { + cell = [[RCTUITableViewCell alloc] initWithStyle:RCTUITableViewCellStyleDefault reuseIdentifier:@"cell"]; + } + cell.textLabel.text = [NSString stringWithFormat:@"%ld:%ld", (long)indexPath.section, (long)indexPath.row]; + return cell; +} + +- (CGFloat)tableView:(RCTUITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath +{ + return self.heightProvider != nil ? self.heightProvider(indexPath) : 24; +} + +- (CGFloat)tableView:(RCTUITableView *)tableView heightForHeaderInSection:(NSInteger)section +{ + return section < (NSInteger)self.headerHeights.count ? self.headerHeights[section].doubleValue : 0; +} + +- (RCTPlatformView *)tableView:(RCTUITableView *)tableView viewForHeaderInSection:(NSInteger)section +{ + NSNumber *key = @(section); + self.headerViewCallCounts[key] = @(self.headerViewCallCounts[key].integerValue + 1); + return self.headerProvider != nil ? self.headerProvider(section) : [RCTPlatformView new]; +} + +- (void)tableView:(RCTUITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + self.selectionCallCount++; + self.selectedIndexPath = indexPath; + if (self.deselectDuringSelection) { + [tableView deselectRowAtIndexPath:indexPath animated:YES]; + } +} + +@end + +@interface RCTUITableViewSingleSectionDataSource : NSObject + +@property (nonatomic, assign) NSInteger rowCount; + +@end + +@implementation RCTUITableViewSingleSectionDataSource + +- (NSInteger)tableView:(RCTUITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + XCTAssertEqual(section, 0); + return self.rowCount; +} + +- (RCTUITableViewCell *)tableView:(RCTUITableView *)tableView + cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + RCTUITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"single-section"]; + return cell ?: [[RCTUITableViewCell alloc] initWithStyle:RCTUITableViewCellStyleDefault + reuseIdentifier:@"single-section"]; +} + +@end + +@interface RCTUIInsetControlDataSource : NSObject +@end + +@implementation RCTUIInsetControlDataSource + +- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView +{ + return 1; +} + +- (NSView *)tableView:(NSTableView *)tableView + viewForTableColumn:(NSTableColumn *)tableColumn + row:(NSInteger)row +{ + return [NSTableCellView new]; +} + +@end + +static NSTableView *RCTUICreateInsetControlTable(CGFloat width, RCTUIInsetControlDataSource *dataSource) +{ + NSTableView *tableView = [[NSTableView alloc] initWithFrame:NSMakeRect(0, 0, width, 400)]; + tableView.dataSource = dataSource; + tableView.delegate = dataSource; + tableView.headerView = nil; + tableView.allowsColumnReordering = NO; + tableView.allowsColumnResizing = NO; + tableView.allowsTypeSelect = NO; + tableView.columnAutoresizingStyle = NSTableViewFirstColumnOnlyAutoresizingStyle; + tableView.backgroundColor = NSColor.clearColor; + tableView.style = NSTableViewStyleInset; + [tableView addTableColumn:[[NSTableColumn alloc] initWithIdentifier:@"ControlColumn"]]; + [tableView reloadData]; + [tableView layoutSubtreeIfNeeded]; + return tableView; +} + +static NSInteger RCTUITrackingCellAllocationCount; + +@interface RCTUITrackingTableCell : RCTUITableViewCell + +@property (nonatomic, assign) NSInteger prepareForReuseCount; +@property (nonatomic, assign) NSInteger vendCount; + +@end + +@implementation RCTUITrackingTableCell + +- (instancetype)initWithStyle:(RCTUITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier +{ + if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) { + RCTUITrackingCellAllocationCount++; + } + return self; +} + +- (void)prepareForReuse +{ + [super prepareForReuse]; + self.prepareForReuseCount++; +} + +@end + +@interface RCTUITableViewTests : XCTestCase +@end + +@implementation RCTUITableViewTests { + NSWindow *_window; + RCTUITableView *_tableView; + id _dataSource; + id _delegate; +} + +- (void)setUp +{ + [super setUp]; + [NSApplication sharedApplication]; + NSApp.activationPolicy = NSApplicationActivationPolicyAccessory; + + _window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 600, 400) + styleMask:NSWindowStyleMaskTitled + backing:NSBackingStoreBuffered + defer:NO]; + [_window setFrameOrigin:NSMakePoint(-4000, -4000)]; + + _tableView = [[RCTUITableView alloc] initWithFrame:NSZeroRect]; + _tableView.translatesAutoresizingMaskIntoConstraints = NO; + [_window.contentView addSubview:_tableView]; + [NSLayoutConstraint activateConstraints:@[ + [_tableView.leadingAnchor constraintEqualToAnchor:_window.contentView.leadingAnchor], + [_tableView.topAnchor constraintEqualToAnchor:_window.contentView.topAnchor], + [_tableView.trailingAnchor constraintEqualToAnchor:_window.contentView.trailingAnchor], + [_tableView.bottomAnchor constraintEqualToAnchor:_window.contentView.bottomAnchor], + ]]; + + [_window orderFront:nil]; +} + +- (void)tearDown +{ + [_window orderOut:nil]; + [_window close]; + _delegate = nil; + _dataSource = nil; + _tableView = nil; + _window = nil; + [super tearDown]; +} + +- (void)installDataSource:(id)dataSource + delegate:(id)delegate +{ + _dataSource = dataSource; + _delegate = delegate; + _tableView.dataSource = dataSource; + _tableView.delegate = delegate; +} + +- (void)setContentWidth:(CGFloat)width +{ + [_window setContentSize:NSMakeSize(width, 400)]; + [self forceLayoutAndDisplay]; +} + +- (void)forceLayoutAndDisplay +{ + [_window.contentView layoutSubtreeIfNeeded]; + [_tableView layoutSubtreeIfNeeded]; + [RCTUIBackingTable(_tableView) layoutSubtreeIfNeeded]; + [_window displayIfNeeded]; + RCTUIPumpRunLoop(0.05); +} + +- (void)reloadAndDisplay +{ + [_tableView reloadData]; + [self forceLayoutAndDisplay]; +} + +- (void)testSectionsDefaultsAndEmptySections +{ + RCTUITableViewTestDataSource *dataSource = [RCTUITableViewTestDataSource new]; + dataSource.rowCounts = @[@1, @0, @4]; + [self installDataSource:dataSource delegate:dataSource]; + [self reloadAndDisplay]; + + XCTAssertEqual(RCTUIBackingTable(_tableView).numberOfRows, 5); + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:3 inSection:2]; + XCTAssertEqual(indexPath.section, 2); + XCTAssertEqual(indexPath.row, 3); + + RCTUITableViewSingleSectionDataSource *singleSection = [RCTUITableViewSingleSectionDataSource new]; + singleSection.rowCount = 2; + [self installDataSource:singleSection delegate:nil]; + [self reloadAndDisplay]; + XCTAssertEqual(RCTUIBackingTable(_tableView).numberOfRows, 2); + + singleSection.rowCount = 0; + [self reloadAndDisplay]; + XCTAssertEqual(RCTUIBackingTable(_tableView).numberOfRows, 0); +} + +- (void)testRowsTrackSuccessiveReloads +{ + RCTUITableViewTestDataSource *dataSource = [RCTUITableViewTestDataSource new]; + [self installDataSource:dataSource delegate:dataSource]; + + for (NSNumber *rowCount in @[@2, @9, @1, @0]) { + dataSource.rowCounts = @[rowCount]; + [self reloadAndDisplay]; + NSTableView *backingTable = RCTUIBackingTable(_tableView); + XCTAssertEqual(backingTable.numberOfRows, rowCount.integerValue); + XCTAssertTrue(isfinite(NSMinY(backingTable.visibleRect))); + XCTAssertGreaterThanOrEqual(NSWidth(backingTable.visibleRect), 0); + XCTAssertGreaterThanOrEqual(NSHeight(backingTable.visibleRect), 0); + } +} + +- (void)testRealReuseIsNilFirstBoundedAndPreparedExactlyOnce +{ + RCTUITrackingCellAllocationCount = 0; + RCTUITableViewTestDataSource *dataSource = [RCTUITableViewTestDataSource new]; + dataSource.rowCounts = @[@200]; + + __block BOOL observedFirstDequeue = NO; + __block BOOL firstDequeueWasNil = NO; + __block BOOL prepareCountMismatch = NO; + __block BOOL doubleAttachmentDetected = NO; + NSMutableArray *allocatedCells = [NSMutableArray new]; + dataSource.cellProvider = ^RCTUITableViewCell *(RCTUITableView *tableView, NSIndexPath *indexPath) { + RCTUITrackingTableCell *cell = [tableView dequeueReusableCellWithIdentifier:@"tracking"]; + if (!observedFirstDequeue) { + observedFirstDequeue = YES; + firstDequeueWasNil = cell == nil; + } + + if (cell == nil) { + cell = [[RCTUITrackingTableCell alloc] initWithStyle:RCTUITableViewCellStyleDefault + reuseIdentifier:@"tracking"]; + [allocatedCells addObject:cell]; + } else { + doubleAttachmentDetected |= cell.superview != nil; + prepareCountMismatch |= cell.prepareForReuseCount != cell.vendCount; + } + + cell.vendCount++; + cell.textLabel.text = [NSString stringWithFormat:@"Row %ld", (long)indexPath.row]; + return cell; + }; + + [self installDataSource:dataSource delegate:dataSource]; + [self reloadAndDisplay]; + + NSTableView *backingTable = RCTUIBackingTable(_tableView); + NSUInteger maximumVisibleRows = [backingTable rowsInRect:backingTable.visibleRect].length; + XCTAssertGreaterThan(maximumVisibleRows, 0u); + + [_tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:199 inSection:0] + atScrollPosition:RCTUITableViewScrollPositionTop + animated:NO]; + [self forceLayoutAndDisplay]; + maximumVisibleRows = MAX(maximumVisibleRows, [backingTable rowsInRect:backingTable.visibleRect].length); + + [_tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] + atScrollPosition:RCTUITableViewScrollPositionTop + animated:NO]; + [self forceLayoutAndDisplay]; + maximumVisibleRows = MAX(maximumVisibleRows, [backingTable rowsInRect:backingTable.visibleRect].length); + + XCTAssertTrue(observedFirstDequeue); + XCTAssertTrue(firstDequeueWasNil); + XCTAssertFalse(prepareCountMismatch); + XCTAssertFalse(doubleAttachmentDetected); + XCTAssertLessThanOrEqual(RCTUITrackingCellAllocationCount, (NSInteger)(3 * maximumVisibleRows)); + for (RCTUITrackingTableCell *cell in allocatedCells) { + XCTAssertEqual(cell.prepareForReuseCount, MAX(cell.vendCount - 1, 0)); + } +} + +- (void)testSelectionDeselectAndHeaderNonselection +{ + RCTUITableViewTestDataSource *dataSource = [RCTUITableViewTestDataSource new]; + dataSource.rowCounts = @[@1, @2]; + dataSource.headerHeights = @[@24, @0]; + dataSource.deselectDuringSelection = YES; + [self installDataSource:dataSource delegate:dataSource]; + [self reloadAndDisplay]; + + NSTableView *backingTable = RCTUIBackingTable(_tableView); + XCTAssertFalse([backingTable.delegate tableView:backingTable shouldSelectRow:0]); + XCTAssertNil(dataSource.selectedIndexPath); + XCTAssertEqual(dataSource.selectionCallCount, 0); + + XCTAssertFalse([backingTable.delegate tableView:backingTable shouldSelectRow:3]); + XCTAssertEqual(dataSource.selectedIndexPath.section, 1); + XCTAssertEqual(dataSource.selectedIndexPath.row, 1); + XCTAssertEqual(dataSource.selectionCallCount, 1); + XCTAssertEqual(backingTable.selectedRow, -1); + + XCTAssertNoThrow([_tableView deselectRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:1] animated:YES]); + XCTAssertEqual(backingTable.selectedRow, -1); +} + +- (void)testMixedAutomaticAndFixedHeightsReflowWithContentWidth +{ + NSString *text = + @"This deliberately long automatic row wraps repeatedly so the test can distinguish a wide content width from " + "a narrow content width while preserving fixed rows. The compatibility layer must measure against the clip " + "view's content width every time the window changes size, without allowing the two fixed rows to drift. This " + "additional sentence keeps the wide layout above fifty points and makes the narrow reflow unambiguous."; + RCTUITableViewTestDataSource *dataSource = [RCTUITableViewTestDataSource new]; + dataSource.rowCounts = @[@1, @2]; + + __block RCTUITableViewCell *automaticCell; + dataSource.cellProvider = ^RCTUITableViewCell *(RCTUITableView *tableView, NSIndexPath *indexPath) { + if (indexPath.section == 0) { + automaticCell = [tableView dequeueReusableCellWithIdentifier:@"automatic"]; + if (automaticCell == nil) { + automaticCell = [[RCTUITableViewCell alloc] initWithStyle:RCTUITableViewCellStyleDefault + reuseIdentifier:@"automatic"]; + } + automaticCell.textLabel.font = [NSFont systemFontOfSize:14]; + automaticCell.textLabel.lineBreakMode = NSLineBreakByWordWrapping; + automaticCell.textLabel.numberOfLines = 0; + automaticCell.textLabel.text = text; + automaticCell.wantsLayer = YES; + automaticCell.layer.cornerRadius = 8; + automaticCell.layer.cornerCurve = kCACornerCurveContinuous; + return automaticCell; + } + + RCTUITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"fixed"]; + if (cell == nil) { + cell = [[RCTUITableViewCell alloc] initWithStyle:RCTUITableViewCellStyleDefault reuseIdentifier:@"fixed"]; + } + cell.textLabel.text = @"Fixed"; + return cell; + }; + dataSource.heightProvider = ^CGFloat(NSIndexPath *indexPath) { + return indexPath.section == 0 ? RCTUITableViewAutomaticDimension : 50; + }; + + [self installDataSource:dataSource delegate:dataSource]; + [self setContentWidth:880]; + [self reloadAndDisplay]; + + NSTableView *backingTable = RCTUIBackingTable(_tableView); + XCTAssertTrue(backingTable.usesAutomaticRowHeights); + XCTAssertEqual(backingTable.effectiveStyle, NSTableViewStyleInset); + + RCTUIInsetControlDataSource *controlDataSource = [RCTUIInsetControlDataSource new]; + NSTableView *wideControlTable = + RCTUICreateInsetControlTable(NSWidth(backingTable.bounds), controlDataSource); + NSRect wideCellFrame = [backingTable frameOfCellAtColumn:0 row:0]; + NSRect wideControlFrame = [wideControlTable frameOfCellAtColumn:0 row:0]; + XCTAssertEqual(wideCellFrame.origin.x, wideControlFrame.origin.x); + XCTAssertEqual(wideCellFrame.size.width, wideControlFrame.size.width); + CGFloat wideLeftInset = NSMinX(wideCellFrame); + CGFloat wideRightInset = NSWidth(backingTable.bounds) - NSMaxX(wideCellFrame); + XCTAssertEqualWithAccuracy(wideLeftInset, wideRightInset, 0.5); + + CGFloat rowSpacing = backingTable.intercellSpacing.height; + CGFloat wideAutomaticHeight = NSHeight([backingTable rectOfRow:0]) - rowSpacing; + CGFloat wideFixedHeight = NSHeight([backingTable rectOfRow:1]); + XCTAssertEqual([backingTable.delegate tableView:backingTable heightOfRow:1], 50); + XCTAssertEqual([backingTable.delegate tableView:backingTable heightOfRow:2], 50); + XCTAssertEqualWithAccuracy(wideFixedHeight, 50 + rowSpacing, 0.5); + XCTAssertEqualWithAccuracy(NSHeight([backingTable rectOfRow:2]), wideFixedHeight, 0.5); + XCTAssertGreaterThan(wideAutomaticHeight, 50); + XCTAssertGreaterThan(automaticCell.textLabel.preferredMaxLayoutWidth, 0); + CGFloat widePreferredWidth = automaticCell.textLabel.preferredMaxLayoutWidth; + XCTAssertEqual(automaticCell.layer.cornerRadius, 8); + XCTAssertEqualObjects(automaticCell.layer.cornerCurve, kCACornerCurveContinuous); + + [self setContentWidth:320]; + NSTableView *narrowControlTable = + RCTUICreateInsetControlTable(NSWidth(backingTable.bounds), controlDataSource); + NSRect narrowCellFrame = [backingTable frameOfCellAtColumn:0 row:0]; + NSRect narrowControlFrame = [narrowControlTable frameOfCellAtColumn:0 row:0]; + XCTAssertEqual(narrowCellFrame.origin.x, narrowControlFrame.origin.x); + XCTAssertEqual(narrowCellFrame.size.width, narrowControlFrame.size.width); + CGFloat narrowLeftInset = NSMinX(narrowCellFrame); + CGFloat narrowRightInset = NSWidth(backingTable.bounds) - NSMaxX(narrowCellFrame); + XCTAssertEqualWithAccuracy(narrowLeftInset, narrowRightInset, 0.5); + XCTAssertEqualWithAccuracy(narrowLeftInset, wideLeftInset, 0.5); + + CGFloat narrowAutomaticHeight = NSHeight([backingTable rectOfRow:0]) - rowSpacing; + CGFloat narrowFixedHeight = NSHeight([backingTable rectOfRow:1]); + XCTAssertGreaterThan(narrowAutomaticHeight, wideAutomaticHeight); + XCTAssertLessThan(automaticCell.textLabel.preferredMaxLayoutWidth, widePreferredWidth); + XCTAssertEqualWithAccuracy(narrowFixedHeight, wideFixedHeight, 0.5); + XCTAssertEqualWithAccuracy(NSHeight([backingTable rectOfRow:2]), wideFixedHeight, 0.5); + XCTAssertEqual([backingTable.delegate tableView:backingTable heightOfRow:1], 50); + XCTAssertEqual([backingTable.delegate tableView:backingTable heightOfRow:2], 50); +} + +- (void)testCellLayoutInsetsAndMessageCardLayer +{ + RCTUITableViewTestDataSource *dataSource = [RCTUITableViewTestDataSource new]; + dataSource.rowCounts = @[@2]; + dataSource.heightProvider = ^CGFloat(NSIndexPath *indexPath) { + return 80; + }; + dataSource.cellProvider = ^RCTUITableViewCell *(RCTUITableView *tableView, NSIndexPath *indexPath) { + RCTUITableViewCellStyle style = + indexPath.row == 0 ? RCTUITableViewCellStyleDefault : RCTUITableViewCellStyleSubtitle; + NSString *identifier = indexPath.row == 0 ? @"message-layout" : @"stack-layout"; + RCTUITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier]; + if (cell == nil) { + cell = [[RCTUITableViewCell alloc] initWithStyle:style reuseIdentifier:identifier]; + } + cell.textLabel.text = indexPath.row == 0 ? @"Message" : @"Stack frame"; + cell.detailTextLabel.text = @"RCTUITableViewParity.js:42:7"; + if (indexPath.row == 0) { + cell.wantsLayer = YES; + cell.layer.backgroundColor = [NSColor colorWithRed:0.82 green:0.10 blue:0.15 alpha:1].CGColor; + cell.layer.cornerRadius = 8; + cell.layer.cornerCurve = kCACornerCurveContinuous; + } + return cell; + }; + + [self installDataSource:dataSource delegate:dataSource]; + [self reloadAndDisplay]; + + NSTableView *backingTable = RCTUIBackingTable(_tableView); + for (NSInteger row = 0; row < 2; row++) { + RCTUITableViewCell *cell = [backingTable viewAtColumn:0 row:row makeIfNecessary:YES]; + [cell layoutSubtreeIfNeeded]; + + XCTAssertEqual(cell.rowSizeStyle, NSTableViewRowSizeStyleCustom); + XCTAssertGreaterThanOrEqual(NSMinX(cell.textLabel.frame), 5); + XCTAssertGreaterThanOrEqual(NSMinY(cell.textLabel.frame), 5); + XCTAssertLessThanOrEqual(NSMaxX(cell.textLabel.frame), NSMaxX(cell.contentView.bounds) - 5); + XCTAssertLessThanOrEqual(NSMaxY(cell.textLabel.frame), NSMaxY(cell.contentView.bounds) - 5); + XCTAssertTrue(NSContainsRect(cell.contentView.bounds, cell.textLabel.frame)); + XCTAssertTrue(NSContainsRect(cell.textLabel.bounds, cell.textLabel.visibleRect)); + XCTAssertTrue( + NSContainsRect(cell.textLabel.bounds, [cell.textLabel.cell drawingRectForBounds:cell.textLabel.bounds])); + } + + RCTUITableViewCell *messageCell = [backingTable viewAtColumn:0 row:0 makeIfNecessary:YES]; + XCTAssertEqual(messageCell.layer.cornerRadius, 8); + XCTAssertEqualObjects(messageCell.layer.cornerCurve, kCACornerCurveContinuous); + XCTAssertNotNil((__bridge id)messageCell.layer.backgroundColor); +} + +- (void)testHeadersAreLazyStablePerReloadAndHeightZeroHasNoSlot +{ + RCTUITableViewTestDataSource *dataSource = [RCTUITableViewTestDataSource new]; + dataSource.rowCounts = @[@40, @0, @40]; + dataSource.headerHeights = @[@30, @0, @25]; + dataSource.headerProvider = ^RCTPlatformView *(NSInteger section) { + RCTPlatformView *view = [RCTPlatformView new]; + view.accessibilityIdentifier = [NSString stringWithFormat:@"header-%ld", (long)section]; + return view; + }; + [self installDataSource:dataSource delegate:dataSource]; + [self reloadAndDisplay]; + + NSTableView *backingTable = RCTUIBackingTable(_tableView); + XCTAssertEqual(backingTable.numberOfRows, 82); + XCTAssertEqual([backingTable.delegate tableView:backingTable heightOfRow:0], 30); + XCTAssertEqual([backingTable.delegate tableView:backingTable heightOfRow:41], 25); + XCTAssertEqualWithAccuracy( + NSHeight([backingTable rectOfRow:0]), 30 + backingTable.intercellSpacing.height, 0.5); + XCTAssertEqualWithAccuracy( + NSHeight([backingTable rectOfRow:41]), 25 + backingTable.intercellSpacing.height, 0.5); + XCTAssertFalse([backingTable.delegate tableView:backingTable shouldSelectRow:0]); + XCTAssertFalse([backingTable.delegate tableView:backingTable shouldSelectRow:41]); + + NSView *firstHeader = [backingTable.delegate tableView:backingTable + viewForTableColumn:backingTable.tableColumns.firstObject + row:0]; + NSView *firstHeaderAgain = [backingTable.delegate tableView:backingTable + viewForTableColumn:backingTable.tableColumns.firstObject + row:0]; + XCTAssertEqual(firstHeader, firstHeaderAgain); + NSView *thirdSectionHeader = [backingTable.delegate tableView:backingTable + viewForTableColumn:backingTable.tableColumns.firstObject + row:41]; + NSView *thirdSectionHeaderAgain = [backingTable.delegate tableView:backingTable + viewForTableColumn:backingTable.tableColumns.firstObject + row:41]; + XCTAssertEqual(thirdSectionHeader, thirdSectionHeaderAgain); + XCTAssertEqual(dataSource.headerViewCallCounts[@0].integerValue, 1); + XCTAssertEqual(dataSource.headerViewCallCounts[@1].integerValue, 0); + XCTAssertEqual(dataSource.headerViewCallCounts[@2].integerValue, 1); + + [_tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:39 inSection:2] + atScrollPosition:RCTUITableViewScrollPositionTop + animated:NO]; + [self forceLayoutAndDisplay]; + [_tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] + atScrollPosition:RCTUITableViewScrollPositionTop + animated:NO]; + [self forceLayoutAndDisplay]; + XCTAssertEqual(dataSource.headerViewCallCounts[@0].integerValue, 1); + XCTAssertEqual(dataSource.headerViewCallCounts[@2].integerValue, 1); + + dataSource.headerViewCallCounts[@0] = @0; + dataSource.headerViewCallCounts[@2] = @0; + [self reloadAndDisplay]; + NSView *nextGenerationHeader = [backingTable.delegate tableView:backingTable + viewForTableColumn:backingTable.tableColumns.firstObject + row:0]; + XCTAssertNotEqual(firstHeader, nextGenerationHeader); + XCTAssertEqual(dataSource.headerViewCallCounts[@0].integerValue, 1); + XCTAssertEqual(dataSource.headerViewCallCounts[@1].integerValue, 0); + + dataSource.headerProvider = ^RCTPlatformView *(NSInteger section) { + return nil; + }; + [self reloadAndDisplay]; + NSView *emptyHeader = [backingTable.delegate tableView:backingTable + viewForTableColumn:backingTable.tableColumns.firstObject + row:0]; + XCTAssertNotNil(emptyHeader); + XCTAssertTrue([emptyHeader isKindOfClass:RCTPlatformView.class]); +} + +- (void)testReusedHeaderViewMaintainsOnePrimitiveHeightConstraint +{ + RCTUITableViewTestDataSource *dataSource = [RCTUITableViewTestDataSource new]; + dataSource.rowCounts = @[@1]; + dataSource.headerHeights = @[@30]; + RCTPlatformView *sharedHeader = [RCTPlatformView new]; + dataSource.headerProvider = ^RCTPlatformView *(NSInteger section) { + return sharedHeader; + }; + [self installDataSource:dataSource delegate:dataSource]; + [self reloadAndDisplay]; + + NSTableView *backingTable = RCTUIBackingTable(_tableView); + XCTAssertEqual([backingTable viewAtColumn:0 row:0 makeIfNecessary:YES], sharedHeader); + NSPredicate *primitiveConstraint = + [NSPredicate predicateWithFormat:@"identifier == %@", @"RCTUITableViewHeaderHeight"]; + NSArray *heightConstraints = + [sharedHeader.constraints filteredArrayUsingPredicate:primitiveConstraint]; + XCTAssertFalse(sharedHeader.translatesAutoresizingMaskIntoConstraints); + XCTAssertEqual(heightConstraints.count, 1); + XCTAssertTrue(heightConstraints.firstObject.active); + XCTAssertEqual(heightConstraints.firstObject.constant, 30); + NSLayoutConstraint *originalHeightConstraint = heightConstraints.firstObject; + + dataSource.headerHeights = @[@44]; + [self reloadAndDisplay]; + XCTAssertEqual([backingTable viewAtColumn:0 row:0 makeIfNecessary:YES], sharedHeader); + heightConstraints = [sharedHeader.constraints filteredArrayUsingPredicate:primitiveConstraint]; + XCTAssertEqual(heightConstraints.count, 1); + XCTAssertEqual(heightConstraints.firstObject, originalHeightConstraint); + XCTAssertTrue(heightConstraints.firstObject.active); + XCTAssertEqual(heightConstraints.firstObject.constant, 44); +} + +- (void)testAccessibilityContainerAndCellIdentifiersSurviveReuse +{ + RCTUITableViewTestDataSource *dataSource = [RCTUITableViewTestDataSource new]; + dataSource.rowCounts = @[@100]; + dataSource.cellProvider = ^RCTUITableViewCell *(RCTUITableView *tableView, NSIndexPath *indexPath) { + RCTUITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"accessible"]; + if (cell == nil) { + cell = [[RCTUITableViewCell alloc] initWithStyle:RCTUITableViewCellStyleDefault + reuseIdentifier:@"accessible"]; + } + cell.textLabel.accessibilityIdentifier = + indexPath.row == 0 ? @"redbox-error" : [NSString stringWithFormat:@"cell-%ld", (long)indexPath.row]; + return cell; + }; + + _tableView.accessibilityIdentifier = @"compatibility-table"; + [self installDataSource:dataSource delegate:dataSource]; + [self reloadAndDisplay]; + + NSTableView *backingTable = RCTUIBackingTable(_tableView); + XCTAssertEqualObjects(backingTable.accessibilityRole, NSAccessibilityTableRole); + XCTAssertEqualObjects(backingTable.accessibilityIdentifier, @"compatibility-table"); + + RCTUITableViewCell *firstCell = [backingTable viewAtColumn:0 row:0 makeIfNecessary:YES]; + XCTAssertEqualObjects(firstCell.textLabel.accessibilityIdentifier, @"redbox-error"); + XCTAssertTrue(RCTUIAccessibilityTreeContainsIdentifier(backingTable, @"redbox-error", 6)); + + [_tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:99 inSection:0] + atScrollPosition:RCTUITableViewScrollPositionTop + animated:NO]; + [self forceLayoutAndDisplay]; + XCTAssertTrue(RCTUIAccessibilityTreeContainsIdentifier(backingTable, @"cell-99", 6)); + [_tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] + atScrollPosition:RCTUITableViewScrollPositionTop + animated:NO]; + [self forceLayoutAndDisplay]; + + firstCell = [backingTable viewAtColumn:0 row:0 makeIfNecessary:YES]; + XCTAssertEqualObjects(firstCell.textLabel.accessibilityIdentifier, @"redbox-error"); + XCTAssertTrue(RCTUIAccessibilityTreeContainsIdentifier(backingTable, @"redbox-error", 6)); +} + +- (void)testScrollToTopPositionAndReturnToFirstRow +{ + RCTUITableViewTestDataSource *dataSource = [RCTUITableViewTestDataSource new]; + dataSource.rowCounts = @[@100]; + dataSource.heightProvider = ^CGFloat(NSIndexPath *indexPath) { + return 30; + }; + [self installDataSource:dataSource delegate:dataSource]; + [self reloadAndDisplay]; + + NSTableView *backingTable = RCTUIBackingTable(_tableView); + [_tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:80 inSection:0] + atScrollPosition:RCTUITableViewScrollPositionTop + animated:NO]; + [self forceLayoutAndDisplay]; + + NSRect targetRect = [backingTable rectOfRow:80]; + XCTAssertEqualWithAccuracy(NSMinY(targetRect), NSMinY(backingTable.visibleRect), 1); + XCTAssertTrue(NSContainsRect(backingTable.visibleRect, targetRect)); + + [_tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] + atScrollPosition:RCTUITableViewScrollPositionTop + animated:NO]; + [self forceLayoutAndDisplay]; + XCTAssertEqualWithAccuracy(NSMinY(backingTable.visibleRect), 0, 0.5); +} + +- (void)testLabelButtonActionAndIdentifierForwarding +{ + RCTUILabel *label = [RCTUILabel new]; + label.text = @"forwarded"; + label.numberOfLines = 3; + label.textAlignment = NSTextAlignmentCenter; + XCTAssertEqualObjects(label.text, @"forwarded"); + XCTAssertEqualObjects(label.stringValue, @"forwarded"); + XCTAssertEqual(label.numberOfLines, 3); + XCTAssertEqual(label.maximumNumberOfLines, 3); + XCTAssertEqual(label.textAlignment, NSTextAlignmentCenter); + XCTAssertEqual(label.alignment, NSTextAlignmentCenter); + + RCTUIButton *button = [RCTUIButton new]; + NSFont *font = [NSFont systemFontOfSize:13]; + button.titleLabel.font = font; + button.titleLabel.lineBreakMode = NSLineBreakByWordWrapping; + button.titleLabel.textAlignment = NSTextAlignmentCenter; + [button setTitle:@"Reload" forState:RCTUIControlStateNormal]; + [button setTitleColor:NSColor.whiteColor forState:RCTUIControlStateNormal]; + [button setTitleColor:[NSColor colorWithWhite:1 alpha:0.5] forState:RCTUIControlStateHighlighted]; + button.accessibilityIdentifier = @"redbox-reload"; + + XCTAssertEqualObjects(button.attributedTitle.string, @"Reload"); + XCTAssertEqualObjects( + [button.attributedTitle attribute:NSFontAttributeName atIndex:0 effectiveRange:nil], font); + NSParagraphStyle *paragraphStyle = + [button.attributedTitle attribute:NSParagraphStyleAttributeName atIndex:0 effectiveRange:nil]; + XCTAssertEqual(paragraphStyle.lineBreakMode, NSLineBreakByWordWrapping); + XCTAssertEqual(paragraphStyle.alignment, NSTextAlignmentCenter); + XCTAssertEqualObjects(button.accessibilityIdentifier, @"redbox-reload"); + + __block NSInteger clickCount = 0; + __weak RCTUIAction *weakAction; + @autoreleasepool { + RCTUIAction *action = [RCTUIAction actionWithHandler:^{ + clickCount++; + }]; + weakAction = action; + [button rct_setPrimaryAction:action]; + } + XCTAssertNotNil(weakAction); + [button performClick:nil]; + XCTAssertEqual(clickCount, 1); +} + +@end + +#endif // macOS]