diff --git a/src/components/checkbox/checkbox.js b/src/components/checkbox/checkbox.js index 6f803c061dd..7d0e3f02326 100644 --- a/src/components/checkbox/checkbox.js +++ b/src/components/checkbox/checkbox.js @@ -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() { @@ -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 @@ -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; } diff --git a/src/components/checkbox/checkbox.scss b/src/components/checkbox/checkbox.scss index cf31cb09708..47a0ef40b32 100644 --- a/src/components/checkbox/checkbox.scss +++ b/src/components/checkbox/checkbox.scss @@ -93,4 +93,20 @@ md-checkbox { } } -} \ No newline at end of file +} +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); +} diff --git a/src/components/checkbox/checkbox.spec.js b/src/components/checkbox/checkbox.spec.js index 2cc1d2548ae..bffd7f1cdc2 100644 --- a/src/components/checkbox/checkbox.spec.js +++ b/src/components/checkbox/checkbox.spec.js @@ -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( + '' + + 'I agree to the license.' + + ''); + + 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( '
' + diff --git a/src/components/checkbox/demoLabels/index.html b/src/components/checkbox/demoLabels/index.html new file mode 100644 index 00000000000..60f17ffc8d7 --- /dev/null +++ b/src/components/checkbox/demoLabels/index.html @@ -0,0 +1,38 @@ +
+
+
+ Using Different Layouts and Labels +
+
+ + Default Checkbox and Label + + + Dynamic Label: {{data.cb2 ? 'Checked' : 'Unchecked'}} + +
+
+ + + + +
+ + + Checkbox in an md-input-container + + + Checkbox with an accessible link in the label + + + I agree to the license. + + +
+
+
+
diff --git a/src/components/checkbox/demoLabels/script.js b/src/components/checkbox/demoLabels/script.js new file mode 100644 index 00000000000..706794ad1a1 --- /dev/null +++ b/src/components/checkbox/demoLabels/script.js @@ -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; +}); diff --git a/src/components/checkbox/demoLabels/style.css b/src/components/checkbox/demoLabels/style.css new file mode 100644 index 00000000000..c8c707a1a45 --- /dev/null +++ b/src/components/checkbox/demoLabels/style.css @@ -0,0 +1,12 @@ +fieldset.standard { + border: 1px solid; +} +legend { + color: #3F51B5; +} +label { + cursor: pointer; + margin-right: 10px; + user-select: none; + height: 16px; +}