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

fix(checkbox): handle links in transcluded label in an a11y-friendly way #11154

Merged
merged 1 commit into from
Jun 30, 2020
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
32 changes: 29 additions & 3 deletions src/components/checkbox/checkbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,26 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $
var containerCtrl = ctrls[0];
var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel();
var formCtrl = ctrls[2];
var labelHasLink = element.find('a').length > 0;

// The original component structure is not accessible when the checkbox's label contains a link.
// In order to keep backwards compatibility, we're only changing the structure of the component
// when we detect a link within the label. Using a span after the md-checkbox and attaching it
// via aria-labelledby allows screen readers to find and work with the link within the label.
if (labelHasLink) {
var labelId = 'label-' + $mdUtil.nextUid();
attr.$set('aria-labelledby', labelId);

var label = element.children()[1];
label.remove();
label.removeAttribute('ng-transclude');
label.className = 'md-checkbox-link-label';
label.setAttribute('id', labelId);
element.after(label);
// Make sure that clicking on the label still causes the checkbox to be toggled, when appropriate.
var externalLabel = element.next();
externalLabel.on('click', listener);
}

if (containerCtrl) {
var isErrorGetter = containerCtrl.isErrorGetter || function() {
Expand Down Expand Up @@ -136,7 +156,11 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $
false: attr.tabindex
});

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

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

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

Expand Down
18 changes: 17 additions & 1 deletion src/components/checkbox/checkbox.scss
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,20 @@ md-checkbox {
}

}
}
}
md-input-container .md-checkbox-link-label {
box-sizing: border-box;
position: relative;
display: inline-block;
vertical-align: middle;
white-space: normal;
user-select: text;
cursor: pointer;
// The span is actually after the checkbox in the DOM, but we need it to line up, so we move it up
// while not introducing any breaking changes to existing styles.
top: -21px;

// In this mode, the checkbox's width needs to be factored in as well.
@include rtl(margin-left, $checkbox-text-margin - $checkbox-width, 0);
@include rtl(margin-right, 0, $checkbox-text-margin - $checkbox-width);
}
15 changes: 15 additions & 0 deletions src/components/checkbox/checkbox.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,21 @@ describe('mdCheckbox', function() {
expect(checkboxElement.attr('aria-label')).toBe('Some text');
});

it('should handle text content that contains a link', function() {
var element = compileAndLink(
'<md-input-container>' +
'<md-checkbox ng-model="blue">I agree to the <a href="/license">license</a>.</md-checkbox>' +
'</md-input-container>');

var checkboxElement = element.find('md-checkbox').eq(0);
expect(checkboxElement.attr('aria-labelledby')).toContain('label-');
var labelElement = element.children()[1];
expect(labelElement.getAttribute('id')).toContain('label-');
expect(labelElement.innerHTML).toContain('I agree to the ');
var linkElement = element.find('A').eq(0);
expect(linkElement[0].innerHTML).toBe('license');
});

it('should set checked css class and aria-checked attributes', function() {
var element = compileAndLink(
'<div>' +
Expand Down
38 changes: 38 additions & 0 deletions src/components/checkbox/demoLabels/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<div ng-controller="AppCtrl" class="md-padding" ng-cloak>
<div>
<fieldset class="standard">
<legend>Using Different Layouts and Labels</legend>
<div layout="column">
<div class="md-dense" layout="column">
<md-checkbox ng-model="data.cb1">
Default Checkbox and Label
</md-checkbox>
<md-checkbox ng-model="data.cb2">
Dynamic Label: {{data.cb2 ? 'Checked' : 'Unchecked'}}
</md-checkbox>
</div>
<div layout="row" layout-align="start center" class="md-dense">
<!-- Extra work is needed by the developer to make this work, including a11y. -->
<label for="label-in-front" ng-click="data.cb3 = !data.cb3"
aria-hidden="true" tabindex="-1">
Label in Front
</label>
<md-checkbox ng-model="data.cb3" id="label-in-front"
aria-label="Label in Front">
</md-checkbox>
</div>
<md-input-container>
<md-checkbox ng-model="data.cb4">
Checkbox in an md-input-container
</md-checkbox>
</md-input-container>
<md-subheader>Checkbox with an accessible link in the label</md-subheader>
<md-input-container>
<md-checkbox ng-model="data.cb5">
I agree to the <a href="/license">license</a>.
</md-checkbox>
</md-input-container>
</div>
</fieldset>
</div>
</div>
10 changes: 10 additions & 0 deletions src/components/checkbox/demoLabels/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
angular.module('checkboxDemo1', ['ngMaterial'])

.controller('AppCtrl', function($scope) {
$scope.data = {};
$scope.data.cb1 = true;
$scope.data.cb2 = true;
$scope.data.cb3 = false;
$scope.data.cb4 = false;
$scope.data.cb5 = false;
});
12 changes: 12 additions & 0 deletions src/components/checkbox/demoLabels/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
fieldset.standard {
border: 1px solid;
}
legend {
color: #3F51B5;
}
label {
cursor: pointer;
margin-right: 10px;
user-select: none;
height: 16px;
}