From e2328b66d01cac1b91fcdca73ec1db5a703220b3 Mon Sep 17 00:00:00 2001 From: "Pfeil, Scott Robert" Date: Fri, 7 Aug 2020 18:35:37 -0400 Subject: [PATCH] Top Alert Accessibility changes --- .../TopAlert/MVMCoreUITopAlertBaseView.h | 3 ++ .../TopAlert/MVMCoreUITopAlertBaseView.m | 15 ++++-- .../MVMCoreUITopAlertExpandableView.m | 49 ++++++------------- .../TopAlert/MVMCoreUITopAlertMainView.m | 11 +++++ MVMCoreUI/TopAlert/MVMCoreUITopAlertView.m | 22 ++++++++- MVMCoreUI/Utility/MVMCoreUIUtility.h | 3 ++ MVMCoreUI/Utility/MVMCoreUIUtility.m | 11 +++++ 7 files changed, 75 insertions(+), 39 deletions(-) diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertBaseView.h b/MVMCoreUI/TopAlert/MVMCoreUITopAlertBaseView.h index 3c304651..c4053d30 100644 --- a/MVMCoreUI/TopAlert/MVMCoreUITopAlertBaseView.h +++ b/MVMCoreUI/TopAlert/MVMCoreUITopAlertBaseView.h @@ -29,4 +29,7 @@ // Handles making various parts accessible. - (void)handleAccessibility; +/// Adds the top alert accessibility prefix to the view. ++ (void)amendAccesibilityLabelForView:(nonnull UIView *)view; + @end diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertBaseView.m b/MVMCoreUI/TopAlert/MVMCoreUITopAlertBaseView.m index 8bc031a3..f2401757 100644 --- a/MVMCoreUI/TopAlert/MVMCoreUITopAlertBaseView.m +++ b/MVMCoreUI/TopAlert/MVMCoreUITopAlertBaseView.m @@ -71,18 +71,27 @@ } - (nonnull Button *)addCloseButtonWithAnimationDelegate:(nullable id )animationDelegate { - - return [MVMCoreUICommonViewsUtility addCloseButtonTo:self action:^(Button * _Nonnull button) { + Button *closeButton = [MVMCoreUICommonViewsUtility addCloseButtonTo:self action:^(Button * _Nonnull button) { if (animationDelegate) { [animationDelegate topAlertCloseButtonPressed]; } else { - [[MVMCoreUISession sharedGlobal].topAlertView hideAlertView:nil]; + [[MVMCoreUISession sharedGlobal].topAlertView hideAlertView:YES completionHandler:nil]; } } centeredVertically:YES]; + [MVMCoreUITopAlertBaseView amendAccesibilityLabelForView:closeButton]; + return closeButton; } - (void)updateView:(CGFloat)size {} - (void)handleAccessibility {} ++ (void)amendAccesibilityLabelForView:(nonnull UIView *)view { + NSString *amendment = [MVMCoreUIUtility hardcodedStringWithKey:@"top_alert_notification"]; + NSString *accessibilityLabel = view.accessibilityLabel; + if (accessibilityLabel && ![accessibilityLabel hasPrefix:amendment]) { + view.accessibilityLabel = [NSString stringWithFormat:@"%@ - %@", amendment, accessibilityLabel]; + } +} + @end diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertExpandableView.m b/MVMCoreUI/TopAlert/MVMCoreUITopAlertExpandableView.m index ad9f5e3d..411c187e 100644 --- a/MVMCoreUI/TopAlert/MVMCoreUITopAlertExpandableView.m +++ b/MVMCoreUI/TopAlert/MVMCoreUITopAlertExpandableView.m @@ -52,7 +52,7 @@ - (void)handleAccessibility { - if (self.shortView.label.text.length > 0 && (!self.expanded || !self.onlyShowTopMessageWhenCollapsed)) { + if (self.shortView.label.text.length > 0 && !self.expanded) { UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, self.shortView.label); } else if (self.buttonView.label.text.length > 0) { UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, self.buttonView.label); @@ -61,18 +61,6 @@ } } -- (void)amendAccesibilityLabel { - NSString *amendment = [MVMCoreUIUtility hardcodedStringWithKey:@"top_alert_notification"]; - NSString *accessibilityLabel = self.buttonView.label.accessibilityLabel; - if (!accessibilityLabel) { - // The accessibility label is nil when in non-voice over mode. Therefore assign the label text for the voice over is turned on mid-session (i.e. testers 🤦). - accessibilityLabel = self.buttonView.label.text; - } - if (accessibilityLabel && ![accessibilityLabel hasPrefix:amendment]) { - self.buttonView.label.accessibilityLabel = [NSString stringWithFormat:@"%@ - %@", amendment, accessibilityLabel]; - } -} - #pragma mark - Setup View - (void)updateView:(CGFloat)size { @@ -86,7 +74,6 @@ self.translatesAutoresizingMaskIntoConstraints = NO; self.clipsToBounds = YES; self.expanded = NO; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(accessibilityFocusChanged:) name:UIAccessibilityElementFocusedNotification object:nil]; } return self; } @@ -179,9 +166,7 @@ topAlertWithButton.backgroundColor = [UIColor clearColor]; [self insertSubview:topAlertWithButton belowSubview:self.shortView]; self.buttonView = topAlertWithButton; - - [self amendAccesibilityLabel]; - + self.topConstraint = [NSLayoutConstraint constraintWithItem:topAlertWithButton attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.shortView attribute:NSLayoutAttributeBottom multiplier:1 constant:0]; [NSLayoutConstraint constraintPinSubview:topAlertWithButton pinTop:NO topConstant:0 pinBottom:YES bottomConstant:0 pinLeft:YES leftConstant:0 pinRight:YES rightConstant:0]; } @@ -209,6 +194,8 @@ - (void)setTopMessage:(nullable NSString *)topMessage { [MVMCoreDispatchUtility performBlockOnMainThread:^{ self.shortView.label.text = topMessage; + self.shortView.label.accessibilityLabel = topMessage; + [MVMCoreUITopAlertBaseView amendAccesibilityLabelForView:self.shortView.label]; if (topMessage && (!self.onlyShowTopMessageWhenCollapsed || !self.expanded)) { self.shortViewHeight.active = NO; @@ -222,7 +209,6 @@ [MVMCoreDispatchUtility performBlockOnMainThread:^{ [self setTopMessage:topMessage]; [self.buttonView setupWithMessage:message subMessage:subMessage color:contentColor actionMap:actionMap additionalData:additionalData]; - [self amendAccesibilityLabel]; }]; } @@ -230,7 +216,6 @@ [MVMCoreDispatchUtility performBlockOnMainThread:^{ [self setTopMessage:topMessage]; [self.buttonView setupWithMessage:message subMessage:subMessage color:contentColor buttonTitle:buttonTitle userActionHandler:userActionHandler]; - [self amendAccesibilityLabel]; }]; } @@ -254,6 +239,7 @@ - (void)setShortViewPressToExpand { __weak typeof(self) weakSelf = self; + self.shortView.label.accessibilityTraits = UIAccessibilityTraitButton; [self.shortView.button addActionBlockWithEvent:UIControlEventTouchUpInside :^(Button * _Nonnull button) { if (weakSelf) { [weakSelf expand:YES]; @@ -263,6 +249,7 @@ - (void)setShortViewPressToCollapse { __weak typeof(self) weakSelf = self; + self.shortView.label.accessibilityTraits = UIAccessibilityTraitButton; [self.shortView.button addActionBlockWithEvent:UIControlEventTouchUpInside :^(Button * _Nonnull button) { if (weakSelf) { [weakSelf collapse]; @@ -362,15 +349,19 @@ } dispatch_time_t dispatchTime = dispatch_time(DISPATCH_TIME_NOW, dismissTime * NSEC_PER_SEC); dispatch_after(dispatchTime, dispatch_get_main_queue(), ^(void){ - if (weakSelf && weakSelf.expanded && weakSelf.collapseAutomaticallyAfterExpanded && ![self containsAccessiblityFocus]) { - [weakSelf collapse]; + if (weakSelf && weakSelf.expanded && weakSelf.collapseAutomaticallyAfterExpanded) { + // If accessibility focused, delay collapse. + if ([MVMCoreUIUtility viewContainsAccessiblityFocus:self]) { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(accessibilityFocusChanged:) name:UIAccessibilityElementFocusedNotification object:nil]; + } else { + [weakSelf collapse]; + } } }); } } - (void)collapse { - if (self.expanded) { __weak typeof(self) weakSelf = self; MVMCoreBlockOperation *operation = [MVMCoreBlockOperation blockOperationWithBlock:^(MVMCoreBlockOperation * _Nonnull operation) { @@ -406,19 +397,9 @@ } } -- (BOOL)containsAccessiblityFocus { - if (!UIAccessibilityIsVoiceOverRunning()) { - return NO; - } - id focusedElement = UIAccessibilityFocusedElement(UIAccessibilityNotificationVoiceOverIdentifier); - if (![focusedElement isKindOfClass:[UIView class]]) { - return NO; - } - return [(UIView *)focusedElement isDescendantOfView:self]; -} - - (void)accessibilityFocusChanged:(NSNotification *)notification { - if (![self containsAccessiblityFocus]) { + if (![MVMCoreUIUtility viewContainsAccessiblityFocus:self]) { + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIAccessibilityElementFocusedNotification object:nil]; [self collapse]; } } diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertMainView.m b/MVMCoreUI/TopAlert/MVMCoreUITopAlertMainView.m index a882035e..23beeb64 100644 --- a/MVMCoreUI/TopAlert/MVMCoreUITopAlertMainView.m +++ b/MVMCoreUI/TopAlert/MVMCoreUITopAlertMainView.m @@ -45,6 +45,8 @@ - (void)updateView:(CGFloat)size { [super updateView:size]; self.label.attributedText = [MVMCoreUITopAlertBaseView getStringForMessage:self.message subMessage:self.subMessage color:self.contentColor]; + self.label.accessibilityLabel = self.label.text; + [MVMCoreUITopAlertBaseView amendAccesibilityLabelForView:self.label]; [self.button updateView:size]; } @@ -226,6 +228,8 @@ // Sets the string self.label.attributedText = [MVMCoreUITopAlertBaseView getStringForMessage:message subMessage:subMessage color:color]; + self.label.accessibilityLabel = self.label.text; + [MVMCoreUITopAlertBaseView amendAccesibilityLabelForView:self.label]; // Sets the button [self setupButtonWithActionMap:actionMap additionalData:additionalData]; @@ -245,6 +249,8 @@ // Sets the string self.label.attributedText = [MVMCoreUITopAlertBaseView getStringForMessage:message subMessage:subMessage color:color]; + self.label.accessibilityLabel = self.label.text; + [MVMCoreUITopAlertBaseView amendAccesibilityLabelForView:self.label]; // Sets the color if (color) { @@ -265,6 +271,9 @@ [self setupWithButton:showButton]; if (showButton) { [self.button setTitle:[actionMap stringForKey:KeyTitle] forState:UIControlStateNormal]; + self.button.accessibilityLabel = [self.button titleForState:UIControlStateNormal]; + [MVMCoreUITopAlertBaseView amendAccesibilityLabelForView:self.button]; + [MVMCoreUITopAlertBaseView addActionToButton:self.button actionMap:actionMap additionalData:additionalData]; } }]; @@ -276,6 +285,8 @@ BOOL showButton = buttonTitle.length > 0; [self setupWithButton:showButton]; [self.button setTitle:buttonTitle forState:UIControlStateNormal]; + self.button.accessibilityLabel = [self.button titleForState:UIControlStateNormal]; + [MVMCoreUITopAlertBaseView amendAccesibilityLabelForView:self.button]; if (showButton && userActionHandler) { [self.button addActionBlockWithEvent:UIControlEventTouchUpInside :userActionHandler]; } diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.m b/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.m index d8c21b45..7efa15a3 100644 --- a/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.m +++ b/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.m @@ -44,6 +44,9 @@ NSString * const MFAccTopAlertClosed = @"Top alert notification is closed."; @property (weak, nonatomic) MVMCoreUITopAlertExpandableView *topAlertClearspotView; @property (strong, nonatomic) NSString *time; +/// Used if we delayed the collapse due to accessibility. +@property (copy, nonatomic) void (^ hideCompletionHandler)(BOOL finished); + @end @implementation MVMCoreUITopAlertView @@ -210,7 +213,13 @@ NSString * const MFAccTopAlertClosed = @"Top alert notification is closed."; [[MVMCoreNavigationHandler sharedNavigationHandler] addNavigationOperation:operation]; } -- (void)hideAlertView:(void (^ __nullable)(BOOL finished))completionHandler { +- (void)hideAlertView:(BOOL)forceful completionHandler:(void (^ __nullable)(BOOL finished))completionHandler { + // If accessible and focused, do not collapse until unfocused. + if (!forceful && [MVMCoreUIUtility viewContainsAccessiblityFocus:self]) { + self.hideCompletionHandler = completionHandler; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(accessibilityFocusChanged:) name:UIAccessibilityElementFocusedNotification object:nil]; + return; + } __weak typeof(self) weakSelf = self; MVMCoreBlockOperation *operation = [MVMCoreBlockOperation blockOperationWithBlock:^(MVMCoreBlockOperation * _Nonnull operation) { @@ -257,7 +266,7 @@ NSString * const MFAccTopAlertClosed = @"Top alert notification is closed."; [((MVMCoreUITopAlertExpandableView *)self.currentAlert) collapse]; } else { // Top alert is not collapsable, remove it instead. - [self hideAlertView:NULL]; + [self hideAlertView:NO completionHandler:NULL]; } } } @@ -294,4 +303,13 @@ NSString * const MFAccTopAlertClosed = @"Top alert notification is closed."; } } +/// If the voice over user leaves top alert focus, hide. +- (void)accessibilityFocusChanged:(NSNotification *)notification { + if (![MVMCoreUIUtility viewContainsAccessiblityFocus:self]) { + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIAccessibilityElementFocusedNotification object:nil]; + [self hideAlertView:YES completionHandler:self.hideCompletionHandler]; + self.hideCompletionHandler = nil; + } +} + @end diff --git a/MVMCoreUI/Utility/MVMCoreUIUtility.h b/MVMCoreUI/Utility/MVMCoreUIUtility.h index 78c2608b..4bf6e8d5 100644 --- a/MVMCoreUI/Utility/MVMCoreUIUtility.h +++ b/MVMCoreUI/Utility/MVMCoreUIUtility.h @@ -34,6 +34,9 @@ NS_ASSUME_NONNULL_BEGIN /// Gets the current visible view controller. Checks presented view controllers first, and then it checks on the NavigationController in the session object. + (UIViewController *)getCurrentVisibleController; +/// Checks if the view or any descendents of the view is currently focused for voice over. ++ (BOOL)viewContainsAccessiblityFocus:(nonnull UIView *)view; + #pragma mark - Setters + (void)setMarginsForView:(nullable UIView *)view leading:(CGFloat)leading top:(CGFloat)top trailing:(CGFloat)trailing bottom:(CGFloat)bottom; diff --git a/MVMCoreUI/Utility/MVMCoreUIUtility.m b/MVMCoreUI/Utility/MVMCoreUIUtility.m index cd9e041e..60ff4872 100644 --- a/MVMCoreUI/Utility/MVMCoreUIUtility.m +++ b/MVMCoreUI/Utility/MVMCoreUIUtility.m @@ -81,6 +81,17 @@ return viewController; } ++ (BOOL)viewContainsAccessiblityFocus:(nonnull UIView *)view { + if (!UIAccessibilityIsVoiceOverRunning()) { + return NO; + } + id focusedElement = UIAccessibilityFocusedElement(UIAccessibilityNotificationVoiceOverIdentifier); + if (![focusedElement isKindOfClass:[UIView class]]) { + return NO; + } + return [(UIView *)focusedElement isDescendantOfView:view]; +} + #pragma mark - Setters + (void)setMarginsForView:(nullable UIView *)view leading:(CGFloat)leading top:(CGFloat)top trailing:(CGFloat)trailing bottom:(CGFloat)bottom {