Skip to content
This repository has been archived by the owner on Aug 8, 2023. It is now read-only.

Commit

Permalink
[ios] Made annotation callouts accessible
Browse files Browse the repository at this point in the history
Via nfarina/calloutview#84, SMCalloutView is now accessible. Activating a focused annotation now shows its callout view and focuses its left accessory view, if present, or the title view. There is a “return to map” accessibility element for dismissing the callout view and restoring focus to the annotation on the map.
  • Loading branch information
1ec5 committed Apr 21, 2016
1 parent 9cd6731 commit 397c0b2
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 27 deletions.
5 changes: 5 additions & 0 deletions platform/ios/app/MBXCustomCalloutView.m
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ - (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToV
CGFloat frameOriginY = rect.origin.y - frameHeight;
self.frame = CGRectMake(frameOriginX, frameOriginY,
frameWidth, frameHeight);

if ([self.delegate respondsToSelector:@selector(calloutViewDidAppear:)])
{
[self.delegate performSelector:@selector(calloutViewDidAppear:) withObject:self];
}
}

- (void)dismissCalloutAnimated:(BOOL)animated
Expand Down
18 changes: 18 additions & 0 deletions platform/ios/app/MBXViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,24 @@ - (void)mapView:(__unused MGLMapView *)mapView didChangeUserTrackingMode:(MGLUse
return nil;
}

- (UIView *)mapView:(__unused MGLMapView *)mapView leftCalloutAccessoryViewForAnnotation:(__unused id<MGLAnnotation>)annotation
{
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
button.frame = CGRectZero;
[button setTitle:@"Left" forState:UIControlStateNormal];
[button sizeToFit];
return button;
}

- (UIView *)mapView:(__unused MGLMapView *)mapView rightCalloutAccessoryViewForAnnotation:(__unused id<MGLAnnotation>)annotation
{
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
button.frame = CGRectZero;
[button setTitle:@"Right" forState:UIControlStateNormal];
[button sizeToFit];
return button;
}

- (void)mapView:(MGLMapView *)mapView tapOnCalloutForAnnotation:(id <MGLAnnotation>)annotation
{
if ( ! [annotation isKindOfClass:[MGLPointAnnotation class]])
Expand Down
7 changes: 6 additions & 1 deletion platform/ios/include/MGLCalloutView.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (void)calloutViewWillAppear:(UIView<MGLCalloutView> *)calloutView;

/**
Called after the callout view appears on screen, or after the appearance animation is complete.
*/
- (void)calloutViewDidAppear:(UIView<MGLCalloutView> *)calloutView;

@end

NS_ASSUME_NONNULL_END
NS_ASSUME_NONNULL_END
133 changes: 107 additions & 26 deletions platform/ios/src/MGLMapView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,26 @@ @implementation MGLAnnotationAccessibilityElement
MGLAnnotationAccessibilityElement *accessibilityElement;
};

/** An accessibility element representing the MGLMapView at large. */
@interface MGLMapViewProxyAccessibilityElement : UIAccessibilityElement

@end

@implementation MGLMapViewProxyAccessibilityElement

- (instancetype)initWithAccessibilityContainer:(id)container
{
if (self = [super initWithAccessibilityContainer:container])
{
self.accessibilityTraits = UIAccessibilityTraitButton;
self.accessibilityLabel = self.accessibilityLabel;
self.accessibilityHint = @"Returns to the map";
}
return self;
}

@end

#pragma mark - Private -

@interface MGLMapView () <UIGestureRecognizerDelegate,
Expand Down Expand Up @@ -231,6 +251,7 @@ @interface MGLMapView () <UIGestureRecognizerDelegate,
@property (nonatomic) CGFloat quickZoomStart;
@property (nonatomic, getter=isDormant) BOOL dormant;
@property (nonatomic, readonly, getter=isRotationAllowed) BOOL rotationAllowed;
@property (nonatomic) UIAccessibilityElement *mapViewProxyAccessibilityElement;

@end

Expand Down Expand Up @@ -1335,6 +1356,22 @@ - (void)handleSingleTapGesture:(UITapGestureRecognizer *)singleTap
return;
}
[self trackGestureEvent:MGLEventGestureSingleTap forRecognizer:singleTap];

if (self.mapViewProxyAccessibilityElement.accessibilityElementIsFocused)
{
id nextElement;
if (_userLocationAnnotationIsSelected)
{
nextElement = self.userLocationAnnotationView;
}
else
{
nextElement = _annotationContextsByAnnotationTag[_selectedAnnotationTag].accessibilityElement;
}
[self deselectAnnotation:self.selectedAnnotation animated:YES];
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nextElement);
return;
}

