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

Commit

Permalink
feat(checkbox): add indeterminate checkbox support
Browse files Browse the repository at this point in the history
Closes #7643
  • Loading branch information
Derek Louie authored and ThomasBurleson committed Mar 30, 2016
1 parent 9a8eab0 commit 2776ad2
Show file tree
Hide file tree
Showing 12 changed files with 220 additions and 15 deletions.
2 changes: 0 additions & 2 deletions src/components/checkbox/checkbox-theme.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@


md-checkbox.md-THEME_NAME-theme {
.md-ripple {
color: '{{accent-600}}';
Expand Down
26 changes: 23 additions & 3 deletions src/components/checkbox/checkbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
* <hljs lang="html">
Expand Down Expand Up @@ -55,7 +60,7 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $
transclude: true,
require: '?ngModel',
priority: 210, // Run before ngAria
template:
template:
'<div class="_md-container" md-ink-ripple md-ink-ripple-checkbox>' +
'<div class="_md-icon"></div>' +
'</div>' +
Expand All @@ -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';
Expand All @@ -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(
Expand Down Expand Up @@ -156,6 +167,7 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $
listener(ev);
}
}

function listener(ev) {
if (element[0].hasAttribute('disabled')) {
return;
Expand All @@ -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);
}
};
}
}
1 change: 0 additions & 1 deletion src/components/checkbox/checkbox.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
51 changes: 51 additions & 0 deletions src/components/checkbox/checkbox.spec.js
Original file line number Diff line number Diff line change
@@ -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'));
Expand Down Expand Up @@ -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('<md-checkbox md-indeterminate></md-checkbox>');

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('<md-checkbox md-indeterminate="isIndeterminate()"></md-checkbox>');

expect(checkbox).toHaveClass(INDETERMINATE_CSS);
});

it('should set aria-checked attr to "mixed"', function() {
var checkbox = compileAndLink('<md-checkbox md-indeterminate></md-checkbox>');

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('<md-checkbox md-indeterminate ng-checked="isChecked()"></md-checkbox>');

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('<md-checkbox md-indeterminate="isIndet()" ng-checked="isChecked()"></md-checkbox>');

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);
});

});
});
});
12 changes: 12 additions & 0 deletions src/components/checkbox/demoBasicUsage/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@
Checkbox (md-primary): No Ink
</md-checkbox>
</div>
<div flex-xs flex="50">
<md-checkbox md-indeterminate
aria-label="Checkbox Indeterminate" class="md-primary">
Checkbox: Indeterminate
</md-checkbox>
</div>
<div flex-xs flex="50">
<md-checkbox md-indeterminate aria-label="Checkbox Disabled Indeterminate"
ng-disabled="true" class="md-primary">
Checkbox: Disabled, Indeterminate
</md-checkbox>
</div>
</div>
</fieldset>

Expand Down
29 changes: 29 additions & 0 deletions src/components/checkbox/demoSelectAll/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<div ng-controller="AppCtrl" class="md-padding demo">
<div layout="row" layout-wrap>
<div flex="100" layout="column">
<div>
<!--
In IE, we cannot apply flex directly to <fieldset>
@see https://github.com/philipwalton/flexbugs#9-some-html-elements-cant-be-flex-containers
-->
<fieldset class="demo-fieldset" >
<legend class="demo-legend">Using &lt;md-checkbox&gt; with the 'indeterminate' attribute </legend>
<div layout="row" layout-wrap flex>
<div flex-xs flex="50">
<md-checkbox aria-label="Select All"
ng-checked="isChecked()"
md-indeterminate="isIndeterminate()"
ng-click="toggleAll()">
<span ng-if="isChecked()">Un-</span>Select All
</md-checkbox>
</div>
<div class="demo-select-all-checkboxes" flex="100" ng-repeat="item in items">
<md-checkbox ng-checked="exists(item, selected)" ng-click="toggle(item, selected)">
{{ item }}
</md-checkbox>
</div>
</div>
</fieldset>
</div>
</div>
</div>
37 changes: 37 additions & 0 deletions src/components/checkbox/demoSelectAll/script.js
Original file line number Diff line number Diff line change
@@ -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);
}
};
});
15 changes: 15 additions & 0 deletions src/components/checkbox/demoSelectAll/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.demo {
&-legend {
color: #3F51B5;
}

&-fieldset {
border-style: solid;
border-width: 1px;
height: 100%;
}

&-select-all-checkboxes {
padding-left: 30px;
}
}
8 changes: 6 additions & 2 deletions src/components/checkbox/demoSyncing/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
17 changes: 17 additions & 0 deletions src/core/style/mixins.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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: '';
}
}
}
6 changes: 6 additions & 0 deletions src/core/style/themes.scss
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,10 @@ html, body {
&#{$checkedSelector} ._md-icon:after {
border-color: '{{primary-contrast-0.87}}';
}

& .md-indeterminate[disabled] {
._md-container {
color: '{{foreground-3}}';
}
}
}
31 changes: 24 additions & 7 deletions test/angular-material-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a> 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
Expand Down Expand Up @@ -119,19 +119,25 @@
};

/**
* 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) {
var results = {pass: true};
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;
}
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -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);
}

});

})();

0 comments on commit 2776ad2

Please sign in to comment.