Skip to content

Commit

Permalink
Merge pull request #18155 from wordpress-mobile/feature/menu-vo-order
Browse files Browse the repository at this point in the history
Improve VoiceOver experience when reordering menus
  • Loading branch information
twstokes authored Apr 8, 2022
2 parents 7890e07 + c41c600 commit 9dddadb
Show file tree
Hide file tree
Showing 11 changed files with 406 additions and 3 deletions.
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
19.7
-----
* [*] a11y: VoiceOver has been improved on the Menus view and now announces changes to ordering. [#18155]


19.6
Expand Down
10 changes: 9 additions & 1 deletion WordPress/Classes/Models/MenuItem.h
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,21 @@ extern NSString * const MenuItemLinkTargetBlank;
@param orderedItems an ordered set of items nested top-down as a parent followed by children. (as returned from the Menus API)
@returns MenuItem the last child of self to occur in the ordered set.
*/
- (MenuItem *)lastDescendantInOrderedItems:(NSOrderedSet *)orderedItems;
- (nullable MenuItem *)lastDescendantInOrderedItems:(NSOrderedSet *)orderedItems;

/**
The item's name is nil, empty, or the default string.
*/
- (BOOL)nameIsEmptyOrDefault;

/**
Search for a sibling that precedes self.
A sibling is a MenuItem that shares the same parent.
@param orderedItems an ordered set of items nested top-down as a parent followed by children. (as returned from the Menus API)
@returns MenuItem sibling that precedes self, or nil if there is not one.
*/
- (nullable MenuItem *)precedingSiblingInOrderedItems:(NSOrderedSet *)orderedItems;

@end

@interface MenuItem (CoreDataGeneratedAccessors)
Expand Down
16 changes: 15 additions & 1 deletion WordPress/Classes/Models/MenuItem.m
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ - (BOOL)isDescendantOfItem:(MenuItem *)item
/**
Traverse the orderedItems for parent items equal to self or that are a descendant of self (a child of a child).
*/
- (MenuItem *)lastDescendantInOrderedItems:(NSOrderedSet *)orderedItems
- (nullable MenuItem *)lastDescendantInOrderedItems:(NSOrderedSet *)orderedItems
{
MenuItem *lastChildItem = nil;
NSUInteger parentIndex = [orderedItems indexOfObject:self];
Expand All @@ -115,4 +115,18 @@ - (BOOL)nameIsEmptyOrDefault
return self.name.length == 0 || [self.name isEqualToString:[MenuItem defaultItemNameLocalized]];
}

/**
Return a sibling that precedes self, or nil if one wasn't found.
*/
- (nullable MenuItem *)precedingSiblingInOrderedItems:(NSOrderedSet *)orderedItems
{
for (NSUInteger idx = [orderedItems indexOfObject:self]; idx > 0; idx--) {
MenuItem *previousItem = [orderedItems objectAtIndex:idx - 1];
if (previousItem.parent == self.parent) {
return previousItem;
}
}
return nil;
}

@end
2 changes: 2 additions & 0 deletions WordPress/Classes/System/WordPress-Bridging-Header.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
#import "MediaLibraryPickerDataSource.h"
#import "MediaService.h"
#import "MeHeaderView.h"
#import "MenuItem.h"
#import "MenuItemsViewController.h"

#import "NavBarTitleDropdownButton.h"
#import "NSObject+Helpers.h"
Expand Down
5 changes: 5 additions & 0 deletions WordPress/Classes/ViewRelated/Menus/MenuItemView.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (void)refresh;

/**
Refresh the accessibility labels.
*/
- (void)refreshAccessibilityLabels;

/**
The detectedable region of the view for allowing ordering.
*/
Expand Down
19 changes: 18 additions & 1 deletion WordPress/Classes/ViewRelated/Menus/MenuItemView.m
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@ - (void)setupOrderingButton
UIImage *image = [[UIImage imageNamed:@"menus-move-icon"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
UIButton *button = [self addAccessoryButtonIconViewWithImage:image];
button.accessibilityLabel = NSLocalizedString(@"Move menu item", @"Screen reader text for button that will move the menu item");
button.accessibilityHint = NSLocalizedString(@"Double tap and hold to move this menu item up or down", @"Screen reader hint for button that will move the menu item");
button.accessibilityHint = NSLocalizedString(@"Double tap and hold to move this menu item up or down. Move horizontally to change hierarchy.", @"Screen reader hint for button that will move the menu item");
button.userInteractionEnabled = NO;
// Override the accessibility traits so that VoiceOver doesn't read "Dimmed" due to userInteractionEnabled = NO
[button setAccessibilityTraits:UIAccessibilityTraitButton];
_orderingButton = button;
}

Expand Down Expand Up @@ -109,6 +111,21 @@ - (void)refresh
{
self.iconView.image = [MenuItem iconImageForItemType:self.item.type];
self.textLabel.text = self.item.name;
[self refreshAccessibilityLabels];
}

- (void)refreshAccessibilityLabels
{
NSString *parentString;
if (self.item.parent) {
parentString = [NSString stringWithFormat:NSLocalizedString(@"Child of %@", @"Screen reader text expressing the menu item is a child of another menu item. Argument is a name for another menu item."), self.item.parent.name];
} else {
parentString = NSLocalizedString(@"Top level", @"Screen reader text expressing the menu item is at the top level and has no parent.");
}
self.textLabel.accessibilityLabel = [NSString stringWithFormat:@"%@. %@", self.textLabel.text, parentString];

NSString *labelString = NSLocalizedString(@"Move %@", @"Screen reader text for button that will move the menu item. Argument is menu item's name.");
self.orderingButton.accessibilityLabel = [NSString stringWithFormat:labelString, self.item.name];
}

- (CGRect)orderingToggleRect
Expand Down
8 changes: 8 additions & 0 deletions WordPress/Classes/ViewRelated/Menus/MenuItemsViewController.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (void)removeItem:(MenuItem *)item;

/// Generates a string used by VoiceOver to announce menu ordering.
/// @param parent A new parent of the current item.
/// @param parentChanged If the parent changed. Needed to infer "Top level" when parent is nil.
/// @param before A menu item that now precedes the current item.
/// @param after A menu item that now succeeds the current item.
/// @return An NSString to announce, or nil.
+ (nullable NSString *)generateOrderingChangeVOString:(nullable MenuItem *)parent parentChanged:(BOOL)parentChanged before:(nullable MenuItem *)before after:(nullable MenuItem *)after;

@end

@protocol MenuItemsViewControllerDelegate <NSObject>
Expand Down
62 changes: 62 additions & 0 deletions WordPress/Classes/ViewRelated/Menus/MenuItemsViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ - (void)updateParentChildIndentationForItemViews
itemView.indentationLevel++;
parentItem = parentItem.parent;
}

[itemView refreshAccessibilityLabels];
}
}