CGPoint tapPoint = [singleTap locationInView:self];

Expand Down Expand Up @@ -1553,6 +1590,12 @@ - (void)calloutViewTapped:(__unused MGLCompactCalloutView *)calloutView
}
}

- (void)calloutViewDidAppear:(UIView<MGLCalloutView> *)calloutView
{
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil);
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, calloutView);
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
NSArray *validSimultaneousGestures = @[ self.pan, self.pinch, self.rotate ];
Expand Down Expand Up @@ -1808,25 +1851,53 @@ - (CGRect)accessibilityFrame
UIViewController *viewController = self.viewControllerForLayoutGuides;
if (viewController)
{
UIView *compassContainer = self.compassView.superview;
CGFloat topInset = compassContainer.frame.origin.y + compassContainer.frame.size.height + 5;
CGFloat topInset = viewController.topLayoutGuide.length;
frame.origin.y += topInset;
frame.size.height -= topInset;

CGFloat bottomInset = MIN(self.logoView.frame.origin.y, self.attributionButton.frame.origin.y) - 8;
frame.size.height = bottomInset - frame.origin.y;
frame.size.height -= topInset + viewController.bottomLayoutGuide.length;
}
return frame;
}

- (UIBezierPath *)accessibilityPath
{
UIBezierPath *path = [UIBezierPath bezierPathWithRect:self.accessibilityFrame];

// Exclude any visible annotation callout view.
if (self.calloutViewForSelectedAnnotation)
{
UIBezierPath *calloutViewPath = [UIBezierPath bezierPathWithRect:self.calloutViewForSelectedAnnotation.frame];
[path appendPath:calloutViewPath];
}

return path;
}

- (NSInteger)accessibilityElementCount
{
if (self.calloutViewForSelectedAnnotation)
{
return 2 /* selectedAnnotationCalloutView, mapViewProxyAccessibilityElement */;
}
std::vector<MGLAnnotationTag> visibleAnnotations = [self annotationTagsInRect:self.bounds];
return visibleAnnotations.size() + 3 /* compass, userLocationAnnotationView, attributionButton */;
}

