Skip to content

Commit

Permalink
NIAttributedLabel overrides accessibleElements to essentially 'flatte…
Browse files Browse the repository at this point in the history
…n' an accessibility container layer (link elements) on top of the text.

When the links are sorted in-order then VO+VC work ideally as all accessibility elements sit on one layer. We can make sure that each element in this case has a unique touch point, which we make sure is on top of the link so that VO+VC and along with the UIView all direct gestures through the right selector. Yes you heard that right, the elements (NIViewAccessibilityElement) are actually just views but regardless override accessibility APIs directly instead of using a dummy accessibility view, meaning that the behavior of the links outside of VO are tied to the their VO functionailty (ie the activation point or ordering) but that is a whole other can of worms.
When links are pre-ordered then an accessibleElement for the whole text is generated and added after all the link elements, meaning that VO+VC recognize all accessibleElements and once again the feature WAI. This ordering is weird since links come before the text which makes for a confusing UE.
Links would ideally be post-ordered by since again a VO element is generated in NIAttributedLabel's accessibleElements and this time comes before the links, now we have more issues since VC does not recognize the links (breaking GAR requirements) and also links are no longer tap-able (not a GAR requirement).

This change aligns most of the accessibilityActivationPoints by ensuring they are either the center point when not merging multiline links and is the bottom left point otherwise.

PiperOrigin-RevId: 565122233
  • Loading branch information
Nobody authored and material-automation committed Sep 13, 2023
1 parent fd5143a commit 11237e2
Show file tree
Hide file tree
Showing 2 changed files with 151 additions and 62 deletions.
63 changes: 36 additions & 27 deletions src/attributedlabel/src/NIAttributedLabel.m
Original file line number Diff line number Diff line change
Expand Up @@ -139,12 +139,13 @@ @interface NIViewAccessibilityElement ()
// touch points when merging multiline links into a single NIViewAccessibilityElement.
// When the first link contains either the first or the last word in the sentence and the second
// link spills between more than one line, the NIViewAccessibilityElement
// frames have the same top left point, which is used as the touch point. This causes an
// frames have the same top left point, which can be used as the touch point. This causes an
// issue where NIAttributedLabel can't determine which accessibilityElement a touchPoint
// originated from, meaning that one of the two links can't register events.
// We solve this by using the center point of the frame, which is ensured to be unique
// (only for links in the same text) as multiline element frames would have unique y-coordinates.
@property (nonatomic) BOOL shouldCenterActivationPoint;
// We solve this by using the bottom left of the frame, which is ensured to be unique
// (only for links in the same text) as multiline element frames would have unique y-coordinates,
// and is also ensured to contain the text to maintain tap-functionality.
@property(nonatomic) BOOL shouldCalculateUniqueActivationPoint;

@end

