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

Commit

Permalink
Adding the indeterminate state to md-checkbox.
Browse files Browse the repository at this point in the history
  • Loading branch information
Derek Louie committed Mar 21, 2016
1 parent 9b1a4e3 commit 6c36b21
Show file tree
Hide file tree
Showing 11 changed files with 285 additions and 6 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
53 changes: 52 additions & 1 deletion src/components/checkbox/checkbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ 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 {expression=} 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 indeterminate state.
* If falsy expression is passed in it just looks like a normal unchecked checkbox.
* To use the other indeterminate-* attributes this attribute must be present.
* @param {expression=} indeterminate-checked-when This determines when the
* indeterminate checkbox should appear checked. The 'indeterminate' state is mutually
* exclusive with the 'checked' and 'unchecked' states.
* @param {expression=} indeterminate-click This gets triggered when the
* indeterminate checkbox is clicked.
* Defaults to checkbox's text. If no default text is found, a warning will be logged.
*
* @usage
Expand Down Expand Up @@ -55,7 +65,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 +79,7 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $

function compile (tElement, tAttrs) {
var container = tElement.children();
var indeterminateStateEnabled = tAttrs.hasOwnProperty('indeterminate');

tAttrs.type = 'checkbox';
tAttrs.tabindex = tAttrs.tabindex || '0';
Expand All @@ -91,6 +102,11 @@ 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();
scope.$watch(attr.indeterminate, setIndeterminateState);
}

if (attr.ngChecked) {
scope.$watch(
Expand Down Expand Up @@ -156,11 +172,18 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $
listener(ev);
}
}

function listener(ev) {
if (element[0].hasAttribute('disabled')) {
return;
}

if (indeterminateStateEnabled) {
if (attr.indeterminateClick) {
scope.$eval(attr.indeterminateClick);
}
}

scope.$apply(function() {
// Toggle the checkbox value...
var viewValue = attr.ngChecked ? attr.checked : !ngModelCtrl.$viewValue;
Expand All @@ -171,12 +194,40 @@ 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 = scope.$eval(attr.indeterminate) === false ? false : true;

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

element[0].indeterminate = isIndeterminate ? true : undefined;

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
111 changes: 111 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,115 @@ 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 true by default', function() {
var element = compileAndLink(
'<div>' +
'<md-checkbox indeterminate></md-checkbox>' +
'</div>');

var checkbox = element.find('md-checkbox');

pageScope.$apply();

expect(checkbox.hasClass(INDETERMINATE_CSS)).toBe(true);
});

it('should set its HTMLInputElement.indeterminate to be undefined when not indeterminate', function() {
var element = compileAndLink(
'<div>' +
'<md-checkbox indeterminate="false"></md-checkbox>' +
'</div>');

var checkbox = element.find('md-checkbox');

pageScope.$apply();

expect(checkbox[0].indeterminate).toBe(undefined);
});

it('should set its HTMLInputElement.indeterminate to be "true" when indeterminate', function() {
var element = compileAndLink(
'<div>' +
'<md-checkbox indeterminate></md-checkbox>' +
'</div>');

var checkbox = element.find('md-checkbox');

pageScope.$apply();

expect(checkbox[0].indeterminate).toBe(true);
});

it('should be set "md-indeterminate" class according to a passed in function', function() {

pageScope.isIndeterminate = function() { return true; } ;

var element = compileAndLink(
'<div>' +
'<md-checkbox indeterminate="isIndeterminate()"></md-checkbox>' +
'</div>');

var checkbox = element.find('md-checkbox');

pageScope.$apply();

expect(checkbox.hasClass(INDETERMINATE_CSS)).toBe(true);
});

it('should be set "md-checked" class according to the indeterminate-checked-when attr', function() {

pageScope.isChecked = function() { return true; };

var element = compileAndLink(
'<div>' +
'<md-checkbox indeterminate="false" indeterminate-checked-when="isChecked()"></md-checkbox>' +
'</div>');

var checkbox = element.find('md-checkbox');

pageScope.$apply();

expect(checkbox.hasClass(CHECKED_CSS)).toEqual(true);
});

it('should call the indeterminate-click function passed in when checkbox is clicked', function() {
var clickIndet = jasmine.createSpy('clickIndet');

pageScope.clickIndet = clickIndet;

var element = compileAndLink(
'<div>' +
'<md-checkbox indeterminate="true" indeterminate-click="clickIndet()"></md-checkbox>' +
'</div>');

var checkbox = element.find('md-checkbox');

checkbox.triggerHandler('click');

pageScope.$apply();

expect(clickIndet).toHaveBeenCalled();
});

it('should never have both the "md-indeterminate" and "md-checked" classes at the same time', function() {
pageScope.isChecked = function() { return true; };

var element = compileAndLink(
'<div>' +
'<md-checkbox indeterminate indeterminate-checked-when="isChecked()"></md-checkbox>' +
'</div>');

var checkbox = element.find('md-checkbox');

pageScope.$apply();
expect(checkbox.hasClass(INDETERMINATE_CSS)).toBe(false);
expect(checkbox[0].indeterminate).toBe(undefined);
expect(checkbox.hasClass(CHECKED_CSS)).toEqual(true);
});

});
});
});
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 indeterminate
aria-label="Checkbox Indeterminate" class="md-primary">
Checkbox: Indeterminate
</md-checkbox>
</div>
<div flex-xs flex="50">
<md-checkbox indeterminate aria-label="Checkbox Disabled Indeterminate"
ng-disabled="true" class="md-primary">
Checkbox: Disabled, Indeterminate
</md-checkbox>
</div>
</div>
</fieldset>

Expand Down
31 changes: 31 additions & 0 deletions src/components/checkbox/demoSelectAll/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<div ng-controller="AppCtrl" class="md-padding">
<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="standard" >
<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"
indeterminate="isIndeterminate()"
indeterminate-checked-when="isChecked()"
indeterminate-click="toggleAll()">
<span ng-if="isChecked()">Un-</span>Select All
</md-checkbox>
</div>
<div class="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);
}
};
});
13 changes: 13 additions & 0 deletions src/components/checkbox/demoSelectAll/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
legend {
color: #3F51B5;
}

fieldset.standard {
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}}';
}
}
}

0 comments on commit 6c36b21

Please sign in to comment.