From 431141a827e2734c7a6909ce4a6c6d8f7749f0b1 Mon Sep 17 00:00:00 2001 From: Travis Kaufman Date: Tue, 1 Mar 2016 10:56:28 -0500 Subject: [PATCH] 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 --- src/components/checkbox/checkbox.html | 22 + src/components/checkbox/checkbox.scss | 460 ++++++++++++ src/components/checkbox/checkbox.spec.ts | 733 ++++++++++++++++++++ src/components/checkbox/checkbox.ts | 256 +++++++ src/core/style/_variables.scss | 5 + src/demo-app/checkbox/checkbox-demo.html | 41 ++ src/demo-app/checkbox/checkbox-demo.scss | 0 src/demo-app/checkbox/checkbox-demo.ts | 78 +++ src/demo-app/checkbox/nested-checklist.html | 18 + src/demo-app/demo-app.html | 29 +- src/demo-app/demo-app.ts | 4 +- test/karma.config.ts | 3 +- 12 files changed, 1632 insertions(+), 17 deletions(-) create mode 100644 src/components/checkbox/checkbox.html create mode 100644 src/components/checkbox/checkbox.scss create mode 100644 src/components/checkbox/checkbox.spec.ts create mode 100644 src/components/checkbox/checkbox.ts create mode 100644 src/demo-app/checkbox/checkbox-demo.html create mode 100644 src/demo-app/checkbox/checkbox-demo.scss create mode 100644 src/demo-app/checkbox/checkbox-demo.ts create mode 100644 src/demo-app/checkbox/nested-checklist.html diff --git a/src/components/checkbox/checkbox.html b/src/components/checkbox/checkbox.html new file mode 100644 index 00000000000..7303056d858 --- /dev/null +++ b/src/components/checkbox/checkbox.html @@ -0,0 +1,22 @@ +
+
+
+
+ + + + +
+
+
+ +
diff --git a/src/components/checkbox/checkbox.scss b/src/components/checkbox/checkbox.scss new file mode 100644 index 00000000000..485cd38a190 --- /dev/null +++ b/src/components/checkbox/checkbox.scss @@ -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; + } + } +} diff --git a/src/components/checkbox/checkbox.spec.ts b/src/components/checkbox/checkbox.spec.ts new file mode 100644 index 00000000000..c53f9481438 --- /dev/null +++ b/src/components/checkbox/checkbox.spec.ts @@ -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 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('my 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; + var waitingForChange: boolean; + + function waitForChange(): Promise { + 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'); + (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'); + (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: ` + + my checkbox + + `, + directives: [MdCheckbox] +}) +class CheckboxController { + isChecked: boolean = false; + isIndeterminate: boolean = false; + isDisabled: boolean = false; + eventProxy: EventEmitter = new EventEmitter(); +} + +@Component({ + selector: 'checkbox-multi-controller', + template: ` + + + `, + directives: [MdCheckbox] +}) +class CheckboxMultiController {} + +@Component({ + selector: 'checkbox-custom-id-controller', + template: ` + + `, + directives: [MdCheckbox] +}) +class CheckboxCustomIdController { + checkboxId: string = 'my-checkbox'; +} + +@Component({ + selector: 'checkbox-custom-tabindex-controller', + template: ` + + `, + directives: [MdCheckbox] +}) +class CheckboxCustomTabindexController { + checkboxTabindex: number = 5; + isDisabled: boolean = false; +} + +@Component({ + selector: 'checkbox-custom-arialabel-controller', + template: ` + + `, + directives: [MdCheckbox] +}) +class CheckboxCustomArialabelController { + checkboxLabel: string = 'My awesome checkbox'; +} + +class FormcontrolModel { + private _isChecked = false; + private _changeEmitter = new EventEmitter(); + + 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 { + 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: ` +
+ + +

INVALID!

+
+ `, + directives: [MdCheckbox] +}) +class CheckboxFormcontrolController { + model = new FormcontrolModel(); +} + +@Component({ + selector: 'checkbox-end-aligned-controller', + template: ``, + directives: [MdCheckbox] +}) +class CheckboxEndAlignedController {} diff --git a/src/components/checkbox/checkbox.ts b/src/components/checkbox/checkbox.ts new file mode 100644 index 00000000000..0c8c06e6d10 --- /dev/null +++ b/src/components/checkbox/checkbox.ts @@ -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 = 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}`; + } +} diff --git a/src/core/style/_variables.scss b/src/core/style/_variables.scss index 5d89815cc06..d4ce138117c 100644 --- a/src/core/style/_variables.scss +++ b/src/core/style/_variables.scss @@ -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; diff --git a/src/demo-app/checkbox/checkbox-demo.html b/src/demo-app/checkbox/checkbox-demo.html new file mode 100644 index 00000000000..cd108fb13db --- /dev/null +++ b/src/demo-app/checkbox/checkbox-demo.html @@ -0,0 +1,41 @@ +

md-checkbox: Basic Example

+ + Do you want to foobar the bazquux? + - {{printResult()}} +
+ + + + +
+
+

Alignment:

+
+ + +
+
+ + +
+
+ +

Application Example: Nested Checklist

+

Caution: WIP!

+ diff --git a/src/demo-app/checkbox/checkbox-demo.scss b/src/demo-app/checkbox/checkbox-demo.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/demo-app/checkbox/checkbox-demo.ts b/src/demo-app/checkbox/checkbox-demo.ts new file mode 100644 index 00000000000..a1cf2915565 --- /dev/null +++ b/src/demo-app/checkbox/checkbox-demo.ts @@ -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!'; + } +} diff --git a/src/demo-app/checkbox/nested-checklist.html b/src/demo-app/checkbox/nested-checklist.html new file mode 100644 index 00000000000..439202c1a6e --- /dev/null +++ b/src/demo-app/checkbox/nested-checklist.html @@ -0,0 +1,18 @@ +

Tasks

+
    +
  • + +

    {{task.name}}

    +
    +
      +
    • + + {{subtask.name}} + +
    • +
    +
  • +
diff --git a/src/demo-app/demo-app.html b/src/demo-app/demo-app.html index b86a6681bcb..dcf174e09ae 100644 --- a/src/demo-app/demo-app.html +++ b/src/demo-app/demo-app.html @@ -1,16 +1,15 @@ -
-

Angular Material2 Demos

- - -
- -
+

Angular Material2 Demos

+ + +
+
diff --git a/src/demo-app/demo-app.ts b/src/demo-app/demo-app.ts index ca7b94b6029..c39e78f61a0 100644 --- a/src/demo-app/demo-app.ts +++ b/src/demo-app/demo-app.ts @@ -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 { } diff --git a/test/karma.config.ts b/test/karma.config.ts index 8719f49536a..a89437b2dd8 100644 --- a/test/karma.config.ts +++ b/test/karma.config.ts @@ -86,7 +86,8 @@ export function config(config) { }, browserDisconnectTimeout: 20000, - browserNoActivityTimeout: 100000, + browserNoActivityTimeout: 240000, + captureTimeout: 120000, browsers: ['Chrome'], singleRun: false