Expand Down Expand Up @@ -177,7 +178,6 @@ - (instancetype)initWithAccessibilityContainer:(id)container {
frameInContainer:CGRectZero
pointsInContainer:nil]) {
self.isContainerDataValid = NO;
self.shouldCenterActivationPoint = NO;
}
return self;
}
Expand Down Expand Up @@ -221,7 +221,7 @@ - (CGRect)accessibilityFrame {

- (UIBezierPath *)accessibilityPath {
UIView *accessibilityContainerView = [self validAccessibilityContainer];
if (accessibilityContainerView && NIIsArrayWithObjects(self.pointsInContainer)) {
if (accessibilityContainerView && NIIsArrayWithObjects(_pointsInContainer)) {
UIBezierPath *path = [UIBezierPath bezierPath];
for (NSUInteger i = 1; i < _pointsInContainer.count; ++i) {
CGPoint p = [[_pointsInContainer objectAtIndex:i] CGPointValue];
Expand All @@ -241,18 +241,17 @@ - (UIBezierPath *)accessibilityPath {

- (CGPoint)accessibilityActivationPoint {
UIView *accessibilityContainerView = [self validAccessibilityContainer];
if (accessibilityContainerView && NIIsArrayWithObjects(self.pointsInContainer)) {
if (_shouldCalculateUniqueActivationPoint) {
// Since links cannot overlap, use the bottom left point since it is guaranteed
// to not overlap and also guaranteed to contain the selected link.
CGPoint point = CGPointMake(_frameInContainer.origin.x,
_frameInContainer.origin.y + _frameInContainer.size.height);
point = [accessibilityContainerView convertPoint:point toView:nil];
return [accessibilityContainerView.window convertPoint:point toWindow:nil];
} else if (accessibilityContainerView && NIIsArrayWithObjects(_pointsInContainer)) {
CGPoint point = [[_pointsInContainer firstObject] CGPointValue];
point = [accessibilityContainerView convertPoint:point toView:nil];
point = [accessibilityContainerView.window convertPoint:point toWindow:nil];
return point;
}
if (_shouldCenterActivationPoint) {
// Since links cannot overlap, using the center of the start and end points
// of the explicit link location ensures unique touch points from events.
CGPoint startPoint = [[_pointsInContainer firstObject] CGPointValue];
CGPoint endPoint = [[_pointsInContainer lastObject] CGPointValue];
return CGPointMake((startPoint.x+endPoint.x)/2.0, (startPoint.y+endPoint.y)/2.0);
return [accessibilityContainerView.window convertPoint:point toWindow:nil];
}
return super.accessibilityActivationPoint;
}
Expand Down Expand Up @@ -1166,11 +1165,14 @@ - (NIViewAccessibilityElement *)accessibilityElementForRange:(NSRange)range {
// Calculate multiline link bounds using boundsForRects.
CGRect bounds = [self boundsForRects:rects];
CGRect firstRect = [[rects firstObject] CGRectValue];
// The activation point can be any point in the area. Let's make it the center of the first small
// rect.
CGPoint activationPoint = CGPointMake(firstRect.origin.x + firstRect.size.width / 2,
firstRect.origin.y + firstRect.size.height / 2);
CGRect lastRect = [[rects lastObject] CGRectValue];
// If we are not merging multiline links then the activation point can be any point in the
// element, let's use the center of the text's 'frame'. If we are merging multiline links then the
// activation point can be either the bottom left or top right point, let's use the bottom left
// point for consistency with NIViewAccessibilityElement.
CGPoint activationPoint = _shouldMergeMultilineLinks
? CGPointMake(CGRectGetMinX(lastRect), CGRectGetMaxY(lastRect))
: CGPointMake(CGRectGetMidX(firstRect), CGRectGetMidY(firstRect));
NSArray *pointsArray =
[self pointsWithActivationPoint:activationPoint
rect:bounds
Expand Down Expand Up @@ -1868,7 +1870,7 @@ - (NSArray *)accessibleElements {

// If the entire label is a single link, then we want to only end up with one accessibility
// element - not one UIAccessibilityTraitLink element for the link and another
// UIAccessibilityTraitNone element for the entire label.
// UIAccessibilityTraitStaticText/UIAccessibilityTraitNone element for the entire label.
BOOL entireLabelIsOneLink = NO;
if (allLinks.count == 1) {
NSTextCheckingResult *onlyLink = allLinks.firstObject;
Expand All @@ -1893,14 +1895,14 @@ - (NSArray *)accessibleElements {
NIViewAccessibilityElement *element = [[NIViewAccessibilityElement alloc]
initWithAccessibilityContainer:self
frameInContainer:rectValue.CGRectValue];
element.shouldCenterActivationPoint = _shouldMergeMultilineLinks;
element.shouldCalculateUniqueActivationPoint = _shouldMergeMultilineLinks;
[self updateAccessibilityLabelOnElement:element withAccessibilityLabel:label];

// Set the frame to fallback on if |element|'s accessibility container is changed
// externally.
CGRect rectValueInWindowCoordinates = [self convertRect:rectValue.CGRectValue toView:nil];
CGRect rectValueInScreenCoordinates =
[self.window convertRect:rectValueInWindowCoordinates toWindow:nil];
CGRect rectValueInScreenCoordinates = [self.window convertRect:rectValueInWindowCoordinates
toWindow:nil];
element.accessibilityFrame = rectValueInScreenCoordinates;
element.accessibilityTraits = UIAccessibilityTraitLink;
element.rememberLastValidContainer = self.accessibleElementsRememberLastValidContainer;
Expand All @@ -1911,6 +1913,7 @@ - (NSArray *)accessibleElements {
NIViewAccessibilityElement *element =
[[NIViewAccessibilityElement alloc] initWithAccessibilityContainer:self
frameInContainer:self.bounds];
element.shouldCalculateUniqueActivationPoint = _shouldMergeMultilineLinks;
[self updateAccessibilityLabelOnElement:element
withAccessibilityLabel:self.attributedText.string];

Expand All @@ -1919,7 +1922,8 @@ - (NSArray *)accessibleElements {
CGRect boundsInScreenCoordinates =
[self.window convertRect:boundsInWindowCoordinates toWindow:nil];
element.accessibilityFrame = boundsInScreenCoordinates;
element.accessibilityTraits = UIAccessibilityTraitNone;
element.accessibilityTraits =
_shouldMergeMultilineLinks ? UIAccessibilityTraitStaticText : UIAccessibilityTraitNone;
element.rememberLastValidContainer = self.accessibleElementsRememberLastValidContainer;
// TODO(kaikaiz): remove the first condition when shouldSortLinksLast is fully deprecated.
if (_shouldSortLinksLast || _linkOrdering == NILinkOrderingLast) {
Expand All @@ -1934,12 +1938,16 @@ - (NSArray *)accessibleElements {
NSRange range = result.range;
element = [self accessibilityElementForRange:NSMakeRange(start, range.location - start)];
if (element) {
element.accessibilityTraits = UIAccessibilityTraitNone;
element.accessibilityTraits =
_shouldMergeMultilineLinks ? UIAccessibilityTraitStaticText : UIAccessibilityTraitNone;
element.rememberLastValidContainer = self.accessibleElementsRememberLastValidContainer;
[accessibleElements addObject:element];
}
element = [self accessibilityElementForRange:range];
if (element) {
// Move the accessibilityActivationPoint from the center of the first frame to the
// bottom left of the last frame so it is externally calculable.
element.shouldCalculateUniqueActivationPoint = _shouldMergeMultilineLinks;
element.accessibilityTraits = UIAccessibilityTraitLink;
element.rememberLastValidContainer = self.accessibleElementsRememberLastValidContainer;
[accessibleElements addObject:element];
Expand All @@ -1950,7 +1958,8 @@ - (NSArray *)accessibleElements {
element =
[self accessibilityElementForRange:NSMakeRange(start, self.attributedText.length - start)];
if (element) {
element.accessibilityTraits = UIAccessibilityTraitNone;
element.accessibilityTraits =
_shouldMergeMultilineLinks ? UIAccessibilityTraitStaticText : UIAccessibilityTraitNone;
element.rememberLastValidContainer = self.accessibleElementsRememberLastValidContainer;
[accessibleElements addObject:element];
}
Expand Down
Loading

0 comments on commit 11237e2

Please sign in to comment.