diff --git a/packages/react-native/Libraries/Text/TextProps.js b/packages/react-native/Libraries/Text/TextProps.js index 6fb8b163595a..aa0c76ea3ff2 100644 --- a/packages/react-native/Libraries/Text/TextProps.js +++ b/packages/react-native/Libraries/Text/TextProps.js @@ -101,14 +101,6 @@ export type TextPropsAndroid = { */ selectionColor?: ?ColorValue, - /** - * Types of data converted to clickable URLs in the text element. - * - * @default `'none'` - * @platform android - */ - dataDetectorType?: ?('phoneNumber' | 'link' | 'email' | 'none' | 'all'), - /** * Text break strategy on Android. * @@ -127,6 +119,13 @@ export type TextPropsAndroid = { }; type TextBaseProps = Readonly<{ + /** + * Types of data converted to clickable URLs in the text element. + * + * @default `'none'` + */ + dataDetectorType?: ?('phoneNumber' | 'link' | 'email' | 'none' | 'all'), + onAccessibilityAction?: ?(event: AccessibilityActionEvent) => unknown, /** diff --git a/packages/react-native/Libraries/Text/__tests__/Text-itest.js b/packages/react-native/Libraries/Text/__tests__/Text-itest.js index f2126b41af5d..d548e51f7667 100644 --- a/packages/react-native/Libraries/Text/__tests__/Text-itest.js +++ b/packages/react-native/Libraries/Text/__tests__/Text-itest.js @@ -150,6 +150,48 @@ describe('', () => { }); }); + describe('dataDetectorType', () => { + (['all', 'email', 'link', 'phoneNumber'] as const).forEach(propVal => { + it(`can be set to "${propVal}"`, () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render({TEST_TEXT}); + }); + + expect( + root.getRenderedOutput({props: ['dataDetectorType']}).toJSX(), + ).toEqual( + {TEST_TEXT}, + ); + }); + }); + + it(`can be set to "none"`, () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render({TEST_TEXT}); + }); + + expect( + root.getRenderedOutput({props: ['dataDetectorType']}).toJSX(), + ).toEqual({TEST_TEXT}); + }); + + it(`has "none" as default`, () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render({TEST_TEXT}); + }); + + expect( + root.getRenderedOutput({props: ['dataDetectorType']}).toJSX(), + ).toEqual({TEST_TEXT}); + }); + }); + describe('ellipsizeMode', () => { it(`has 'tail' as default on JS side`, () => { const root = Fantom.createRoot(); diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentAccessibilityProvider.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentAccessibilityProvider.h index 3fb0da7c2a5f..f6391cb25ca3 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentAccessibilityProvider.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentAccessibilityProvider.h @@ -15,6 +15,11 @@ @interface RCTParagraphComponentAccessibilityProvider : NSObject +/** + * The data that the provider adds link elements for. Set it before reading `accessibilityElements`. + */ +@property (nonatomic) RCTTextDataDetectorTypes dataDetectorTypes; + - (instancetype)initWithString:(facebook::react::AttributedString)attributedString layoutManager:(RCTTextLayoutManager *)layoutManager paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentAccessibilityProvider.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentAccessibilityProvider.mm index c2cc3884264f..f467228812ca 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentAccessibilityProvider.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentAccessibilityProvider.mm @@ -73,9 +73,15 @@ - (instancetype)initWithString:(facebook::react::AttributedString)attributedStri [elements addObject:firstElement]; + // The rects below are relative to the text, which starts after the padding. + const CGPoint textOrigin = _frame.origin; + const CGPoint textOriginOnScreen = CGPointMake( + firstElement.accessibilityFrame.origin.x + textOrigin.x, firstElement.accessibilityFrame.origin.y + textOrigin.y); + // add additional elements for those parts of text with embedded link so VoiceOver could specially recognize links [_layoutManager getRectWithAttributedString:_attributedString paragraphAttributes:_paragraphAttributes + dataDetectorTypes:_dataDetectorTypes enumerateAttribute:RCTTextAttributesAccessibilityRoleAttributeName frame:_frame usingBlock:^(CGRect fragmentRect, NSString *_Nonnull fragmentText, NSString *value) { @@ -86,6 +92,9 @@ - (instancetype)initWithString:(facebook::react::AttributedString)attributedStri if ([fragmentText isEqualToString:firstElement.accessibilityLabel]) { if ([value isEqualToString:@"link"]) { firstElement.accessibilityTraits |= UIAccessibilityTraitLink; + firstElement.accessibilityActivationPoint = CGPointMake( + textOriginOnScreen.x + CGRectGetMidX(fragmentRect), + textOriginOnScreen.y + CGRectGetMidY(fragmentRect)); } else if ([value isEqualToString:@"button"]) { firstElement.accessibilityTraits |= UIAccessibilityTraitButton; } @@ -110,7 +119,8 @@ - (instancetype)initWithString:(facebook::react::AttributedString)attributedStri numberOfButtons++; } element.accessibilityLabel = fragmentText; - element.frameInContainerSpace = fragmentRect; + element.frameInContainerSpace = + CGRectOffset(fragmentRect, textOrigin.x, textOrigin.y); [elements addObject:element]; }]; diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm index 4d7c72408486..c918bed73784 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm @@ -9,6 +9,7 @@ #import "RCTParagraphComponentAccessibilityProvider.h" #import +#import #import #import #import @@ -44,19 +45,36 @@ static BOOL RCTAttributedStringHasAttachments(const AttributedString &attributed return NO; } +static RCTTextDataDetectorTypes RCTTextDataDetectorTypesFromProp(DataDetectorType dataDetectorType) +{ + switch (dataDetectorType) { + case DataDetectorType::All: + return RCTTextDataDetectorTypePhoneNumber | RCTTextDataDetectorTypeLink | RCTTextDataDetectorTypeEmail; + case DataDetectorType::Email: + return RCTTextDataDetectorTypeEmail; + case DataDetectorType::Link: + return RCTTextDataDetectorTypeLink; + case DataDetectorType::None: + return RCTTextDataDetectorTypeNone; + case DataDetectorType::PhoneNumber: + return RCTTextDataDetectorTypePhoneNumber; + } +} + // ParagraphTextView is an auxiliary view we set as contentView so the drawing // can happen on top of the layers manipulated by RCTViewComponentView (the parent view) @interface RCTParagraphTextView : UIView @property (nonatomic) ParagraphShadowNode::ConcreteState::Shared state; @property (nonatomic) ParagraphAttributes paragraphAttributes; +@property (nonatomic) RCTTextDataDetectorTypes dataDetectorTypes; @property (nonatomic) LayoutMetrics layoutMetrics; @property (nonatomic) CGRect drawingFrame; @end #if !TARGET_OS_TV -@interface RCTParagraphComponentView () +@interface RCTParagraphComponentView () @property (nonatomic, nullable) UIEditMenuInteraction *editMenuInteraction API_AVAILABLE(ios(16.0)); @@ -68,8 +86,12 @@ @interface RCTParagraphComponentView () @implementation RCTParagraphComponentView { ParagraphAttributes _paragraphAttributes; + RCTTextDataDetectorTypes _dataDetectorTypes; RCTParagraphComponentAccessibilityProvider *_accessibilityProvider; UILongPressGestureRecognizer *_longPressGestureRecognizer; +#if !TARGET_OS_TV + UITapGestureRecognizer *_detectedURLTapGestureRecognizer; +#endif RCTParagraphTextView *_textView; CGRect _textLayoutFrame; } @@ -132,6 +154,15 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & _paragraphAttributes = newParagraphProps.paragraphAttributes; _textView.paragraphAttributes = _paragraphAttributes; + RCTTextDataDetectorTypes dataDetectorTypes = RCTTextDataDetectorTypesFromProp(newParagraphProps.dataDetectorType); + if (dataDetectorTypes != _dataDetectorTypes) { + _dataDetectorTypes = dataDetectorTypes; + _textView.dataDetectorTypes = dataDetectorTypes; + _accessibilityProvider = nil; + [_textView setNeedsDisplay]; + [self _updateDetectedURLTapGestureRecognizer]; + } + if (newParagraphProps.isSelectable != oldParagraphProps.isSelectable) { if (newParagraphProps.isSelectable) { [self enableContextMenu]; @@ -297,6 +328,7 @@ - (NSArray *)accessibilityElements paragraphAttributes:data.paragraphAttributes frame:frame view:self]; + _accessibilityProvider.dataDetectorTypes = _dataDetectorTypes; } } @@ -379,6 +411,72 @@ - (SharedTouchEventEmitter)touchEventEmitterAtPoint:(CGPoint)point return std::static_pointer_cast(eventEmitter); } +#pragma mark - Detected URLs + +#if !TARGET_OS_TV +- (void)_updateDetectedURLTapGestureRecognizer +{ + BOOL needsRecognizer = _dataDetectorTypes != RCTTextDataDetectorTypeNone; + if (needsRecognizer && _detectedURLTapGestureRecognizer == nil) { + _detectedURLTapGestureRecognizer = + [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(_handleDetectedURLTap:)]; + _detectedURLTapGestureRecognizer.cancelsTouchesInView = NO; + _detectedURLTapGestureRecognizer.delegate = self; + [self addGestureRecognizer:_detectedURLTapGestureRecognizer]; + } else if (!needsRecognizer && _detectedURLTapGestureRecognizer != nil) { + [self removeGestureRecognizer:_detectedURLTapGestureRecognizer]; + _detectedURLTapGestureRecognizer = nil; + } +} + +- (nullable NSURL *)_detectedURLAtPoint:(CGPoint)point +{ + if (!_textView.state) { + return nil; + } + const auto &stateData = _textView.state->getData(); + auto textLayoutManager = stateData.layoutManager.lock(); + if (!textLayoutManager) { + return nil; + } + RCTTextLayoutManager *nativeTextLayoutManager = + (RCTTextLayoutManager *)unwrapManagedObject(textLayoutManager->getNativeTextLayoutManager()); + return [nativeTextLayoutManager detectedURLWithAttributedString:stateData.attributedString + paragraphAttributes:_paragraphAttributes + dataDetectorTypes:_dataDetectorTypes + frame:_textLayoutFrame + atPoint:point]; +} + +- (void)_handleDetectedURLTap:(UITapGestureRecognizer *)gesture +{ + NSURL *url = [self _detectedURLAtPoint:[gesture locationInView:self]]; + if (url != nil) { + [RCTSharedApplication() openURL:url options:@{} completionHandler:nil]; + } +} + +// Called when the tap is about to be recognized, so a touch that starts a scroll doesn't pay for this. +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer +{ + if (gestureRecognizer != _detectedURLTapGestureRecognizer) { + return [super gestureRecognizerShouldBegin:gestureRecognizer]; + } + return [self _detectedURLAtPoint:[gestureRecognizer locationInView:self]] != nil; +} + +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer + shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer +{ + // A long press on selectable text shows the context menu instead of opening the URL. + return otherGestureRecognizer == _longPressGestureRecognizer; +} +#else +- (void)_updateDetectedURLTapGestureRecognizer +{ +} +#endif + #pragma mark - Context Menu #if !TARGET_OS_TV @@ -504,6 +602,7 @@ - (void)drawRect:(CGRect)rect [nativeTextLayoutManager drawAttributedString:stateData.attributedString paragraphAttributes:_paragraphAttributes + dataDetectorTypes:_dataDetectorTypes frame:frame drawHighlightPath:^(UIBezierPath *highlightPath) { if (highlightPath) { diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/platform/cxx/react/renderer/components/text/HostPlatformParagraphProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/platform/cxx/react/renderer/components/text/HostPlatformParagraphProps.cpp new file mode 100644 index 000000000000..c4f218550747 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/text/platform/cxx/react/renderer/components/text/HostPlatformParagraphProps.cpp @@ -0,0 +1,54 @@ +/* + * 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. + */ + +#include "HostPlatformParagraphProps.h" + +#include +#include +#include + +namespace facebook::react { + +HostPlatformParagraphProps::HostPlatformParagraphProps( + const PropsParserContext& context, + const HostPlatformParagraphProps& sourceProps, + const RawProps& rawProps) + : BaseParagraphProps(context, sourceProps, rawProps), + dataDetectorType(convertRawProp( + context, + rawProps, + "dataDetectorType", + sourceProps.dataDetectorType, + DataDetectorType::None)) {} + +void HostPlatformParagraphProps::setProp( + const PropsParserContext& context, + RawPropsPropNameHash hash, + const char* propName, + const RawValue& value) { + // All Props structs setProp methods must always, unconditionally, + // call all super::setProp methods, since multiple structs may + // reuse the same values. + BaseParagraphProps::setProp(context, hash, propName, value); + + static auto defaults = HostPlatformParagraphProps{}; + + switch (hash) { RAW_SET_PROP_SWITCH_CASE_BASIC(dataDetectorType); } +} + +#pragma mark - DebugStringConvertible + +#if RN_DEBUG_STRING_CONVERTIBLE +SharedDebugStringConvertibleList HostPlatformParagraphProps::getDebugProps() + const { + return BaseParagraphProps::getDebugProps() + + SharedDebugStringConvertibleList{debugStringConvertibleItem( + "dataDetectorType", dataDetectorType, DataDetectorType::None)}; +} +#endif + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/platform/cxx/react/renderer/components/text/HostPlatformParagraphProps.h b/packages/react-native/ReactCommon/react/renderer/components/text/platform/cxx/react/renderer/components/text/HostPlatformParagraphProps.h index 3d88cc6439ce..a622efaa6a87 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/platform/cxx/react/renderer/components/text/HostPlatformParagraphProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/text/platform/cxx/react/renderer/components/text/HostPlatformParagraphProps.h @@ -10,7 +10,35 @@ #include #include +#include namespace facebook::react { -using HostPlatformParagraphProps = BaseParagraphProps; + +/* + * Props of component. + * Most of the props are directly stored in composed `ParagraphAttributes` + * object. + */ +class HostPlatformParagraphProps : public BaseParagraphProps { + public: + HostPlatformParagraphProps() = default; + HostPlatformParagraphProps( + const PropsParserContext &context, + const HostPlatformParagraphProps &sourceProps, + const RawProps &rawProps); + + void + setProp(const PropsParserContext &context, RawPropsPropNameHash hash, const char *propName, const RawValue &value); + +#pragma mark - Props + + DataDetectorType dataDetectorType{DataDetectorType::None}; + +#pragma mark - DebugStringConvertible + +#if RN_DEBUG_STRING_CONVERTIBLE + SharedDebugStringConvertibleList getDebugProps() const override; +#endif +}; + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/platform/cxx/react/renderer/components/text/conversions.h b/packages/react-native/ReactCommon/react/renderer/components/text/platform/cxx/react/renderer/components/text/conversions.h new file mode 100644 index 000000000000..28a71e0f8aed --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/text/platform/cxx/react/renderer/components/text/conversions.h @@ -0,0 +1,70 @@ +/* + * 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. + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +inline void fromRawValue(const PropsParserContext & /*context*/, const RawValue &value, DataDetectorType &result) +{ + react_native_expect(value.hasType()); + if (value.hasType()) { + auto string = (std::string)value; + if (string == "all") { + result = DataDetectorType::All; + } else if (string == "email") { + result = DataDetectorType::Email; + } else if (string == "link") { + result = DataDetectorType::Link; + } else if (string == "none") { + result = DataDetectorType::None; + } else if (string == "phoneNumber") { + result = DataDetectorType::PhoneNumber; + } else { + // sane default + LOG(ERROR) << "Unsupported DataDetectorType value: " << string; + react_native_expect(false); + result = DataDetectorType::None; + } + return; + } + + LOG(ERROR) << "Unsupported DataDetectorType type"; + react_native_expect(false); + result = DataDetectorType::None; +} + +inline std::string toString(const DataDetectorType &value) +{ + switch (value) { + case DataDetectorType::All: + return "all"; + case DataDetectorType::Email: + return "email"; + case DataDetectorType::Link: + return "link"; + case DataDetectorType::None: + return "none"; + case DataDetectorType::PhoneNumber: + return "phoneNumber"; + } + + LOG(ERROR) << "Unsupported DataDetectorType value"; + react_native_expect(false); + return "none"; +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/platform/cxx/react/renderer/components/text/primitives.h b/packages/react-native/ReactCommon/react/renderer/components/text/platform/cxx/react/renderer/components/text/primitives.h new file mode 100644 index 000000000000..22241c7b7f36 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/text/platform/cxx/react/renderer/components/text/primitives.h @@ -0,0 +1,22 @@ +/* + * 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. + */ + +#pragma once + +#include + +namespace facebook::react { + +enum class DataDetectorType { + All, + Email, + Link, + None, + PhoneNumber, +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.h index 2cd7f1bce247..187afc128f80 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.h @@ -24,6 +24,16 @@ NS_ASSUME_NONNULL_BEGIN using RCTTextLayoutFragmentEnumerationBlock = void (^)(CGRect fragmentRect, NSString *_Nonnull fragmentText, NSString *value); +/** + * The kinds of data to make tappable, as chosen by the `dataDetectorType` prop of `Text`. + */ +typedef NS_OPTIONS(NSUInteger, RCTTextDataDetectorTypes) { + RCTTextDataDetectorTypeNone = 0, + RCTTextDataDetectorTypePhoneNumber = 1 << 0, + RCTTextDataDetectorTypeLink = 1 << 1, + RCTTextDataDetectorTypeEmail = 1 << 2, +}; + /** * iOS-specific TextLayoutManager */ @@ -44,6 +54,12 @@ using RCTTextLayoutFragmentEnumerationBlock = frame:(CGRect)frame drawHighlightPath:(void (^_Nullable)(UIBezierPath *highlightPath))block; +- (void)drawAttributedString:(const facebook::react::AttributedString &)attributedString + paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes + dataDetectorTypes:(RCTTextDataDetectorTypes)dataDetectorTypes + frame:(CGRect)frame + drawHighlightPath:(void (^_Nullable)(UIBezierPath *highlightPath))block; + - (facebook::react::LinesMeasurements) getLinesForAttributedString:(const facebook::react::AttributedString &)attributedString paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes @@ -55,8 +71,24 @@ using RCTTextLayoutFragmentEnumerationBlock = frame:(CGRect)frame atPoint:(CGPoint)point; +/** + * Returns the URL of the data detected at a point in the coordinates of `frame`'s parent, if any. + */ +- (nullable NSURL *)detectedURLWithAttributedString:(const facebook::react::AttributedString &)attributedString + paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes + dataDetectorTypes:(RCTTextDataDetectorTypes)dataDetectorTypes + frame:(CGRect)frame + atPoint:(CGPoint)point; + +- (void)getRectWithAttributedString:(const facebook::react::AttributedString &)attributedString + paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes + enumerateAttribute:(NSString *)enumerateAttribute + frame:(CGRect)frame + usingBlock:(RCTTextLayoutFragmentEnumerationBlock)block; + - (void)getRectWithAttributedString:(const facebook::react::AttributedString &)attributedString paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes + dataDetectorTypes:(RCTTextDataDetectorTypes)dataDetectorTypes enumerateAttribute:(NSString *)enumerateAttribute frame:(CGRect)frame usingBlock:(RCTTextLayoutFragmentEnumerationBlock)block; diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm index 3e93b60e64be..33fafb4dc77b 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm @@ -21,6 +21,85 @@ using namespace facebook::react; +static NSString *const RCTDetectedURLAttributeName = @"RCTDetectedURL"; + +static NSURL *_Nullable RCTPhoneURLFromNumber(NSString *number) +{ + // NSDataDetector appends an extension after `;` or `,`. Dial the number itself. + NSRange extension = [number rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@";,"]]; + if (extension.location != NSNotFound) { + number = [number substringToIndex:extension.location]; + } + NSCharacterSet *removed = [[NSCharacterSet characterSetWithCharactersInString:@"+0123456789"] invertedSet]; + number = [[number componentsSeparatedByCharactersInSet:removed] componentsJoinedByString:@""]; + return number.length > 0 ? [NSURL URLWithString:[@"tel:" stringByAppendingString:number]] : nil; +} + +static NSURL *_Nullable RCTURLFromTextCheckingResult(NSTextCheckingResult *result, RCTTextDataDetectorTypes types) +{ + if (result.resultType == NSTextCheckingTypePhoneNumber) { + return (types & RCTTextDataDetectorTypePhoneNumber) != 0 ? RCTPhoneURLFromNumber(result.phoneNumber) : nil; + } + // NSDataDetector reports email addresses as mailto: links. It also matches any other scheme, which must not make + // text in the app open arbitrary deep links. + NSString *scheme = result.URL.scheme.lowercaseString; + if ([scheme isEqualToString:@"mailto"]) { + return (types & RCTTextDataDetectorTypeEmail) != 0 ? result.URL : nil; + } + if ([scheme isEqualToString:@"http"] || [scheme isEqualToString:@"https"]) { + return (types & RCTTextDataDetectorTypeLink) != 0 ? result.URL : nil; + } + return nil; +} + +// Runs on the text that is displayed, after `textTransform`. The attributes don't change the layout, so measuring +// doesn't need them. +static void RCTApplyDataDetector(NSTextStorage *textStorage, RCTTextDataDetectorTypes types) +{ + if (types == RCTTextDataDetectorTypeNone || textStorage.length == 0) { + return; + } + + static NSDataDetector *detector; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + detector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink | NSTextCheckingTypePhoneNumber error:NULL]; + }); + + [textStorage beginEditing]; + [detector enumerateMatchesInString:textStorage.string + options:0 + range:NSMakeRange(0, textStorage.length) + usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { + NSURL *url = RCTURLFromTextCheckingResult(result, types); + if (url == nil) { + return; + } + [textStorage addAttributes:@{ + RCTDetectedURLAttributeName : url, + NSForegroundColorAttributeName : UIColor.linkColor, + NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle), + RCTTextAttributesAccessibilityRoleAttributeName : @"link", + } + range:result.range]; + }]; + [textStorage endEditing]; +} + +// Glyphs that a trailing ellipsis replaces are still laid out, so leave them out. +static NSRange RCTVisibleGlyphRange(NSLayoutManager *layoutManager, NSTextContainer *textContainer) +{ + NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer]; + if (glyphRange.length == 0) { + return glyphRange; + } + NSRange truncatedRange = [layoutManager truncatedGlyphRangeInLineFragmentForGlyphAtIndex:NSMaxRange(glyphRange) - 1]; + if (truncatedRange.location != NSNotFound) { + glyphRange.length = truncatedRange.location - glyphRange.location; + } + return glyphRange; +} + @implementation RCTTextLayoutManager { SimpleThreadSafeCache, 256> _cache; } @@ -132,11 +211,25 @@ - (void)drawAttributedString:(const AttributedString &)attributedString paragraphAttributes:(ParagraphAttributes)paragraphAttributes frame:(CGRect)frame drawHighlightPath:(void (^_Nullable)(UIBezierPath *highlightPath))block +{ + [self drawAttributedString:attributedString + paragraphAttributes:paragraphAttributes + dataDetectorTypes:RCTTextDataDetectorTypeNone + frame:frame + drawHighlightPath:block]; +} + +- (void)drawAttributedString:(const AttributedString &)attributedString + paragraphAttributes:(ParagraphAttributes)paragraphAttributes + dataDetectorTypes:(RCTTextDataDetectorTypes)dataDetectorTypes + frame:(CGRect)frame + drawHighlightPath:(void (^_Nullable)(UIBezierPath *highlightPath))block { NSTextStorage *textStorage = [self _textStorageAndLayoutManagerWithAttributesString:[self _nsAttributedStringFromAttributedString:attributedString] paragraphAttributes:paragraphAttributes size:frame.size]; + RCTApplyDataDetector(textStorage, dataDetectorTypes); NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject; NSTextContainer *textContainer = layoutManager.textContainers.firstObject; @@ -497,22 +590,71 @@ - (NSTextStorage *)_textStorageAndLayoutManagerWithAttributesString:(NSAttribute return nil; } +- (NSURL *)detectedURLWithAttributedString:(const AttributedString &)attributedString + paragraphAttributes:(ParagraphAttributes)paragraphAttributes + dataDetectorTypes:(RCTTextDataDetectorTypes)dataDetectorTypes + frame:(CGRect)frame + atPoint:(CGPoint)point +{ + NSTextStorage *textStorage = [self + _textStorageAndLayoutManagerWithAttributesString:[self _nsAttributedStringFromAttributedString:attributedString] + paragraphAttributes:paragraphAttributes + size:frame.size]; + if (textStorage.length == 0) { + return nil; + } + RCTApplyDataDetector(textStorage, dataDetectorTypes); + + NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject; + NSTextContainer *textContainer = layoutManager.textContainers.firstObject; + CGPoint pointInContainer = CGPointMake(point.x - frame.origin.x, point.y - frame.origin.y); + + // The glyph index is that of the nearest glyph, so check that the point is really on it. + NSUInteger glyphIndex = [layoutManager glyphIndexForPoint:pointInContainer inTextContainer:textContainer]; + CGRect glyphRect = [layoutManager boundingRectForGlyphRange:NSMakeRange(glyphIndex, 1) inTextContainer:textContainer]; + if (!CGRectContainsPoint(glyphRect, pointInContainer) || + !NSLocationInRange(glyphIndex, RCTVisibleGlyphRange(layoutManager, textContainer))) { + return nil; + } + return [textStorage attribute:RCTDetectedURLAttributeName + atIndex:[layoutManager characterIndexForGlyphAtIndex:glyphIndex] + effectiveRange:NULL]; +} + - (void)getRectWithAttributedString:(const AttributedString &)attributedString paragraphAttributes:(ParagraphAttributes)paragraphAttributes enumerateAttribute:(NSString *)enumerateAttribute frame:(CGRect)frame usingBlock:(RCTTextLayoutFragmentEnumerationBlock)block +{ + [self getRectWithAttributedString:attributedString + paragraphAttributes:paragraphAttributes + dataDetectorTypes:RCTTextDataDetectorTypeNone + enumerateAttribute:enumerateAttribute + frame:frame + usingBlock:block]; +} + +- (void)getRectWithAttributedString:(const AttributedString &)attributedString + paragraphAttributes:(ParagraphAttributes)paragraphAttributes + dataDetectorTypes:(RCTTextDataDetectorTypes)dataDetectorTypes + enumerateAttribute:(NSString *)enumerateAttribute + frame:(CGRect)frame + usingBlock:(RCTTextLayoutFragmentEnumerationBlock)block { NSTextStorage *textStorage = [self _textStorageAndLayoutManagerWithAttributesString:[self _nsAttributedStringFromAttributedString:attributedString] paragraphAttributes:paragraphAttributes size:frame.size]; + RCTApplyDataDetector(textStorage, dataDetectorTypes); NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject; NSTextContainer *textContainer = layoutManager.textContainers.firstObject; [layoutManager ensureLayoutForTextContainer:textContainer]; - NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer]; + NSRange glyphRange = dataDetectorTypes != RCTTextDataDetectorTypeNone + ? RCTVisibleGlyphRange(layoutManager, textContainer) + : [layoutManager glyphRangeForTextContainer:textContainer]; NSRange characterRange = [layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL]; [textStorage enumerateAttribute:enumerateAttribute diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index c0f8309f0ac5..730a2ad03459 100644 --- a/packages/react-native/ReactNativeApi.d.ts +++ b/packages/react-native/ReactNativeApi.d.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<41528fad88b43dfd16d9d71cf8685fc1>> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -4867,6 +4867,7 @@ declare type TextBaseProps = { readonly allowFontScaling?: boolean readonly android_hyphenationFrequency?: "full" | "none" | "normal" readonly children?: React.ReactNode + readonly dataDetectorType?: "all" | "email" | "link" | "none" | "phoneNumber" readonly ellipsizeMode?: "clip" | "head" | "middle" | "tail" readonly id?: string readonly lineBreak?: "auto" | "loose" | "normal" | "strict" @@ -5191,7 +5192,6 @@ declare interface TextProps extends Readonly< > {} declare type TextPropsAndroid = { adjustsFontSizeToFit?: boolean - dataDetectorType?: "all" | "email" | "link" | "none" | "phoneNumber" disabled?: boolean minimumFontScale?: number selectionColor?: ColorValue @@ -5791,7 +5791,7 @@ export { AlertOptions, // 8a116d2a AlertType, // 5ab91217 AndroidKeyboardEvent, // e03becc8 - Animated, // 33fe7464 + Animated, // 9623d6d3 AppConfig, // 35c0ca70 AppRegistry, // edec2a30 AppState, // 12012be5 @@ -6025,7 +6025,7 @@ export { TVViewPropsIOS, // 1c612e4e TargetedEvent, // 16e98910 TaskProvider, // 266dedf2 - Text, // 935ec305 + Text, // 54f0f649 TextContentType, // 239b3ecc TextInput, // cf6467dc TextInputAndroidProps, // 9ebbc103 @@ -6042,7 +6042,7 @@ export { TextInputSubmitEditingEvent, // e811fdbd TextInstance, // 7c7e2069 TextLayoutEvent, // 1a21e24a - TextProps, // b3707dfc + TextProps, // 3d8670c8 TextStyle, // 898adfd1 ToastAndroid, // 88a8969a TouchableHighlight, // a015710f diff --git a/packages/react-native/types_DEPRECATED/Libraries/Text/Text.d.ts b/packages/react-native/types_DEPRECATED/Libraries/Text/Text.d.ts index e793c03bc6d5..a75a10277fd2 100644 --- a/packages/react-native/types_DEPRECATED/Libraries/Text/Text.d.ts +++ b/packages/react-native/types_DEPRECATED/Libraries/Text/Text.d.ts @@ -77,13 +77,6 @@ export interface TextPropsAndroid { */ textBreakStrategy?: 'simple' | 'highQuality' | 'balanced' | undefined; - /** - * Determines the types of data converted to clickable URLs in the text element. - * By default no data types are detected. - */ - dataDetectorType?: - null | 'phoneNumber' | 'link' | 'email' | 'none' | 'all' | undefined; - /** * Hyphenation strategy */ @@ -106,6 +99,13 @@ export interface TextPropsAndroid { // https://reactnative.dev/docs/text#props export interface TextProps extends TextPropsIOS, TextPropsAndroid, AccessibilityProps { + /** + * Determines the types of data converted to clickable URLs in the text element. + * By default no data types are detected. + */ + dataDetectorType?: + null | 'phoneNumber' | 'link' | 'email' | 'none' | 'all' | undefined; + /** * Specifies whether fonts should scale to respect Text Size accessibility settings. * The default is `true`. diff --git a/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj b/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj index 39dceb0ee86c..7710b45b8965 100644 --- a/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj +++ b/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ CD10C7A5290BD4EB0033E1ED /* RCTEventEmitterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CD10C7A4290BD4EB0033E1ED /* RCTEventEmitterTests.m */; }; E62F11832A5C6580000BF1C8 /* FlexibleSizeExampleView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 27F441E81BEBE5030039B79C /* FlexibleSizeExampleView.mm */; }; E62F11842A5C6584000BF1C8 /* UpdatePropertiesExampleView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 272E6B3C1BEA849E001FCF37 /* UpdatePropertiesExampleView.mm */; }; + D1A7C0DE00000000000000A1 /* RCTTextDataDetectorTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = D1A7C0DE00000000000000A2 /* RCTTextDataDetectorTests.mm */; }; E7DB20D122B2BAA6005AC45F /* RCTBundleURLProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E7DB20A922B2BAA3005AC45F /* RCTBundleURLProviderTests.m */; }; E7DB20D322B2BAA6005AC45F /* RCTBlobManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E7DB20AB22B2BAA3005AC45F /* RCTBlobManagerTests.m */; }; E7DB20D522B2BAA6005AC45F /* RCTPerformanceLoggerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E7DB20AD22B2BAA3005AC45F /* RCTPerformanceLoggerTests.m */; }; @@ -100,6 +101,7 @@ E771AEEA22B44E3100EA1189 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = RNTester/Info.plist; sourceTree = ""; }; E7DB209F22B2BA84005AC45F /* RNTesterUnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RNTesterUnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; E7DB20A322B2BA84005AC45F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D1A7C0DE00000000000000A2 /* RCTTextDataDetectorTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTTextDataDetectorTests.mm; sourceTree = ""; }; E7DB20A922B2BAA3005AC45F /* RCTBundleURLProviderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTBundleURLProviderTests.m; sourceTree = ""; }; E7DB20AB22B2BAA3005AC45F /* RCTBlobManagerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTBlobManagerTests.m; sourceTree = ""; }; E7DB20AD22B2BAA3005AC45F /* RCTPerformanceLoggerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTPerformanceLoggerTests.m; sourceTree = ""; }; @@ -292,6 +294,7 @@ E7DB20C622B2BAA5005AC45F /* RCTAllocationTests.m */, E7DB20B222B2BAA4005AC45F /* RCTAnimationUtilsTests.m */, E7DB20AB22B2BAA3005AC45F /* RCTBlobManagerTests.m */, + D1A7C0DE00000000000000A2 /* RCTTextDataDetectorTests.mm */, E7DB20A922B2BAA3005AC45F /* RCTBundleURLProviderTests.m */, E7DB20CA22B2BAA5005AC45F /* RCTConvert_NSURLTests.m */, E7DB20CE22B2BAA5005AC45F /* RCTConvert_YGValueTests.m */, @@ -611,6 +614,7 @@ E7DB20DF22B2BAA6005AC45F /* RCTImageLoaderTests.m in Sources */, E7DB20D522B2BAA6005AC45F /* RCTPerformanceLoggerTests.m in Sources */, E7DB20D922B2BAA6005AC45F /* RCTAnimationUtilsTests.m in Sources */, + D1A7C0DE00000000000000A1 /* RCTTextDataDetectorTests.mm in Sources */, E7DB20D122B2BAA6005AC45F /* RCTBundleURLProviderTests.m in Sources */, E7DB20EC22B2BAA6005AC45F /* RCTMultipartStreamReaderTests.m in Sources */, E7DB20E022B2BAA6005AC45F /* RCTMethodArgumentTests.m in Sources */, diff --git a/packages/rn-tester/RNTesterUnitTests/RCTTextDataDetectorTests.mm b/packages/rn-tester/RNTesterUnitTests/RCTTextDataDetectorTests.mm new file mode 100644 index 000000000000..9a47e0521e20 --- /dev/null +++ b/packages/rn-tester/RNTesterUnitTests/RCTTextDataDetectorTests.mm @@ -0,0 +1,138 @@ +/* + * 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 + +using namespace facebook::react; + +static const RCTTextDataDetectorTypes kAllTypes = + RCTTextDataDetectorTypePhoneNumber | RCTTextDataDetectorTypeLink | RCTTextDataDetectorTypeEmail; + +static AttributedString RCTAttributedStringWithText(const std::string &text) +{ + auto fragment = AttributedString::Fragment{}; + fragment.string = text; + fragment.textAttributes.fontSize = 14; + auto attributedString = AttributedString{}; + attributedString.appendFragment(std::move(fragment)); + return attributedString; +} + +@interface RCTTextDataDetectorTests : XCTestCase +@end + +@implementation RCTTextDataDetectorTests { + RCTTextLayoutManager *_layoutManager; +} + +- (void)setUp +{ + [super setUp]; + _layoutManager = [RCTTextLayoutManager new]; +} + +// Returns the URL that each detected link opens when tapped in the middle. +- (NSArray *)_URLsInText:(const std::string &)text + types:(RCTTextDataDetectorTypes)types + frame:(CGRect)frame + paragraphAttributes:(ParagraphAttributes)paragraphAttributes +{ + auto attributedString = RCTAttributedStringWithText(text); + NSMutableArray *urls = [NSMutableArray new]; + [_layoutManager getRectWithAttributedString:attributedString + paragraphAttributes:paragraphAttributes + dataDetectorTypes:types + enumerateAttribute:RCTTextAttributesAccessibilityRoleAttributeName + frame:frame + usingBlock:^(CGRect fragmentRect, NSString *fragmentText, NSString *value) { + CGPoint point = CGPointMake( + frame.origin.x + CGRectGetMidX(fragmentRect), + frame.origin.y + CGRectGetMidY(fragmentRect)); + NSURL *url = + [self->_layoutManager detectedURLWithAttributedString:attributedString + paragraphAttributes:paragraphAttributes + dataDetectorTypes:types + frame:frame + atPoint:point]; + [urls addObject:url.absoluteString ?: @"nil"]; + }]; + return urls; +} + +- (NSArray *)_URLsInText:(const std::string &)text types:(RCTTextDataDetectorTypes)types +{ + return [self _URLsInText:text types:types frame:CGRectMake(0, 0, 1000, 200) paragraphAttributes:{}]; +} + +- (void)testDetectsOnlyTheRequestedTypes +{ + std::string text = "Call 123-123-1234 or see https://example.com or mail a@example.com"; + XCTAssertEqualObjects([self _URLsInText:text types:RCTTextDataDetectorTypeNone], @[]); + XCTAssertEqualObjects([self _URLsInText:text types:RCTTextDataDetectorTypePhoneNumber], @[ @"tel:1231231234" ]); + XCTAssertEqualObjects([self _URLsInText:text types:RCTTextDataDetectorTypeLink], @[ @"https://example.com" ]); + XCTAssertEqualObjects([self _URLsInText:text types:RCTTextDataDetectorTypeEmail], @[ @"mailto:a@example.com" ]); + NSArray *expected = @[ @"tel:1231231234", @"https://example.com", @"mailto:a@example.com" ]; + XCTAssertEqualObjects([self _URLsInText:text types:kAllTypes], expected); +} + +- (void)testPhoneExtensionIsNotDialedAsPartOfTheNumber +{ + XCTAssertEqualObjects( + [self _URLsInText:"Call 123-123-1234 ext. 55 today" types:RCTTextDataDetectorTypePhoneNumber], + @[ @"tel:1231231234" ]); +} + +- (void)testOtherSchemesAreNotLinks +{ + std::string text = "myapp://path file:///etc/hosts ssh://user@example.com"; + XCTAssertEqualObjects([self _URLsInText:text types:RCTTextDataDetectorTypeLink], @[]); + XCTAssertEqualObjects([self _URLsInText:text types:kAllTypes], @[]); +} + +- (void)testPointsAreRelativeToTheFrame +{ + auto attributedString = RCTAttributedStringWithText("https://example.com"); + ParagraphAttributes paragraphAttributes; + CGRect frame = CGRectMake(40, 40, 300, 100); + XCTAssertNotNil([_layoutManager detectedURLWithAttributedString:attributedString + paragraphAttributes:paragraphAttributes + dataDetectorTypes:RCTTextDataDetectorTypeLink + frame:frame + atPoint:CGPointMake(50, 45)]); + // Inside the text if the frame were at the origin, but in the padding here. + XCTAssertNil([_layoutManager detectedURLWithAttributedString:attributedString + paragraphAttributes:paragraphAttributes + dataDetectorTypes:RCTTextDataDetectorTypeLink + frame:frame + atPoint:CGPointMake(10, 5)]); + // Past the end of the text. + XCTAssertNil([_layoutManager detectedURLWithAttributedString:attributedString + paragraphAttributes:paragraphAttributes + dataDetectorTypes:RCTTextDataDetectorTypeLink + frame:frame + atPoint:CGPointMake(330, 45)]); +} + +- (void)testLinksHiddenByTruncationAreNotReported +{ + ParagraphAttributes paragraphAttributes; + paragraphAttributes.maximumNumberOfLines = 1; + paragraphAttributes.ellipsizeMode = EllipsizeMode::Tail; + std::string text = "https://first.example.com and a long run of words that will not fit https://hidden.example.com"; + NSArray *urls = [self _URLsInText:text + types:RCTTextDataDetectorTypeLink + frame:CGRectMake(0, 0, 300, 20) + paragraphAttributes:paragraphAttributes]; + XCTAssertEqualObjects(urls, @[ @"https://first.example.com" ]); +} + +@end diff --git a/packages/rn-tester/js/examples/Text/TextExample.ios.js b/packages/rn-tester/js/examples/Text/TextExample.ios.js index ff5ff29a097f..9a359bf0a284 100644 --- a/packages/rn-tester/js/examples/Text/TextExample.ios.js +++ b/packages/rn-tester/js/examples/Text/TextExample.ios.js @@ -860,6 +860,27 @@ const examples = [ ); }, }, + { + title: 'Data detectors', + name: 'dataDetectorType', + render: function (): React.Node { + return ( + + Phone number: 123-123-1234 + Link: https://www.facebook.com + Email: employee@facebook.com + + Phone number: 123-123-1234 Link: https://www.facebook.com Email: + employee@facebook.com + + + Phone number: 123-123-1234 Link: https://www.facebook.com Email: + employee@facebook.com + + + ); + }, + }, { title: 'Text Decoration', name: 'textDecoration', diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index 0dd275db8b98..e68daa755ddd 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -1430,6 +1430,7 @@ interface RCTObjectAnimatedNode : public RCTAnimatedNode { } interface RCTParagraphComponentAccessibilityProvider : public NSObject { + public @property (assign) RCTTextDataDetectorTypes dataDetectorTypes; public virtual BOOL isUpToDate:(facebook::react::AttributedString currentAttributedString); public virtual NSArray* accessibilityElements(); public virtual instancetype initWithString:layoutManager:paragraphAttributes:frame:view:(facebook::react::AttributedString attributedString, RCTTextLayoutManager* layoutManager, facebook::react::ParagraphAttributes paragraphAttributes, CGRect frame, UIView* view); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api index af03ebeb5a9c..e4faf50292fa 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api @@ -1473,6 +1473,7 @@ interface RCTObjectAnimatedNode : public RCTAnimatedNode { } interface RCTParagraphComponentAccessibilityProvider : public NSObject { + public @property (assign) RCTTextDataDetectorTypes dataDetectorTypes; public virtual BOOL isUpToDate:(facebook::react::AttributedString currentAttributedString); public virtual NSArray* accessibilityElements(); public virtual instancetype initWithString:layoutManager:paragraphAttributes:frame:view:(facebook::react::AttributedString attributedString, RCTTextLayoutManager* layoutManager, facebook::react::ParagraphAttributes paragraphAttributes, CGRect frame, UIView* view); @@ -1872,11 +1873,14 @@ interface RCTTextInputComponentView : public RCTViewComponentView { } interface RCTTextLayoutManager : public NSObject { + public virtual _Nullable NSURL* detectedURLWithAttributedString:paragraphAttributes:dataDetectorTypes:frame:atPoint:(const facebook::react::AttributedString& attributedString, facebook::react::ParagraphAttributes paragraphAttributes, RCTTextDataDetectorTypes dataDetectorTypes, CGRect frame, CGPoint point); public virtual facebook::react::LinesMeasurements getLinesForAttributedString:paragraphAttributes:size:(const facebook::react::AttributedString& attributedString, facebook::react::ParagraphAttributes paragraphAttributes, CGSize size); public virtual facebook::react::TextMeasurement measureAttributedString:paragraphAttributes:layoutContext:layoutConstraints:(const facebook::react::AttributedString& attributedString, facebook::react::ParagraphAttributes paragraphAttributes, facebook::react::TextLayoutContext layoutContext, facebook::react::LayoutConstraints layoutConstraints); public virtual facebook::react::TextMeasurement measureNSAttributedString:paragraphAttributes:layoutContext:layoutConstraints:(NSAttributedString* attributedString, facebook::react::ParagraphAttributes paragraphAttributes, facebook::react::TextLayoutContext layoutContext, facebook::react::LayoutConstraints layoutConstraints); public virtual std::shared_ptr getEventEmitterWithAttributeString:paragraphAttributes:frame:atPoint:(const facebook::react::AttributedString& attributedString, facebook::react::ParagraphAttributes paragraphAttributes, CGRect frame, CGPoint point); + public virtual void drawAttributedString:paragraphAttributes:dataDetectorTypes:frame:drawHighlightPath:(const facebook::react::AttributedString& attributedString, facebook::react::ParagraphAttributes paragraphAttributes, RCTTextDataDetectorTypes dataDetectorTypes, CGRect frame, void(^)(UIBezierPath* highlightPath) block); public virtual void drawAttributedString:paragraphAttributes:frame:drawHighlightPath:(const facebook::react::AttributedString& attributedString, facebook::react::ParagraphAttributes paragraphAttributes, CGRect frame, void(^)(UIBezierPath* highlightPath) block); + public virtual void getRectWithAttributedString:paragraphAttributes:dataDetectorTypes:enumerateAttribute:frame:usingBlock:(const facebook::react::AttributedString& attributedString, facebook::react::ParagraphAttributes paragraphAttributes, RCTTextDataDetectorTypes dataDetectorTypes, NSString* enumerateAttribute, CGRect frame, RCTTextLayoutFragmentEnumerationBlock block); public virtual void getRectWithAttributedString:paragraphAttributes:enumerateAttribute:frame:usingBlock:(const facebook::react::AttributedString& attributedString, facebook::react::ParagraphAttributes paragraphAttributes, NSString* enumerateAttribute, CGRect frame, RCTTextLayoutFragmentEnumerationBlock block); } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index 6de636e0f56d..5df7bdbf625a 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -1430,6 +1430,7 @@ interface RCTObjectAnimatedNode : public RCTAnimatedNode { } interface RCTParagraphComponentAccessibilityProvider : public NSObject { + public @property (assign) RCTTextDataDetectorTypes dataDetectorTypes; public virtual BOOL isUpToDate:(facebook::react::AttributedString currentAttributedString); public virtual NSArray* accessibilityElements(); public virtual instancetype initWithString:layoutManager:paragraphAttributes:frame:view:(facebook::react::AttributedString attributedString, RCTTextLayoutManager* layoutManager, facebook::react::ParagraphAttributes paragraphAttributes, CGRect frame, UIView* view); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 7ab648c8073e..34640dd547de 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -1430,6 +1430,7 @@ interface RCTObjectAnimatedNode : public RCTAnimatedNode { } interface RCTParagraphComponentAccessibilityProvider : public NSObject { + public @property (assign) RCTTextDataDetectorTypes dataDetectorTypes; public virtual BOOL isUpToDate:(facebook::react::AttributedString currentAttributedString); public virtual NSArray* accessibilityElements(); public virtual instancetype initWithString:layoutManager:paragraphAttributes:frame:view:(facebook::react::AttributedString attributedString, RCTTextLayoutManager* layoutManager, facebook::react::ParagraphAttributes paragraphAttributes, CGRect frame, UIView* view);