diff --git a/src/components/checkbox/checkbox-theme.scss b/src/components/checkbox/checkbox-theme.scss index 63b0f4ff7c9..de1f971d75c 100644 --- a/src/components/checkbox/checkbox-theme.scss +++ b/src/components/checkbox/checkbox-theme.scss @@ -1,5 +1,3 @@ - - md-checkbox.md-THEME_NAME-theme { .md-ripple { color: '{{accent-600}}'; diff --git a/src/components/checkbox/checkbox.js b/src/components/checkbox/checkbox.js index 6b2e0f1ffa1..66c19f541c0 100644 --- a/src/components/checkbox/checkbox.js +++ b/src/components/checkbox/checkbox.js @@ -27,7 +27,12 @@ angular * @param {string=} ng-change Angular expression to be executed when input changes due to user interaction with the input element. * @param {boolean=} md-no-ink Use of attribute indicates use of ripple ink effects * @param {string=} aria-label Adds label to checkbox for accessibility. - * Defaults to checkbox's text. If no default text is found, a warning will be logged. + * Defaults to checkbox's text. If no default text is found, a warning will be logged. + * @param {expression=} md-indeterminate This determines when the checkbox should be rendered as 'indeterminate'. + * If a truthy expression or no value is passed in the checkbox renders in the md-indeterminate state. + * If falsy expression is passed in it just looks like a normal unchecked checkbox. + * The indeterminate, checked, and unchecked states are mutually exclusive. A box cannot be in any two states at the same time. + * When a checkbox is indeterminate that overrides any checked/unchecked rendering logic. * * @usage * @@ -55,7 +60,7 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $ transclude: true, require: '?ngModel', priority: 210, // Run before ngAria - template: + template: '
' + '
' + '
' + @@ -69,6 +74,7 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $ function compile (tElement, tAttrs) { var container = tElement.children(); + var mdIndeterminateStateEnabled = tAttrs.hasOwnProperty('mdIndeterminate'); tAttrs.type = 'checkbox'; tAttrs.tabindex = tAttrs.tabindex || '0'; @@ -89,8 +95,13 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $ }); return function postLink(scope, element, attr, ngModelCtrl) { + var isIndeterminate; ngModelCtrl = ngModelCtrl || $mdUtil.fakeNgModel(); $mdTheming(element); + if (mdIndeterminateStateEnabled) { + setIndeterminateState(); + scope.$watch(attr.mdIndeterminate, setIndeterminateState); + } if (attr.ngChecked) { scope.$watch( @@ -156,6 +167,7 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $ listener(ev); } } + function listener(ev) { if (element[0].hasAttribute('disabled')) { return; @@ -171,12 +183,20 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $ } function render() { - if(ngModelCtrl.$viewValue) { + if(ngModelCtrl.$viewValue && !isIndeterminate) { element.addClass(CHECKED_CSS); } else { element.removeClass(CHECKED_CSS); } } + + function setIndeterminateState(newValue) { + isIndeterminate = newValue !== false; + if (isIndeterminate) { + element.attr('aria-checked', 'mixed'); + } + element.toggleClass('md-indeterminate', isIndeterminate); + } }; } } diff --git a/src/components/checkbox/checkbox.scss b/src/components/checkbox/checkbox.scss index bf4acd5a553..472dcd82186 100644 --- a/src/components/checkbox/checkbox.scss +++ b/src/components/checkbox/checkbox.scss @@ -9,7 +9,6 @@ $checkbox-margin: 16px !default; $checkbox-text-margin: 10px !default; $checkbox-top: 12px !default; - .md-inline-form { md-checkbox { margin: 19px 0 18px; diff --git a/src/components/checkbox/checkbox.spec.js b/src/components/checkbox/checkbox.spec.js index 5a2ab924433..4762840681c 100644 --- a/src/components/checkbox/checkbox.spec.js +++ b/src/components/checkbox/checkbox.spec.js @@ -1,6 +1,7 @@ describe('mdCheckbox', function() { var CHECKED_CSS = 'md-checked'; + var INDETERMINATE_CSS = 'md-indeterminate'; var $compile, $log, pageScope, $mdConstant; beforeEach(module('ngAria', 'material.components.checkbox')); @@ -246,5 +247,55 @@ describe('mdCheckbox', function() { expect(isChecked(checkbox)).toBe(false); expect(checkbox.hasClass('ng-invalid')).toBe(true); }); + + describe('with the md-indeterminate attribute', function() { + + it('should set md-indeterminate attr to true by default', function() { + var checkbox = compileAndLink(''); + + expect(checkbox).toHaveClass(INDETERMINATE_CSS); + }); + + it('should be set "md-indeterminate" class according to a passed in function', function() { + pageScope.isIndeterminate = function() { return true; }; + + var checkbox = compileAndLink(''); + + expect(checkbox).toHaveClass(INDETERMINATE_CSS); + }); + + it('should set aria-checked attr to "mixed"', function() { + var checkbox = compileAndLink(''); + + expect(checkbox.attr('aria-checked')).toEqual('mixed'); + }); + + it('should never have both the "md-indeterminate" and "md-checked" classes at the same time', function() { + pageScope.isChecked = function() { return true; }; + + var checkbox = compileAndLink(''); + + expect(checkbox).toHaveClass(INDETERMINATE_CSS); + expect(checkbox).not.toHaveClass(CHECKED_CSS); + }); + + it('should change from the indeterminate to checked state correctly', function() { + var checked = false; + pageScope.isChecked = function() { return checked; }; + pageScope.isIndet = function() { return !checked; }; + + var checkbox = compileAndLink(''); + + expect(checkbox).toHaveClass(INDETERMINATE_CSS); + expect(checkbox).not.toHaveClass(CHECKED_CSS); + + checked = true; + pageScope.$apply(); + + expect(checkbox).not.toHaveClass(INDETERMINATE_CSS); + expect(checkbox).toHaveClass(CHECKED_CSS); + }); + + }); }); }); diff --git a/src/components/checkbox/demoBasicUsage/index.html b/src/components/checkbox/demoBasicUsage/index.html index 35f6a1043d9..a8f56dcb46d 100644 --- a/src/components/checkbox/demoBasicUsage/index.html +++ b/src/components/checkbox/demoBasicUsage/index.html @@ -43,6 +43,18 @@ Checkbox (md-primary): No Ink +
+ + Checkbox: Indeterminate + +
+
+ + Checkbox: Disabled, Indeterminate + +
diff --git a/src/components/checkbox/demoSelectAll/index.html b/src/components/checkbox/demoSelectAll/index.html new file mode 100644 index 00000000000..7fb8322b1cc --- /dev/null +++ b/src/components/checkbox/demoSelectAll/index.html @@ -0,0 +1,29 @@ +
+
+
+
+ +
+ Using <md-checkbox> with the 'indeterminate' attribute +
+
+ + Un-Select All + +
+
+ + {{ item }} + +
+
+
+
+
+
diff --git a/src/components/checkbox/demoSelectAll/script.js b/src/components/checkbox/demoSelectAll/script.js new file mode 100644 index 00000000000..2e638c780df --- /dev/null +++ b/src/components/checkbox/demoSelectAll/script.js @@ -0,0 +1,37 @@ + +angular.module('checkboxDemo3', ['ngMaterial']) + +.controller('AppCtrl', function($scope) { + $scope.items = [1,2,3,4,5]; + $scope.selected = [1]; + $scope.toggle = function (item, list) { + var idx = list.indexOf(item); + if (idx > -1) { + list.splice(idx, 1); + } + else { + list.push(item); + } + }; + + $scope.exists = function (item, list) { + return list.indexOf(item) > -1; + }; + + $scope.isIndeterminate = function() { + return ($scope.selected.length !== 0 && + $scope.selected.length !== $scope.items.length); + }; + + $scope.isChecked = function() { + return $scope.selected.length === $scope.items.length; + }; + + $scope.toggleAll = function() { + if ($scope.selected.length === $scope.items.length) { + $scope.selected = []; + } else if ($scope.selected.length === 0 || $scope.selected.length > 0) { + $scope.selected = $scope.items.slice(0); + } + }; +}); diff --git a/src/components/checkbox/demoSelectAll/style.css b/src/components/checkbox/demoSelectAll/style.css new file mode 100644 index 00000000000..c26e656e05f --- /dev/null +++ b/src/components/checkbox/demoSelectAll/style.css @@ -0,0 +1,15 @@ +.demo { + &-legend { + color: #3F51B5; + } + + &-fieldset { + border-style: solid; + border-width: 1px; + height: 100%; + } + + &-select-all-checkboxes { + padding-left: 30px; + } +} diff --git a/src/components/checkbox/demoSyncing/script.js b/src/components/checkbox/demoSyncing/script.js index d36b666016a..348f37233d6 100644 --- a/src/components/checkbox/demoSyncing/script.js +++ b/src/components/checkbox/demoSyncing/script.js @@ -8,8 +8,12 @@ angular.module('checkboxDemo2', ['ngMaterial']) $scope.toggle = function (item, list) { var idx = list.indexOf(item); - if (idx > -1) list.splice(idx, 1); - else list.push(item); + if (idx > -1) { + list.splice(idx, 1); + } + else { + list.push(item); + } }; $scope.exists = function (item, list) { diff --git a/src/core/style/mixins.scss b/src/core/style/mixins.scss index 79cf87603dd..006e8161c0c 100644 --- a/src/core/style/mixins.scss +++ b/src/core/style/mixins.scss @@ -251,4 +251,21 @@ cursor: default; } + &.md-indeterminate ._md-icon { + &:after { + box-sizing: border-box; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: table; + width: $width * 0.6; + height: $border-width; + border-width: $border-width; + border-style: solid; + border-top: 0; + border-left: 0; + content: ''; + } + } } diff --git a/src/core/style/themes.scss b/src/core/style/themes.scss index d409f7020f8..6b0feb3e17b 100644 --- a/src/core/style/themes.scss +++ b/src/core/style/themes.scss @@ -39,4 +39,10 @@ html, body { &#{$checkedSelector} ._md-icon:after { border-color: '{{primary-contrast-0.87}}'; } + + & .md-indeterminate[disabled] { + ._md-container { + color: '{{foreground-3}}'; + } + } } diff --git a/test/angular-material-spec.js b/test/angular-material-spec.js index 7b6bbba7e2e..ec53caae643 100644 --- a/test/angular-material-spec.js +++ b/test/angular-material-spec.js @@ -8,8 +8,8 @@ (function() { - // Patch since PhantomJS does not implement click() on HTMLElement. In some - // cases we need to execute the native click on an element. However, jQuery's + // Patch since PhantomJS does not implement click() on HTMLElement. In some + // cases we need to execute the native click on an element. However, jQuery's // $.fn.click() does not dispatch to the native function on elements, so we // can't use it in our implementations: $el[0].click() to correctly dispatch. // Borrowed from https://stackoverflow.com/questions/15739263/phantomjs-click-an-element @@ -119,11 +119,17 @@ }; /** - * Add special matchers used in the Angular-Material specs - * + * Add special matchers used in the Angular-Material spec. */ jasmine.addMatchers({ + /** + * Asserts that an element has a given class name. + * Accepts any of: + * {string} - A CSS selector. + * {angular.JQLite} - The result of a jQuery query. + * {Element} - A DOM element. + */ toHaveClass: function() { return { compare: function(actual, expected) { @@ -131,7 +137,7 @@ var classes = expected.trim().split(/\s+/); for (var i = 0; i < classes.length; ++i) { - if (!angular.element(actual).hasClass(classes[i])) { + if (!getElement(actual).hasClass(classes[i])) { results.pass = false; } } @@ -141,7 +147,7 @@ results.message = ""; results.message += "Expected '"; results.message += angular.mock.dump(actual); - results.message += negation + "to have class '" + expected + "'."; + results.message += "'" + negation + "to have class '" + expected + "'."; return results; } @@ -194,10 +200,21 @@ return results; } }; - } + }, }); + /** + * Returns the angular element associated with a css selector or element. + * @param el {string|!angular.JQLite|!Element} + * @returns {!angular.JQLite} + */ + function getElement(el) { + var queryResult = angular.isString(el) ? + document.querySelector(el) : el; + return angular.element(queryResult); + } + }); })();