From 397c0b2f9df57c8277414d233ee08340a74be092 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguy=E1=BB=85n?= Date: Sat, 16 May 2015 11:23:45 -0700 Subject: [PATCH] [ios] Made annotation callouts accessible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- platform/ios/app/MBXCustomCalloutView.m | 5 + platform/ios/app/MBXViewController.m | 18 +++ platform/ios/include/MGLCalloutView.h | 7 +- platform/ios/src/MGLMapView.mm | 133 ++++++++++++++---- .../ios/src/MGLUserLocationAnnotationView.m | 6 + 5 files changed, 142 insertions(+), 27 deletions(-) diff --git a/platform/ios/app/MBXCustomCalloutView.m b/platform/ios/app/MBXCustomCalloutView.m index 11ce86e76a5..9edc00f6e99 100644 --- a/platform/ios/app/MBXCustomCalloutView.m +++ b/platform/ios/app/MBXCustomCalloutView.m @@ -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 diff --git a/platform/ios/app/MBXViewController.m b/platform/ios/app/MBXViewController.m index 2f4caf9939c..904ed6e7362 100644 --- a/platform/ios/app/MBXViewController.m +++ b/platform/ios/app/MBXViewController.m @@ -719,6 +719,24 @@ - (void)mapView:(__unused MGLMapView *)mapView didChangeUserTrackingMode:(MGLUse return nil; } +- (UIView *)mapView:(__unused MGLMapView *)mapView leftCalloutAccessoryViewForAnnotation:(__unused id)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)annotation +{ + UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem]; + button.frame = CGRectZero; + [button setTitle:@"Right" forState:UIControlStateNormal]; + [button sizeToFit]; + return button; +} + - (void)mapView:(MGLMapView *)mapView tapOnCalloutForAnnotation:(id )annotation { if ( ! [annotation isKindOfClass:[MGLPointAnnotation class]]) diff --git a/platform/ios/include/MGLCalloutView.h b/platform/ios/include/MGLCalloutView.h index 59f52adb6d9..641976dfeed 100644 --- a/platform/ios/include/MGLCalloutView.h +++ b/platform/ios/include/MGLCalloutView.h @@ -67,6 +67,11 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)calloutViewWillAppear:(UIView *)calloutView; +/** + Called after the callout view appears on screen, or after the appearance animation is complete. + */ +- (void)calloutViewDidAppear:(UIView *)calloutView; + @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index 766e4fe1d9f..a71f9242809 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -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 () *)calloutView +{ + UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil); + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, calloutView); +} + - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { NSArray *validSimultaneousGestures = @[ self.pan, self.pinch, self.rotate ]; @@ -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 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 visibleAnnotations = [self annotationTagsInRect:self.bounds]; // Ornaments @@ -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; @@ -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 @@ -3048,14 +3133,16 @@ - (void)selectAnnotation:(id )annotation animated:(BOOL)animated [self.delegate mapView:self annotationCanShowCallout:annotation]) { // build the callout + UIView *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) { @@ -3070,41 +3157,38 @@ - (void)selectAnnotation:(id )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 @@ -3186,8 +3270,6 @@ - (void)deselectAnnotation:(id )annotation animated:(BOOL)animate self.calloutViewForSelectedAnnotation = nil; self.selectedAnnotation = nil; - UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); - // notify delegate if ([self.delegate respondsToSelector:@selector(mapView:didDeselectAnnotation:)]) { @@ -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]; } diff --git a/platform/ios/src/MGLUserLocationAnnotationView.m b/platform/ios/src/MGLUserLocationAnnotationView.m index 7afa2f71f83..f6285dfe289 100644 --- a/platform/ios/src/MGLUserLocationAnnotationView.m +++ b/platform/ios/src/MGLUserLocationAnnotationView.m @@ -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; @@ -102,6 +103,11 @@ - (NSString *)accessibilityValue } } +- (CGRect)accessibilityFrame +{ + return CGRectInset(self.frame, -15, -15); +} + - (UIBezierPath *)accessibilityPath { return [UIBezierPath bezierPathWithOvalInRect:self.frame];