mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(md-checkbox): Implement md-checkbox.
This commit adds the `md-checkbox` component to the material2 repo. It includes unit tests and demos for the component. Note that while there is a demo which exhibits all functionality of the component, the "Nested Checklist" demo is still a WIP. Fixes #35 Closes #131
This commit is contained in:
committed by
Jeremy Elbourn
parent
f513be83b4
commit
431141a827
@@ -0,0 +1,22 @@
|
||||
<div class="md-checkbox-layout">
|
||||
<div class="md-checkbox-inner-container">
|
||||
<div class="md-checkbox-frame"></div>
|
||||
<div class="md-checkbox-background">
|
||||
<svg version="1.1"
|
||||
class="md-checkbox-checkmark"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
xml:space="preserve">
|
||||
<path class="md-checkbox-checkmark-path"
|
||||
fill="none"
|
||||
stroke="white"
|
||||
d="M4.1,12.7 9,17.6 20.3,6.3"/>
|
||||
</svg>
|
||||
<!-- Element for rendering the indeterminate state checkbox. -->
|
||||
<div class="md-checkbox-mixedmark"></div>
|
||||
</div>
|
||||
</div>
|
||||
<label [id]="labelId">
|
||||
<ng-content></ng-content>
|
||||
</label>
|
||||
</div>
|
||||
@@ -0,0 +1,460 @@
|
||||
@import "default-theme";
|
||||
@import "theme-functions";
|
||||
@import "variables";
|
||||
|
||||
/** The width/height of the checkbox element. */
|
||||
$md-checkbox-size: 18px !default;
|
||||
/** The width of the line used to draw the checkmark / mixedmark. */
|
||||
$md-checkbox-mark-stroke-size: 2/15 * $md-checkbox-size !default;
|
||||
/** The width of the checkbox border shown when the checkbox is unchecked. */
|
||||
$md-checkbox-border-width: 2px;
|
||||
/** The color of the checkbox border. */
|
||||
$md-checkbox-border-color: if($md-is-dark-theme, rgba(white, 0.7), rgba(black, 0.54)) !default;
|
||||
/** The color of the checkbox's checkmark / mixedmark. */
|
||||
$md-checkbox-mark-color: md-color($md-background, background);
|
||||
/** The color that is used as the checkbox background when it is checked. */
|
||||
$md-checkbox-background-color: md-color($md-primary, 500);
|
||||
/** The base duration used for the majority of transitions for the checkbox. */
|
||||
$md-checkbox-transition-duration: 140ms;
|
||||
/** The amount of spacing between the checkbox and its label. */
|
||||
$md-checkbox-item-spacing: 4px;
|
||||
|
||||
// Manual calculation done on SVG
|
||||
$_md-checkbox-mark-path-length: 22.910259;
|
||||
$_md-checkbox-indeterminate-checked-easing-function: cubic-bezier(0.14, 0, 0, 1);
|
||||
|
||||
/**
|
||||
* Fades in the background of the checkbox when it goes from unchecked -> {checked,indeterminate}.
|
||||
*/
|
||||
@keyframes md-checkbox-fade-in-background {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fades out the background of the checkbox when it goes from {checked,indeterminate} -> unchecked.
|
||||
*/
|
||||
@keyframes md-checkbox-fade-out-background {
|
||||
0%, 50% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* "Draws" in the checkmark when the checkbox goes from unchecked -> checked.
|
||||
*/
|
||||
@keyframes md-checkbox-unchecked-checked-checkmark-path {
|
||||
0%, 50% {
|
||||
stroke-dashoffset: $_md-checkbox-mark-path-length;
|
||||
}
|
||||
|
||||
50% {
|
||||
animation-timing-function: $md-linear-out-slow-in-timing-function;
|
||||
}
|
||||
|
||||
100% {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Horizontally expands the mixedmark when the checkbox goes from unchecked -> indeterminate.
|
||||
*/
|
||||
@keyframes md-checkbox-unchecked-indeterminate-mixedmark {
|
||||
0%, 68.2% {
|
||||
transform: scaleX(0);
|
||||
}
|
||||
|
||||
68.2% {
|
||||
animation-timing-function: cubic-bezier(0, 0, 0, 1);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* "Erases" the checkmark when the checkbox goes from checked -> unchecked.
|
||||
*/
|
||||
@keyframes md-checkbox-checked-unchecked-checkmark-path {
|
||||
from {
|
||||
animation-timing-function: $md-fast-out-linear-in-timing-function;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
stroke-dashoffset: $_md-checkbox-mark-path-length * -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates and fades out the checkmark when the checkbox goes from checked -> indeterminate. This
|
||||
* animation helps provide the illusion of the checkmark "morphing" into the mixedmark.
|
||||
*/
|
||||
@keyframes md-checkbox-checked-indeterminate-checkmark {
|
||||
from {
|
||||
animation-timing-function: $md-linear-out-slow-in-timing-function;
|
||||
opacity: 1;
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates and fades the checkmark back into position when the checkbox goes from indeterminate ->
|
||||
* checked. This animation helps provide the illusion that the mixedmark is "morphing" into the
|
||||
* checkmark.
|
||||
*/
|
||||
@keyframes md-checkbox-indeterminate-checked-checkmark {
|
||||
from {
|
||||
animation-timing-function: $_md-checkbox-indeterminate-checked-easing-function;
|
||||
opacity: 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates and fades in the mixedmark when the checkbox goes from checked -> indeterminate. This
|
||||
* animation, similar to md-checkbox-checked-indeterminate-checkmark, helps provide an illusion
|
||||
* of "morphing" from checkmark -> mixedmark.
|
||||
*/
|
||||
@keyframes md-checkbox-checked-indeterminate-mixedmark {
|
||||
from {
|
||||
animation-timing-function: $md-linear-out-slow-in-timing-function;
|
||||
opacity: 0;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates and fades out the mixedmark when the checkbox goes from indeterminate -> checked. This
|
||||
* animation, similar to md-checkbox-indeterminate-checked-checkmark, helps provide an illusion
|
||||
* of "morphing" from mixedmark -> checkmark.
|
||||
*/
|
||||
@keyframes md-checkbox-indeterminate-checked-mixedmark {
|
||||
from {
|
||||
animation-timing-function: $_md-checkbox-indeterminate-checked-easing-function;
|
||||
opacity: 1;
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: rotate(315deg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Horizontally collapses and fades out the mixedmark when the checkbox goes from indeterminate ->
|
||||
* unchecked.
|
||||
*/
|
||||
@keyframes md-checkbox-indeterminate-unchecked-mixedmark {
|
||||
0% {
|
||||
animation-timing-function: linear;
|
||||
opacity: 1;
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
32.8%, 100% {
|
||||
opacity: 0;
|
||||
transform: scaleX(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applied to elements that cover the checkbox's entire inner container.
|
||||
*/
|
||||
%md-checkbox-cover-element {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applied to elements that are considered "marks" within the checkbox, e.g. the checkmark and
|
||||
* the mixedmark.
|
||||
*/
|
||||
%md-checkbox-mark {
|
||||
$width-padding-inset: 2 * $md-checkbox-border-width;
|
||||
|
||||
width: calc(100% - #{$width-padding-inset});
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applied to elements that appear to make up the outer box of the checkmark, such as the frame
|
||||
* that contains the border and the actual background element that contains the marks.
|
||||
*/
|
||||
%md-checkbox-outer-box {
|
||||
@extend %md-checkbox-cover-element;
|
||||
border-radius: 2px;
|
||||
box-sizing: border-box;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.md-checkbox {
|
||||
cursor: pointer;
|
||||
|
||||
&:focus {
|
||||
// TODO(traviskaufman): Add ink ripple on focus state, once ripple is implemented.
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.md-checkbox-layout {
|
||||
align-items: baseline;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.md-checkbox-inner-container {
|
||||
display: inline-block;
|
||||
height: $md-checkbox-size;
|
||||
line-height: 0;
|
||||
margin: auto;
|
||||
margin-right: $md-checkbox-item-spacing;
|
||||
order: 0;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
width: $md-checkbox-size;
|
||||
|
||||
[dir="rtl"] & {
|
||||
margin:{
|
||||
left: $md-checkbox-item-spacing;
|
||||
right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.md-checkbox-frame {
|
||||
@extend %md-checkbox-outer-box;
|
||||
|
||||
background-color: transparent;
|
||||
border: $md-checkbox-border-width solid $md-checkbox-border-color;
|
||||
transition: border-color $md-checkbox-transition-duration $md-linear-out-slow-in-timing-function;
|
||||
will-change: border-color;
|
||||
}
|
||||
|
||||
.md-checkbox-background {
|
||||
@extend %md-checkbox-outer-box;
|
||||
|
||||
align-items: center;
|
||||
background-color: $md-checkbox-background-color;
|
||||
opacity: 0;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
transition: background-color $md-checkbox-transition-duration
|
||||
$md-linear-out-slow-in-timing-function,
|
||||
opacity $md-checkbox-transition-duration $md-linear-out-slow-in-timing-function;
|
||||
will-change: background-color, opacity;
|
||||
}
|
||||
|
||||
.md-checkbox-checkmark {
|
||||
@extend %md-checkbox-cover-element;
|
||||
@extend %md-checkbox-mark;
|
||||
|
||||
fill: $md-checkbox-mark-color;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.md-checkbox-checkmark-path {
|
||||
// !important is needed here because a stroke must be set as an attribute on the SVG in order
|
||||
// for line animation to work properly.
|
||||
stroke: $md-checkbox-mark-color !important;
|
||||
stroke: {
|
||||
dashoffset: $_md-checkbox-mark-path-length;
|
||||
dasharray: $_md-checkbox-mark-path-length;
|
||||
width: $md-checkbox-mark-stroke-size;
|
||||
}
|
||||
}
|
||||
|
||||
.md-checkbox-mixedmark {
|
||||
@extend %md-checkbox-mark;
|
||||
|
||||
background-color: $md-checkbox-mark-color;
|
||||
height: $md-checkbox-mark-stroke-size;
|
||||
opacity: 0;
|
||||
transform: scaleX(0) rotate(0deg);
|
||||
}
|
||||
|
||||
.md-checkbox-align-end {
|
||||
.md-checkbox-inner-container {
|
||||
order: 1;
|
||||
margin: {
|
||||
left: $md-checkbox-item-spacing;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
[dir="rtl"] & {
|
||||
margin: {
|
||||
left: auto;
|
||||
right: $md-checkbox-item-spacing;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.md-checkbox-checked {
|
||||
.md-checkbox-checkmark {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.md-checkbox-checkmark-path {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
|
||||
.md-checkbox-mixedmark {
|
||||
transform: scaleX(1) rotate(-45deg);
|
||||
}
|
||||
|
||||
.md-checkbox-background {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.md-checkbox-indeterminate {
|
||||
.md-checkbox-background {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.md-checkbox-checkmark {
|
||||
opacity: 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.md-checkbox-checkmark-path {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
|
||||
.md-checkbox-mixedmark {
|
||||
opacity: 1;
|
||||
transform: scaleX(1) rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
.md-checkbox-disabled {
|
||||
// NOTE(traviskaufman): While the spec calls for translucent blacks/whites for disabled colors,
|
||||
// this does not work well with elements layered on top of one another. To get around this we
|
||||
// blend the colors together based on the base color and the theme background.
|
||||
$white-30pct-opacity-on-dark-theme: #686868;
|
||||
$black-26pct-opacity-on-light-theme: #b0b0b0;
|
||||
$disabled-color: if(
|
||||
$md-is-dark-theme, $white-30pct-opacity-on-dark-theme, $black-26pct-opacity-on-light-theme);
|
||||
|
||||
cursor: default;
|
||||
|
||||
&.md-checkbox-checked,
|
||||
&.md-checkbox-indeterminate {
|
||||
.md-checkbox-background {
|
||||
background-color: $disabled-color;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.md-checkbox-checked) {
|
||||
.md-checkbox-frame {
|
||||
border-color: $disabled-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.md-checkbox-anim {
|
||||
$indeterminate-change-duration: 500ms;
|
||||
|
||||
&-unchecked-checked {
|
||||
.md-checkbox-background {
|
||||
animation: $md-checkbox-transition-duration * 2 linear 0s md-checkbox-fade-in-background;
|
||||
}
|
||||
|
||||
.md-checkbox-checkmark-path {
|
||||
// Instead of delaying the animation, we simply multiply its length by 2 and begin the
|
||||
// animation at 50% in order to prevent a flash of styles applied to a checked checkmark
|
||||
// as the background is fading in before the animation begins.
|
||||
animation:
|
||||
$md-checkbox-transition-duration * 2 linear 0s md-checkbox-unchecked-checked-checkmark-path;
|
||||
}
|
||||
}
|
||||
|
||||
&-unchecked-indeterminate {
|
||||
.md-checkbox-background {
|
||||
animation: $md-checkbox-transition-duration * 2 linear 0s md-checkbox-fade-in-background;
|
||||
}
|
||||
|
||||
.md-checkbox-mixedmark {
|
||||
animation:
|
||||
$md-checkbox-transition-duration linear 0s md-checkbox-unchecked-indeterminate-mixedmark;
|
||||
}
|
||||
}
|
||||
|
||||
&-checked-unchecked {
|
||||
.md-checkbox-background {
|
||||
animation: $md-checkbox-transition-duration * 2 linear 0s md-checkbox-fade-out-background;
|
||||
}
|
||||
|
||||
.md-checkbox-checkmark-path {
|
||||
animation:
|
||||
$md-checkbox-transition-duration linear 0s md-checkbox-checked-unchecked-checkmark-path;
|
||||
}
|
||||
}
|
||||
|
||||
&-checked-indeterminate {
|
||||
.md-checkbox-checkmark {
|
||||
animation:
|
||||
$md-checkbox-transition-duration linear 0s md-checkbox-checked-indeterminate-checkmark;
|
||||
}
|
||||
|
||||
.md-checkbox-mixedmark {
|
||||
animation:
|
||||
$md-checkbox-transition-duration linear 0s md-checkbox-checked-indeterminate-mixedmark;
|
||||
}
|
||||
}
|
||||
|
||||
&-indeterminate-checked {
|
||||
.md-checkbox-checkmark {
|
||||
animation:
|
||||
$indeterminate-change-duration linear 0s md-checkbox-indeterminate-checked-checkmark;
|
||||
}
|
||||
|
||||
.md-checkbox-mixedmark {
|
||||
animation:
|
||||
$indeterminate-change-duration linear 0s md-checkbox-indeterminate-checked-mixedmark;
|
||||
}
|
||||
}
|
||||
|
||||
&-indeterminate-unchecked {
|
||||
.md-checkbox-background {
|
||||
animation: $md-checkbox-transition-duration * 2 linear 0s md-checkbox-fade-out-background;
|
||||
}
|
||||
|
||||
.md-checkbox-mixedmark {
|
||||
animation:
|
||||
$indeterminate-change-duration * 0.6 linear 0s
|
||||
md-checkbox-indeterminate-unchecked-mixedmark;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,733 @@
|
||||
import {
|
||||
fakeAsync,
|
||||
inject,
|
||||
tick,
|
||||
ComponentFixture,
|
||||
TestComponentBuilder,
|
||||
} from 'angular2/testing';
|
||||
|
||||
import {
|
||||
it,
|
||||
iit,
|
||||
describe,
|
||||
ddescribe,
|
||||
expect,
|
||||
beforeEach,
|
||||
} from '../../core/facade/testing';
|
||||
|
||||
import {Component, DebugElement, EventEmitter} from 'angular2/core';
|
||||
import {By} from 'angular2/platform/browser';
|
||||
|
||||
import {MdCheckbox} from './checkbox';
|
||||
|
||||
// IE11 does not support event constructors, so we need to perform this check.
|
||||
var BROWSER_SUPPORTS_EVENT_CONSTRUCTORS: boolean = (function() {
|
||||
// See: https://github.com/rauschma/event_constructors_check/blob/gh-pages/index.html#L39
|
||||
try {
|
||||
return new Event('submit', { bubbles: false }).bubbles === false &&
|
||||
new Event('submit', { bubbles: true }).bubbles === true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
export function main() {
|
||||
describe('MdCheckbox', function() {
|
||||
var builder: TestComponentBuilder;
|
||||
|
||||
beforeEach(inject([TestComponentBuilder], function(tcb: TestComponentBuilder) {
|
||||
builder = tcb;
|
||||
}));
|
||||
|
||||
it('attaches a class "md-checkbox" to the host element', function(done: () => void) {
|
||||
builder.createAsync(CheckboxController).then(function(fixture) {
|
||||
fixture.detectChanges();
|
||||
let el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
expect(el).not.toBeNull();
|
||||
}).then(done).catch(done);
|
||||
});
|
||||
it('attaches a unique id to the host element', function(done: () => void) {
|
||||
builder.createAsync(CheckboxMultiController).then(function(fixture) {
|
||||
fixture.detectChanges();
|
||||
let first = fixture.debugElement.query(By.css('.md-checkbox:first-of-type'));
|
||||
let second = fixture.debugElement.query(By.css('.md-checkbox:nth-of-type(2)'));
|
||||
expect(first.nativeElement.id).toMatch(/^md\-checkbox\-\d$/g);
|
||||
expect(second.nativeElement.id).toMatch(/^md\-checkbox\-\d$/g);
|
||||
expect(first.nativeElement.id).not.toEqual(second.nativeElement.id);
|
||||
}).then(done).catch(done);
|
||||
});
|
||||
|
||||
it('allows clients to provide their own id', function(done: () => void) {
|
||||
builder.createAsync(CheckboxCustomIdController).then(function(fixture) {
|
||||
fixture.detectChanges();
|
||||
let component = fixture.componentInstance;
|
||||
let el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
expect(el.nativeElement.id).toEqual(component.checkboxId);
|
||||
}).then(done).catch(done);
|
||||
});
|
||||
|
||||
it('creates a label with an id based off the checkbox id', function(done: () => void) {
|
||||
builder.createAsync(CheckboxController).then(function(fixture) {
|
||||
fixture.detectChanges();
|
||||
let el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
let label = el.nativeElement.querySelector('label');
|
||||
expect(label.id).toEqual(`${el.nativeElement.id}-label`);
|
||||
}).then(done).catch(done);
|
||||
});
|
||||
|
||||
it('uses <ng-content></ng-content> for the label markup', function(done: () => void) {
|
||||
builder.createAsync(CheckboxController).then(function(fixture) {
|
||||
fixture.detectChanges();
|
||||
let el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
let label = el.nativeElement.querySelector('label');
|
||||
expect(label.innerHTML.trim()).toEqual('<em>my</em> checkbox');
|
||||
}).then(done).catch(done);
|
||||
});
|
||||
|
||||
it('adds a checkbox role attribute to the host element', function(done: () => void) {
|
||||
builder.createAsync(CheckboxController).then(function(fixture) {
|
||||
fixture.detectChanges();
|
||||
let el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
expect(el.nativeElement.getAttribute('role')).toEqual('checkbox');
|
||||
}).then(done).catch(done);
|
||||
});
|
||||
|
||||
it('defaults "aria-checked" to "false" on the host element', function(done: () => void) {
|
||||
builder.createAsync(CheckboxController).then(function(fixture) {
|
||||
fixture.detectChanges();
|
||||
let el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
expect(el.nativeElement.getAttribute('aria-checked')).toEqual('false');
|
||||
}).then(done).catch(done);
|
||||
});
|
||||
|
||||
it('defaults "aria-disabled" to "false" on the host element', function(done: () => void) {
|
||||
builder.createAsync(CheckboxController).then(function(fixture) {
|
||||
fixture.detectChanges();
|
||||
let el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
expect(el.nativeElement.getAttribute('aria-disabled')).toEqual('false');
|
||||
}).then(done).catch(done);
|
||||
});
|
||||
|
||||
it('defaults tabindex to 0 to the host element', function(done: () => void) {
|
||||
builder.createAsync(CheckboxController).then(function(fixture) {
|
||||
fixture.detectChanges();
|
||||
let el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
expect(el.nativeElement.tabIndex).toEqual(0);
|
||||
}).then(done).catch(done);
|
||||
});
|
||||
|
||||
it('allows clients to provide their own tabindex', function(done: () => void) {
|
||||
builder.createAsync(CheckboxCustomTabindexController).then(function(fixture) {
|
||||
fixture.detectChanges();
|
||||
let component = fixture.componentInstance;
|
||||
let el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
expect(el.nativeElement.tabIndex).toEqual(component.checkboxTabindex);
|
||||
}).then(done).catch(done);
|
||||
});
|
||||
|
||||
it('allows clients to provide an aria-label', function(done: () => void) {
|
||||
builder.createAsync(CheckboxCustomArialabelController).then(function(fixture) {
|
||||
fixture.detectChanges();
|
||||
let component = fixture.componentInstance;
|
||||
let el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
expect(el.nativeElement.getAttribute('aria-label')).toEqual(component.checkboxLabel);
|
||||
}).then(done).catch(done);
|
||||
});
|
||||
|
||||
it('sets the "aria-labelledby" attribute to the id of the label', function(done: () => void) {
|
||||
builder.createAsync(CheckboxController).then(function(fixture) {
|
||||
fixture.detectChanges();
|
||||
let el = fixture.debugElement.query(By.css('.mc-checkbox'));
|
||||
let label = el.nativeElement.querySelector('label');
|
||||
expect(el.nativeElement.getAttribute('aria-labelledby')).toEqual(label.id);
|
||||
}).then(done).catch(done);
|
||||
});
|
||||
|
||||
describe('when given an "align" input with a value of "end"', function() {
|
||||
var fixture: ComponentFixture;
|
||||
|
||||
beforeEach(function(done: () => void) {
|
||||
builder.createAsync(CheckboxEndAlignedController).then(function(f) {
|
||||
fixture = f;
|
||||
fixture.detectChanges();
|
||||
}).then(done).catch(done);
|
||||
});
|
||||
|
||||
it('sets an "md-checkbox-align-end" class on the host element', function() {
|
||||
let el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
expect(el.nativeElement.className).toContain('md-checkbox-align-end');
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when the checkbox's checked value is set`, function() {
|
||||
var fixture: ComponentFixture;
|
||||
var controller: CheckboxController;
|
||||
var changePromise: Promise<boolean>;
|
||||
var waitingForChange: boolean;
|
||||
|
||||
function waitForChange(): Promise<boolean> {
|
||||
if (waitingForChange) {
|
||||
throw new Error('You are already waiting for a change!');
|
||||
}
|
||||
waitingForChange = true;
|
||||
return new Promise(function(resolve, reject) {
|
||||
controller.eventProxy.subscribe(resolve, reject);
|
||||
}).then(function(val) {
|
||||
waitingForChange = false;
|
||||
return val;
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(function(done: () => void) {
|
||||
builder.createAsync(CheckboxController).then(function(f) {
|
||||
fixture = f;
|
||||
controller = fixture.componentInstance;
|
||||
|
||||
changePromise = waitForChange();
|
||||
controller.isChecked = true;
|
||||
fixture.detectChanges();
|
||||
}).then(done).catch(done);
|
||||
});
|
||||
|
||||
it('adds a "md-checkbox-checked" modifier class to the host element', function() {
|
||||
let el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
expect(el.nativeElement.className).toContain('md-checkbox-checked');
|
||||
});
|
||||
|
||||
it('sets "aria-checked" to be "true" on the host element', function() {
|
||||
let el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
expect(el.nativeElement.getAttribute('aria-checked')).toEqual('true');
|
||||
});
|
||||
|
||||
it('emits a change event with the currently checked value', function(done: () => void) {
|
||||
changePromise.then(function(isChecked) {
|
||||
expect(isChecked).toBe(true);
|
||||
|
||||
let nextChangePromise = waitForChange();
|
||||
controller.isChecked = false;
|
||||
fixture.detectChanges();
|
||||
|
||||
return nextChangePromise;
|
||||
}).then(function(isChecked) {
|
||||
expect(isChecked).toBe(false);
|
||||
}).then(done).catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the checkbox is indeterminate', function() {
|
||||
var fixture: ComponentFixture;
|
||||
var controller: CheckboxController;
|
||||
|
||||
beforeEach(function(done: () => void) {
|
||||
builder.createAsync(CheckboxController).then(function(f) {
|
||||
fixture = f;
|
||||
controller = fixture.componentInstance;
|
||||
|
||||
controller.isIndeterminate = true;
|
||||
fixture.detectChanges();
|
||||
}).then(done).catch(done);
|
||||
});
|
||||
|
||||
it('adds a "md-checkbox-indeterminate" class to the host element', function() {
|
||||
let el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
expect(el.nativeElement.className).toContain('md-checkbox-indeterminate');
|
||||
});
|
||||
|
||||
it('sets "aria-checked" to "mixed" on the host element', function() {
|
||||
let el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
expect(el.nativeElement.getAttribute('aria-checked')).toEqual('mixed');
|
||||
});
|
||||
|
||||
describe('when re-checked to true', function() {
|
||||
beforeEach(function() {
|
||||
controller.isChecked = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('removes md-checkbox-indeterminate', function() {
|
||||
let el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
expect(el.nativeElement.className).not.toContain('md-checkbox-indeterminate');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when re-checked to false', function() {
|
||||
beforeEach(function() {
|
||||
controller.isChecked = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
controller.isIndeterminate = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
controller.isChecked = false;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('removes md-checkbox-indeterminate', function() {
|
||||
let el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
expect(el.nativeElement.className).not.toContain('md-checkbox-indeterminate');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the checkbox is disabled', function() {
|
||||
var fixture: ComponentFixture;
|
||||
var controller: CheckboxController;
|
||||
|
||||
beforeEach(function(done: () => void) {
|
||||
builder.createAsync(CheckboxController).then(function(f) {
|
||||
fixture = f;
|
||||
controller = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
controller.isDisabled = true;
|
||||
fixture.detectChanges();
|
||||
}).then(done).catch(done);
|
||||
});
|
||||
|
||||
it('adds a "md-checkbox-disabled" class to the host element', function() {
|
||||
let el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
expect(el.nativeElement.className).toContain('md-checkbox-disabled');
|
||||
});
|
||||
|
||||
it('sets the tabindex to -1 on the host element', function() {
|
||||
let el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
expect(el.nativeElement.getAttribute('tabindex')).toEqual('-1');
|
||||
});
|
||||
|
||||
it('sets "aria-disabled" to "true" on the host element', function() {
|
||||
let el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
expect(el.nativeElement.getAttribute('aria-disabled')).toEqual('true');
|
||||
});
|
||||
|
||||
it('restores the previously set tabindex when re-enabled', function(done: () => void) {
|
||||
builder.createAsync(CheckboxCustomTabindexController).then(function(f) {
|
||||
fixture = f;
|
||||
let tabindexController: CheckboxCustomTabindexController = fixture.componentInstance;
|
||||
|
||||
tabindexController.isDisabled = true;
|
||||
fixture.detectChanges();
|
||||
let el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
expect(el.nativeElement.getAttribute('tabindex')).toEqual('-1');
|
||||
|
||||
tabindexController.isDisabled = false;
|
||||
fixture.detectChanges();
|
||||
el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
expect(el.nativeElement.getAttribute('tabindex')).toEqual(
|
||||
String(tabindexController.checkboxTabindex));
|
||||
}).then(done).catch(done);
|
||||
});
|
||||
|
||||
describe('when the tabindex input is changed while disabled', function() {
|
||||
var tabindexController: CheckboxCustomTabindexController;
|
||||
var newTabindex: number;
|
||||
|
||||
beforeEach(function(done: () => void) {
|
||||
newTabindex = 10;
|
||||
|
||||
builder.createAsync(CheckboxCustomTabindexController).then(function(f) {
|
||||
fixture = f;
|
||||
tabindexController = fixture.componentInstance;
|
||||
|
||||
tabindexController.isDisabled = true;
|
||||
tabindexController.checkboxTabindex = newTabindex;
|
||||
fixture.detectChanges();
|
||||
}).then(done).catch(done);
|
||||
});
|
||||
|
||||
it('keeps the tabindex at -1', function() {
|
||||
let el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
expect(el.nativeElement.getAttribute('tabindex')).toEqual('-1');
|
||||
});
|
||||
|
||||
it('uses the newly changed tabindex when re-enabled', function() {
|
||||
tabindexController.isDisabled = false;
|
||||
fixture.detectChanges();
|
||||
|
||||
let el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
expect(el.nativeElement.getAttribute('tabindex')).toEqual(String(newTabindex));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the checkbox is clicked', function() {
|
||||
var fixture: ComponentFixture;
|
||||
var controller: CheckboxController;
|
||||
var el: DebugElement;
|
||||
|
||||
function clickCheckbox(): Event {
|
||||
return click(el, fixture);
|
||||
}
|
||||
|
||||
beforeEach(function(done: () => void) {
|
||||
builder.createAsync(CheckboxController).then(function(f) {
|
||||
fixture = f;
|
||||
controller = fixture.componentInstance;
|
||||
|
||||
fixture.detectChanges();
|
||||
el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
}).then(done).catch(done);
|
||||
});
|
||||
|
||||
it('toggles the checked value', function() {
|
||||
clickCheckbox();
|
||||
expect(el.nativeElement.className).toContain('md-checkbox-checked');
|
||||
|
||||
clickCheckbox();
|
||||
expect(el.nativeElement.className).not.toContain('md-checkbox-checked');
|
||||
});
|
||||
|
||||
describe('when the checkbox is disabled', function() {
|
||||
beforeEach(function() {
|
||||
controller.isDisabled = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('stops the click event from propagating', function() {
|
||||
let evt = clickCheckbox();
|
||||
expect(evt.stopPropagation).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not alter the checked value', function() {
|
||||
clickCheckbox();
|
||||
expect(el.nativeElement.className).not.toContain('md-checkbox-checked');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a spacebar press occurs on the checkbox', function() {
|
||||
var fixture: ComponentFixture;
|
||||
var controller: CheckboxController;
|
||||
var el: DebugElement;
|
||||
|
||||
function spacePress(): Event {
|
||||
return keyup(el, ' ', fixture);
|
||||
}
|
||||
|
||||
beforeEach(function(done: () => void) {
|
||||
builder.createAsync(CheckboxController).then(function(f) {
|
||||
fixture = f;
|
||||
controller = fixture.componentInstance;
|
||||
|
||||
fixture.detectChanges();
|
||||
el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
}).then(done).catch(done);
|
||||
});
|
||||
|
||||
it('toggles the checked value', function() {
|
||||
spacePress();
|
||||
expect(el.nativeElement.className).toContain('md-checkbox-checked');
|
||||
|
||||
spacePress();
|
||||
expect(el.nativeElement.className).not.toContain('md-checkbox-checked');
|
||||
});
|
||||
|
||||
describe('when the checkbox is disabled', function() {
|
||||
beforeEach(function() {
|
||||
controller.isDisabled = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('stops the click event from propagating', function() {
|
||||
let evt = spacePress();
|
||||
expect(evt.stopPropagation).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not alter the checked value', function() {
|
||||
spacePress();
|
||||
expect(el.nativeElement.className).not.toContain('md-checkbox-checked');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('usage as a form control', function() {
|
||||
var fixture: ComponentFixture;
|
||||
var controller: CheckboxFormcontrolController;
|
||||
|
||||
beforeEach(function(done: () => void) {
|
||||
builder.createAsync(CheckboxFormcontrolController).then(function(f) {
|
||||
fixture = f;
|
||||
controller = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
}).then(done).catch(done);
|
||||
});
|
||||
|
||||
// NOTE(traviskaufman): This test is not that elegant, but I have not found a better way
|
||||
// to test through ngModel as of now.
|
||||
// See: https://github.com/angular/angular/issues/7409
|
||||
it('supports ngModel/ngControl', function(done: () => void) {
|
||||
var el: DebugElement;
|
||||
var invalidMsg: DebugElement;
|
||||
|
||||
fakeAsync(function() {
|
||||
el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
invalidMsg = fixture.debugElement.query(By.css('#invalid-msg'));
|
||||
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
expect(el.nativeElement.className).toContain('ng-untouched');
|
||||
expect(el.nativeElement.className).toContain('ng-pristine');
|
||||
expect(invalidMsg.nativeElement.hidden).toBe(true);
|
||||
|
||||
controller.model.isChecked = true;
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(el.nativeElement.className).toContain('md-checkbox-checked');
|
||||
expect(el.nativeElement.className).toContain('ng-dirty');
|
||||
expect(el.nativeElement.className).toContain('ng-valid');
|
||||
|
||||
var blur: Event;
|
||||
if (BROWSER_SUPPORTS_EVENT_CONSTRUCTORS) {
|
||||
blur = new Event('blur');
|
||||
} else {
|
||||
blur = document.createEvent('UIEvent');
|
||||
(<UIEvent>blur).initUIEvent('blur', true, true, window, 0);
|
||||
}
|
||||
el.nativeElement.dispatchEvent(blur);
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
expect(el.nativeElement.className).toContain('ng-touched');
|
||||
})();
|
||||
|
||||
let onceChanged = controller.model.waitForChange();
|
||||
click(el, fixture);
|
||||
onceChanged.then(function() {
|
||||
expect(controller.model.isChecked).toBe(false);
|
||||
}).then(done).catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('applying transition classes', function() {
|
||||
var fixture: ComponentFixture;
|
||||
var controller: CheckboxController;
|
||||
|
||||
beforeEach(function(done: () => void) {
|
||||
builder.createAsync(CheckboxController).then(function(f) {
|
||||
fixture = f;
|
||||
controller = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
}).then(done).catch(done);
|
||||
});
|
||||
|
||||
it('applies transition classes when going from unchecked <-> checked', function() {
|
||||
let el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
|
||||
controller.isChecked = true;
|
||||
fixture.detectChanges();
|
||||
expect(el.nativeElement.className).toContain('md-checkbox-anim-unchecked-checked');
|
||||
|
||||
controller.isChecked = false;
|
||||
fixture.detectChanges();
|
||||
expect(el.nativeElement.className).not.toContain('md-checkbox-anim-unchecked-checked');
|
||||
expect(el.nativeElement.className).toContain('md-checkbox-anim-checked-unchecked');
|
||||
});
|
||||
|
||||
it('applies transition classes when going from unchecked <-> indeterminate', function() {
|
||||
let el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
|
||||
controller.isIndeterminate = true;
|
||||
fixture.detectChanges();
|
||||
expect(el.nativeElement.className).toContain('md-checkbox-anim-unchecked-indeterminate');
|
||||
|
||||
controller.isIndeterminate = false;
|
||||
fixture.detectChanges();
|
||||
expect(el.nativeElement.className).not.toContain(
|
||||
'md-checkbox-anim-unchecked-indeterminate');
|
||||
expect(el.nativeElement.className).toContain('md-checkbox-anim-indeterminate-unchecked');
|
||||
});
|
||||
|
||||
it('applies a transition class when going from checked -> indeterminate', function() {
|
||||
let el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
|
||||
controller.isChecked = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
controller.isIndeterminate = true;
|
||||
fixture.detectChanges();
|
||||
expect(el.nativeElement.className).not.toContain('md-checkbox-anim-unchecked-checked');
|
||||
expect(el.nativeElement.className).toContain('md-checkbox-anim-checked-indeterminate');
|
||||
});
|
||||
|
||||
it('applies a transition class when going from indeterminate -> checked', function() {
|
||||
let el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
|
||||
controller.isIndeterminate = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
controller.isChecked = true;
|
||||
fixture.detectChanges();
|
||||
expect(el.nativeElement.className).not.toContain(
|
||||
'md-checkbox-anim-unchecked-indeterminate');
|
||||
expect(el.nativeElement.className).toContain('md-checkbox-anim-indeterminate-checked');
|
||||
});
|
||||
|
||||
it('does not apply any transition classes when there is nothing to transition', function() {
|
||||
let el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
|
||||
controller.isChecked = controller.isChecked;
|
||||
fixture.detectChanges();
|
||||
expect(el.nativeElement.className).not.toMatch(/^md\-checkbox\-anim/g);
|
||||
|
||||
controller.isIndeterminate = controller.isIndeterminate;
|
||||
fixture.detectChanges();
|
||||
expect(el.nativeElement.className).not.toMatch(/^md\-checkbox\-anim/g);
|
||||
});
|
||||
|
||||
it('does not apply any transition classes when the component is initialized', function() {
|
||||
let el = fixture.debugElement.query(By.css('.md-checkbox'));
|
||||
expect(el.nativeElement.className).not.toMatch(/^md\-checkbox\-anim/g);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function click(el: DebugElement, fixture: ComponentFixture) {
|
||||
var clickEvent: Event;
|
||||
if (BROWSER_SUPPORTS_EVENT_CONSTRUCTORS) {
|
||||
clickEvent = new Event('click');
|
||||
} else {
|
||||
clickEvent = document.createEvent('UIEvent');
|
||||
(<UIEvent>clickEvent).initUIEvent('click', true, true, window, 1);
|
||||
}
|
||||
spyOn(clickEvent, 'stopPropagation').and.callThrough();
|
||||
el.nativeElement.dispatchEvent(clickEvent);
|
||||
fixture.detectChanges();
|
||||
return clickEvent;
|
||||
}
|
||||
|
||||
// TODO(traviskaufman): Reinvestigate implementation of this method once tests in Dart begin to run.
|
||||
function keyup(el: DebugElement, key: string, fixture: ComponentFixture) {
|
||||
var kbdEvent: Event;
|
||||
if (BROWSER_SUPPORTS_EVENT_CONSTRUCTORS) {
|
||||
kbdEvent = new KeyboardEvent('keyup');
|
||||
} else {
|
||||
kbdEvent = document.createEvent('Events');
|
||||
kbdEvent.initEvent('keyup', true, true);
|
||||
}
|
||||
// Hack DOM Level 3 Events "key" prop into keyboard event.
|
||||
Object.defineProperty(kbdEvent, 'key', {
|
||||
value: ' ',
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
configurable: true
|
||||
});
|
||||
spyOn(kbdEvent, 'stopPropagation').and.callThrough();
|
||||
el.nativeElement.dispatchEvent(kbdEvent);
|
||||
fixture.detectChanges();
|
||||
return kbdEvent;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'checkbox-controller',
|
||||
template: `
|
||||
<md-checkbox [checked]="isChecked"
|
||||
[indeterminate]="isIndeterminate"
|
||||
[disabled]="isDisabled"
|
||||
(change)="eventProxy.emit($event)">
|
||||
<em>my</em> checkbox
|
||||
</md-checkbox>
|
||||
`,
|
||||
directives: [MdCheckbox]
|
||||
})
|
||||
class CheckboxController {
|
||||
isChecked: boolean = false;
|
||||
isIndeterminate: boolean = false;
|
||||
isDisabled: boolean = false;
|
||||
eventProxy: EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'checkbox-multi-controller',
|
||||
template: `
|
||||
<md-checkbox></md-checkbox>
|
||||
<md-checkbox></md-checkbox>
|
||||
`,
|
||||
directives: [MdCheckbox]
|
||||
})
|
||||
class CheckboxMultiController {}
|
||||
|
||||
@Component({
|
||||
selector: 'checkbox-custom-id-controller',
|
||||
template: `
|
||||
<md-checkbox [id]="checkboxId"></md-checkbox>
|
||||
`,
|
||||
directives: [MdCheckbox]
|
||||
})
|
||||
class CheckboxCustomIdController {
|
||||
checkboxId: string = 'my-checkbox';
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'checkbox-custom-tabindex-controller',
|
||||
template: `
|
||||
<md-checkbox [tabindex]="checkboxTabindex" [disabled]="isDisabled"></md-checkbox>
|
||||
`,
|
||||
directives: [MdCheckbox]
|
||||
})
|
||||
class CheckboxCustomTabindexController {
|
||||
checkboxTabindex: number = 5;
|
||||
isDisabled: boolean = false;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'checkbox-custom-arialabel-controller',
|
||||
template: `
|
||||
<md-checkbox [aria-label]="checkboxLabel"></md-checkbox>
|
||||
`,
|
||||
directives: [MdCheckbox]
|
||||
})
|
||||
class CheckboxCustomArialabelController {
|
||||
checkboxLabel: string = 'My awesome checkbox';
|
||||
}
|
||||
|
||||
class FormcontrolModel {
|
||||
private _isChecked = false;
|
||||
private _changeEmitter = new EventEmitter<boolean>();
|
||||
|
||||
get isChecked(): boolean {
|
||||
return this._isChecked;
|
||||
}
|
||||
|
||||
set isChecked(isChecked: boolean) {
|
||||
let shouldEmitChange = this._isChecked !== isChecked;
|
||||
this._isChecked = isChecked;
|
||||
if (shouldEmitChange) {
|
||||
this._changeEmitter.emit(this._isChecked);
|
||||
}
|
||||
}
|
||||
|
||||
waitForChange(): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let subscription: {unsubscribe: () => void};
|
||||
let subscriber = function(isChecked: boolean) {
|
||||
subscription.unsubscribe();
|
||||
resolve(isChecked);
|
||||
};
|
||||
subscription = this._changeEmitter.subscribe(subscriber, reject);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'checkbox-formcontrol-controller',
|
||||
template: `
|
||||
<form>
|
||||
<md-checkbox [(ngModel)]="model.isChecked"
|
||||
ngControl="cb" #cb="ngForm">
|
||||
</md-checkbox>
|
||||
<p id="invalid-msg" [hidden]="cb.valid || cb.pristine">INVALID!</p>
|
||||
</form>
|
||||
`,
|
||||
directives: [MdCheckbox]
|
||||
})
|
||||
class CheckboxFormcontrolController {
|
||||
model = new FormcontrolModel();
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'checkbox-end-aligned-controller',
|
||||
template: `<md-checkbox [align]="'end'"></md-checkbox>`,
|
||||
directives: [MdCheckbox]
|
||||
})
|
||||
class CheckboxEndAlignedController {}
|
||||
@@ -0,0 +1,256 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
Provider,
|
||||
Renderer,
|
||||
ViewEncapsulation,
|
||||
forwardRef
|
||||
} from 'angular2/core';
|
||||
|
||||
import {
|
||||
NG_VALUE_ACCESSOR,
|
||||
ControlValueAccessor
|
||||
} from 'angular2/src/common/forms/directives/control_value_accessor';
|
||||
import {CONST_EXPR} from 'angular2/src/facade/lang';
|
||||
|
||||
import { OneOf } from '../../core/annotations/one-of';
|
||||
|
||||
/**
|
||||
* Monotonically increasing integer used to auto-generate unique ids for checkbox components.
|
||||
*/
|
||||
let nextId = 0;
|
||||
|
||||
/**
|
||||
* Provider Expression that allows md-checkbox to register as a ControlValueAccessor. This allows it
|
||||
* to support [(ngModel)] and ngControl.
|
||||
*/
|
||||
const MD_CHECKBOX_CONTROL_VALUE_ACCESSOR = CONST_EXPR(new Provider(
|
||||
NG_VALUE_ACCESSOR, {
|
||||
useExisting: forwardRef(() => MdCheckbox),
|
||||
multi: true
|
||||
}));
|
||||
|
||||
/**
|
||||
* Represents the different states that require custom transitions between them.
|
||||
*/
|
||||
enum TransitionCheckState {
|
||||
/** The initial state of the component before any user interaction. */
|
||||
Init,
|
||||
/** The state representing the component when it's becoming checked. */
|
||||
Checked,
|
||||
/** The state representing the component when it's becoming unchecked. */
|
||||
Unchecked,
|
||||
/** The state representing the component when it's becoming indeterminate. */
|
||||
Indeterminate
|
||||
}
|
||||
|
||||
/**
|
||||
* A material design checkbox component. Supports all of the functionality of an HTML5 checkbox,
|
||||
* and exposes a similar API. An MdCheckbox can be either checked, unchecked, indeterminate, or
|
||||
* disabled. Note that all additional accessibility attributes are taken care of by the component,
|
||||
* so there is no need to provide them yourself. However, if you want to omit a label and still
|
||||
* have the checkbox be accessible, you may supply an [aria-label] input.
|
||||
* See: https://www.google.com/design/spec/components/selection-controls.html
|
||||
*/
|
||||
@Component({
|
||||
selector: 'md-checkbox',
|
||||
templateUrl: './components/checkbox/checkbox.html',
|
||||
styleUrls: ['./components/checkbox/checkbox.css'],
|
||||
host: {
|
||||
'role': 'checkbox',
|
||||
'[id]': 'id',
|
||||
'[class.md-checkbox]': 'true',
|
||||
'[class.md-checkbox-indeterminate]': 'indeterminate',
|
||||
'[class.md-checkbox-checked]': 'checked',
|
||||
'[class.md-checkbox-disabled]': 'disabled',
|
||||
'[class.md-checkbox-align-end]': 'align == "end"',
|
||||
'[tabindex]': 'disabled ? -1 : tabindex',
|
||||
'[attr.aria-label]': 'ariaLabel',
|
||||
'[attr.aria-labelledby]': 'labelId',
|
||||
'[attr.aria-checked]': 'getAriaChecked()',
|
||||
'[attr.aria-disabled]': 'disabled',
|
||||
'(click)': 'onInteractionEvent($event)',
|
||||
'(keyup.space)': 'onInteractionEvent($event)',
|
||||
'(blur)': 'onTouched()'
|
||||
},
|
||||
providers: [MD_CHECKBOX_CONTROL_VALUE_ACCESSOR],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class MdCheckbox implements ControlValueAccessor {
|
||||
/**
|
||||
* Attached to the aria-label attribute of the host element. In most cases, arial-labelledby will
|
||||
* take precedence so this may be omitted.
|
||||
*/
|
||||
@Input('aria-label') ariaLabel: string = '';
|
||||
|
||||
/** A unique id for the checkbox. If one is not supplied, it is auto-generated. */
|
||||
@Input() id: string = `md-checkbox-${++nextId}`;
|
||||
|
||||
/** Whether or not the checkbox should come before or after the label. */
|
||||
@Input() @OneOf(['start', 'end']) align: string = 'start';
|
||||
|
||||
/**
|
||||
* Whether the checkbox is disabled. When the checkbox is disabled it cannot be interacted with.
|
||||
* The correct ARIA attributes are applied to denote this to assistive technology.
|
||||
*/
|
||||
@Input() disabled: boolean = false;
|
||||
|
||||
/**
|
||||
* The tabindex attribute for the checkbox. Note that when the checkbox is disabled, the attribute
|
||||
* on the host element will be set to -1, regardless of the actual tabindex value.
|
||||
*/
|
||||
@Input() tabindex: number = 0;
|
||||
|
||||
/** Event emitted when the checkbox's `checked` value changes. */
|
||||
@Output() change: EventEmitter<boolean> = new EventEmitter();
|
||||
|
||||
/** Called when the checkbox is blurred. Needed to properly implement ControlValueAccessor. */
|
||||
onTouched: () => any = () => {};
|
||||
|
||||
private _currentAnimationClass: string = '';
|
||||
|
||||
private _currentCheckState: TransitionCheckState = TransitionCheckState.Init;
|
||||
|
||||
private _checked: boolean = false;
|
||||
|
||||
private _indeterminate: boolean = false;
|
||||
|
||||
private _changeSubscription: {unsubscribe: () => any} = null;
|
||||
|
||||
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}
|
||||
|
||||
/**
|
||||
* Whether the checkbox is checked. Note that setting `checked` will immediately set
|
||||
* `indeterminate` to false.
|
||||
*/
|
||||
@Input() get checked() {
|
||||
return this._checked;
|
||||
}
|
||||
|
||||
set checked(checked: boolean) {
|
||||
this._indeterminate = false;
|
||||
this._checked = checked;
|
||||
this._transitionCheckState(
|
||||
this._checked ? TransitionCheckState.Checked : TransitionCheckState.Unchecked);
|
||||
this.change.emit(this._checked);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the checkbox is indeterminate. This is also known as "mixed" mode and can be used to
|
||||
* represent a checkbox with three states, e.g. a checkbox that represents a nested list of
|
||||
* checkable items. Note that whenever `checked` is set, indeterminate is immediately set to
|
||||
* false. This differs from the web platform in that indeterminate state on native
|
||||
* checkboxes is only remove when the user manually checks the checkbox (rather than setting the
|
||||
* `checked` property programmatically). However, we feel that this behavior is more accommodating
|
||||
* to the way consumers would envision using this component.
|
||||
*/
|
||||
@Input() get indeterminate() {
|
||||
return this._indeterminate;
|
||||
}
|
||||
|
||||
set indeterminate(indeterminate: boolean) {
|
||||
this._indeterminate = indeterminate;
|
||||
if (this._indeterminate) {
|
||||
this._transitionCheckState(TransitionCheckState.Indeterminate);
|
||||
} else {
|
||||
this._transitionCheckState(
|
||||
this.checked ? TransitionCheckState.Checked : TransitionCheckState.Unchecked);
|
||||
}
|
||||
}
|
||||
|
||||
/** The id that is attached to the checkbox's label. */
|
||||
get labelId() {
|
||||
return `${this.id}-label`;
|
||||
}
|
||||
|
||||
/** Returns the proper aria-checked attribute value based on the checkbox's state. */
|
||||
getAriaChecked() {
|
||||
if (this.indeterminate) {
|
||||
return 'mixed';
|
||||
}
|
||||
return this.checked ? 'true' : 'false';
|
||||
}
|
||||
|
||||
/** Toggles the checked state of the checkbox. If the checkbox is disabled, this does nothing. */
|
||||
toggle() {
|
||||
this.checked = !this.checked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler used for both (click) and (keyup.space) events. Delegates to toggle().
|
||||
*/
|
||||
onInteractionEvent(event: Event) {
|
||||
if (this.disabled) {
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
this.toggle();
|
||||
}
|
||||
|
||||
/** Implemented as part of ControlValueAccessor. */
|
||||
writeValue(value: any) {
|
||||
this.checked = !!value;
|
||||
}
|
||||
|
||||
/** Implemented as part of ControlValueAccessor. */
|
||||
registerOnChange(fn: any) {
|
||||
if (this._changeSubscription) {
|
||||
this._changeSubscription.unsubscribe();
|
||||
}
|
||||
this._changeSubscription = <{unsubscribe: () => any}>this.change.subscribe(fn);
|
||||
}
|
||||
|
||||
/** Implemented as part of ControlValueAccessor. */
|
||||
registerOnTouched(fn: any) {
|
||||
this.onTouched = fn;
|
||||
}
|
||||
|
||||
private _transitionCheckState(newState: TransitionCheckState) {
|
||||
let oldState = this._currentCheckState;
|
||||
let renderer = this._renderer;
|
||||
let elementRef = this._elementRef;
|
||||
|
||||
if (oldState === newState) {
|
||||
return;
|
||||
}
|
||||
if (this._currentAnimationClass.length > 0) {
|
||||
renderer.setElementClass(elementRef.nativeElement, this._currentAnimationClass, false);
|
||||
}
|
||||
|
||||
this._currentAnimationClass = this._getAnimationClassForCheckStateTransition(
|
||||
oldState, newState);
|
||||
this._currentCheckState = newState;
|
||||
|
||||
if (this._currentAnimationClass.length > 0) {
|
||||
renderer.setElementClass(elementRef.nativeElement, this._currentAnimationClass, true);
|
||||
}
|
||||
}
|
||||
|
||||
private _getAnimationClassForCheckStateTransition(
|
||||
oldState: TransitionCheckState, newState: TransitionCheckState): string {
|
||||
var animSuffix: string;
|
||||
|
||||
switch (oldState) {
|
||||
case TransitionCheckState.Init:
|
||||
return '';
|
||||
case TransitionCheckState.Unchecked:
|
||||
animSuffix = newState === TransitionCheckState.Checked ?
|
||||
'unchecked-checked' : 'unchecked-indeterminate';
|
||||
break;
|
||||
case TransitionCheckState.Checked:
|
||||
animSuffix = newState === TransitionCheckState.Unchecked ?
|
||||
'checked-unchecked' : 'checked-indeterminate';
|
||||
break;
|
||||
case TransitionCheckState.Indeterminate:
|
||||
animSuffix = newState === TransitionCheckState.Checked ?
|
||||
'indeterminate-checked' : 'indeterminate-unchecked';
|
||||
}
|
||||
|
||||
return `md-checkbox-anim-${animSuffix}`;
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,11 @@ $pi: 3.14159265;
|
||||
// Easing Curves
|
||||
// TODO(jelbourn): all of these need to be revisited
|
||||
|
||||
// The default animation curves used by material design.
|
||||
$md-linear-out-slow-in-timing-function: cubic-bezier(0.0, 0.0, 0.2, 0.1) !default;
|
||||
$md-fast-out-slow-in-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1) !default;
|
||||
$md-fast-out-linear-in-timing-function: cubic-bezier(0.4, 0.0, 1, 1) !default;
|
||||
|
||||
$ease-in-out-curve-function: cubic-bezier(0.35, 0, 0.25, 1) !default;
|
||||
|
||||
$swift-ease-out-duration: 0.4s !default;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<h1>md-checkbox: Basic Example</h1>
|
||||
<md-checkbox [(ngModel)]="isChecked"
|
||||
(change)="isIndeterminate = false"
|
||||
[indeterminate]="isIndeterminate"
|
||||
[disabled]="isDisabled"
|
||||
[align]="alignment">
|
||||
Do you want to <em>foobar</em> the <em>bazquux</em>?
|
||||
</md-checkbox> - <strong>{{printResult()}}</strong>
|
||||
<div>
|
||||
<input id="indeterminate-toggle"
|
||||
type="checkbox"
|
||||
[(ngModel)]="isIndeterminate"
|
||||
[disabled]="isDisabled">
|
||||
<label for="indeterminate-toggle">Toggle Indeterminate</label>
|
||||
<input id="disabled-toggle" type="checkbox" [(ngModel)]="isDisabled">
|
||||
<label for="disabled-toggle">Toggle Disabled</label>
|
||||
</div>
|
||||
<div>
|
||||
<p>Alignment:</p>
|
||||
<div>
|
||||
<input #start type="radio"
|
||||
value="start"
|
||||
id="align-start"
|
||||
name="alignment"
|
||||
(click)="alignment = start.value"
|
||||
checked>
|
||||
<label for="align-start">Start</label>
|
||||
</div>
|
||||
<div>
|
||||
<input #end type="radio"
|
||||
value="end"
|
||||
id="align-end"
|
||||
name="alignment"
|
||||
(click)="alignment = end.value">
|
||||
<label for="align-end">End</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1>Application Example: Nested Checklist</h1>
|
||||
<h2><em>Caution: WIP!</em></h2>
|
||||
<md-checkbox-demo-nested-checklist></md-checkbox-demo-nested-checklist>
|
||||
@@ -0,0 +1,78 @@
|
||||
import {Component} from 'angular2/core';
|
||||
import {MdCheckbox} from '../../components/checkbox/checkbox';
|
||||
|
||||
interface Task {
|
||||
name: string;
|
||||
completed: boolean;
|
||||
subtasks?: Task[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'md-checkbox-demo-nested-checklist',
|
||||
styles: [`
|
||||
li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
`],
|
||||
templateUrl: 'demo-app/checkbox/nested-checklist.html',
|
||||
directives: [MdCheckbox]
|
||||
})
|
||||
class MdCheckboxDemoNestedChecklist {
|
||||
tasks: Task[] = [
|
||||
{
|
||||
name: 'Reminders',
|
||||
completed: false,
|
||||
subtasks: [
|
||||
{ name: 'Cook Dinner', completed: false },
|
||||
{ name: 'Read the Material Design Spec', completed: false },
|
||||
{ name: 'Upgrade Application to Angular2', completed: false }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Groceries',
|
||||
completed: false,
|
||||
subtasks: [
|
||||
{ name: 'Organic Eggs', completed: false },
|
||||
{ name: 'Protein Powder', completed: false },
|
||||
{ name: 'Almond Meal Flour', completed: false }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
allComplete(tasks: Task[]): boolean {
|
||||
return tasks.every(t => t.completed);
|
||||
}
|
||||
|
||||
someComplete(tasks: Task[]): boolean {
|
||||
const numComplete = tasks.filter(t => t.completed).length;
|
||||
return numComplete > 0 && numComplete < tasks.length;
|
||||
}
|
||||
|
||||
setAllCompleted(tasks: Task[], completed: boolean) {
|
||||
tasks.forEach(t => t.completed = completed);
|
||||
}
|
||||
|
||||
updateOnSubtaskChange(task: Task) {
|
||||
task.completed = this.allComplete(task.subtasks);
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'md-checkbox-demo',
|
||||
templateUrl: 'demo-app/checkbox/checkbox-demo.html',
|
||||
styleUrls: ['demo-app/checkbox/checkbox-demo.css'],
|
||||
directives: [MdCheckbox, MdCheckboxDemoNestedChecklist]
|
||||
})
|
||||
export class CheckboxDemo {
|
||||
isIndeterminate: boolean = false;
|
||||
isChecked: boolean = false;
|
||||
isDisabled: boolean = false;
|
||||
alignment: string = 'start';
|
||||
|
||||
printResult() {
|
||||
if (this.isIndeterminate) {
|
||||
return 'Maybe!';
|
||||
}
|
||||
return this.isChecked ? 'Yes!' : 'No!';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<h2>Tasks</h2>
|
||||
<ul>
|
||||
<li *ngFor="#task of tasks">
|
||||
<md-checkbox [(ngModel)]="task.completed"
|
||||
[checked]="allComplete(task.subtasks)"
|
||||
[indeterminate]="someComplete(task.subtasks)"
|
||||
(change)="setAllCompleted(task.subtasks, $event)">
|
||||
<h3>{{task.name}}</h3>
|
||||
</md-checkbox>
|
||||
<ul>
|
||||
<li *ngFor="#subtask of task.subtasks">
|
||||
<md-checkbox [(ngModel)]="subtask.completed">
|
||||
{{subtask.name}}
|
||||
</md-checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
+14
-15
@@ -1,16 +1,15 @@
|
||||
<div class="demo-content">
|
||||
<h1 class="title">Angular Material2 Demos</h1>
|
||||
<ul>
|
||||
<li><a [routerLink]="['ButtonDemo']">Button demo</a></li>
|
||||
<li><a [routerLink]="['CardDemo']">Card demo</a></li>
|
||||
<li><a [routerLink]="['SidenavDemo']">Sidenav demo</a></li>
|
||||
<li><a [routerLink]="['ProgressCircleDemo']">Progress Circle demo</a></li>
|
||||
<li><a [routerLink]="['PortalDemo']">Portal demo</a></li>
|
||||
</ul>
|
||||
<button md-raised-button (click)="root.dir = (root.dir == 'rtl' ? 'ltr' : 'rtl')">
|
||||
{{root.dir.toUpperCase()}}
|
||||
</button>
|
||||
<div #root="$implicit" dir="ltr">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
<h1 class="title">Angular Material2 Demos</h1>
|
||||
<ul>
|
||||
<li><a [routerLink]="['ButtonDemo']">Button demo</a></li>
|
||||
<li><a [routerLink]="['CardDemo']">Card demo</a></li>
|
||||
<li><a [routerLink]="['SidenavDemo']">Sidenav demo</a></li>
|
||||
<li><a [routerLink]="['ProgressCircleDemo']">Progress Circle demo</a></li>
|
||||
<li><a [routerLink]="['PortalDemo']">Portal demo</a></li>
|
||||
<li><a [routerLink]="['CheckboxDemo']">Checkbox demo</a></li>
|
||||
</ul>
|
||||
<button md-raised-button (click)="root.dir = (root.dir == 'rtl' ? 'ltr' : 'rtl')">
|
||||
{{root.dir.toUpperCase()}}
|
||||
</button>
|
||||
<div #root="$implicit" dir="ltr">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import {Component} from 'angular2/core';
|
||||
import {Route, RouteConfig, ROUTER_DIRECTIVES} from 'angular2/router';
|
||||
import {CardDemo} from './card/card-demo';
|
||||
import {ButtonDemo} from './button/button-demo';
|
||||
import {SidenavDemo} from './sidenav/sidenav-demo';
|
||||
import {ProgressCircleDemo} from './progress-circle/progress-circle-demo';
|
||||
import {Route, RouteConfig, ROUTER_DIRECTIVES} from 'angular2/router';
|
||||
import {CheckboxDemo} from './checkbox/checkbox-demo';
|
||||
import {Dir} from '../directives/dir/dir';
|
||||
import {MdButton} from '../components/button/button';
|
||||
import {PortalDemo} from './portal/portal-demo';
|
||||
@@ -30,5 +31,6 @@ export class Home {}
|
||||
new Route({path: '/sidenav', name: 'SidenavDemo', component: SidenavDemo}),
|
||||
new Route({path: '/progress-circle', name: 'ProgressCircleDemo', component: ProgressCircleDemo}),
|
||||
new Route({path: '/portal', name: 'PortalDemo', component: PortalDemo}),
|
||||
new Route({path: '/checkbox', name: 'CheckboxDemo', component: CheckboxDemo})
|
||||
])
|
||||
export class DemoApp { }
|
||||
|
||||
@@ -86,7 +86,8 @@ export function config(config) {
|
||||
},
|
||||
|
||||
browserDisconnectTimeout: 20000,
|
||||
browserNoActivityTimeout: 100000,
|
||||
browserNoActivityTimeout: 240000,
|
||||
captureTimeout: 120000,
|
||||
browsers: ['Chrome'],
|
||||
|
||||
singleRun: false
|
||||
|
||||
Reference in New Issue
Block a user