diff --git a/packages/react-native/React/Fabric/Mounting/RCTComponentViewFactory.mm b/packages/react-native/React/Fabric/Mounting/RCTComponentViewFactory.mm index ddf36b22959..47bd98c12a5 100644 --- a/packages/react-native/React/Fabric/Mounting/RCTComponentViewFactory.mm +++ b/packages/react-native/React/Fabric/Mounting/RCTComponentViewFactory.mm @@ -19,6 +19,7 @@ #import #import #import +#import #import #import #import @@ -47,6 +48,18 @@ using namespace facebook; using namespace facebook::react; +@interface RCTSelectableParagraphComponentView : RCTParagraphComponentView +@end + +@implementation RCTSelectableParagraphComponentView + ++ (ComponentDescriptorProvider)componentDescriptorProvider +{ + return concreteComponentDescriptorProvider(); +} + +@end + @interface RCTComponentViewFactory () - (void)_registerComponentIfPossible:(const std::string &)name; - (BOOL)_wasComponentRegistered:(const std::string &)name; @@ -85,6 +98,7 @@ + (RCTComponentViewFactory *)currentComponentViewFactory componentViewFactory = [RCTComponentViewFactory new]; [componentViewFactory registerComponentViewClass:[RCTRootComponentView class]]; [componentViewFactory registerComponentViewClass:[RCTParagraphComponentView class]]; + [componentViewFactory registerComponentViewClass:[RCTSelectableParagraphComponentView class]]; componentViewFactory->_providerRegistry.add( concreteComponentDescriptorProvider()); diff --git a/packages/react-native/React/Tests/Text/RCTSelectableParagraphComponentViewTests.mm b/packages/react-native/React/Tests/Text/RCTSelectableParagraphComponentViewTests.mm new file mode 100644 index 00000000000..d2e7335c341 --- /dev/null +++ b/packages/react-native/React/Tests/Text/RCTSelectableParagraphComponentViewTests.mm @@ -0,0 +1,117 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +using facebook::react::AccessibilityRole; +using facebook::react::ComponentBuilder; +using facebook::react::Element; +using facebook::react::ParagraphProps; +using facebook::react::ParagraphShadowNode; +using facebook::react::RawTextProps; +using facebook::react::RawTextShadowNode; +using facebook::react::RootProps; +using facebook::react::RootShadowNode; +using facebook::react::SelectableParagraphShadowNode; +using facebook::react::TextProps; +using facebook::react::TextShadowNode; + +@interface RCTSelectableParagraphComponentViewTests : XCTestCase +@end + +@implementation RCTSelectableParagraphComponentViewTests + +- (void)checkParagraphWithName:(const std::string &)name selectable:(BOOL)selectable +{ + auto factory = [RCTComponentViewFactory currentComponentViewFactory]; + auto registry = [factory createComponentDescriptorRegistryWithParameters:{.eventDispatcher = {}, + .contextContainer = nullptr, + .flavor = nullptr}]; + const auto &descriptor = registry->at(name); + auto view = [factory createComponentViewWithComponentHandle:descriptor.getComponentHandle()].view; + XCTAssertTrue([view isKindOfClass:RCTParagraphComponentView.class]); + if (![view isKindOfClass:RCTParagraphComponentView.class]) { + return; + } + + auto paragraphProps = std::make_shared(); + paragraphProps->accessible = true; + paragraphProps->isSelectable = selectable; + paragraphProps->yogaStyle.setDimension( + facebook::yoga::Dimension::Width, facebook::yoga::StyleSizeLength::points(200)); + auto textProps = std::make_shared(); + textProps->textAttributes.accessibilityRole = AccessibilityRole::Link; + auto rawTextProps = std::make_shared(); + rawTextProps->text = "Selectable paragraph link"; + auto text = Element().props(textProps).children({Element().props(rawTextProps)}); + auto rootProps = std::make_shared(); + rootProps->layoutConstraints = { + .minimumSize = {.width = 0, .height = 0}, .maximumSize = {.width = 200, .height = 200}}; + ComponentBuilder builder{registry}; + std::shared_ptr paragraph; + std::shared_ptr root; + if (selectable) { + std::shared_ptr selectableParagraph; + builder.build( + Element().reference(root).props(rootProps).children({Element() + .reference(selectableParagraph) + .props(paragraphProps) + .children({text})})); + paragraph = selectableParagraph; + } else { + builder.build( + Element().reference(root).props(rootProps).children( + {Element().reference(paragraph).props(paragraphProps).children({text})})); + } + root->layoutIfNeeded(); + [view updateProps:paragraph->getProps() oldProps:nullptr]; + [view updateEventEmitter:paragraph->getEventEmitter()]; + [view updateState:paragraph->getState() oldState:nullptr]; + [view updateLayoutMetrics:paragraph->getLayoutMetrics() oldLayoutMetrics:{}]; + [view finalizeUpdates:RNComponentViewUpdateMaskAll]; + auto paragraphView = (RCTParagraphComponentView *)view; + + XCTAssertEqualObjects(paragraphView.attributedText.string, @"Selectable paragraph link"); + XCTAssertGreaterThan(paragraphView.bounds.size.height, 0); + XCTAssertEqualObjects(paragraphView.accessibilityLabel, @"Selectable paragraph link"); + NSArray *elements = paragraphView.accessibilityElements; + XCTAssertEqual(elements.count, 1); + XCTAssertTrue(elements.firstObject.accessibilityTraits & UIAccessibilityTraitLink); + XCTAssertEqual([paragraphView canBecomeFirstResponder], selectable); + XCTAssertEqual([paragraphView canPerformAction:@selector(copy:) withSender:nil], selectable); + XCTAssertEqual( + [paragraphView touchEventEmitterAtPoint:CGPointMake(1000, 1000)].get(), paragraph->getEventEmitter().get()); + + auto nonSelectableProps = std::make_shared(*paragraphProps); + nonSelectableProps->isSelectable = false; + [view updateProps:nonSelectableProps oldProps:paragraphProps]; + XCTAssertFalse([paragraphView canBecomeFirstResponder]); + [view prepareForRecycle]; + XCTAssertNil(paragraphView.attributedText); + XCTAssertEqual(paragraphView.accessibilityElements.count, 0); +} + +- (void)testOrdinaryParagraphPreservesText +{ + [self checkParagraphWithName:"RCTText" selectable:NO]; +} + +- (void)testSelectableParagraphPreservesTextAndSelection +{ + [self checkParagraphWithName:"RCTSelectableText" selectable:YES]; +} + +@end