diff --git a/src/lib/checkbox/_checkbox-theme.scss b/src/lib/checkbox/_checkbox-theme.scss index 6b0e22a5c740..6693d8dcc550 100644 --- a/src/lib/checkbox/_checkbox-theme.scss +++ b/src/lib/checkbox/_checkbox-theme.scss @@ -2,71 +2,9 @@ @mixin md-checkbox-theme($theme) { - $is-dark-theme: map-get($theme, is-dark); $primary: map-get($theme, primary); $accent: map-get($theme, accent); $warn: map-get($theme, warn); - $background: map-get($theme, background); - - - // The color of the checkbox border. - $checkbox-border-color: if($is-dark-theme, rgba(white, 0.7), rgba(black, 0.54)) !default; - - // The color of the checkbox's checkmark / mixedmark. - $checkbox-mark-color: md-color($background, background); - - // 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: #686868; - $black-26pct-opacity-on-light: #b0b0b0; - $disabled-color: if($is-dark-theme, $white-30pct-opacity-on-dark, $black-26pct-opacity-on-light); - - .md-checkbox-frame { - border-color: $checkbox-border-color; - } - - .md-checkbox-checkmark { - fill: $checkbox-mark-color; - } - - .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: $checkbox-mark-color !important; - } - - .md-checkbox-mixedmark { - background-color: $checkbox-mark-color; - } - - .md-checkbox-indeterminate, .md-checkbox-checked { - &.md-primary .md-checkbox-background { - background-color: md-color($primary, 500); - } - - &.md-accent .md-checkbox-background { - background-color: md-color($accent, 500); - } - - &.md-warn .md-checkbox-background { - background-color: md-color($warn, 500); - } - } - - .md-checkbox-disabled { - &.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:not(.md-checkbox-disabled) { &.md-primary .md-checkbox-ripple .md-ripple-foreground { diff --git a/src/lib/checkbox/checkbox.html b/src/lib/checkbox/checkbox.html index 3196048f4992..cc3f8c59185a 100644 --- a/src/lib/checkbox/checkbox.html +++ b/src/lib/checkbox/checkbox.html @@ -20,21 +20,12 @@ [mdRippleCentered]="true" [mdRippleSpeedFactor]="0.3" mdRippleBackgroundColor="rgba(0, 0, 0, 0)"> -
-
- - - - -
-
+ + diff --git a/src/lib/checkbox/checkbox.scss b/src/lib/checkbox/checkbox.scss index 1e0a0d040002..0fa9bc59818b 100644 --- a/src/lib/checkbox/checkbox.scss +++ b/src/lib/checkbox/checkbox.scss @@ -1,202 +1,18 @@ -@import '../core/theming/theming'; -@import '../core/style/elevation'; @import '../core/style/variables'; @import '../core/ripple/ripple'; - -// The width/height of the checkbox element. -$md-checkbox-size: $md-toggle-size !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 base duration used for the majority of transitions for the checkbox. -$md-checkbox-transition-duration: 90ms; -// The amount of spacing between the checkbox and its label. -$md-checkbox-item-spacing: $md-toggle-padding; - -// 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); - // The ripple size of the checkbox $md-checkbox-ripple-size: 15px; -// 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}); -} - -// 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; -} +// The amount of spacing between the checkbox and its label. +$md-checkbox-item-spacing: $md-toggle-padding; md-checkbox { cursor: pointer; +} - // Animation - transition: background $swift-ease-out-duration $swift-ease-out-timing-function, - md-elevation-transition-property-value(); +.md-checkbox-disabled { + cursor: default; } .md-checkbox-layout { @@ -210,15 +26,12 @@ md-checkbox { .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; flex-shrink: 0; [dir='rtl'] & { @@ -234,50 +47,6 @@ md-checkbox { line-height: 24px; } -.md-checkbox-frame { - @extend %md-checkbox-outer-box; - - background-color: transparent; - border: $md-checkbox-border-width solid; - 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; - 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; - - width: 100%; -} - -.md-checkbox-checkmark-path { - 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; - - height: floor($md-checkbox-mark-stroke-size); - opacity: 0; - transform: scaleX(0) rotate(0deg); -} - .md-checkbox-label-before { .md-checkbox-inner-container { order: 1; @@ -295,123 +64,6 @@ md-checkbox { } } -.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-indeterminate { - .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-unchecked { - .md-checkbox-background { - background-color: transparent; - } -} - -.md-checkbox-disabled { - cursor: default; -} - -.md-checkbox-anim { - $indeterminate-change-duration: 500ms; - - &-unchecked-checked { - .md-checkbox-background { - animation: $md-checkbox-transition-duration * 2 linear 0ms 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 0ms md-checkbox-unchecked-checked-checkmark-path; - } - } - - &-unchecked-indeterminate { - .md-checkbox-background { - animation: $md-checkbox-transition-duration * 2 linear 0ms md-checkbox-fade-in-background; - } - - .md-checkbox-mixedmark { - animation: - $md-checkbox-transition-duration linear 0ms md-checkbox-unchecked-indeterminate-mixedmark; - } - } - - &-checked-unchecked { - .md-checkbox-background { - animation: $md-checkbox-transition-duration * 2 linear 0ms md-checkbox-fade-out-background; - } - - .md-checkbox-checkmark-path { - animation: - $md-checkbox-transition-duration linear 0ms md-checkbox-checked-unchecked-checkmark-path; - } - } - - &-checked-indeterminate { - .md-checkbox-checkmark { - animation: - $md-checkbox-transition-duration linear 0ms md-checkbox-checked-indeterminate-checkmark; - } - - .md-checkbox-mixedmark { - animation: - $md-checkbox-transition-duration linear 0ms md-checkbox-checked-indeterminate-mixedmark; - } - } - - &-indeterminate-checked { - .md-checkbox-checkmark { - animation: - $indeterminate-change-duration linear 0ms md-checkbox-indeterminate-checked-checkmark; - } - - .md-checkbox-mixedmark { - animation: - $indeterminate-change-duration linear 0ms md-checkbox-indeterminate-checked-mixedmark; - } - } - - &-indeterminate-unchecked { - .md-checkbox-background { - animation: $md-checkbox-transition-duration * 2 linear 0ms md-checkbox-fade-out-background; - } - - .md-checkbox-mixedmark { - animation: - $indeterminate-change-duration * 0.6 linear 0ms - md-checkbox-indeterminate-unchecked-mixedmark; - } - } -} - .md-checkbox-input { // Move the input to the bottom and in the middle. // Visual improvement to properly show browser popups when being required. diff --git a/src/lib/checkbox/checkbox.spec.ts b/src/lib/checkbox/checkbox.spec.ts index ff830032ffa6..43e106f759d6 100644 --- a/src/lib/checkbox/checkbox.spec.ts +++ b/src/lib/checkbox/checkbox.spec.ts @@ -18,7 +18,6 @@ import {ViewportRuler} from '../core/overlay/position/viewport-ruler'; import {FakeViewportRuler} from '../core/overlay/position/fake-viewport-ruler'; - describe('MdCheckbox', () => { let fixture: ComponentFixture; @@ -66,40 +65,34 @@ describe('MdCheckbox', () => { it('should add and remove the checked state', () => { expect(checkboxInstance.checked).toBe(false); - expect(checkboxNativeElement.classList).not.toContain('md-checkbox-checked'); expect(inputElement.checked).toBe(false); testComponent.isChecked = true; fixture.detectChanges(); expect(checkboxInstance.checked).toBe(true); - expect(checkboxNativeElement.classList).toContain('md-checkbox-checked'); expect(inputElement.checked).toBe(true); testComponent.isChecked = false; fixture.detectChanges(); expect(checkboxInstance.checked).toBe(false); - expect(checkboxNativeElement.classList).not.toContain('md-checkbox-checked'); expect(inputElement.checked).toBe(false); }); it('should add and remove indeterminate state', () => { - expect(checkboxNativeElement.classList).not.toContain('md-checkbox-checked'); expect(inputElement.checked).toBe(false); expect(inputElement.indeterminate).toBe(false); testComponent.isIndeterminate = true; fixture.detectChanges(); - expect(checkboxNativeElement.classList).toContain('md-checkbox-indeterminate'); expect(inputElement.checked).toBe(false); expect(inputElement.indeterminate).toBe(true); testComponent.isIndeterminate = false; fixture.detectChanges(); - expect(checkboxNativeElement.classList).not.toContain('md-checkbox-indeterminate'); expect(inputElement.checked).toBe(false); expect(inputElement.indeterminate).toBe(false); }); @@ -228,13 +221,13 @@ describe('MdCheckbox', () => { spyOn(testComponent, 'onCheckboxClick'); expect(inputElement.checked).toBe(false); - expect(checkboxNativeElement.classList).not.toContain('md-checkbox-checked'); + expect(checkboxInstance.checked).toBe(false); labelElement.click(); fixture.detectChanges(); - expect(checkboxNativeElement.classList).toContain('md-checkbox-checked'); expect(inputElement.checked).toBe(true); + expect(checkboxInstance.checked).toBe(true); expect(testComponent.onCheckboxClick).toHaveBeenCalledTimes(1); }); @@ -243,13 +236,13 @@ describe('MdCheckbox', () => { spyOn(testComponent, 'onCheckboxChange'); expect(inputElement.checked).toBe(false); - expect(checkboxNativeElement.classList).not.toContain('md-checkbox-checked'); + expect(checkboxInstance.checked).toBe(false); labelElement.click(); fixture.detectChanges(); expect(inputElement.checked).toBe(true); - expect(checkboxNativeElement.classList).toContain('md-checkbox-checked'); + expect(checkboxInstance.checked).toBe(true); // Wait for the fixture to become stable, because the EventEmitter for the change event, // will only fire after the zone async change detection has finished. @@ -264,13 +257,13 @@ describe('MdCheckbox', () => { spyOn(testComponent, 'onCheckboxChange'); expect(inputElement.checked).toBe(false); - expect(checkboxNativeElement.classList).not.toContain('md-checkbox-checked'); + expect(checkboxInstance.checked).toBe(false); testComponent.isChecked = true; fixture.detectChanges(); expect(inputElement.checked).toBe(true); - expect(checkboxNativeElement.classList).toContain('md-checkbox-checked'); + expect(checkboxInstance.checked).toBe(true); // Wait for the fixture to become stable, because the EventEmitter for the change event, // will only fire after the zone async change detection has finished. @@ -303,89 +296,6 @@ describe('MdCheckbox', () => { expect(document.activeElement).toBe(inputElement); }); - describe('color behaviour', () => { - it('should apply class based on color attribute', () => { - testComponent.checkboxColor = 'primary'; - fixture.detectChanges(); - expect(checkboxDebugElement.nativeElement.classList.contains('md-primary')).toBe(true); - - testComponent.checkboxColor = 'accent'; - fixture.detectChanges(); - expect(checkboxDebugElement.nativeElement.classList.contains('md-accent')).toBe(true); - }); - - it('should should not clear previous defined classes', () => { - checkboxDebugElement.nativeElement.classList.add('custom-class'); - - testComponent.checkboxColor = 'primary'; - fixture.detectChanges(); - - expect(checkboxDebugElement.nativeElement.classList.contains('md-primary')).toBe(true); - expect(checkboxDebugElement.nativeElement.classList.contains('custom-class')).toBe(true); - - testComponent.checkboxColor = 'accent'; - fixture.detectChanges(); - - expect(checkboxDebugElement.nativeElement.classList.contains('md-primary')).toBe(false); - expect(checkboxDebugElement.nativeElement.classList.contains('md-accent')).toBe(true); - expect(checkboxDebugElement.nativeElement.classList.contains('custom-class')).toBe(true); - - }); - }); - - describe('state transition css classes', () => { - it('should transition unchecked -> checked -> unchecked', () => { - testComponent.isChecked = true; - fixture.detectChanges(); - expect(checkboxNativeElement.classList).toContain('md-checkbox-anim-unchecked-checked'); - - testComponent.isChecked = false; - fixture.detectChanges(); - expect(checkboxNativeElement.classList).not.toContain('md-checkbox-anim-unchecked-checked'); - expect(checkboxNativeElement.classList).toContain('md-checkbox-anim-checked-unchecked'); - }); - - it('should transition unchecked -> indeterminate -> unchecked', () => { - testComponent.isIndeterminate = true; - fixture.detectChanges(); - - expect(checkboxNativeElement.classList) - .toContain('md-checkbox-anim-unchecked-indeterminate'); - - testComponent.isIndeterminate = false; - fixture.detectChanges(); - - expect(checkboxNativeElement.classList) - .not.toContain('md-checkbox-anim-unchecked-indeterminate'); - expect(checkboxNativeElement.classList) - .toContain('md-checkbox-anim-indeterminate-unchecked'); - }); - - it('should transition indeterminate -> checked', () => { - testComponent.isIndeterminate = true; - fixture.detectChanges(); - - testComponent.isChecked = true; - fixture.detectChanges(); - - expect(checkboxNativeElement.classList).not.toContain( - 'md-checkbox-anim-unchecked-indeterminate'); - expect(checkboxNativeElement.classList).toContain('md-checkbox-anim-indeterminate-checked'); - }); - - it('should not apply transition classes when there is no state change', () => { - testComponent.isChecked = checkboxInstance.checked; - fixture.detectChanges(); - expect(checkboxNativeElement).not.toMatch(/^md\-checkbox\-anim/g); - - testComponent.isIndeterminate = checkboxInstance.indeterminate; - expect(checkboxNativeElement).not.toMatch(/^md\-checkbox\-anim/g); - }); - - it('should not initially have any transition classes', () => { - expect(checkboxNativeElement).not.toMatch(/^md\-checkbox\-anim/g); - }); - }); }); describe('with change event and no initial value', () => { diff --git a/src/lib/checkbox/checkbox.ts b/src/lib/checkbox/checkbox.ts index 94836ece0188..d0b2d37dc8e1 100644 --- a/src/lib/checkbox/checkbox.ts +++ b/src/lib/checkbox/checkbox.ts @@ -16,7 +16,7 @@ import { import {CommonModule} from '@angular/common'; import {NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms'; import {coerceBooleanProperty} from '../core/coercion/boolean-property'; -import {MdRippleModule, DefaultStyleCompatibilityModeModule} from '../core'; +import {MdRippleModule, MdSelectionModule, DefaultStyleCompatibilityModeModule} from '../core'; import {ViewportRuler} from '../core/overlay/position/viewport-ruler'; @@ -34,20 +34,6 @@ export const MD_CHECKBOX_CONTROL_VALUE_ACCESSOR: any = { multi: true }; -/** - * Represents the different states that require custom transitions between them. - * @docs-private - */ -export 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 -} /** Change event object emitted by MdCheckbox. */ export class MdCheckboxChange { @@ -69,11 +55,8 @@ export class MdCheckboxChange { templateUrl: 'checkbox.html', styleUrls: ['checkbox.css'], host: { - '[class.md-checkbox-indeterminate]': 'indeterminate', - '[class.md-checkbox-checked]': 'checked', - '[class.md-checkbox-disabled]': 'disabled', '[class.md-checkbox-label-before]': 'labelPosition == "before"', - '[class.md-checkbox-focused]': '_hasFocus', + '[class.md-checkbox-disabled]': 'disabled', }, providers: [MD_CHECKBOX_CONTROL_VALUE_ACCESSOR], encapsulation: ViewEncapsulation.None, @@ -157,25 +140,16 @@ export class MdCheckbox implements ControlValueAccessor { */ onTouched: () => any = () => {}; - private _currentAnimationClass: string = ''; - - private _currentCheckState: TransitionCheckState = TransitionCheckState.Init; - private _checked: boolean = false; - private _indeterminate: boolean = false; - - private _color: string; - private _controlValueAccessorChangeFn: (value: any) => void = (value) => {}; _hasFocus: boolean = false; - constructor(private _renderer: Renderer, - private _elementRef: ElementRef, - private _changeDetectorRef: ChangeDetectorRef) { - this.color = 'accent'; - } + constructor( + private _renderer: Renderer, + private _elementRef: ElementRef, + private _changeDetectorRef: ChangeDetectorRef) { } /** * Whether the checkbox is checked. Note that setting `checked` will immediately set @@ -187,10 +161,8 @@ export class MdCheckbox implements ControlValueAccessor { set checked(checked: boolean) { if (checked != this.checked) { - this._indeterminate = false; + this.indeterminate = false; this._checked = checked; - this._transitionCheckState( - this._checked ? TransitionCheckState.Checked : TransitionCheckState.Unchecked); this._changeDetectorRef.markForCheck(); } } @@ -204,36 +176,10 @@ export class MdCheckbox implements ControlValueAccessor { * `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); - } - } + @Input() indeterminate: boolean = false; /** The color of the button. Can be `primary`, `accent`, or `warn`. */ - @Input() - get color(): string { return this._color; } - set color(value: string) { this._updateColor(value); } - - _updateColor(newColor: string) { - this._setElementColor(this._color, false); - this._setElementColor(newColor, true); - this._color = newColor; - } - - _setElementColor(color: string, isAdd: boolean) { - if (color != null && color != '') { - this._renderer.setElementClass(this._elementRef.nativeElement, `md-${color}`, isAdd); - } - } + @Input() color: string = 'accent'; _isRippleDisabled() { return this.disableRipple || this.disabled; @@ -273,27 +219,6 @@ export class MdCheckbox implements ControlValueAccessor { this.disabled = isDisabled; } - 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 _emitChangeEvent() { let event = new MdCheckboxChange(); event.source = this; @@ -357,36 +282,6 @@ export class MdCheckbox implements ControlValueAccessor { event.stopPropagation(); } - private _getAnimationClassForCheckStateTransition( - oldState: TransitionCheckState, newState: TransitionCheckState): string { - var animSuffix: string; - - switch (oldState) { - case TransitionCheckState.Init: - // Handle edge case where user interacts with checkbox that does not have [(ngModel)] or - // [checked] bound to it. - if (newState === TransitionCheckState.Checked) { - animSuffix = 'unchecked-checked'; - } else { - return ''; - } - break; - 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}`; - } - _getHostElement() { return this._elementRef.nativeElement; } @@ -394,7 +289,12 @@ export class MdCheckbox implements ControlValueAccessor { @NgModule({ - imports: [CommonModule, MdRippleModule, DefaultStyleCompatibilityModeModule], + imports: [ + CommonModule, + MdRippleModule, + MdSelectionModule, + DefaultStyleCompatibilityModeModule + ], exports: [MdCheckbox, DefaultStyleCompatibilityModeModule], declarations: [MdCheckbox], }) diff --git a/src/lib/core/_core.scss b/src/lib/core/_core.scss index 0df852437e9e..be56f00b134f 100644 --- a/src/lib/core/_core.scss +++ b/src/lib/core/_core.scss @@ -5,6 +5,7 @@ @import 'ripple/ripple'; @import 'option/option'; @import 'option/option-theme'; +@import 'selection/pseudo-checkbox/pseudo-checkbox-theme'; // Mixin that renders all of the core styles that are not theme-dependent. @mixin md-core() { @@ -27,4 +28,5 @@ @mixin md-core-theme($theme) { @include md-ripple-theme($theme); @include md-option-theme($theme); + @include md-pseudo-checkbox-theme($theme); } diff --git a/src/lib/core/core.ts b/src/lib/core/core.ts index 062c430e90fb..c8bb037e4e85 100644 --- a/src/lib/core/core.ts +++ b/src/lib/core/core.ts @@ -3,6 +3,7 @@ import {MdLineModule} from './line/line'; import {RtlModule} from './rtl/dir'; import {ObserveContentModule} from './observe-content/observe-content'; import {MdOptionModule} from './option/option'; +import {MdSelectionModule} from './selection/index'; import {MdRippleModule} from './ripple/ripple'; import {PortalModule} from './portal/portal-directives'; import {OverlayModule} from './overlay/overlay-directives'; @@ -18,6 +19,9 @@ export {ObserveContentModule, ObserveContent} from './observe-content/observe-co export {MdOptionModule, MdOption} from './option/option'; +// Selection +export * from './selection/index'; + // Portals export { Portal, @@ -128,7 +132,8 @@ export {NoConflictStyleCompatibilityMode} from './compatibility/no-conflict-mode PortalModule, OverlayModule, A11yModule, - MdOptionModule + MdOptionModule, + MdSelectionModule, ], exports: [ MdLineModule, @@ -138,8 +143,9 @@ export {NoConflictStyleCompatibilityMode} from './compatibility/no-conflict-mode PortalModule, OverlayModule, A11yModule, - MdOptionModule - ], + MdOptionModule, + MdSelectionModule, + ] }) export class MdCoreModule { static forRoot(): ModuleWithProviders { diff --git a/src/lib/core/selection/index.ts b/src/lib/core/selection/index.ts new file mode 100644 index 000000000000..9ab6875da845 --- /dev/null +++ b/src/lib/core/selection/index.ts @@ -0,0 +1,17 @@ +import {NgModule, ModuleWithProviders} from '@angular/core'; +import {MdPseudoCheckbox} from './pseudo-checkbox/pseudo-checkbox'; + +export * from './pseudo-checkbox/pseudo-checkbox'; + +@NgModule({ + exports: [MdPseudoCheckbox], + declarations: [MdPseudoCheckbox] +}) +export class MdSelectionModule { + static forRoot(): ModuleWithProviders { + return { + ngModule: MdSelectionModule, + providers: [] + }; + } +} diff --git a/src/lib/core/selection/pseudo-checkbox/_pseudo-checkbox-theme.scss b/src/lib/core/selection/pseudo-checkbox/_pseudo-checkbox-theme.scss new file mode 100644 index 000000000000..fa8f122b3267 --- /dev/null +++ b/src/lib/core/selection/pseudo-checkbox/_pseudo-checkbox-theme.scss @@ -0,0 +1,70 @@ +@import '../../theming/theming'; + + +@mixin md-pseudo-checkbox-theme($theme) { + $is-dark-theme: map-get($theme, is-dark); + $primary: map-get($theme, primary); + $accent: map-get($theme, accent); + $warn: map-get($theme, warn); + $background: map-get($theme, background); + + + // The color of the checkbox border. + $checkbox-border-color: if($is-dark-theme, rgba(white, 0.7), rgba(black, 0.54)) !default; + + // The color of the checkbox's checkmark / mixedmark. + $checkbox-mark-color: md-color($background, background); + + // 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: #686868; + $black-26pct-opacity-on-light: #b0b0b0; + $disabled-color: if($is-dark-theme, $white-30pct-opacity-on-dark, $black-26pct-opacity-on-light); + + .md-pseudo-checkbox-frame { + border-color: $checkbox-border-color; + } + + .md-pseudo-checkbox-checkmark { + fill: $checkbox-mark-color; + } + + .md-pseudo-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: $checkbox-mark-color !important; + } + + .md-pseudo-checkbox-mixedmark { + background-color: $checkbox-mark-color; + } + + .md-pseudo-checkbox-indeterminate, .md-pseudo-checkbox-checked { + &.md-primary .md-pseudo-checkbox-background { + background-color: md-color($primary, 500); + } + + &.md-accent .md-pseudo-checkbox-background { + background-color: md-color($accent, 500); + } + + &.md-warn .md-pseudo-checkbox-background { + background-color: md-color($warn, 500); + } + } + + .md-pseudo-checkbox-disabled { + &.md-pseudo-checkbox-checked, &.md-pseudo-checkbox-indeterminate { + .md-pseudo-checkbox-background { + background-color: $disabled-color; + } + } + + &:not(.md-pseudo-checkbox-checked) { + .md-pseudo-checkbox-frame { + border-color: $disabled-color; + } + } + } +} diff --git a/src/lib/core/selection/pseudo-checkbox/pseudo-checkbox.html b/src/lib/core/selection/pseudo-checkbox/pseudo-checkbox.html new file mode 100644 index 000000000000..5374ae40f629 --- /dev/null +++ b/src/lib/core/selection/pseudo-checkbox/pseudo-checkbox.html @@ -0,0 +1,15 @@ +
+
+ + + + +
+
diff --git a/src/lib/core/selection/pseudo-checkbox/pseudo-checkbox.scss b/src/lib/core/selection/pseudo-checkbox/pseudo-checkbox.scss new file mode 100644 index 000000000000..e847a3306846 --- /dev/null +++ b/src/lib/core/selection/pseudo-checkbox/pseudo-checkbox.scss @@ -0,0 +1,357 @@ +@import '../../style/variables'; +@import '../../style/elevation'; + +// The width/height of the checkbox element. +$md-pseudo-checkbox-size: $md-toggle-size !default; +// The width of the line used to draw the checkmark / mixedmark. +$md-pseudo-checkbox-mark-stroke-size: 2 / 15 * $md-pseudo-checkbox-size !default; +// The width of the checkbox border shown when the checkbox is unchecked. +$md-pseudo-checkbox-border-width: 2px; +// The base duration used for the majority of transitions for the checkbox. +$md-pseudo-checkbox-transition-duration: 90ms; + +// Manual calculation done on SVG +$_md-pseudo-checkbox-mark-path-length: 22.910259; +$_md-pseudo-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-pseudo-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-pseudo-checkbox-fade-out-background { + 0%, 50% { + opacity: 1; + } + + 100% { + opacity: 0; + } +} + +// "Draws" in the checkmark when the checkbox goes from unchecked -> checked. +@keyframes md-pseudo-checkbox-unchecked-checked-checkmark-path { + 0%, 50% { + stroke-dashoffset: $_md-pseudo-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-pseudo-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-pseudo-checkbox-checked-unchecked-checkmark-path { + from { + animation-timing-function: $md-fast-out-linear-in-timing-function; + stroke-dashoffset: 0; + } + + to { + stroke-dashoffset: $_md-pseudo-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-pseudo-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-pseudo-checkbox-indeterminate-checked-checkmark { + from { + animation-timing-function: $_md-pseudo-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-pseudo-checkbox-checked-indeterminate-checkmark, helps provide an +// illusion of "morphing" from checkmark -> mixedmark. +@keyframes md-pseudo-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-pseudo-checkbox-indeterminate-checked-checkmark, helps provide an +// illusion of "morphing" from mixedmark -> checkmark. +@keyframes md-pseudo-checkbox-indeterminate-checked-mixedmark { + from { + animation-timing-function: $_md-pseudo-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-pseudo-checkbox-indeterminate-unchecked-mixedmark { + 0% { + animation-timing-function: linear; + opacity: 1; + transform: scaleX(1); + } + + 32.8%, 100% { + opacity: 0; + transform: scaleX(0); + } +} + +md-pseudo-checkbox { + transition: background $swift-ease-out-duration $swift-ease-out-timing-function, + md-elevation-transition-property-value(); + + height: $md-pseudo-checkbox-size; + width: $md-pseudo-checkbox-size; + display: inline-block; + position: relative; + vertical-align: middle; +} + +// Applied to elements that are considered "marks" within the checkbox, e.g. the checkmark and +// the mixedmark. +%md-pseudo-checkbox-mark { + $width-padding-inset: 2 * $md-pseudo-checkbox-border-width; + width: calc(100% - #{$width-padding-inset}); +} + +// Applied to elements that cover the checkbox's entire inner container. +%md-pseudo-checkbox-cover-element { + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; +} + +// 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-pseudo-checkbox-outer-box { + @extend %md-pseudo-checkbox-cover-element; + border-radius: 2px; + box-sizing: border-box; + pointer-events: none; +} + +.md-pseudo-checkbox-frame { + @extend %md-pseudo-checkbox-outer-box; + + background-color: transparent; + border: $md-pseudo-checkbox-border-width solid; + will-change: border-color; + transition: border-color $md-pseudo-checkbox-transition-duration + $md-linear-out-slow-in-timing-function; +} + +.md-pseudo-checkbox-background { + @extend %md-pseudo-checkbox-outer-box; + + align-items: center; + display: inline-flex; + justify-content: center; + transition: background-color $md-pseudo-checkbox-transition-duration + $md-linear-out-slow-in-timing-function, + opacity $md-pseudo-checkbox-transition-duration + $md-linear-out-slow-in-timing-function; + will-change: background-color, opacity; +} + +.md-pseudo-checkbox-checkmark { + @extend %md-pseudo-checkbox-cover-element; + @extend %md-pseudo-checkbox-mark; + + width: 100%; +} + +.md-pseudo-checkbox-checkmark-path { + stroke: { + dashoffset: $_md-pseudo-checkbox-mark-path-length; + dasharray: $_md-pseudo-checkbox-mark-path-length; + width: $md-pseudo-checkbox-mark-stroke-size; + } +} + +.md-pseudo-checkbox-mixedmark { + @extend %md-pseudo-checkbox-mark; + + height: floor($md-pseudo-checkbox-mark-stroke-size); + opacity: 0; + transform: scaleX(0) rotate(0deg); +} + +.md-pseudo-checkbox-checked { + .md-pseudo-checkbox-checkmark { + opacity: 1; + } + + .md-pseudo-checkbox-checkmark-path { + stroke-dashoffset: 0; + } + + .md-pseudo-checkbox-mixedmark { + transform: scaleX(1) rotate(-45deg); + } +} + +.md-pseudo-checkbox-indeterminate { + .md-pseudo-checkbox-checkmark { + opacity: 0; + transform: rotate(45deg); + } + + .md-pseudo-checkbox-checkmark-path { + stroke-dashoffset: 0; + } + + .md-pseudo-checkbox-mixedmark { + opacity: 1; + transform: scaleX(1) rotate(0deg); + } +} + +.md-pseudo-checkbox-unchecked { + .md-pseudo-checkbox-background { + background-color: transparent; + } +} + +.md-pseudo-checkbox-anim { + $indeterminate-change-duration: 500ms; + + &-unchecked-checked { + .md-pseudo-checkbox-background { + animation: $md-pseudo-checkbox-transition-duration * 2 linear 0ms + md-pseudo-checkbox-fade-in-background; + } + + .md-pseudo-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-pseudo-checkbox-transition-duration * 2 linear 0ms + md-pseudo-checkbox-unchecked-checked-checkmark-path; + } + } + + &-unchecked-indeterminate { + .md-pseudo-checkbox-background { + animation: $md-pseudo-checkbox-transition-duration * 2 linear 0ms + md-pseudo-checkbox-fade-in-background; + } + + .md-pseudo-checkbox-mixedmark { + animation: $md-pseudo-checkbox-transition-duration linear 0ms + md-pseudo-checkbox-unchecked-indeterminate-mixedmark; + } + } + + &-checked-unchecked { + .md-pseudo-checkbox-background { + animation: $md-pseudo-checkbox-transition-duration * 2 linear 0ms + md-pseudo-checkbox-fade-out-background; + } + + .md-pseudo-checkbox-checkmark-path { + animation: $md-pseudo-checkbox-transition-duration linear 0ms + md-pseudo-checkbox-checked-unchecked-checkmark-path; + } + } + + &-checked-indeterminate { + .md-pseudo-checkbox-checkmark { + animation: $md-pseudo-checkbox-transition-duration linear 0ms + md-pseudo-checkbox-checked-indeterminate-checkmark; + } + + .md-pseudo-checkbox-mixedmark { + animation: $md-pseudo-checkbox-transition-duration linear 0ms + md-pseudo-checkbox-checked-indeterminate-mixedmark; + } + } + + &-indeterminate-checked { + .md-pseudo-checkbox-checkmark { + animation: $indeterminate-change-duration linear 0ms + md-pseudo-checkbox-indeterminate-checked-checkmark; + } + + .md-pseudo-checkbox-mixedmark { + animation: $indeterminate-change-duration linear 0ms + md-pseudo-checkbox-indeterminate-checked-mixedmark; + } + } + + &-indeterminate-unchecked { + .md-pseudo-checkbox-background { + animation: $md-pseudo-checkbox-transition-duration * 2 linear 0ms + md-pseudo-checkbox-fade-out-background; + } + + .md-pseudo-checkbox-mixedmark { + animation: $indeterminate-change-duration * 0.6 linear 0ms + md-pseudo-checkbox-indeterminate-unchecked-mixedmark; + } + } +} + diff --git a/src/lib/core/selection/pseudo-checkbox/pseudo-checkbox.spec.ts b/src/lib/core/selection/pseudo-checkbox/pseudo-checkbox.spec.ts new file mode 100644 index 000000000000..c5428d24bfad --- /dev/null +++ b/src/lib/core/selection/pseudo-checkbox/pseudo-checkbox.spec.ts @@ -0,0 +1,189 @@ +import { + async, + ComponentFixture, + TestBed, +} from '@angular/core/testing'; +import {Component, DebugElement} from '@angular/core'; +import {By} from '@angular/platform-browser'; +import {MdSelectionModule} from '../index'; +import {MdPseudoCheckbox} from './pseudo-checkbox'; + + +describe('MdPseudoCheckbox', () => { + let fixture: ComponentFixture; + let checkboxDebugElement: DebugElement; + let checkboxNativeElement: HTMLElement; + let checkboxInstance: MdPseudoCheckbox; + let testComponent: SimplePseudoCheckbox; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [MdSelectionModule.forRoot()], + declarations: [SimplePseudoCheckbox], + }); + + TestBed.compileComponents().then(() => { + fixture = TestBed.createComponent(SimplePseudoCheckbox); + fixture.detectChanges(); + + checkboxDebugElement = fixture.debugElement.query(By.directive(MdPseudoCheckbox)); + checkboxNativeElement = checkboxDebugElement.nativeElement; + checkboxInstance = checkboxDebugElement.componentInstance; + testComponent = fixture.debugElement.componentInstance; + }); + })); + + it('should add and remove the checked state', () => { + expect(checkboxInstance.checked).toBe(false); + expect(checkboxNativeElement.classList).not.toContain('md-pseudo-checkbox-checked'); + + testComponent.checked = true; + fixture.detectChanges(); + + expect(checkboxInstance.checked).toBe(true); + expect(checkboxNativeElement.classList).toContain('md-pseudo-checkbox-checked'); + + testComponent.checked = false; + fixture.detectChanges(); + + expect(checkboxInstance.checked).toBe(false); + expect(checkboxNativeElement.classList).not.toContain('md-pseudo-checkbox-checked'); + }); + + it('should add and remove indeterminate state', () => { + expect(checkboxNativeElement.classList).not.toContain('md-pseudo-checkbox-checked'); + + testComponent.indeterminate = true; + fixture.detectChanges(); + + expect(checkboxNativeElement.classList).toContain('md-pseudo-checkbox-indeterminate'); + + testComponent.indeterminate = false; + fixture.detectChanges(); + + expect(checkboxNativeElement.classList).not.toContain('md-pseudo-checkbox-indeterminate'); + }); + + it('should add and remove disabled state', () => { + expect(checkboxInstance.disabled).toBe(false); + expect(checkboxNativeElement.classList).not.toContain('md-pseudo-checkbox-disabled'); + + testComponent.disabled = true; + fixture.detectChanges(); + + expect(checkboxInstance.disabled).toBe(true); + expect(checkboxNativeElement.classList).toContain('md-pseudo-checkbox-disabled'); + + testComponent.disabled = false; + fixture.detectChanges(); + + expect(checkboxInstance.disabled).toBe(false); + expect(checkboxNativeElement.classList).not.toContain('md-pseudo-checkbox-disabled'); + }); + + describe('transition classes', () => { + it('should transition unchecked -> checked -> unchecked', () => { + testComponent.checked = true; + fixture.detectChanges(); + expect(checkboxNativeElement.classList).toContain( + 'md-pseudo-checkbox-anim-unchecked-checked'); + + testComponent.checked = false; + fixture.detectChanges(); + expect(checkboxNativeElement.classList).not.toContain( + 'md-pseudo-checkbox-anim-unchecked-checked'); + expect(checkboxNativeElement.classList).toContain( + 'md-pseudo-checkbox-anim-checked-unchecked'); + }); + + it('should transition unchecked -> indeterminate -> unchecked', () => { + testComponent.indeterminate = true; + fixture.detectChanges(); + + expect(checkboxNativeElement.classList) + .toContain('md-pseudo-checkbox-anim-unchecked-indeterminate'); + + testComponent.indeterminate = false; + fixture.detectChanges(); + + expect(checkboxNativeElement.classList) + .not.toContain('md-pseudo-checkbox-anim-unchecked-indeterminate'); + expect(checkboxNativeElement.classList) + .toContain('md-pseudo-checkbox-anim-indeterminate-unchecked'); + }); + + it('should transition indeterminate -> checked', () => { + testComponent.indeterminate = true; + fixture.detectChanges(); + + testComponent.checked = true; + fixture.detectChanges(); + + expect(checkboxNativeElement.classList).not.toContain( + 'md-pseudo-checkbox-anim-unchecked-indeterminate'); + expect(checkboxNativeElement.classList).toContain( + 'md-pseudo-checkbox-anim-indeterminate-checked'); + }); + + it('should not apply transition classes when there is no state change', () => { + testComponent.checked = checkboxInstance.checked; + fixture.detectChanges(); + expect(checkboxNativeElement).not.toMatch(/^md-pseudo-checkbox-anim/g); + + testComponent.indeterminate = checkboxInstance.indeterminate; + expect(checkboxNativeElement).not.toMatch(/^md-pseudo-checkbox-anim/g); + }); + + it('should not initially have any transition classes', () => { + expect(checkboxNativeElement).not.toMatch(/^md-pseudo-checkbox-anim/g); + }); + }); + + describe('color behaviour', () => { + it('should apply class based on color attribute', () => { + testComponent.color = 'primary'; + fixture.detectChanges(); + expect(checkboxDebugElement.nativeElement.classList).toContain('md-primary'); + + testComponent.color = 'accent'; + fixture.detectChanges(); + expect(checkboxDebugElement.nativeElement.classList).toContain('md-accent'); + }); + + it('should should not clear previous defined classes', () => { + checkboxDebugElement.nativeElement.classList.add('custom-class'); + + testComponent.color = 'primary'; + fixture.detectChanges(); + + expect(checkboxDebugElement.nativeElement.classList).toContain('md-primary'); + expect(checkboxDebugElement.nativeElement.classList).toContain('custom-class'); + + testComponent.color = 'accent'; + fixture.detectChanges(); + + expect(checkboxDebugElement.nativeElement.classList).not.toContain('md-primary'); + expect(checkboxDebugElement.nativeElement.classList).toContain('md-accent'); + expect(checkboxDebugElement.nativeElement.classList).toContain('custom-class'); + + }); + }); + +}); + + +@Component({ + template: ` + + ` +}) +export class SimplePseudoCheckbox { + checked = false; + indeterminate = false; + disabled = false; + color = 'accent'; +} diff --git a/src/lib/core/selection/pseudo-checkbox/pseudo-checkbox.ts b/src/lib/core/selection/pseudo-checkbox/pseudo-checkbox.ts new file mode 100644 index 000000000000..5c95f7771437 --- /dev/null +++ b/src/lib/core/selection/pseudo-checkbox/pseudo-checkbox.ts @@ -0,0 +1,156 @@ +import { + Component, + Input, + Renderer, + ElementRef, + ChangeDetectorRef, + ViewEncapsulation, +} from '@angular/core'; + +export type MdPseudoCheckboxState = 'unchecked' | 'checked' | 'indeterminate'; + +/** + * Represents the different states that require custom transitions between them. + * @docs-private + */ +export 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 +} + +/** + * Represents a check box, without any of the underlying form control functionality. + * Intended to be used for composing other components. + * @docs-private + */ +@Component({ + moduleId: module.id, + encapsulation: ViewEncapsulation.None, + selector: 'md-pseudo-checkbox', + styleUrls: ['pseudo-checkbox.css'], + templateUrl: 'pseudo-checkbox.html', + host: { + '[class.md-pseudo-checkbox-indeterminate]': 'indeterminate', + '[class.md-pseudo-checkbox-checked]': 'checked', + '[class.md-pseudo-checkbox-disabled]': 'disabled', + }, +}) +export class MdPseudoCheckbox { + constructor( + private _renderer: Renderer, + private _elementRef: ElementRef, + private _changeDetectorRef: ChangeDetectorRef) { + + this.color = 'accent'; + } + + @Input() disabled: boolean = false; + + @Input() + get checked() { return this._checked; } + set checked(checked: boolean) { + if (checked != this.checked) { + this._indeterminate = false; + this._checked = checked; + this._transitionCheckState( + this._checked ? TransitionCheckState.Checked : TransitionCheckState.Unchecked); + this._changeDetectorRef.markForCheck(); + } + } + + @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 color of the button. Can be `primary`, `accent`, or `warn`. */ + @Input() + get color(): string { return this._color; } + set color(value: string) { this._updateColor(value); } + + private _checked: boolean = false; + + private _indeterminate: boolean = false; + + private _currentCheckState: TransitionCheckState = TransitionCheckState.Init; + + private _currentAnimationClass: string = ''; + + private _color: string; + + 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: + // Handle edge case where user interacts with checkbox that does not have [(ngModel)] or + // [checked] bound to it. + if (newState === TransitionCheckState.Checked) { + animSuffix = 'unchecked-checked'; + } else { + return ''; + } + break; + 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-pseudo-checkbox-anim-${animSuffix}`; + } + + private _updateColor(newColor: string) { + this._setElementColor(this._color, false); + this._setElementColor(newColor, true); + this._color = newColor; + } + + private _setElementColor(color: string, isAdd: boolean) { + if (color != null && color != '') { + this._renderer.setElementClass(this._elementRef.nativeElement, `md-${color}`, isAdd); + } + } +}