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

Commit 2776ad2

Browse files
Derek LouieThomasBurleson
authored andcommitted
feat(checkbox): add indeterminate checkbox support
Closes #7643
1 parent 9a8eab0 commit 2776ad2

File tree

12 files changed

+220
-15
lines changed

12 files changed

+220
-15
lines changed

src/components/checkbox/checkbox-theme.scss

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
2-
31
md-checkbox.md-THEME_NAME-theme {
42
.md-ripple {
53
color: '{{accent-600}}';

src/components/checkbox/checkbox.js

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,12 @@ angular
2727
* @param {string=} ng-change Angular expression to be executed when input changes due to user interaction with the input element.
2828
* @param {boolean=} md-no-ink Use of attribute indicates use of ripple ink effects
2929
* @param {string=} aria-label Adds label to checkbox for accessibility.
30-
* Defaults to checkbox's text. If no default text is found, a warning will be logged.
30+
* Defaults to checkbox's text. If no default text is found, a warning will be logged.
31+
* @param {expression=} md-indeterminate This determines when the checkbox should be rendered as 'indeterminate'.
32+
* If a truthy expression or no value is passed in the checkbox renders in the md-indeterminate state.
33+
* If falsy expression is passed in it just looks like a normal unchecked checkbox.
34+
* The indeterminate, checked, and unchecked states are mutually exclusive. A box cannot be in any two states at the same time.
35+
* When a checkbox is indeterminate that overrides any checked/unchecked rendering logic.
3136
*
3237
* @usage
3338
* <hljs lang="html">
@@ -55,7 +60,7 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $
5560
transclude: true,
5661
require: '?ngModel',
5762
priority: 210, // Run before ngAria
58-
template:
63+
template:
5964
'<div class="_md-container" md-ink-ripple md-ink-ripple-checkbox>' +
6065
'<div class="_md-icon"></div>' +
6166
'</div>' +
@@ -69,6 +74,7 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $
6974

7075
function compile (tElement, tAttrs) {
7176
var container = tElement.children();
77+
var mdIndeterminateStateEnabled = tAttrs.hasOwnProperty('mdIndeterminate');
7278

7379
tAttrs.type = 'checkbox';
7480
tAttrs.tabindex = tAttrs.tabindex || '0';
@@ -89,8 +95,13 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $
8995
});
9096

9197
return function postLink(scope, element, attr, ngModelCtrl) {
98+
var isIndeterminate;
9299
ngModelCtrl = ngModelCtrl || $mdUtil.fakeNgModel();
93100
$mdTheming(element);
101+
if (mdIndeterminateStateEnabled) {
102+
setIndeterminateState();
103+
scope.$watch(attr.mdIndeterminate, setIndeterminateState);
104+
}
94105

95106
if (attr.ngChecked) {
96107
scope.$watch(
@@ -156,6 +167,7 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $
156167
listener(ev);
157168
}
158169
}
170+
159171
function listener(ev) {
160172
if (element[0].hasAttribute('disabled')) {
161173
return;
@@ -171,12 +183,20 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $
171183
}
172184

173185
function render() {
174-
if(ngModelCtrl.$viewValue) {
186+
if(ngModelCtrl.$viewValue && !isIndeterminate) {
175187
element.addClass(CHECKED_CSS);
176188
} else {
177189
element.removeClass(CHECKED_CSS);
178190
}
179191
}
192+
193+
function setIndeterminateState(newValue) {
194+
isIndeterminate = newValue !== false;
195+
if (isIndeterminate) {
196+
element.attr('aria-checked', 'mixed');
197+
}
198+
element.toggleClass('md-indeterminate', isIndeterminate);
199+
}
180200
};
181201
}
182202
}

src/components/checkbox/checkbox.scss

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ $checkbox-margin: 16px !default;
99
$checkbox-text-margin: 10px !default;
1010
$checkbox-top: 12px !default;
1111

12-
1312
.md-inline-form {
1413
md-checkbox {
1514
margin: 19px 0 18px;

src/components/checkbox/checkbox.spec.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11

22
describe('mdCheckbox', function() {
33
var CHECKED_CSS = 'md-checked';
4+
var INDETERMINATE_CSS = 'md-indeterminate';
45
var $compile, $log, pageScope, $mdConstant;
56

67
beforeEach(module('ngAria', 'material.components.checkbox'));
@@ -246,5 +247,55 @@ describe('mdCheckbox', function() {
246247
expect(isChecked(checkbox)).toBe(false);
247248
expect(checkbox.hasClass('ng-invalid')).toBe(true);
248249
});
250+
251+
describe('with the md-indeterminate attribute', function() {
252+
253+
it('should set md-indeterminate attr to true by default', function() {
254+
var checkbox = compileAndLink('<md-checkbox md-indeterminate></md-checkbox>');
255+
256+
expect(checkbox).toHaveClass(INDETERMINATE_CSS);
257+
});
258+
259+
it('should be set "md-indeterminate" class according to a passed in function', function() {
260+
pageScope.isIndeterminate = function() { return true; };
261+
262+
var checkbox = compileAndLink('<md-checkbox md-indeterminate="isIndeterminate()"></md-checkbox>');
263+
264+
expect(checkbox).toHaveClass(INDETERMINATE_CSS);
265+
});
266+
267+
it('should set aria-checked attr to "mixed"', function() {
268+
var checkbox = compileAndLink('<md-checkbox md-indeterminate></md-checkbox>');
269+
270+
expect(checkbox.attr('aria-checked')).toEqual('mixed');
271+
});
272+
273+
it('should never have both the "md-indeterminate" and "md-checked" classes at the same time', function() {
274+
pageScope.isChecked = function() { return true; };
275+
276+
var checkbox = compileAndLink('<md-checkbox md-indeterminate ng-checked="isChecked()"></md-checkbox>');
277+
278+
expect(checkbox).toHaveClass(INDETERMINATE_CSS);
279+
expect(checkbox).not.toHaveClass(CHECKED_CSS);
280+
});
281+
282+
it('should change from the indeterminate to checked state correctly', function() {
283+
var checked = false;
284+
pageScope.isChecked = function() { return checked; };
285+
pageScope.isIndet = function() { return !checked; };
286+
287+
var checkbox = compileAndLink('<md-checkbox md-indeterminate="isIndet()" ng-checked="isChecked()"></md-checkbox>');
288+
289+
expect(checkbox).toHaveClass(INDETERMINATE_CSS);
290+
expect(checkbox).not.toHaveClass(CHECKED_CSS);
291+
292+
checked = true;
293+
pageScope.$apply();
294+
295+
expect(checkbox).not.toHaveClass(INDETERMINATE_CSS);
296+
expect(checkbox).toHaveClass(CHECKED_CSS);
297+
});
298+
299+
});
249300
});
250301
});

src/components/checkbox/demoBasicUsage/index.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,18 @@
4343
Checkbox (md-primary): No Ink
4444
</md-checkbox>
4545
</div>
46+
<div flex-xs flex="50">
47+
<md-checkbox md-indeterminate
48+
aria-label="Checkbox Indeterminate" class="md-primary">
49+
Checkbox: Indeterminate
50+
</md-checkbox>
51+
</div>
52+
<div flex-xs flex="50">
53+
<md-checkbox md-indeterminate aria-label="Checkbox Disabled Indeterminate"
54+
ng-disabled="true" class="md-primary">
55+
Checkbox: Disabled, Indeterminate
56+
</md-checkbox>
57+
</div>
4658
</div>
4759
</fieldset>
4860

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<div ng-controller="AppCtrl" class="md-padding demo">
2+
<div layout="row" layout-wrap>
3+
<div flex="100" layout="column">
4+
<div>
5+
<!--
6+
In IE, we cannot apply flex directly to <fieldset>
7+
@see https://github.com/philipwalton/flexbugs#9-some-html-elements-cant-be-flex-containers
8+
-->
9+
<fieldset class="demo-fieldset" >
10+
<legend class="demo-legend">Using &lt;md-checkbox&gt; with the 'indeterminate' attribute </legend>
11+
<div layout="row" layout-wrap flex>
12+
<div flex-xs flex="50">
13+
<md-checkbox aria-label="Select All"
14+
ng-checked="isChecked()"
15+
md-indeterminate="isIndeterminate()"
16+
ng-click="toggleAll()">
17+
<span ng-if="isChecked()">Un-</span>Select All
18+
</md-checkbox>
19+
</div>
20+
<div class="demo-select-all-checkboxes" flex="100" ng-repeat="item in items">
21+
<md-checkbox ng-checked="exists(item, selected)" ng-click="toggle(item, selected)">
22+
{{ item }}
23+
</md-checkbox>
24+
</div>
25+
</div>
26+
</fieldset>
27+
</div>
28+
</div>
29+
</div>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
2+
angular.module('checkboxDemo3', ['ngMaterial'])
3+
4+
.controller('AppCtrl', function($scope) {
5+
$scope.items = [1,2,3,4,5];
6+
$scope.selected = [1];
7+
$scope.toggle = function (item, list) {
8+
var idx = list.indexOf(item);
9+
if (idx > -1) {
10+
list.splice(idx, 1);
11+
}
12+
else {
13+
list.push(item);
14+
}
15+
};
16+
17+
$scope.exists = function (item, list) {
18+
return list.indexOf(item) > -1;
19+
};
20+
21+
$scope.isIndeterminate = function() {
22+
return ($scope.selected.length !== 0 &&
23+
$scope.selected.length !== $scope.items.length);
24+
};
25+
26+
$scope.isChecked = function() {
27+
return $scope.selected.length === $scope.items.length;
28+
};
29+
30+
$scope.toggleAll = function() {
31+
if ($scope.selected.length === $scope.items.length) {
32+
$scope.selected = [];
33+
} else if ($scope.selected.length === 0 || $scope.selected.length > 0) {
34+
$scope.selected = $scope.items.slice(0);
35+
}
36+
};
37+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
.demo {
2+
&-legend {
3+
color: #3F51B5;
4+
}
5+
6+
&-fieldset {
7+
border-style: solid;
8+
border-width: 1px;
9+
height: 100%;
10+
}
11+
12+
&-select-all-checkboxes {
13+
padding-left: 30px;
14+
}
15+
}

src/components/checkbox/demoSyncing/script.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@ angular.module('checkboxDemo2', ['ngMaterial'])
88

99
$scope.toggle = function (item, list) {
1010
var idx = list.indexOf(item);
11-
if (idx > -1) list.splice(idx, 1);
12-
else list.push(item);
11+
if (idx > -1) {
12+
list.splice(idx, 1);
13+
}
14+
else {
15+
list.push(item);
16+
}
1317
};
1418

1519
$scope.exists = function (item, list) {

src/core/style/mixins.scss

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,4 +251,21 @@
251251
cursor: default;
252252
}
253253

254+
&.md-indeterminate ._md-icon {
255+
&:after {
256+
box-sizing: border-box;
257+
position: absolute;
258+
top: 50%;
259+
left: 50%;
260+
transform: translate(-50%, -50%);
261+
display: table;
262+
width: $width * 0.6;
263+
height: $border-width;
264+
border-width: $border-width;
265+
border-style: solid;
266+
border-top: 0;
267+
border-left: 0;
268+
content: '';
269+
}
270+
}
254271
}

0 commit comments

Comments
 (0)