First foray into accessibility
30 November 2009 3:06 pm UTC
Recently I received feedback from a blind user complimenting Pagehand on its accessibility. This surprised me because accessibility is one of those things I intended to look into one day, and I had not done anything special to make it happen.
Curious, I put Pagehand through its paces in VoiceOver. The exercise left me horrified. Far from deserving praise from this user, I instead deserve severe disapprobation for neglect.
It took a while (and some great help from the accessibility mailing list) to figure out how to fix the worst of my transgressions. For the benefit of others who, like me, are a blank slate when it comes to accessibility support, I’d like to share what I’ve discovered so far.
First discovery: VoiceOver reads tooltips.
It turns out that I got a big jump on accessibility simply by filling in the Tool Tip fields in Interface Builder. VoiceOver reads tooltips as accessibility help tags. Without my doing anything special, VoiceOver users got the benefit of those tooltips:

Second discovery: VoiceOver sees things that we do not see.
The most surprising discovery I’ve made is that VoiceOver’s notion of what is visible is different from what is in fact visible on screen. Specifically, VoiceOver sends a -isHidden message to a NSView to determine whether the view is visible. It ignores the view’s frame. If you hide a view by setting its height to 0, or move it out sight, the view is invisible on the screen but fully visible to VoiceOver.
Pagehand uses simple proxy animations to open and close views. Controls are subviews of an NSView subclass named Palette, and the animation hides the Palette by setting its height to 0. Palettes are placed side by side in another NSView subclass named Sidebar, and Sidebars are made visible by adjusting the x origin of their superview, again using animation. The superview is the width of one Sidebar, so the Sidebars appear to slide horizontally into place.
This all looks fine on screen, but VoiceOver does not understand any of it. VoiceOver thinks all the Palettes in all the Sidebars are visible all the time.
I can think of three simple approaches for fixing this problem: send setHidden:YES at the end of animation that hides a Palette or Sidebar, and setHidden:NO at the beginning of all other animations; override setFrame: to send setHidden: as appropriate; or override setHidden: to look at the receiver’s frame and return YES if the Palette or Sidebar is completely hidden.
For now, I am using the third approach. I don’t know which is best.
Third discovery: You cannot supply accessibility text for an NSSegmentedControl in Interface Builder.
VoiceOver will read the items in an NSSegmentedControl as “radio button 2 of 4.” You might expect that the Accessibility Description field in Interface Builder would allow you to set the description for each cell. But the Description field applies to the entire segmented control, not to individual cells. You have to provide the accessibility description in code.
You can see how to do this in Apple’s ImageMapExample. To help keep the accessibility strings organized, I subclassed NSSegmentedControl and set a unique tag for each control in Interface Builder. Basically it’s just a copy-and-paste job from the example:
@implementation CCMAXSegmentedControl
// copied from ImageMapExampleController.m:
static void SetSegmentDescriptions(NSSegmentedControl *control, NSString *firstDescription, ...) {
// Use NSAccessibilityUnignoredDescendant to be sure we start with the correct object.
id segmentElement = NSAccessibilityUnignoredDescendant(control);
// Use the accessibility protocol to get the children.
NSArray *segments = [segmentElement accessibilityAttributeValue:NSAccessibilityChildrenAttribute];
va_list args;
va_start(args, firstDescription);
id segment;
NSString *description = firstDescription;
NSEnumerator *e = [segments objectEnumerator];
while ((segment = [e nextObject])) {
if (description != nil) {
[segment accessibilitySetOverrideValue:description forAttribute:NSAccessibilityDescriptionAttribute];
} else {
// Exit loop if we run out of descriptions.
break;
}
description = va_arg(args, id);
}
va_end(args);
}
- (void)awakeFromNib {
if ([self tag] == 2) { // paragraph alignment:
NSString *leftAlign = NSLocalizedStringFromTable(@"left align", @"AX", "left align");
NSString *centerAlign = NSLocalizedStringFromTable(@"center align", @"AX", "center align");
NSString *rightAlign = NSLocalizedStringFromTable(@"right align", @"AX", "right align");
NSString *justifyAlign = NSLocalizedStringFromTable(@"justify align", @"AX", "justify align");
SetSegmentDescriptions(self, leftAlign, centerAlign, rightAlign, justifyAlign, nil);
}
}
NSAccessibility reminds me of AppleScript: sometimes it’s hard to figure out what it wants, and you save a lot of time by just copying what someone else has done.
To summarize, I addressed the worst of my accessibility problems by:
1. Supplying tooltips and descriptions in Interface Builder;
2. Making sure that non-visible views return YES from -isHidden;
3. Writing code to provide accessibility descriptions for segmented controls.
This is just the beginning. Plenty of accessibility work lies ahead.
Leave a Comment
9 December 2009 7:19 pm
Makes me wonder (and appreciate) how any application ever gets finished to the point of user satisfaction. Keep up the good work.
John T