Skip to content
This repository was archived by the owner on Sep 5, 2024. It is now read-only.

Commit 79f3d96

Browse files
committed
fix(checkbox): handle links in transcluded label in an a11y-friendly way
don't toggle the checkbox on link actions don't output aria warnings when using aria-labelledby require md-input-container when using links in checkbox labels update license copyright year Fixes #11134
1 parent 6e46351 commit 79f3d96

File tree

6 files changed

+101
-4
lines changed

6 files changed

+101
-4
lines changed

docs/app/partials/license.tmpl.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<p>The MIT License</p>
44

55
<p>
6-
Copyright (c) 2014-2016 Google, Inc.
6+
Copyright (c) 2014-2018 Google, Inc.
77
<a href="http://angularjs.org">http://angularjs.org</a>
88
</p>
99

src/components/checkbox/checkbox.js

+27-3
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,24 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $
9797
var containerCtrl = ctrls[0];
9898
var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel();
9999
var formCtrl = ctrls[2];
100+
var labelHasLink = element.find('a').length > 0;
101+
102+
// The original component structure is not accessible when the checkbox's label contains a link.
103+
// In order to keep backwards compatibility, we're only changing the structure of the component
104+
// when we detect a link within the label. Using a span after the md-checkbox and attaching it
105+
// via aria-labelledby allows screen readers to find and work with the link within the label.
106+
if (labelHasLink) {
107+
var labelId = 'label-' + $mdUtil.nextUid();
108+
attr.$set('aria-labelledby', labelId);
109+
110+
var label = element.children()[1];
111+
var labelContents = label.innerHTML;
112+
label.remove();
113+
element.after('<span id="' + labelId + '" class="md-checkbox-link-label">' + labelContents + '</span>');
114+
// Make sure that clicking on the label still causes the checkbox to be toggled, when appropriate.
115+
var externalLabel = element.next();
116+
externalLabel.on('click', listener);
117+
}
100118

101119
if (containerCtrl) {
102120
var isErrorGetter = containerCtrl.isErrorGetter || function() {
@@ -133,7 +151,11 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $
133151
false: attr.tabindex
134152
});
135153

136-
$mdAria.expectWithText(element, 'aria-label');
154+
// Don't emit a warning when the label has a link within it. In that case we'll use
155+
// aria-labelledby to point to another span that should be read as the label.
156+
if (!labelHasLink) {
157+
$mdAria.expectWithText(element, 'aria-label');
158+
}
137159

138160
// Reuse the original input[type=checkbox] directive from AngularJS core.
139161
// This is a bit hacky as we need our own event listener and own render
@@ -177,8 +199,10 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $
177199

178200
function listener(ev) {
179201
// skipToggle boolean is used by the switch directive to prevent the click event
180-
// when releasing the drag. There will be always a click if releasing the drag over the checkbox
181-
if (element[0].hasAttribute('disabled') || scope.skipToggle) {
202+
// when releasing the drag. There will be always a click if releasing the drag over the checkbox.
203+
// If the click came from a link in the checkbox, don't toggle the value.
204+
// We want the link to be opened without changing the value in this case.
205+
if (element[0].hasAttribute('disabled') || scope.skipToggle || ev.target.tagName === 'A') {
182206
return;
183207
}
184208

src/components/checkbox/checkbox.scss

+15
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,18 @@ md-checkbox {
6868

6969
}
7070
}
71+
md-input-container .md-checkbox-link-label {
72+
box-sizing: border-box;
73+
position: relative;
74+
display: inline-block;
75+
vertical-align: middle;
76+
white-space: normal;
77+
user-select: text;
78+
cursor: pointer;
79+
// The span is actually after the checkbox in the DOM, but we need it to line up, so we move it up
80+
// while not introducing any breaking changes to existing styles.
81+
top: -21px;
82+
83+
@include rtl(margin-left, $checkbox-text-margin, 0);
84+
@include rtl(margin-right, 0, $checkbox-text-margin);
85+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<div ng-controller="AppCtrl" class="md-padding" ng-cloak>
2+
<div>
3+
<fieldset class="standard">
4+
<legend>Using Different Layouts and Labels</legend>
5+
<div layout="column">
6+
<md-checkbox ng-model="data.cb1">
7+
Default Checkbox and Label
8+
</md-checkbox>
9+
<md-checkbox ng-model="data.cb2">
10+
Dynamic Label: {{data.cb2 ? 'Checked' : 'Unchecked'}}
11+
</md-checkbox>
12+
<div>
13+
<!-- Extra work is needed by the developer to make this work, including a11y. -->
14+
<label for="label-in-front" ng-click="data.cb3 = !data.cb3"
15+
aria-hidden="true" tabindex="-1">
16+
Label in Front
17+
</label>
18+
<md-checkbox ng-model="data.cb3" id="label-in-front"
19+
aria-label="Label in Front">
20+
</md-checkbox>
21+
</div>
22+
<md-input-container>
23+
<md-checkbox ng-model="data.cb4">
24+
Checkbox in an md-input-container
25+
</md-checkbox>
26+
</md-input-container>
27+
<md-subheader>Checkbox with an accessible link in the label</md-subheader>
28+
<md-input-container>
29+
<md-checkbox ng-model="data.cb5">
30+
I agree to the <a href="/license">license</a>.
31+
</md-checkbox>
32+
</md-input-container>
33+
</div>
34+
</fieldset>
35+
</div>
36+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
angular.module('checkboxDemo1', ['ngMaterial'])
2+
3+
.controller('AppCtrl', function($scope) {
4+
$scope.data = {};
5+
$scope.data.cb1 = true;
6+
$scope.data.cb2 = true;
7+
$scope.data.cb3 = false;
8+
$scope.data.cb4 = false;
9+
$scope.data.cb5 = false;
10+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
fieldset.standard {
2+
border: 1px solid;
3+
}
4+
legend {
5+
color: #3F51B5;
6+
}
7+
label {
8+
cursor: pointer;
9+
margin-right: 10px;
10+
outline: none;
11+
user-select: none;
12+
}

0 commit comments

Comments
 (0)