Bug
A link (<a href="...">) nested inside an <md-checkbox ...> element should be recognized as a link by screen readers such as JAWS. This entails being accessible via screen reader specific navigation shortcuts (U and V keys for JAWS), and upon receiving focus it should be announced that the focused element is a link.
Such links are not accessible via shortcut keys, and are not announced as links.
https://codepen.io/anon/pen/VQdGBX
<a> inside a <md-checkbox> (such as the CodePen example linked above)A11y - Currently, a standard practice (placing links inside a checkbox label) results in an inaccessible page.
Angular 1.6
Material 1.1
Windows 10 Enterprize
Firefox 52.6.0 ESR
JAWS 2018 (1712 10 ILM), NVDA 2017.4
The HTML output by Angular results in the following accessibility tree:
checkbox "I agree to the privacy policy"
GenericContainer
link "privacy policy"
text "privacy policy"
I suspect a link typically should be inside a label, not nested directly within the checkbox.
Likely as a side effect of this, the text in the link is voiced twice by the screen reader. When there is no link, the content of the checkbox is "ignored" in the accessibility tree.
I enhanced the CodePen a bit for me to test with. This included adding a form and ng-model so that aria-checked would be set properly. Using ChromeVox on macOS, I don't really see any significant problems.
I will try to do some more testing using VoiceOver and Safari.
The issue does not manifest itself in ChromeVox (neither in your example nor in mine), but it does occur in JAWS (at least on Firefox on Windows).
We currently parse everything inside of the <md-checkbox>here</md-checkbox> into <span> elements with other HTML elements preserved.
I.e. <span>Please accept</span><a href="blah">privacy policy</a><span>.</span>
This seems like it would clearly cause some extra screen reader output.
My research into more a11y-friendly/normal implementations would use:
<label for="cb"><input id="cb" type="checkbox">Please accept the <a href="blah">privacy policy</a></label>. Links within label elements seem to be within the standards. Angular Material does this.
However moving to the label/native input method would be a breaking change. We're going to try to solve this by testing without the <span> elements or using aria attributes and see if we can come up with a non-breaking way of solving this.
Understood. I agree with the "normal implementation" you described, but understand that it would be a breaking change. Your proposed alternative sounds good to me, thanks!
I am able to reproduce this on macOS and Safari with Voiceover. When navigating by links, the privacy policy link within the checkbox is skipped.
The extra <span> elements that I was previously seeing appear to have been an artifact of CodePen as I don't see them in Chrome or Safari where the following is rendered:
<md-checkbox ng-model="data.cb1" tabindex="0" type="checkbox" role="checkbox"
class="ng-pristine ng-untouched ng-valid ng-not-empty md-checked"
aria-label="I agree to the privacy policy." aria-checked="true"
aria-invalid="false">
<div class="md-container md-ink-ripple" md-ink-ripple="" md-ink-ripple-checkbox="">
<div class="md-icon"></div>
</div>
<div ng-transclude="" class="md-label">
I agree to the <a href="https://www.google.com/"
ng-click="$event.stopPropagation()" class="ng-scope"> privacy policy</a>.
</div>
</md-checkbox>
Setting tabindex="0" on the md-checkbox is generally what we want. However, this means that any focus within the element must be handled by the element itself (normally via the arrow keys). That makes sense for menus and selects, but doesn't make much sense in this case. This also means that a link within this element is not focusable via tabs or screen reader next link navigation.
I tried a number of approaches with different combinations of tabindex as well as trying to forward focus to links within the checkbox's md-labelvia JavaScript, but none of them produced satisfactory results. I also looked at the available aria attributes and did not see anything that would help with this situation. I did try out the aria-role="link" attribute, but adding that to an <a> element doesn't help, or make much sense.
For the time being, the second example in my CodePen appears to be the best approach to doing this in an accesible way with md-checkbox. I.e. keep the link text out of the md-checkbox and use an appropriate aria-label.
As far as I've been able to determine, supporting accessible links within md-checkbox labels will require a rewrite of much of the component in order to use a native <input type="checkbox">. This would be a significant breaking change that would need to wait for 1.2.0.
It's also something that Angular Material (v2+) has already done.
Would a workaround with aria-labelledby solve this? E.g.
<div class="my-checkbox-label" id="agree-label">
I agree to the <a href="...">Terms of service</a>
</div>
<md-checkbox aria-labelledby="agree-label"></md-checkbox>
The following has the same visual layout and behavior while also working properly in Voiceover:
<md-checkbox ng-model="data.cb1" aria-labelledby="agree-label"></md-checkbox>
<span id="agree-label" onclick="data.cb1 = !data.cb1">
I agree to the <a href="https://www.google.com/">privacy policy</a>.
</span>
To make this part of the directive, we need to solve some problems
aria-labelledby. Can be done via $mdUtil.nextUid().<span> after the directive. Should this only happen if there is a link (probably not breaking) or always (breaking change)?I have the first draft of a PR up for this. To use a link in a checkbox label you would need to do the following:
<md-input-container>
<md-checkbox ng-model="data.cb5">
I agree to the <a href="/license">license</a>.
</md-checkbox>
</md-input-container>
If you want to omit the md-input-container, then you would be responsible for laying out the label and styling properly as we can't ensure that will happen when we don't have control over the parent element and it's layout. If the only issue is the md-input-container margins, then just overriding those specific styles for the element may be easiest.
As this change is a breaking change, I've pushed it to 1.2.0. More details in https://github.com/angular/material/pull/11154#issuecomment-377337921.