- (id)accessibilityElementAtIndex:(NSInteger)index
{
if (self.calloutViewForSelectedAnnotation)
{
if (index == 0)
{
return self.calloutViewForSelectedAnnotation;
}
if (index == 1)
{
self.mapViewProxyAccessibilityElement.accessibilityFrame = self.accessibilityFrame;
self.mapViewProxyAccessibilityElement.accessibilityPath = self.accessibilityPath;
return self.mapViewProxyAccessibilityElement;
}
return nil;
}
std::vector<MGLAnnotationTag> visibleAnnotations = [self annotationTagsInRect:self.bounds];

// Ornaments
Expand Down Expand Up @@ -1884,6 +1955,11 @@ - (id)accessibilityElementAtIndex:(NSInteger)index

- (NSInteger)indexOfAccessibilityElement:(id)element
{
if (self.calloutViewForSelectedAnnotation)
{
return [@[self.calloutViewForSelectedAnnotation, self.mapViewProxyAccessibilityElement]
indexOfObject:element];
}
if (element == self.compassView)
{
return 0;
Expand All @@ -1910,6 +1986,15 @@ - (NSInteger)indexOfAccessibilityElement:(id)element
else return std::distance(visibleAnnotations.begin(), foundElement) + 2 /* compass, userLocationAnnotationView */;
}

- (UIAccessibilityElement *)mapViewProxyAccessibilityElement
{
if ( ! _mapViewProxyAccessibilityElement)
{
_mapViewProxyAccessibilityElement = [[MGLAnnotationAccessibilityElement alloc] initWithAccessibilityContainer:self];
}
return _mapViewProxyAccessibilityElement;
}

#pragma mark - Geography -

+ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingCenterCoordinate
Expand Down Expand Up @@ -3048,14 +3133,16 @@ - (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated
[self.delegate mapView:self annotationCanShowCallout:annotation])
{
// build the callout
UIView <MGLCalloutView> *calloutView;
if ([self.delegate respondsToSelector:@selector(mapView:calloutViewForAnnotation:)])
{
self.calloutViewForSelectedAnnotation = [self.delegate mapView:self calloutViewForAnnotation:annotation];
calloutView = [self.delegate mapView:self calloutViewForAnnotation:annotation];
}
if (!self.calloutViewForSelectedAnnotation)
if (!calloutView)
{
self.calloutViewForSelectedAnnotation = [self calloutViewForAnnotation:annotation];
calloutView = [self calloutViewForAnnotation:annotation];
}
self.calloutViewForSelectedAnnotation = calloutView;

if (_userLocationAnnotationIsSelected)
{
Expand All @@ -3070,41 +3157,38 @@ - (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated
// consult delegate for left and/or right accessory views
if ([self.delegate respondsToSelector:@selector(mapView:leftCalloutAccessoryViewForAnnotation:)])
{
self.calloutViewForSelectedAnnotation.leftAccessoryView =
[self.delegate mapView:self leftCalloutAccessoryViewForAnnotation:annotation];
calloutView.leftAccessoryView = [self.delegate mapView:self leftCalloutAccessoryViewForAnnotation:annotation];

if ([self.calloutViewForSelectedAnnotation.leftAccessoryView isKindOfClass:[UIControl class]])
if ([calloutView.leftAccessoryView isKindOfClass:[UIControl class]])
{
UITapGestureRecognizer *calloutAccessoryTap = [[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(handleCalloutAccessoryTapGesture:)];

[self.calloutViewForSelectedAnnotation.leftAccessoryView addGestureRecognizer:calloutAccessoryTap];
[calloutView.leftAccessoryView addGestureRecognizer:calloutAccessoryTap];
}
}

if ([self.delegate respondsToSelector:@selector(mapView:rightCalloutAccessoryViewForAnnotation:)])
{
self.calloutViewForSelectedAnnotation.rightAccessoryView =
[self.delegate mapView:self rightCalloutAccessoryViewForAnnotation:annotation];
calloutView.rightAccessoryView = [self.delegate mapView:self rightCalloutAccessoryViewForAnnotation:annotation];

if ([self.calloutViewForSelectedAnnotation.rightAccessoryView isKindOfClass:[UIControl class]])
if ([calloutView.rightAccessoryView isKindOfClass:[UIControl class]])
{
UITapGestureRecognizer *calloutAccessoryTap = [[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(handleCalloutAccessoryTapGesture:)];

[self.calloutViewForSelectedAnnotation.rightAccessoryView addGestureRecognizer:calloutAccessoryTap];
[calloutView.rightAccessoryView addGestureRecognizer:calloutAccessoryTap];
}
}

// set annotation delegate to handle taps on the callout view
self.calloutViewForSelectedAnnotation.delegate = self;
calloutView.delegate = self;

// present popup
[self.calloutViewForSelectedAnnotation presentCalloutFromRect:positioningRect
inView:self.glView
constrainedToView:self.glView
animated:animated];
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
[calloutView presentCalloutFromRect:positioningRect
inView:self.glView
constrainedToView:self.glView
animated:animated];
}

// notify delegate
Expand Down Expand Up @@ -3186,8 +3270,6 @@ - (void)deselectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animate
self.calloutViewForSelectedAnnotation = nil;
self.selectedAnnotation = nil;

UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);

// notify delegate
if ([self.delegate respondsToSelector:@selector(mapView:didDeselectAnnotation:)])
{
Expand Down Expand Up @@ -3377,7 +3459,6 @@ - (void)setShowsUserLocation:(BOOL)showsUserLocation
self.userLocationAnnotationView = [[MGLUserLocationAnnotationView alloc] initInMapView:self];
self.userLocationAnnotationView.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin |
UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin);
self.userLocationAnnotationView.isAccessibilityElement = YES;

[self validateLocationServices];
}
Expand Down
6 changes: 6 additions & 0 deletions platform/ios/src/MGLUserLocationAnnotationView.m
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ - (instancetype)initInMapView:(MGLMapView *)mapView
self.annotation = [[MGLUserLocation alloc] initWithMapView:mapView];
_mapView = mapView;
[self setupLayers];
self.isAccessibilityElement = YES;
self.accessibilityTraits = UIAccessibilityTraitButton;
}
return self;
Expand Down Expand Up @@ -102,6 +103,11 @@ - (NSString *)accessibilityValue
}
}

- (CGRect)accessibilityFrame
{
return CGRectInset(self.frame, -15, -15);
}

- (UIBezierPath *)accessibilityPath
{
return [UIBezierPath bezierPathWithOvalInRect:self.frame];
Expand Down

0 comments on commit 397c0b2

Please sign in to comment.