diff --git a/src/components/checkbox/checkbox-theme.scss b/src/components/checkbox/checkbox-theme.scss index afe6ebabab6..d9389e9871e 100644 --- a/src/components/checkbox/checkbox-theme.scss +++ b/src/components/checkbox/checkbox-theme.scss @@ -30,6 +30,12 @@ {$checkedSelector} ._md-icon:after { border-color: '{{primary-contrast-0.87}}'; } + + & [indeterminate][disabled] { + ._md-container { + color: '{{foreground-3}}'; + } + } } md-checkbox.md-THEME_NAME-theme { diff --git a/src/components/checkbox/checkbox.js b/src/components/checkbox/checkbox.js index 6b2e0f1ffa1..a30eebb96c7 100644 --- a/src/components/checkbox/checkbox.js +++ b/src/components/checkbox/checkbox.js @@ -27,6 +27,19 @@ 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. + * @param {boolean=} indeterminate Specifies that this checkbox can be rendered + * indeterminate. If true is passed in the checkbox renders in the indeterminate + * state. If no value is passed in this defaults to false. To use the other + * indeterminate-* attributes this attribute must be present. + * @param {expression=} indeterminate-when This determines when the checkbox + * should switch from an indeterminate to non-indeterminate state. + * @param {expression=} indeterminate-checked-when This determines when the + * indeterminate checkbox should appear as just a normal check box. If the + * checkbox has indeterminate="false" and is not checked, it appears as just a + * normal unchecked checkbox. + * @param {expression=} indeterminate-click This gets run when the + * indeterminate checkbox is clicked. This allows the user to trigger a function + * when an indeterminate checkbox is clicked. * Defaults to checkbox's text. If no default text is found, a warning will be logged. * * @usage @@ -69,6 +82,7 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $ function compile (tElement, tAttrs) { var container = tElement.children(); + var indeterminateStateEnabled = tElement[0].hasAttribute('indeterminate'); tAttrs.type = 'checkbox'; tAttrs.tabindex = tAttrs.tabindex || '0'; @@ -91,6 +105,13 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $ return function postLink(scope, element, attr, ngModelCtrl) { ngModelCtrl = ngModelCtrl || $mdUtil.fakeNgModel(); $mdTheming(element); + if (indeterminateStateEnabled) { + // Bootstrap initial state + setIndeterminateState(); + if (attr.indeterminateWhen) { + scope.$watch(attr.indeterminateWhen, setIndeterminateState); + } + } if (attr.ngChecked) { scope.$watch( @@ -156,11 +177,18 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $ listener(ev); } } + function listener(ev) { if (element[0].hasAttribute('disabled')) { return; } + if (indeterminateStateEnabled) { + if (attr.indeterminateClick) { + scope.$apply(element.attr('indeterminate-click')); + } + } + scope.$apply(function() { // Toggle the checkbox value... var viewValue = attr.ngChecked ? attr.checked : !ngModelCtrl.$viewValue; @@ -171,12 +199,48 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $ } function render() { + if (indeterminateStateEnabled) { + setIndeterminateState(); + // The checkbox should never be in an indeterminate="true" AND checked state. + // To prevent this, if the checkbox is in an indetermiante + // state, we skip the code below. + return; + } + if(ngModelCtrl.$viewValue) { element.addClass(CHECKED_CSS); } else { element.removeClass(CHECKED_CSS); } } + + function setIndeterminateState() { + var isIndeterminate = element.attr('indeterminate') === 'true' || false; + + if (attr.indeterminateWhen) { + isIndeterminate = scope.$eval(attr.indeterminateWhen); + } + + // We never want a checkbox to be checked AND indeterminate, they are + // mutually exclusive states. + if (attr.indeterminateCheckedWhen) { + var isChecked = scope.$eval(attr.indeterminateCheckedWhen); + if (isChecked) { + element.addClass(CHECKED_CSS); + isIndeterminate = false; + } else { + element.removeClass(CHECKED_CSS); + } + } + + if (isIndeterminate) { + element[0].indeterminate = true; + } else { + element[0].indeterminate = undefined; + } + + element.attr('indeterminate', isIndeterminate); + } }; } } diff --git a/src/components/checkbox/checkbox.scss b/src/components/checkbox/checkbox.scss index 11978ba1301..a64316b6959 100644 --- a/src/components/checkbox/checkbox.scss +++ b/src/components/checkbox/checkbox.scss @@ -105,6 +105,24 @@ $checkbox-top: 12px !default; cursor: default; } + &[indeterminate="true"] ._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: ''; + } + } + } .md-inline-form { diff --git a/src/components/checkbox/checkbox.spec.js b/src/components/checkbox/checkbox.spec.js index 5a2ab924433..3b718f51be2 100644 --- a/src/components/checkbox/checkbox.spec.js +++ b/src/components/checkbox/checkbox.spec.js @@ -246,5 +246,116 @@ describe('mdCheckbox', function() { expect(isChecked(checkbox)).toBe(false); expect(checkbox.hasClass('ng-invalid')).toBe(true); }); + + + describe('with the indeterminate attribute', function() { + + it('should set indeterminate attr to false by default', function() { + var element = compileAndLink( + '