Expand Down Expand Up @@ -488,6 +490,9 @@ - (void)orderingTouchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)ev

if (newParent) {
selectedItem.parent = newParent;
MenuItem *precedingSibling = [selectedItem precedingSiblingInOrderedItems:orderedItems];
[self announceOrderingChange:newParent parentChanged:YES before:nil after:precedingSibling];

modelUpdated = YES;
}

Expand All @@ -500,6 +505,9 @@ - (void)orderingTouchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)ev
// try to move up the parent tree
MenuItem *parent = selectedItem.parent.parent;
selectedItem.parent = parent;
MenuItem *precedingSibling = [selectedItem precedingSiblingInOrderedItems:orderedItems];
[self announceOrderingChange:parent parentChanged:YES before:nil after:precedingSibling];

modelUpdated = YES;
}
}
Expand Down Expand Up @@ -606,6 +614,8 @@ - (BOOL)handleOrderingTouchForItemView:(MenuItemView *)itemView withOtherItemVie
if ([self nextAvailableItemForOrderingAfterItem:item] == otherItem) {
// take the parent of the otherItem, or nil
item.parent = otherItem.parent;
[self announceOrderingChange:item.parent parentChanged:YES before:otherItem after:nil];

updated = YES;
}
}
Expand All @@ -616,9 +626,11 @@ - (BOOL)handleOrderingTouchForItemView:(MenuItemView *)itemView withOtherItemVie
if (otherItem.children.count) {
// if ordering after a parent, we need to become a child
item.parent = otherItem;
[self announceOrderingChange:otherItem parentChanged:YES before:nil after:nil];
} else {
// assuming the item will take the parent of the otherItem's parent, or nil
item.parent = otherItem.parent;
[self announceOrderingChange:nil parentChanged:NO before:nil after:otherItem];
}

moveItemAndDescendantsOrderingWithOtherItem(YES);
Expand All @@ -631,6 +643,11 @@ - (BOOL)handleOrderingTouchForItemView:(MenuItemView *)itemView withOtherItemVie

