diff --git a/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentAccessibilityProvider.h b/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentAccessibilityProvider.h index 30ad700e02abe2..87573f789f5e17 100644 --- a/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentAccessibilityProvider.h +++ b/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentAccessibilityProvider.h @@ -8,16 +8,27 @@ #import #import +#import +#import #import "RCTParagraphComponentView.h" @interface RCTParagraphComponentAccessibilityProvider : NSObject -- (instancetype)initWithString:(facebook::react::AttributedString)attributedString view:(UIView *)view; +- (instancetype)initWithString:(facebook::react::AttributedString)attributedString + layoutManager:(RCTTextLayoutManager *)layoutManager + paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes + frame:(CGRect)frame + view:(UIView *)view; + +/* + * Returns an array of `UIAccessibilityElement`s to be used for `UIAccessibilityContainer` implementation. + */ +- (NSArray *)accessibilityElements; /** - @abstract Array of accessibleElements for use in UIAccessibilityContainer implementation. + @abstract To make sure the provider is up to date. */ -- (NSArray *)accessibilityElements; +- (BOOL)isUpToDate:(facebook::react::AttributedString)currentAttributedString; @end diff --git a/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentAccessibilityProvider.mm b/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentAccessibilityProvider.mm index 248e5a437f31cb..4b0ab5a1bed131 100644 --- a/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentAccessibilityProvider.mm +++ b/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentAccessibilityProvider.mm @@ -9,6 +9,7 @@ #import #import +#import #import #import @@ -20,13 +21,23 @@ @implementation RCTParagraphComponentAccessibilityProvider { NSMutableArray *_accessibilityElements; AttributedString _attributedString; + RCTTextLayoutManager *_layoutManager; + ParagraphAttributes _paragraphAttributes; + CGRect _frame; __weak UIView *_view; } -- (instancetype)initWithString:(AttributedString)attributedString view:(UIView *)view +- (instancetype)initWithString:(facebook::react::AttributedString)attributedString + layoutManager:(RCTTextLayoutManager *)layoutManager + paragraphAttributes:(ParagraphAttributes)paragraphAttributes + frame:(CGRect)frame + view:(UIView *)view { if (self = [super init]) { _attributedString = attributedString; + _layoutManager = layoutManager; + _paragraphAttributes = paragraphAttributes; + _frame = frame; _view = view; } return self; @@ -34,13 +45,65 @@ - (instancetype)initWithString:(AttributedString)attributedString view:(UIView * - (NSArray *)accessibilityElements { - // verify if accessibleElements are exist + if (_accessibilityElements) { + return _accessibilityElements; + } + + __block NSInteger numOfLink = 0; // build an array of the accessibleElements + NSMutableArray *elements = [NSMutableArray new]; + + NSString *accessibilityLabel = [_view valueForKey:@"accessibilityLabel"]; + if (!accessibilityLabel.length) { + accessibilityLabel = RCTNSStringFromString(_attributedString.getString()); + } // add first element has the text for the whole textview in order to read out the whole text + UIAccessibilityElement *firstElement = [[UIAccessibilityElement alloc] initWithAccessibilityContainer:_view]; + firstElement.isAccessibilityElement = YES; + firstElement.accessibilityTraits = UIAccessibilityTraitStaticText; + firstElement.accessibilityLabel = accessibilityLabel; + firstElement.accessibilityFrameInContainerSpace = _view.bounds; + [elements addObject:firstElement]; + // add additional elements for those parts of text with embedded link so VoiceOver could specially recognize links + + [_layoutManager getRectWithAttributedString:_attributedString + paragraphAttributes:_paragraphAttributes + enumerateAttribute:RCTTextAttributesAccessibilityRoleAttributeName + frame:_frame + usingBlock:^(CGRect fragmentRect, NSString *_Nonnull fragmentText, NSString *value) { + UIAccessibilityElement *element = + [[UIAccessibilityElement alloc] initWithAccessibilityContainer:self->_view]; + element.isAccessibilityElement = YES; + if ([value isEqualToString:@"link"]) { + element.accessibilityTraits = UIAccessibilityTraitLink; + numOfLink++; + } + element.accessibilityLabel = fragmentText; + element.accessibilityFrameInContainerSpace = fragmentRect; + [elements addObject:element]; + }]; + + if (numOfLink > 0) { + [elements enumerateObjectsUsingBlock:^(UIAccessibilityElement *element, NSUInteger idx, BOOL *_Nonnull stop) { + element.accessibilityHint = [NSString stringWithFormat:@"Link %ld of %ld.", (unsigned long)idx, (long)numOfLink]; + }]; + + NSString *firstElementHint = (numOfLink == 1) + ? @"One link found, swipe right to move to the link." + : [NSString stringWithFormat:@"%ld links found, swipe right to move to the first link.", (long)numOfLink]; + + firstElement.accessibilityHint = firstElementHint; + } + // add accessible element for truncation attributed string for automation purposes only - _accessibilityElements = [NSMutableArray new]; + _accessibilityElements = elements; return _accessibilityElements; } +- (BOOL)isUpToDate:(facebook::react::AttributedString)currentAttributedString +{ + return currentAttributedString == _attributedString; +} + @end diff --git a/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm b/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm index 42fa2ebe86ac2d..ba5a1f34c73ae3 100644 --- a/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm +++ b/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm @@ -6,6 +6,7 @@ */ #import "RCTParagraphComponentView.h" +#import "RCTParagraphComponentAccessibilityProvider.h" #import #import @@ -26,6 +27,7 @@ @implementation RCTParagraphComponentView { ParagraphShadowNode::ConcreteState::Shared _state; ParagraphAttributes _paragraphAttributes; + RCTParagraphComponentAccessibilityProvider *_accessibilityProvider; } - (instancetype)initWithFrame:(CGRect)frame @@ -35,7 +37,6 @@ - (instancetype)initWithFrame:(CGRect)frame _props = defaultProps; self.isAccessibilityElement = YES; - self.accessibilityTraits |= UIAccessibilityTraitStaticText; self.opaque = NO; self.contentMode = UIViewContentModeRedraw; } @@ -138,6 +139,29 @@ - (NSString *)accessibilityLabel return RCTNSStringFromString(_state->getData().attributedString.getString()); } +- (NSArray *)accessibilityElements +{ + if (![_accessibilityProvider isUpToDate:_state->getData().attributedString]) { + RCTTextLayoutManager *textLayoutManager = + (RCTTextLayoutManager *)unwrapManagedObject(_state->getData().layoutManager->getNativeTextLayoutManager()); + CGRect frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame()); + _accessibilityProvider = + [[RCTParagraphComponentAccessibilityProvider alloc] initWithString:_state->getData().attributedString + layoutManager:textLayoutManager + paragraphAttributes:_state->getData().paragraphAttributes + frame:frame + view:self]; + } + + self.isAccessibilityElement = NO; + return _accessibilityProvider.accessibilityElements; +} + +- (UIAccessibilityTraits)accessibilityTraits +{ + return [super accessibilityTraits] | UIAccessibilityTraitStaticText; +} + - (SharedTouchEventEmitter)touchEventEmitterAtPoint:(CGPoint)point { if (!_state) { diff --git a/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTAttributedTextUtils.h b/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTAttributedTextUtils.h index dd142aa3106155..eda44cf6ff6d56 100644 --- a/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTAttributedTextUtils.h +++ b/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTAttributedTextUtils.h @@ -15,6 +15,7 @@ NS_ASSUME_NONNULL_BEGIN NSString *const RCTAttributedStringIsHighlightedAttributeName = @"IsHighlighted"; NSString *const RCTAttributedStringEventEmitterKey = @"EventEmitter"; +NSString *const RCTTextAttributesAccessibilityRoleAttributeName = @"AccessibilityRole"; /* * Creates `NSTextAttributes` from given `facebook::react::TextAttributes` diff --git a/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTAttributedTextUtils.mm b/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTAttributedTextUtils.mm index 4bc9870a628870..df67fc85fedfb8 100644 --- a/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTAttributedTextUtils.mm +++ b/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTAttributedTextUtils.mm @@ -212,6 +212,11 @@ inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const Tex attributes[RCTAttributedStringIsHighlightedAttributeName] = @YES; } + if (!textAttributes.accessibilityRole.empty()) { + attributes[RCTTextAttributesAccessibilityRoleAttributeName] = + [NSString stringWithCString:textAttributes.accessibilityRole.c_str() encoding:NSUTF8StringEncoding]; + } + return [attributes copy]; } diff --git a/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTTextLayoutManager.h b/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTTextLayoutManager.h index 0e10ce2a085e95..eb495776e8c1d3 100644 --- a/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTTextLayoutManager.h +++ b/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTTextLayoutManager.h @@ -15,6 +15,13 @@ NS_ASSUME_NONNULL_BEGIN +/** + @abstract Enumeration block for text fragments. +*/ + +using RCTTextLayoutFragmentEnumerationBlock = + void (^)(CGRect fragmentRect, NSString *_Nonnull fragmentText, NSString *value); + /** * iOS-specific TextLayoutManager */ @@ -38,6 +45,12 @@ NS_ASSUME_NONNULL_BEGIN frame:(CGRect)frame atPoint:(CGPoint)point; +- (void)getRectWithAttributedString:(facebook::react::AttributedString)attributedString + paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes + enumerateAttribute:(NSString *)enumerateAttribute + frame:(CGRect)frame + usingBlock:(RCTTextLayoutFragmentEnumerationBlock)block; + @end NS_ASSUME_NONNULL_END diff --git a/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTTextLayoutManager.mm b/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTTextLayoutManager.mm index 7e0c4a9231a22c..de6e354aa55997 100644 --- a/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTTextLayoutManager.mm +++ b/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTTextLayoutManager.mm @@ -182,4 +182,44 @@ - (NSAttributedString *)_nsAttributedStringFromAttributedString:(AttributedStrin return unwrapManagedObject(sharedNSAttributedString); } +- (void)getRectWithAttributedString:(AttributedString)attributedString + paragraphAttributes:(ParagraphAttributes)paragraphAttributes + enumerateAttribute:(NSString *)enumerateAttribute + frame:(CGRect)frame + usingBlock:(RCTTextLayoutFragmentEnumerationBlock)block +{ + NSTextStorage *textStorage = [self + _textStorageAndLayoutManagerWithAttributesString:[self _nsAttributedStringFromAttributedString:attributedString] + paragraphAttributes:paragraphAttributes + size:frame.size]; + + NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject; + NSTextContainer *textContainer = layoutManager.textContainers.firstObject; + [layoutManager ensureLayoutForTextContainer:textContainer]; + + NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer]; + NSRange characterRange = [layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL]; + + [textStorage enumerateAttribute:enumerateAttribute + inRange:characterRange + options:0 + usingBlock:^(NSString *value, NSRange range, BOOL *pause) { + if (!value) { + return; + } + + [layoutManager + enumerateEnclosingRectsForGlyphRange:range + withinSelectedGlyphRange:range + inTextContainer:textContainer + usingBlock:^(CGRect enclosingRect, BOOL *_Nonnull stop) { + block( + enclosingRect, + [textStorage attributedSubstringFromRange:range].string, + value); + *stop = YES; + }]; + }]; +} + @end