This post discusses the importance of including the purpose of hyperlinks in the text shown on the link Itself. It also details specific challenges that can arise for customers using screen readers when they encounter some Win32 SysLink controls and WinForms LinkLabel controls, and suggests steps to avoid those challenges. The technical steps involved can also relate to enhancing the experience in other scenarios, and so any Win32 and WinForms dev will want to know about them.
Apology up-front: When I uploaded this post to the blog site, the images did not get uploaded with the alt text that I'd set on them. So any images are followed by a title.
Important: Now that you know what the post refers to, you might well be thinking:
"Thanks very much Barker. I wasted my time visiting this post because I now know it doesn't apply to me. Couldn't you have included the purpose of the post in the title?".
Well, yes, I could. And that's the point of the post. Learn more…
Introduction
Many products present hyperlinks which provide an efficient mechanism for their customers to learn more about some topic pertinent to the product. And often the text shown on the links is such that the purpose of the link is made clear, even if the link were considered in isolation, and not as part of whatever other UI happens to be nearby. For example, I'm writing this in Word 2016, and went to the Options UI where I found a section on "Office intelligent services". That section contains a brief passage on what "Office intelligent services" are, and then presents a link which shows "About intelligent services". That link provides access to a much more in-depth description of the topic.
However, many products choose not to include topic-specific text in the text shown on the link. For example, UI might contain topic-specific static text, and follow that text with a link that only shows "Learn more". For customers who consume the link text and the nearby static text almost concurrently, this provides an efficient experience. But what about all your other customers? How would you feel if some of your customers who encounter the link have to take additional steps to learn what the "Learn more" link relates to?
For example, consider the experience for your sighted customers who use a mouse. You probably wouldn't ship a "Learn more" link that had no static text presented nearby describing the purpose of the link, and only present that purpose when the mouse is hovered over the link. While technically the purpose of the link is accessible to those customers in that scenario, it would be an inefficient and unfriendly experience, and you probably wouldn't ship it. In fact, you'd probably not want to ship an inefficient and unfriendly experience to any of your customers.
So let's consider the experience you're delivering to your customers who are blind, and who are controlling the Narrator screen reader through use of a keyboard. Say you present some descriptive static text visually and follow it in that visual UI with a link that says "Learn more". When your customers tab to the link, Narrator will announce "Learn more, link". While that's accurate, is the announcement as helpful as it can be? Wouldn't it be more helpful for it to say "Learn more about towhees", (assuming the current topic relates to towhees). For the link to only say "Learn more", forces your customer to take action to explore the nearby UI and figure out what the link's referring to.
What's more, screen readers often have a way to present their own UI which contains only the set of links in your UI. This usually makes it very efficient for your customers to interact with those links. But if your UI contains a bunch of paragraphs, all ending with links that say "Learn more", then your customer can get presented with a set of links in the screen reader's UI, and they all say "Learn more". That's a frustrating experience which you'll not want to deliver to your customers.
So by default, I'd recommend you make all the text on your links unique, and sufficiently informative so that your customer knows what the link refers to. This will provide an efficient experience whether tabbing through the UI and encountering a link, or reviewing the set of links presented in the screen reader's own UI.
But say your UI includes a Win32 SysLink or a WinForms LinkLabel. Both of those provide a way for you to include a text string in your UI, and for a particular subset of that text to be considered the interactive link. By default, when your customers using Narrator encounter a link where the link text is a subset of the full text, the UI Automation (UIA) Name properties associated with the links do not include the full text on the control, and so the Narrator announcement is not as helpful as it could be for your customers. This could lead to confusion or frustration for your customers. So the details below describe action you could take in your app to provide a more efficient, and friendlier experience for your customers who encounter those types of Win32 or WinForms controls.
Important: Never change your UI based solely on the experience with the Narrator screen reader. To do so would run the risk of degrading the experience with other assistive technologies, or even future versions of Narrator. Instead base your changes on potential enhancements to the representation of the UI through the UIA API. The UIA representation should be a clean, logical match for the meaning of the UI.
The Win32 SysLink
Say your .rc file contains a SysLink in a dialog similar to that shown in the following example.
IDD_ABOUTBOX DIALOGEX 0, 0, 270, 62
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "About Grosbeak Translator"
FONT 8, "MS Shell Dlg", 0, 0, 0x1
BEGIN
ICON IDI_GROSBEAK, IDC_GROSBEAK_IMAGE,14,14,21,20
LTEXT "Grosbeak Translator, Version 3.1",IDC_STATIC,42,14,114,8,SS_NOPREFIX
CONTROL "Grosbeak translation services are available in your area. <a href=""http://microsoft.com"">Learn more</a>",IDC_GROSBEAK_LEARNMORE,"SysLink",WS_TABSTOP,42,26,220,8
DEFPUSHBUTTON "OK",IDOK,212,41,50,14,WS_GROUP
END
The following screenshot shows the visuals associated with the dialog when run.
Figure 1: A dialog box showing a SysLink control, whose subtext "Learn more" is presented as a link.
So next let's point the Inspect SDK tool to the dialog box. The screenshot below shows the hierarchy of the UIA elements associated with the SysLink. It reports that there's an element whose ControlType is Link, and whose Name is "Learn more". And it reports that this element is a child of an element whose ControlType is Pane, and whose Name is "Grosbeak translation services are available in your area. Learn more".
Figure 2: Inspect reporting the UIA hierarchy of the dialog containing the SysLink control.
What this means is as the customer using Narrator tabs around the UI, they'll encounter the link, and the announcement will be "Learn more, link". But learn more about what? This forces your customer to take additional action in order to learn the purpose of the link. This isn't the efficient experience I want to deliver to my customer.
Important: What we'll discuss below is how to customize the SysLink's UIA representation, such that a more efficient experience can be delivered. But it's not trivial to do that, and it would be great to avoid it if practical. So before taking any of that action, do consider whether it's practical to replace the use of the problematic SysLink with a link whose full, helpful text can be made the interactive link. For example, in the case above, make the interactive link have the text "Learn more about the grosbeak translations services available in your area". By doing that, your customer will know exactly what the link refers to when they tab to it, and you won't be spending time customizing the experience.
Ok, say for some reason you will be sticking with the SysLink with the subtext being the interactive part of the link. In this case we could consider customizing the Name of the Link element exposed through UIA to be the full SysLink text. While UIA Name properties should be concise, the full SysLink text doesn't seem too long to use as the Link's Name and would seem way more helpful if heard in isolation than "Learn more". In order to achieve this, we'll take two steps. The first is to get the text we want to set on the Link, and the second is to actually set it.
Getting the text of interest
We could add a new localized string containing exactly the text of interest, but as it happens the text we need is already available. It's the accessible name of the Pane element that contains the text. We can access that by calling AccessibleObjectFromWindow() with the SysLink hwnd, and then calling get_accName() to get the Name.
Setting the text on the Link
Having got the text, we can then call SetHwndPropStr() to set it on the Link element. However, we need to be very careful here to set it on the element we intended to. The hwnd involved with this UI is the SysLink hwnd, so we need to be careful not to set it on the Pane element, from which we just accessed the Name. To set the Name of the child Link element, I use the Inspect SDK tool again and note the LegacyIAccessible.ChildId property of that Link element. In this case the ChildId property has a value of 1, as shown in Figure 2 above. So I'll pass that value in with the call to SetHwndPropStr().
Note: I'm rarely interested in the LegacyIAccessible properties. I'm only interested in them here because the UI is Win32, and that was originally designed to support the legacy MSAA API rather than the UIA API.
By making the changes described above, I can then point Inspect to the Link and verify that its Name is now the more useful full SysLink text.
Figure 3: The Inspect SDK tool reporting that the Link element now has a UIA Name of "Grosbeak translation services are available in your area. Learn more".
And importantly, this means that when your customer tabs to the link, the announcement is "Grosbeak translation services are available in your area. Learn more". Your customer learns of the purpose of the link in an efficient manner, and so no more spending time trying to figure out its purpose.
Now, the above change alone is a helpful enhancement. But there is one more change we might consider. Now that the Link provides all the helpful information that the customer needs, the parent Pane element doesn't really serve much purpose. If your customers were to encounter that Pane element with the screen reader, then it would seem to be distracting duplicated information. So one additional change we could consider, would be to only expose that Pane element through the Raw view of the UIA tree. UIA has three views of its hierarchical tree. The Raw view contains all elements exposed through UIA. The Control view contains everything of interest to your customer, including all interactable controls and important static text. And there's also the Content view which I've never really paid much attention to. So if we expose the Pane element only through the Raw view, then the UIA tree that's exposed would seem cleaner.
The screenshot below shows the SysLink UI being represented only with a Link element through UIA's Control view.
Figure 4: Inspect reporting that the SysLink UI is now exposed through the UIA Control view as a single Link element.
We could consider taking this exploration even further. The UIA BoundingRectangle property of the Link element matches the location and size of the link part of the control. So a Narrator customer using touch input, would have to touch on that link part in order to hit the control. So perhaps we should set the BoundingRectangle property of the element to be the bounding rectangle of the parent Pane element. (I should say that I've never actually tried doing that.) But if we did that, a customer using the Windows Magnifier might tab to the Link, and find the magnified view change to bring the full SysLink into view, and that might not result in the keyboard focused link part being pulled into the view. This is an interesting consideration, but for now I'll leave the BoundingRectangle as it is.
Bonus tip: Since we're having so much fun polishing up the UIA representation, why not enhance the image element's representation while we're at it? The dialog contains an image of a grosbeak at a bird feeder. When customers using Narrator encounter the image, they'll hear "image" but not know what the image relates to. While the UI dev might feel that that's not an issue because the image really isn't that important, their customers won't know that. So rather than leaving your customers wondering if they're being blocked from important information being conveyed in the image, let's provide a more helpful experience. If the image was purely decorative, we could remove the UIA element from the Control view of the UIA tree in the same way that we did earlier with that outer Pane element. But in this case, let's say the image is conveying information that we want the customer to access, so we'll set an accurate, concise, unique and localized UIA Name property on the image, by calling SetHwndPropertyStr().
The screenshot below shows Inspect reporting the new UIA Name property on the image.
Figure 5: Inspect reporting that the UIA Name property of the previously nameless image, is now "A Grosbeak hanging on the side of a bird feeder".
Our original goal was to provide a more helpful experience when the customer using Narrator tabs to the link, and we achieved that by exposing a more helpful UIA Name property for the link. But having continued the exploration let's compare the announcements made by Narrator in response to the Capslock+W keyboard shortcut, relating to the main part of the dialog.
Before:
About Grosbeak Translator dialog,
image,
About Grosbeak Translator dialog,
Grosbeak Translator, Version 3.1,
Grosbeak translation services are available in your area. Learn more pane,
Learn more, link,
After:
About Grosbeak Translator dialog,
A Grosbeak hanging on the side of a bird feeder, image,
About Grosbeak Translator dialog,
Grosbeak Translator, Version 3.1,
Grosbeak translation services are available in your area. Learn more, link,
Overall this seems a more helpful experience.
The code I added to my Win32 project in order to make the changes described above is as follows:
// Near the top of the file...
#include <initguid.h>
#include "objbase.h"
#include "uiautomation.h"
IAccPropServices* _pAccPropServices = NULL;
// When the UI is created...
HRESULT hr = CoCreateInstance(
CLSID_AccPropServices,
nullptr,
CLSCTX_INPROC,
IID_PPV_ARGS(&_pAccPropServices));
if (SUCCEEDED(hr))
{
// Get the hwnd for the SysLink control.
HWND hWndSysLink = GetDlgItem(hDlg, IDC_GROSBEAK_LEARNMORE);
// Do not expose the Pane element through either UIA's Control view or Content view.
VARIANT varView;
varView.vt = VT_BOOL;
varView.boolVal = VARIANT_FALSE;
hr = _pAccPropServices->SetHwndProp(
hWndSysLink,
OBJID_CLIENT,
CHILDID_SELF,
IsControlElement_Property_GUID,
varView);
if (SUCCEEDED(hr))
{
hr = _pAccPropServices->SetHwndProp(
hWndSysLink,
OBJID_CLIENT,
CHILDID_SELF,
IsContentElement_Property_GUID,
varView);
}
// Get an IAccessible associated with the SysLink control.
IAccessible* pAcc = NULL;
hr = AccessibleObjectFromWindow(hWndSysLink, OBJID_CLIENT, IID_IAccessible, (LPVOID*)&pAcc);
if (SUCCEEDED(hr))
{
BSTR bstrName;
// Now get the Name property of the main element associated with the SysLink.
// In this situation, if will access the Name for the parent Pane element,
// and not the Name of the child Link element.
VARIANT varChild;
varChild.vt = VT_I4;
varChild.lVal = CHILDID_SELF;
hr = pAcc->get_accName(varChild, &bstrName);
if (SUCCEEDED(hr))
{
// Now set the Name on the child Link element.
hr = _pAccPropServices->SetHwndPropStr(
hWndSysLink,
OBJID_CLIENT,
1, // Pass in the LegacyIAccessible.ChildId for the child Link element.
Name_Property_GUID,
bstrName);
}
SysFreeString(bstrName);
// And finally, give the image a useful Name.
WCHAR szImageName[MAX_LOADSTRING];
LoadString(
hInst,
IDS_GROSBEAK_IMAGE,
szImageName,
ARRAYSIZE(szImageName));
hr = _pAccPropServices->SetHwndPropStr(
GetDlgItem(hDlg, IDC_GROSBEAK_IMAGE),
OBJID_CLIENT,
CHILDID_SELF,
Name_Property_GUID,
szImageName);
}
}
// When the UI is destroyed...
if (_pAccPropServices != nullptr)
{
HWND hWndSysLink = GetDlgItem(hDlg, IDC_GROSBEAK_LEARNMORE);
// Clear all the properties we set on the controls.
MSAAPROPID propsPane[] = {
IsControlElement_Property_GUID,
IsContentElement_Property_GUID};
_pAccPropServices->ClearHwndProps(
hWndSysLink,
OBJID_CLIENT,
CHILDID_SELF,
propsPane,
ARRAYSIZE(propsPane));
MSAAPROPID propName[] = {
Name_Property_GUID };
_pAccPropServices->ClearHwndProps(
hWndSysLink,
OBJID_CLIENT,
1, // Pass in the LegacyIAccessible.ChildId for the child Link element.
propName,
ARRAYSIZE(propName));
_pAccPropServices->ClearHwndProps(
GetDlgItem(hDlg, IDC_GROSBEAK_IMAGE),
OBJID_CLIENT,
CHILDID_SELF,
propName,
ARRAYSIZE(propName));
_pAccPropServices->Release();
_pAccPropServices = NULL;
}
WinForms
Say your WinForms UI contains a LinkLabel, and you've set its LinkArea property such that text showing "Learn more" at the end of the LinkLabel becomes the interactive link.
Figure 6: WinForms UI presenting a LinkLabel with its LinkArea property set such that the interactive link part only contains the text "Learn more".
Now let's point the Inspect SDK tool at the UI and consider how this is being represented through the UIA API. The screenshot below shows that there's a Link element, which is a child of a text element. This is a similar structure to the UIA representation of the Win32 SysLink discussed above, other than the outer element has a ControlType of Text rather than Pane, and very importantly, the Link element here has no UIA Name property at all.
Figure 7: Inspect reporting that the UIA Link element associated with the LinkLabel has no UIA Name.
This means that when the customer using Narrator tabs to the link, the announcement will only be "Link". Technically the customer could use Narrator's Scan or Item navigation modes to explore what other elements are near to the nameless link, and assume the link is associated with the descriptive Text element. But that's a really frustrating thing to force your customers to do, and so if we can avoid it, we should.
Note: You may be thinking at this point, "But this is a framework issue, why should we as app devs have to spend time accounting for that?". And if you are thinking that, I'd say that'd be a fair point. Who knows, perhaps the framework will be updated at some point to set some helpful UIA Name on the link, but in the meantime, your customers are going to be frustrated. So this is the sort of situation where I suggest at least considering whether it is practical to help your customers with an app-side change, and if it is, go for it!
Important: The first approach I'd consider here is to drop the use of the LinkArea in the LinkLabel. In the example above, perhaps the text on the LinkLabel could be changed to be "Learn more about the grosbeak translation services available in your area" and the LinkArea use removed. That would mean the UIA Name on the link is set from the full text, and would be announced when customers using Narrator tab to it. That really would seem the simplest way to help your customers here.
If for some reason you really want to stick to using the LinkArea, one approach to consider would be to present your own custom class, derived from LinkLabel. When an AccessibleObject for the custom class is requested by the system, you would provide a custom AccessibleObject, and that would provide whatever custom Name property you feel is most helpful. Sometimes when doing this sort of thing, it's really not much work because the base class of interest has an AccessibleObject which specifically relates to that class. But MSDN makes no mention of anything like a LinkLabelAccessibleObject at AccessibleObject Class, so I'll pick the closest available, the ControlAccessibleObject.
Because I'll be using an AccessibleObject which isn't specific to the LinkLabel, I will need to override a few more things in order to replicate the UIA representation of a LinkLabel with that of my custom class. Whenever I do this sort of thing, I point the Inspect SDK tool to a standard LinkLabel, and then to my custom LinkLabel-derived class, and update my class's AccessibleObject until the UIA representations of the two controls are in all the ways that matter, the same.
The screenshot below shows Inspect reporting that with my custom LinkLabel-derived class, the Link element now has the helpful UIA Name set from the entire LinkLabel text.
Figure 8: Inspect reporting that the UIA Link element now has a UIA Name of "Grosbeak translation services are available in your area. Learn more".
Interestingly, by doing this, the parent/child structure associated with the original LinkLabel has now been replaced with a single Link element. What's more, the UIA BoundingRectangle property of this element covers the entire text, which seems potentially useful for customers using Narrator with touch. This again raises the question of whether this is the best experience for a customer using Windows Magnifier and who tabs to the link, but overall, to have the Link now exposed with a helpful Name seems a big improvement.
Oh my goodness! The Order!
Whenever examining the UIA representation of a WinForms app, it's very important to consider the order in which the elements are exposed through the UIA tree. The order in which the elements get exposed is based on the order in which the controls are programmatically added to the form. So that's often not the same as the order in which the elements are visually laid out, and not the same as the tab order. And sure enough, the Inspect screenshot above shows that the order of the button, the link and the static text in the UIA tree, is the opposite of the visual, logical order. So if my customer uses Narrator's Scan or Item mode navigation to move "next" through the UI, Narrator will actually move backwards through it. This will not do, so following the steps described in the "Order of elements in the UIA tree" section of Considerations around the accessibility of a WinForms Store app, I updated the app to have the order of the elements in the UIA tree match that of the visual, logical order.
Figure 9: Inspect reporting that the order of the elements exposed through UIA now matches the logical, visual order.
The code I added to my WinForms project in order to make the changes described above is as follows:
// Create a custom class which will provide the helpful accessible name for the link...
public class LinkLabelWithCustomAccessibleName : LinkLabel
{
protected override AccessibleObject CreateAccessibilityInstance()
{
return new LinkLabelWithCustomAccessibleNameAccessibleObject(this);
}
public class LinkLabelWithCustomAccessibleNameAccessibleObject : ControlAccessibleObject
{
private LinkLabelWithCustomAccessibleName LinkLabelWithCustomAccessibleName;
public LinkLabelWithCustomAccessibleNameAccessibleObject(LinkLabelWithCustomAccessibleName owner) : base(owner)
{
this.LinkLabelWithCustomAccessibleName = owner;
}
// Set the accessible name of the link part of the LinkLabel to be the entire LinkLabel text.
public override string Name
{
get
{
return LinkLabelWithCustomAccessibleName.Text;
}
set
{
LinkLabelWithCustomAccessibleName.Text = value;
}
}
// The remainder of the action taken by the class is required to replicate the default
// accessibility of the LinkLabel.
public override AccessibleStates State
{
get
{
// Return a state that accounts for whether the LinkLabel currently has keyboard focus.
return AccessibleStates.Focusable | // The link is always keyboard focusable.
(this.LinkLabelWithCustomAccessibleName.Focused ? AccessibleStates.Focused : 0);
}
}
public override AccessibleRole Role
{
get
{
// The element has a control type of a link.
return AccessibleRole.Link;
}
}
// Activate the link when the default programmatic action is taken on the link.
public override void DoDefaultAction()
{
// Assume this LinkLabel does contain a link.
this.LinkLabelWithCustomAccessibleName.OnLinkClicked(
new LinkLabelLinkClickedEventArgs(this.LinkLabelWithCustomAccessibleName.Links[0]));
}
}
}
// Instantiate that class in the form designer, instead of a standard LinkLabel...
this.linkLabelWithCustomAccessibleName1 = new LinkLabelWithCustomAccessibleName();
// Make sure that when the controls are added to the form in the designer,
// the order in which they're added matches the visual, logical order...
this.Controls.Add(this.label1);
this.Controls.Add(this. linkLabelWithCustomAccessibleName1);
this.Controls.Add(this.buttonOK);
Summary
Wherever practical, an app dev needs to deliver an accessible experience to all their customers that's both efficient and intuitive, with as little dev work as possible. After all, we don't have enough time to do the things we need to do, so we can't be spending any time on anything that's not necessary. Ideally, you won't have to take the steps I described above to adjust the default experience when screen readers encounter your links. And often you can avoid such work by presenting links with unique text which describes the purpose of the link, and which doesn't involve the type of control which presents the link text as an imprecise subset of a larger string.
But if for whatever reason the default experience at your app isn't as your customers need it to be, consider using SetHwndProp() or SetHwndPropStr() to enhance the accessibility of your Win32 UI, or using a custom AccessibleObject for your WinForms UI. Note that the steps shown above apply to far more than just links. Many controls could be enhanced through this sort of action if you feel it would help your customers.
Learn more about enhancing the programmatic accessibility of your Win32 and WinForms apps
Guy