if (orderingTouchesBeforeOtherItem) {
// trying to order the item before the otherItem
if (item.parent != otherItem.parent) {
[self announceOrderingChange:otherItem.parent parentChanged:YES before:otherItem after:nil];
} else {
[self announceOrderingChange:nil parentChanged:NO before:otherItem after:nil];
}

// assuming the item will become the parent of the otherItem's parent, or nil
item.parent = otherItem.parent;
Expand Down Expand Up @@ -899,4 +916,49 @@ - (void)cleanUpOrderingFeedbackGenerator
self.orderingFeedbackGenerator = nil;
}

#pragma mark - VoiceOver

+ (nullable NSString *)generateOrderingChangeVOString:(nullable MenuItem *)parent parentChanged:(BOOL)parentChanged before:(nullable MenuItem *)before after:(nullable MenuItem *)after {
NSMutableArray *stringArray = [[NSMutableArray alloc] init];

if (parentChanged) {
if (parent) {
NSString *parentString = NSLocalizedString(@"Child of %@", @"Screen reader text expressing the menu item is a child of another menu item. Argument is a name for another menu item.");
[stringArray addObject:[NSString stringWithFormat:parentString, parent.name]];
} else {
NSString *parentString = NSLocalizedString(@"Top level", @"Screen reader text expressing the menu item is at the top level and has no parent.");
[stringArray addObject:parentString];
}
}
if (after) {
NSString *afterString = NSLocalizedString(@"After %@", @"Screen reader text expressing the menu item is after another menu item. Argument is a name for another menu item.");
[stringArray addObject:[NSString stringWithFormat:afterString, after.name]];
}
if (before) {
NSString *beforeString = NSLocalizedString(@"Before %@", @"Screen reader text expressing the menu item is before another menu item. Argument is a name for another menu item.");
[stringArray addObject:[NSString stringWithFormat:beforeString, before.name]];
}
if (!stringArray.count) {
return nil;
}

return [stringArray componentsJoinedByString:@". "];
}

/// Used by VoiceOver to announce changes of the Menu UI.
/// @param parent A new parent of the current item.
/// @param parentChanged If the parent changed. Needed to infer "Top level" when parent is nil.
/// @param before A menu item that now precedes the current item.
/// @param after A menu item that now succeeds the current item.
- (void)announceOrderingChange:(MenuItem *)parent parentChanged:(BOOL)parentChanged before:(MenuItem *)before after:(MenuItem *)after {
if (!UIAccessibilityIsVoiceOverRunning()) {
return;
}

NSString *announcement = [MenuItemsViewController generateOrderingChangeVOString:parent parentChanged:parentChanged before:before after:after];
if (announcement) {
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcement);
}
}

