mirror of
https://github.com/status-im/react-native.git
synced 2025-01-16 20:44:10 +00:00
915a020fca
Summary: **PR changes** The RCTText class originally overrode the accessibilityLabel and returned the raw text of the class ignoring if the accessibilityLabel was set explicitly in code. Example: <Text accessibilityLabel="Example"> Hello World </Text> // returns "Hello World" instead of "Example" for the accessibility label My update checks if the super's accessibilityLabel is not nil and returns the value else it returns the raw text itself as a default to mirror what a UIKit's UILabel does. The super's accessibilityLabel is nil if the accessibilityLabel is not ever set in code. I don't check the length of the label because if the value was set to an empty purposely then it will respect that and return whatever was set in code. With the new changes: <Text accessibilityLabel="Example"> Hello World </Text> // returns "Example" for the accessibilityLabel This change doesn't support nested <Text> components with both accessibilityLabel's value set respectively. The parent's value will return. Example: // returns "Example" instead of "Example Test" for the accessibility label <Text accessibilityLabel="Example"> Hello <Text accessibilityLabel="Test"> World </Text> </Text> The workaround is just to set the only the parent view's accessibilityLabel with the label desired for it and all its nested views or just not nest the views if possible. I believe a bigger change would be needed to support accessibility for nested views, for now the changes I have made should satisfy the requirements. Reviewed By: shergin Differential Revision: D5806097 fbshipit-source-id: aef2d7cec4657317fcd7dd557448905e4b767f1a
273 lines
7.7 KiB
Objective-C
273 lines
7.7 KiB
Objective-C
/**
|
|
* Copyright (c) 2015-present, Facebook, Inc.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the BSD-style license found in the
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*/
|
|
|
|
#import "RCTText.h"
|
|
|
|
#import <MobileCoreServices/UTCoreTypes.h>
|
|
|
|
#import <React/RCTUtils.h>
|
|
#import <React/UIView+React.h>
|
|
|
|
#import "RCTShadowText.h"
|
|
|
|
static void collectNonTextDescendants(RCTText *view, NSMutableArray *nonTextDescendants)
|
|
{
|
|
for (UIView *child in view.reactSubviews) {
|
|
if ([child isKindOfClass:[RCTText class]]) {
|
|
collectNonTextDescendants((RCTText *)child, nonTextDescendants);
|
|
} else if (!CGRectEqualToRect(child.frame, CGRectZero)) {
|
|
[nonTextDescendants addObject:child];
|
|
}
|
|
}
|
|
}
|
|
|
|
@implementation RCTText
|
|
{
|
|
NSTextStorage *_textStorage;
|
|
CAShapeLayer *_highlightLayer;
|
|
UILongPressGestureRecognizer *_longPressGestureRecognizer;
|
|
}
|
|
|
|
- (instancetype)initWithFrame:(CGRect)frame
|
|
{
|
|
if ((self = [super initWithFrame:frame])) {
|
|
_textStorage = [NSTextStorage new];
|
|
self.isAccessibilityElement = YES;
|
|
self.accessibilityTraits |= UIAccessibilityTraitStaticText;
|
|
|
|
self.opaque = NO;
|
|
self.contentMode = UIViewContentModeRedraw;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (NSString *)description
|
|
{
|
|
NSString *superDescription = super.description;
|
|
NSRange semicolonRange = [superDescription rangeOfString:@";"];
|
|
NSString *replacement = [NSString stringWithFormat:@"; reactTag: %@; text: %@", self.reactTag, self.textStorage.string];
|
|
return [superDescription stringByReplacingCharactersInRange:semicolonRange withString:replacement];
|
|
}
|
|
|
|
- (void)setSelectable:(BOOL)selectable
|
|
{
|
|
if (_selectable == selectable) {
|
|
return;
|
|
}
|
|
|
|
_selectable = selectable;
|
|
|
|
if (_selectable) {
|
|
[self enableContextMenu];
|
|
}
|
|
else {
|
|
[self disableContextMenu];
|
|
}
|
|
}
|
|
|
|
- (void)reactSetFrame:(CGRect)frame
|
|
{
|
|
// Text looks super weird if its frame is animated.
|
|
// This disables the frame animation, without affecting opacity, etc.
|
|
[UIView performWithoutAnimation:^{
|
|
[super reactSetFrame:frame];
|
|
}];
|
|
}
|
|
|
|
- (void)reactSetInheritedBackgroundColor:(UIColor *)inheritedBackgroundColor
|
|
{
|
|
self.backgroundColor = inheritedBackgroundColor;
|
|
}
|
|
|
|
- (void)didUpdateReactSubviews
|
|
{
|
|
// Do nothing, as subviews are managed by `setTextStorage:` method
|
|
}
|
|
|
|
- (void)setTextStorage:(NSTextStorage *)textStorage
|
|
{
|
|
if (_textStorage != textStorage) {
|
|
_textStorage = textStorage;
|
|
|
|
// Update subviews
|
|
NSMutableArray *nonTextDescendants = [NSMutableArray new];
|
|
collectNonTextDescendants(self, nonTextDescendants);
|
|
NSArray *subviews = self.subviews;
|
|
if (![subviews isEqualToArray:nonTextDescendants]) {
|
|
for (UIView *child in subviews) {
|
|
if (![nonTextDescendants containsObject:child]) {
|
|
[child removeFromSuperview];
|
|
}
|
|
}
|
|
for (UIView *child in nonTextDescendants) {
|
|
[self addSubview:child];
|
|
}
|
|
}
|
|
|
|
[self setNeedsDisplay];
|
|
}
|
|
}
|
|
|
|
- (void)drawRect:(CGRect)rect
|
|
{
|
|
NSLayoutManager *layoutManager = [_textStorage.layoutManagers firstObject];
|
|
NSTextContainer *textContainer = [layoutManager.textContainers firstObject];
|
|
|
|
NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
|
|
CGRect textFrame = self.textFrame;
|
|
[layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:textFrame.origin];
|
|
[layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:textFrame.origin];
|
|
|
|
__block UIBezierPath *highlightPath = nil;
|
|
NSRange characterRange = [layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL];
|
|
[layoutManager.textStorage enumerateAttribute:RCTIsHighlightedAttributeName inRange:characterRange options:0 usingBlock:^(NSNumber *value, NSRange range, BOOL *_) {
|
|
if (!value.boolValue) {
|
|
return;
|
|
}
|
|
|
|
[layoutManager enumerateEnclosingRectsForGlyphRange:range withinSelectedGlyphRange:range inTextContainer:textContainer usingBlock:^(CGRect enclosingRect, __unused BOOL *__) {
|
|
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(enclosingRect, -2, -2) cornerRadius:2];
|
|
if (highlightPath) {
|
|
[highlightPath appendPath:path];
|
|
} else {
|
|
highlightPath = path;
|
|
}
|
|
}];
|
|
}];
|
|
|
|
if (highlightPath) {
|
|
if (!_highlightLayer) {
|
|
_highlightLayer = [CAShapeLayer layer];
|
|
_highlightLayer.fillColor = [UIColor colorWithWhite:0 alpha:0.25].CGColor;
|
|
[self.layer addSublayer:_highlightLayer];
|
|
}
|
|
_highlightLayer.position = (CGPoint){_contentInset.left, _contentInset.top};
|
|
_highlightLayer.path = highlightPath.CGPath;
|
|
} else {
|
|
[_highlightLayer removeFromSuperlayer];
|
|
_highlightLayer = nil;
|
|
}
|
|
}
|
|
|
|
- (NSNumber *)reactTagAtPoint:(CGPoint)point
|
|
{
|
|
NSNumber *reactTag = self.reactTag;
|
|
|
|
CGFloat fraction;
|
|
NSLayoutManager *layoutManager = _textStorage.layoutManagers.firstObject;
|
|
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
|
|
NSUInteger characterIndex = [layoutManager characterIndexForPoint:point
|
|
inTextContainer:textContainer
|
|
fractionOfDistanceBetweenInsertionPoints:&fraction];
|
|
|
|
// If the point is not before (fraction == 0.0) the first character and not
|
|
// after (fraction == 1.0) the last character, then the attribute is valid.
|
|
if (_textStorage.length > 0 && (fraction > 0 || characterIndex > 0) && (fraction < 1 || characterIndex < _textStorage.length - 1)) {
|
|
reactTag = [_textStorage attribute:RCTReactTagAttributeName atIndex:characterIndex effectiveRange:NULL];
|
|
}
|
|
return reactTag;
|
|
}
|
|
|
|
- (void)didMoveToWindow
|
|
{
|
|
[super didMoveToWindow];
|
|
|
|
if (!self.window) {
|
|
self.layer.contents = nil;
|
|
if (_highlightLayer) {
|
|
[_highlightLayer removeFromSuperlayer];
|
|
_highlightLayer = nil;
|
|
}
|
|
} else if (_textStorage.length) {
|
|
[self setNeedsDisplay];
|
|
}
|
|
}
|
|
|
|
|
|
#pragma mark - Accessibility
|
|
|
|
- (NSString *)accessibilityLabel
|
|
{
|
|
NSString *superAccessibilityLabel = [super accessibilityLabel];
|
|
if (superAccessibilityLabel) {
|
|
return superAccessibilityLabel;
|
|
}
|
|
return _textStorage.string;
|
|
}
|
|
|
|
#pragma mark - Context Menu
|
|
|
|
- (void)enableContextMenu
|
|
{
|
|
_longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
|
|
[self addGestureRecognizer:_longPressGestureRecognizer];
|
|
}
|
|
|
|
- (void)disableContextMenu
|
|
{
|
|
[self removeGestureRecognizer:_longPressGestureRecognizer];
|
|
_longPressGestureRecognizer = nil;
|
|
}
|
|
|
|
- (void)handleLongPress:(UILongPressGestureRecognizer *)gesture
|
|
{
|
|
#if !TARGET_OS_TV
|
|
UIMenuController *menuController = [UIMenuController sharedMenuController];
|
|
|
|
if (menuController.isMenuVisible) {
|
|
return;
|
|
}
|
|
|
|
if (!self.isFirstResponder) {
|
|
[self becomeFirstResponder];
|
|
}
|
|
|
|
[menuController setTargetRect:self.bounds inView:self];
|
|
[menuController setMenuVisible:YES animated:YES];
|
|
#endif
|
|
}
|
|
|
|
- (BOOL)canBecomeFirstResponder
|
|
{
|
|
return _selectable;
|
|
}
|
|
|
|
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
|
|
{
|
|
if (_selectable && action == @selector(copy:)) {
|
|
return YES;
|
|
}
|
|
|
|
return [self.nextResponder canPerformAction:action withSender:sender];
|
|
}
|
|
|
|
- (void)copy:(id)sender
|
|
{
|
|
#if !TARGET_OS_TV
|
|
NSAttributedString *attributedString = _textStorage;
|
|
|
|
NSMutableDictionary *item = [NSMutableDictionary new];
|
|
|
|
NSData *rtf = [attributedString dataFromRange:NSMakeRange(0, attributedString.length)
|
|
documentAttributes:@{NSDocumentTypeDocumentAttribute: NSRTFDTextDocumentType}
|
|
error:nil];
|
|
|
|
if (rtf) {
|
|
[item setObject:rtf forKey:(id)kUTTypeFlatRTFD];
|
|
}
|
|
|
|
[item setObject:attributedString.string forKey:(id)kUTTypeUTF8PlainText];
|
|
|
|
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
|
|
pasteboard.items = @[item];
|
|
#endif
|
|
}
|
|
|
|
@end
|