Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve VoiceOver experience when reordering menus #18155

Merged
merged 14 commits into from
Apr 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -2159,9 +2159,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 @@ -6893,9 +6895,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 @@ -10347,6 +10351,7 @@
24C69AC12612467C00312D9A /* UserSettingsTestsObjc.m */,
2481B1E7260D4EAC00AE59DB /* WPAccount+LookupTests.swift */,
2481B20B260D8FED00AE59DB /* WPAccount+ObjCLookupTests.m */,
C38C5D8027F61D2C002F517E /* MenuItemTests.swift */,
);
name = Models;
sourceTree = "<group>";
Expand Down Expand Up @@ -13371,6 +13376,7 @@
325D3B3A23A8372500766DF6 /* Comments */,
BEC8A3FD1B4BAA08001CB8C3 /* Blog */,
E1B921BA1C0ED481003EA3CB /* Cells */,
C3E42AAD27F4D2CF00546706 /* Menus */,
8B7623352384372200AB3EE7 /* Pages */,
937E3AB41E3EBDC900CDA01A /* Post */,
FE3D057C26C3D5A1002A51B0 /* Sharing */,
Expand Down Expand Up @@ -13491,6 +13497,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 @@ -19400,6 +19422,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 @@ -19548,6 +19571,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