@end
24 changes: 24 additions & 0 deletions WordPress/WordPress.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -2163,9 +2163,11 @@
C3234F5427EBBACA004ADB29 /* SiteIntentVertical.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3234F5327EBBACA004ADB29 /* SiteIntentVertical.swift */; };
C3234F5527EBBACA004ADB29 /* SiteIntentVertical.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3234F5327EBBACA004ADB29 /* SiteIntentVertical.swift */; };
C387B7A22638D66F00BDEF86 /* PostAuthorSelectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3E2462826277B7700B99EA6 /* PostAuthorSelectorViewController.swift */; };
C38C5D8127F61D2C002F517E /* MenuItemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38C5D8027F61D2C002F517E /* MenuItemTests.swift */; };
C3C39B0726F50D3900B1238D /* WordPressSupportSourceTag+Editor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C39B0626F50D3900B1238D /* WordPressSupportSourceTag+Editor.swift */; };
C3C39B0826F50D3900B1238D /* WordPressSupportSourceTag+Editor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C39B0626F50D3900B1238D /* WordPressSupportSourceTag+Editor.swift */; };
C3E2462926277B7700B99EA6 /* PostAuthorSelectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3E2462826277B7700B99EA6 /* PostAuthorSelectorViewController.swift */; };
C3E42AB027F4D30E00546706 /* MenuItemsViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3E42AAF27F4D30E00546706 /* MenuItemsViewControllerTests.swift */; };
C533CF350E6D3ADA000C3DE8 /* CommentsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C533CF340E6D3ADA000C3DE8 /* CommentsViewController.m */; };
C545E0A21811B9880020844C /* ContextManager.m in Sources */ = {isa = PBXBuildFile; fileRef = C545E0A11811B9880020844C /* ContextManager.m */; };
C56636E91868D0CE00226AAB /* StatsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C56636E71868D0CE00226AAB /* StatsViewController.m */; };
Expand Down Expand Up @@ -6899,9 +6901,11 @@
C3234F5327EBBACA004ADB29 /* SiteIntentVertical.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteIntentVertical.swift; sourceTree = "<group>"; };
C3302CC427EB67D0004229D3 /* IntentCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = IntentCell.xib; sourceTree = "<group>"; };
C3302CC527EB67D0004229D3 /* IntentCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentCell.swift; sourceTree = "<group>"; };
C38C5D8027F61D2C002F517E /* MenuItemTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MenuItemTests.swift; path = Menus/MenuItemTests.swift; sourceTree = "<group>"; };
C3ABE791263099F7009BD402 /* WordPress 121.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 121.xcdatamodel"; sourceTree = "<group>"; };
C3C39B0626F50D3900B1238D /* WordPressSupportSourceTag+Editor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "WordPressSupportSourceTag+Editor.swift"; sourceTree = "<group>"; };
C3E2462826277B7700B99EA6 /* PostAuthorSelectorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostAuthorSelectorViewController.swift; sourceTree = "<group>"; };
C3E42AAF27F4D30E00546706 /* MenuItemsViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuItemsViewControllerTests.swift; sourceTree = "<group>"; };
C52812131832E071008931FD /* WordPress 13.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 13.xcdatamodel"; sourceTree = "<group>"; };
C533CF330E6D3ADA000C3DE8 /* CommentsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CommentsViewController.h; sourceTree = "<group>"; };
C533CF340E6D3ADA000C3DE8 /* CommentsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CommentsViewController.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -10362,6 +10366,7 @@
24C69AC12612467C00312D9A /* UserSettingsTestsObjc.m */,
2481B1E7260D4EAC00AE59DB /* WPAccount+LookupTests.swift */,
2481B20B260D8FED00AE59DB /* WPAccount+ObjCLookupTests.m */,
C38C5D8027F61D2C002F517E /* MenuItemTests.swift */,
);
name = Models;
sourceTree = "<group>";
Expand Down Expand Up @@ -13386,6 +13391,7 @@
325D3B3A23A8372500766DF6 /* Comments */,
BEC8A3FD1B4BAA08001CB8C3 /* Blog */,
E1B921BA1C0ED481003EA3CB /* Cells */,
C3E42AAD27F4D2CF00546706 /* Menus */,
8B7623352384372200AB3EE7 /* Pages */,
937E3AB41E3EBDC900CDA01A /* Post */,
FE3D057C26C3D5A1002A51B0 /* Sharing */,
Expand Down Expand Up @@ -13506,6 +13512,22 @@
path = Login;
sourceTree = "<group>";
};
C3E42AAD27F4D2CF00546706 /* Menus */ = {
isa = PBXGroup;
children = (
C3E42AAE27F4D2EC00546706 /* Controllers */,
);
path = Menus;
sourceTree = "<group>";
};
C3E42AAE27F4D2EC00546706 /* Controllers */ = {
isa = PBXGroup;
children = (
C3E42AAF27F4D30E00546706 /* MenuItemsViewControllerTests.swift */,
);
path = Controllers;
sourceTree = "<group>";
};
C533CF320E6D3AB3000C3DE8 /* Comments */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -19418,6 +19440,7 @@
8BE7C84123466927006EDE70 /* I18n.swift in Sources */,
806E53E427E01CFE0064315E /* DashboardStatsViewModelTests.swift in Sources */,
D88A649E208D82D2008AE9BC /* XCTestCase+Wait.swift in Sources */,
C38C5D8127F61D2C002F517E /* MenuItemTests.swift in Sources */,
E18549DB230FBFEF003C620E /* BlogServiceDeduplicationTests.swift in Sources */,
400A2C8F2217AD7F000A8A59 /* ClicksStatsRecordValueTests.swift in Sources */,
7320C8BD2190C9FC0082FED5 /* UITextView+SummaryTests.swift in Sources */,
Expand Down Expand Up @@ -19566,6 +19589,7 @@
321955C124BE4EBF00E3F316 /* ReaderSelectInterestsCoordinatorTests.swift in Sources */,
59B48B621B99E132008EBB84 /* JSONLoader.swift in Sources */,
3F82310F24564A870086E9B8 /* ReaderTabViewTests.swift in Sources */,
C3E42AB027F4D30E00546706 /* MenuItemsViewControllerTests.swift in Sources */,
D842EA4021FABB1800210E96 /* SiteSegmentTests.swift in Sources */,
436D55F5211632B700CEAA33 /* RegisterDomainDetailsViewModelTests.swift in Sources */,
E180BD4C1FB462FF00D0D781 /* CookieJarTests.swift in Sources */,
Expand Down
Loading

0 comments on commit 9dddadb

Please sign in to comment.