diff --git a/src/demo-app/slide-toggle/slide-toggle-demo.html b/src/demo-app/slide-toggle/slide-toggle-demo.html index a1c6192e1563..0a1bcca960fc 100644 --- a/src/demo-app/slide-toggle/slide-toggle-demo.html +++ b/src/demo-app/slide-toggle/slide-toggle-demo.html @@ -1,29 +1,29 @@ -
- - - Default Slide Toggle - - - - Disabled Slide Toggle - - - - Disable Bound - - -

Example where the slide toggle is required inside of a form.

- -
- - - Slide Toggle - - -

- -

- -
- +
+ + + Default Slide Toggle + + + + Disabled Slide Toggle + + + + Disable Bound + + +

Example where the slide toggle is required inside of a form.

+ +
+ + + Slide Toggle + + +

+ +

+ +
+
\ No newline at end of file diff --git a/src/demo-app/slide-toggle/slide-toggle-demo.ts b/src/demo-app/slide-toggle/slide-toggle-demo.ts index 7a8127dc3ac7..26ba4280b577 100644 --- a/src/demo-app/slide-toggle/slide-toggle-demo.ts +++ b/src/demo-app/slide-toggle/slide-toggle-demo.ts @@ -1,17 +1,17 @@ -import {Component} from '@angular/core'; - - -@Component({ - moduleId: module.id, - selector: 'switch-demo', - templateUrl: 'slide-toggle-demo.html', - styleUrls: ['slide-toggle-demo.css'], -}) -export class SlideToggleDemo { - firstToggle: boolean; - - onFormSubmit() { - alert(`You submitted the form.`); - } - -} +import {Component} from '@angular/core'; + + +@Component({ + moduleId: module.id, + selector: 'switch-demo', + templateUrl: 'slide-toggle-demo.html', + styleUrls: ['slide-toggle-demo.css'], +}) +export class SlideToggleDemo { + firstToggle: boolean; + + onFormSubmit() { + alert(`You submitted the form.`); + } + +} diff --git a/src/lib/slide-toggle/slide-toggle.html b/src/lib/slide-toggle/slide-toggle.html index 810fef49b038..ea096cdd8016 100644 --- a/src/lib/slide-toggle/slide-toggle.html +++ b/src/lib/slide-toggle/slide-toggle.html @@ -1,33 +1,33 @@ - + diff --git a/src/lib/slide-toggle/slide-toggle.scss b/src/lib/slide-toggle/slide-toggle.scss index 2959da562c5b..90001626e110 100644 --- a/src/lib/slide-toggle/slide-toggle.scss +++ b/src/lib/slide-toggle/slide-toggle.scss @@ -1,148 +1,148 @@ -@import '../core/style/variables'; -@import '../core/ripple/ripple'; -@import '../core/style/elevation'; - - -$md-slide-toggle-width: 36px !default; -$md-slide-toggle-height: 24px !default; -$md-slide-toggle-bar-height: 14px !default; -$md-slide-toggle-thumb-size: 20px !default; -$md-slide-toggle-margin: 16px !default; -$md-slide-toggle-spacing: 8px !default; - - -@mixin md-switch-ripple() { - // Temporary ripple effect for the thumb of the slide-toggle. - // Bind to the parent selector and specify the current palette. - @include md-temporary-ink-ripple(slide-toggle, true); -} - -md-slide-toggle { - display: flex; - height: $md-slide-toggle-height; - - margin: $md-slide-toggle-margin 0; - line-height: $md-slide-toggle-height; - - white-space: nowrap; - - // Disable user selection to ensure that dragging is smooth without grabbing - // some elements accidentally. - user-select: none; - - outline: none; - - &.md-checked { - .md-slide-toggle-thumb-container { - transform: translate3d(100%, 0, 0); - } - } - - @include md-switch-ripple(); - - &.md-disabled { - - .md-slide-toggle-label, .md-slide-toggle-container { - cursor: default; - } - } -} - -// The content element is responsible for the users content. -// It will apply the given typography styles and align at the end of the slide-toggle. -.md-slide-toggle-content { - font-size: $md-body-font-size-base; - font-family: $md-font-family; - font-weight: 500; -} - -// The label element is our root container for the slide-toggle / switch indicator and label text. -// It has to be a label, to support accessibility for the visual hidden input. -.md-slide-toggle-label { - display: flex; - flex: 1; - - cursor: pointer; -} - -// Container for the composition of the slide-toggle / switch indicator. -.md-slide-toggle-container { - cursor: grab; - width: $md-slide-toggle-width; - height: $md-slide-toggle-height; - - position: relative; - - margin-right: $md-slide-toggle-spacing; - - [dir='rtl'] & { - margin-left: $md-slide-toggle-spacing; - margin-right: 0; - } -} - -// The thumb container is responsible for the dragging functionality. -// It moves around and holds the actual circle as a thumb. -.md-slide-toggle-thumb-container { - position: absolute; - top: $md-slide-toggle-height / 2 - $md-slide-toggle-thumb-size / 2; - left: 0; - z-index: 1; - - width: $md-slide-toggle-width - $md-slide-toggle-thumb-size; - - transform: translate3d(0, 0, 0); - - transition: $swift-linear; - transition-property: transform; - - // Once the thumb container is being dragged around, we remove the transition duration to - // make the drag feeling fast and not delayed. - &.md-dragging { - transition-duration: 0ms; - } -} - -// The thumb will be elevated from the slide-toggle bar. -// Also the thumb is bound to its parent thumb-container, which manages the movement of the thumb. -.md-slide-toggle-thumb { - position: absolute; - margin: 0; - left: 0; - top: 0; - - height: $md-slide-toggle-thumb-size; - width: $md-slide-toggle-thumb-size; - border-radius: 50%; - - @include md-elevation(1); -} - -// Horizontal bar for the slide-toggle. -// The slide-toggle bar is shown behind the thumb container. -.md-slide-toggle-bar { - position: absolute; - left: 1px; - top: $md-slide-toggle-height / 2 - $md-slide-toggle-bar-height / 2; - - width: $md-slide-toggle-width - 2px; - height: $md-slide-toggle-bar-height; - - border-radius: 8px; -} - -// The slide toggle shows a visually hidden input inside of the component, which is used -// to take advantage of the native browser functionality. -.md-slide-toggle-input { - // Move the input to the bottom and in the middle of the thumb. - // Visual improvement to properly show browser popups when being required. - bottom: 0; - left: $md-slide-toggle-thumb-size / 2; -} - -.md-slide-toggle-bar, -.md-slide-toggle-thumb { - transition: $swift-linear; - transition-property: background-color; - transition-delay: 50ms; -} +@import '../core/style/variables'; +@import '../core/ripple/ripple'; +@import '../core/style/elevation'; + + +$md-slide-toggle-width: 36px !default; +$md-slide-toggle-height: 24px !default; +$md-slide-toggle-bar-height: 14px !default; +$md-slide-toggle-thumb-size: 20px !default; +$md-slide-toggle-margin: 16px !default; +$md-slide-toggle-spacing: 8px !default; + + +@mixin md-switch-ripple() { + // Temporary ripple effect for the thumb of the slide-toggle. + // Bind to the parent selector and specify the current palette. + @include md-temporary-ink-ripple(slide-toggle, true); +} + +md-slide-toggle { + display: flex; + height: $md-slide-toggle-height; + + margin: $md-slide-toggle-margin 0; + line-height: $md-slide-toggle-height; + + white-space: nowrap; + + // Disable user selection to ensure that dragging is smooth without grabbing + // some elements accidentally. + user-select: none; + + outline: none; + + &.md-checked { + .md-slide-toggle-thumb-container { + transform: translate3d(100%, 0, 0); + } + } + + @include md-switch-ripple(); + + &.md-disabled { + + .md-slide-toggle-label, .md-slide-toggle-container { + cursor: default; + } + } +} + +// The content element is responsible for the users content. +// It will apply the given typography styles and align at the end of the slide-toggle. +.md-slide-toggle-content { + font-size: $md-body-font-size-base; + font-family: $md-font-family; + font-weight: 500; +} + +// The label element is our root container for the slide-toggle / switch indicator and label text. +// It has to be a label, to support accessibility for the visual hidden input. +.md-slide-toggle-label { + display: flex; + flex: 1; + + cursor: pointer; +} + +// Container for the composition of the slide-toggle / switch indicator. +.md-slide-toggle-container { + cursor: grab; + width: $md-slide-toggle-width; + height: $md-slide-toggle-height; + + position: relative; + + margin-right: $md-slide-toggle-spacing; + + [dir='rtl'] & { + margin-left: $md-slide-toggle-spacing; + margin-right: 0; + } +} + +// The thumb container is responsible for the dragging functionality. +// It moves around and holds the actual circle as a thumb. +.md-slide-toggle-thumb-container { + position: absolute; + top: $md-slide-toggle-height / 2 - $md-slide-toggle-thumb-size / 2; + left: 0; + z-index: 1; + + width: $md-slide-toggle-width - $md-slide-toggle-thumb-size; + + transform: translate3d(0, 0, 0); + + transition: $swift-linear; + transition-property: transform; + + // Once the thumb container is being dragged around, we remove the transition duration to + // make the drag feeling fast and not delayed. + &.md-dragging { + transition-duration: 0ms; + } +} + +// The thumb will be elevated from the slide-toggle bar. +// Also the thumb is bound to its parent thumb-container, which manages the movement of the thumb. +.md-slide-toggle-thumb { + position: absolute; + margin: 0; + left: 0; + top: 0; + + height: $md-slide-toggle-thumb-size; + width: $md-slide-toggle-thumb-size; + border-radius: 50%; + + @include md-elevation(1); +} + +// Horizontal bar for the slide-toggle. +// The slide-toggle bar is shown behind the thumb container. +.md-slide-toggle-bar { + position: absolute; + left: 1px; + top: $md-slide-toggle-height / 2 - $md-slide-toggle-bar-height / 2; + + width: $md-slide-toggle-width - 2px; + height: $md-slide-toggle-bar-height; + + border-radius: 8px; +} + +// The slide toggle shows a visually hidden input inside of the component, which is used +// to take advantage of the native browser functionality. +.md-slide-toggle-input { + // Move the input to the bottom and in the middle of the thumb. + // Visual improvement to properly show browser popups when being required. + bottom: 0; + left: $md-slide-toggle-thumb-size / 2; +} + +.md-slide-toggle-bar, +.md-slide-toggle-thumb { + transition: $swift-linear; + transition-property: background-color; + transition-delay: 50ms; +} diff --git a/src/lib/slide-toggle/slide-toggle.spec.ts b/src/lib/slide-toggle/slide-toggle.spec.ts index 754dcd50b44c..c1b8061c3e78 100644 --- a/src/lib/slide-toggle/slide-toggle.spec.ts +++ b/src/lib/slide-toggle/slide-toggle.spec.ts @@ -1,574 +1,574 @@ -import {async, ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing'; -import {By, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser'; -import {Component} from '@angular/core'; -import {MdSlideToggle, MdSlideToggleChange, MdSlideToggleModule} from './slide-toggle'; -import {FormsModule, NgControl} from '@angular/forms'; -import {TestGestureConfig} from '../slider/test-gesture-config'; - -describe('MdSlideToggle', () => { - - let gestureConfig: TestGestureConfig; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [MdSlideToggleModule.forRoot(), FormsModule], - declarations: [SlideToggleTestApp, SlideToggleFormsTestApp], - providers: [ - {provide: HAMMER_GESTURE_CONFIG, useFactory: () => gestureConfig = new TestGestureConfig()} - ] - }); - - TestBed.compileComponents(); - })); - - describe('basic behavior', () => { - let fixture: ComponentFixture; - - let testComponent: SlideToggleTestApp; - let slideToggle: MdSlideToggle; - let slideToggleElement: HTMLElement; - let slideToggleControl: NgControl; - let labelElement: HTMLLabelElement; - let inputElement: HTMLInputElement; - - // This initialization is async() because it needs to wait for ngModel to set the initial value. - beforeEach(async(() => { - fixture = TestBed.createComponent(SlideToggleTestApp); - - testComponent = fixture.debugElement.componentInstance; - - // Enable jasmine spies on event functions, which may trigger at initialization - // of the slide-toggle component. - spyOn(fixture.debugElement.componentInstance, 'onSlideChange').and.callThrough(); - spyOn(fixture.debugElement.componentInstance, 'onSlideClick').and.callThrough(); - - // Initialize the slide-toggle component, by triggering the first change detection cycle. - fixture.detectChanges(); - - let slideToggleDebug = fixture.debugElement.query(By.css('md-slide-toggle')); - - slideToggle = slideToggleDebug.componentInstance; - slideToggleElement = slideToggleDebug.nativeElement; - slideToggleControl = slideToggleDebug.injector.get(NgControl); - inputElement = fixture.debugElement.query(By.css('input')).nativeElement; - labelElement = fixture.debugElement.query(By.css('label')).nativeElement; - })); - - // TODO(kara); update when core/testing adds fix - it('should update the model correctly', async(() => { - expect(slideToggleElement.classList).not.toContain('md-checked'); - - testComponent.slideModel = true; - fixture.detectChanges(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - expect(slideToggleElement.classList).toContain('md-checked'); - }); - - })); - - it('should apply class based on color attribute', () => { - testComponent.slideColor = 'primary'; - fixture.detectChanges(); - - expect(slideToggleElement.classList).toContain('md-primary'); - - testComponent.slideColor = 'accent'; - fixture.detectChanges(); - - expect(slideToggleElement.classList).toContain('md-accent'); - }); - - it('should correctly update the disabled property', () => { - expect(inputElement.disabled).toBeFalsy(); - - testComponent.isDisabled = true; - fixture.detectChanges(); - - expect(inputElement.disabled).toBeTruthy(); - }); - - it('should correctly update the checked property', () => { - expect(slideToggle.checked).toBeFalsy(); - - testComponent.slideChecked = true; - fixture.detectChanges(); - - expect(inputElement.checked).toBeTruthy(); - }); - - it('should set the toggle to checked on click', () => { - expect(slideToggle.checked).toBe(false); - expect(slideToggleElement.classList).not.toContain('md-checked'); - - labelElement.click(); - fixture.detectChanges(); - - expect(slideToggleElement.classList).toContain('md-checked'); - expect(slideToggle.checked).toBe(true); - }); - - it('should not trigger the click event multiple times', () => { - // By default, when clicking on a label element, a generated click will be dispatched - // on the associated input element. - // Since we're using a label element and a visual hidden input, this behavior can led - // to an issue, where the click events on the slide-toggle are getting executed twice. - - expect(slideToggle.checked).toBe(false); - expect(slideToggleElement.classList).not.toContain('md-checked'); - - labelElement.click(); - fixture.detectChanges(); - - expect(slideToggleElement.classList).toContain('md-checked'); - expect(slideToggle.checked).toBe(true); - expect(testComponent.onSlideClick).toHaveBeenCalledTimes(1); - }); - - it('should trigger the change event properly', () => { - expect(inputElement.checked).toBe(false); - expect(slideToggleElement.classList).not.toContain('md-checked'); - - labelElement.click(); - fixture.detectChanges(); - - expect(inputElement.checked).toBe(true); - expect(slideToggleElement.classList).toContain('md-checked'); - expect(testComponent.onSlideChange).toHaveBeenCalledTimes(1); - }); - - it('should not trigger the change event by changing the native value', async(() => { - expect(inputElement.checked).toBe(false); - expect(slideToggleElement.classList).not.toContain('md-checked'); - - testComponent.slideChecked = true; - fixture.detectChanges(); - - expect(inputElement.checked).toBe(true); - expect(slideToggleElement.classList).toContain('md-checked'); - - // The change event shouldn't fire because the value change was not caused - // by any interaction. Use whenStable to ensure an event isn't fired asynchronously. - fixture.whenStable().then(() => { - expect(testComponent.onSlideChange).not.toHaveBeenCalled(); - }); - })); - - it('should not trigger the change event on initialization', async(() => { - expect(inputElement.checked).toBe(false); - expect(slideToggleElement.classList).not.toContain('md-checked'); - - testComponent.slideChecked = true; - fixture.detectChanges(); - - expect(inputElement.checked).toBe(true); - expect(slideToggleElement.classList).toContain('md-checked'); - - // The change event shouldn't fire, because the native input element is not focused. - // Use whenStable to ensure an event isn't fired asynchronously. - fixture.whenStable().then(() => { - expect(testComponent.onSlideChange).not.toHaveBeenCalled(); - }); - })); - - it('should add a suffix to the inputs id', () => { - testComponent.slideId = 'myId'; - fixture.detectChanges(); - - expect(inputElement.id).toBe('myId-input'); - - testComponent.slideId = 'nextId'; - fixture.detectChanges(); - - expect(inputElement.id).toBe('nextId-input'); - - testComponent.slideId = null; - fixture.detectChanges(); - - // Once the id input is falsy, we use a default prefix with a incrementing unique number. - expect(inputElement.id).toMatch(/md-slide-toggle-[0-9]+-input/g); - }); - - it('should forward the specified name to the input', () => { - testComponent.slideName = 'myName'; - fixture.detectChanges(); - - expect(inputElement.name).toBe('myName'); - - testComponent.slideName = 'nextName'; - fixture.detectChanges(); - - expect(inputElement.name).toBe('nextName'); - - testComponent.slideName = null; - fixture.detectChanges(); - - expect(inputElement.name).toBe(''); - }); - - it('should forward the aria-label attribute to the input', () => { - testComponent.slideLabel = 'ariaLabel'; - fixture.detectChanges(); - - expect(inputElement.getAttribute('aria-label')).toBe('ariaLabel'); - - testComponent.slideLabel = null; - fixture.detectChanges(); - - expect(inputElement.hasAttribute('aria-label')).toBeFalsy(); - }); - - it('should forward the aria-labelledby attribute to the input', () => { - testComponent.slideLabelledBy = 'ariaLabelledBy'; - fixture.detectChanges(); - - expect(inputElement.getAttribute('aria-labelledby')).toBe('ariaLabelledBy'); - - testComponent.slideLabelledBy = null; - fixture.detectChanges(); - - expect(inputElement.hasAttribute('aria-labelledby')).toBeFalsy(); - }); - - it('should be initially set to ng-pristine', () => { - expect(slideToggleElement.classList).toContain('ng-pristine'); - expect(slideToggleElement.classList).not.toContain('ng-dirty'); - }); - - it('should emit the new values properly', async(() => { - labelElement.click(); - fixture.detectChanges(); - - fixture.whenStable().then(() => { - // We're checking the arguments type / emitted value to be a boolean, because sometimes the - // emitted value can be a DOM Event, which is not valid. - // See angular/angular#4059 - expect(testComponent.lastEvent.checked).toBe(true); - }); - })); - - it('should support subscription on the change observable', () => { - slideToggle.change.subscribe((event: MdSlideToggleChange) => { - expect(event.checked).toBe(true); - }); - - slideToggle.toggle(); - fixture.detectChanges(); - }); - - it('should have the correct control state initially and after interaction', () => { - // The control should start off valid, pristine, and untouched. - expect(slideToggleControl.valid).toBe(true); - expect(slideToggleControl.pristine).toBe(true); - expect(slideToggleControl.touched).toBe(false); - - // After changing the value programmatically, the control should - // become dirty (not pristine), but remain untouched. - slideToggle.checked = true; - fixture.detectChanges(); - - expect(slideToggleControl.valid).toBe(true); - expect(slideToggleControl.pristine).toBe(false); - expect(slideToggleControl.touched).toBe(false); - - // After a user interaction occurs (such as a click), the control should remain dirty and - // now also be touched. - labelElement.click(); - fixture.detectChanges(); - - expect(slideToggleControl.valid).toBe(true); - expect(slideToggleControl.pristine).toBe(false); - expect(slideToggleControl.touched).toBe(true); - }); - - it('should not set the control to touched when changing the state programmatically', () => { - // The control should start off with being untouched. - expect(slideToggleControl.touched).toBe(false); - - testComponent.slideChecked = true; - fixture.detectChanges(); - - expect(slideToggleControl.touched).toBe(false); - expect(slideToggleElement.classList).toContain('md-checked'); - - // After a user interaction occurs (such as a click), the control should remain dirty and - // now also be touched. - inputElement.click(); - fixture.detectChanges(); - - expect(slideToggleControl.touched).toBe(true); - expect(slideToggleElement.classList).not.toContain('md-checked'); - }); - - // TODO(kara): update when core/testing adds fix - it('should not set the control to touched when changing the model', async(() => { - // The control should start off with being untouched. - expect(slideToggleControl.touched).toBe(false); - - testComponent.slideModel = true; - fixture.detectChanges(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - expect(slideToggleControl.touched).toBe(false); - expect(slideToggle.checked).toBe(true); - expect(slideToggleElement.classList).toContain('md-checked'); - }); - })); - - it('should correctly set the slide-toggle to checked on focus', () => { - expect(slideToggleElement.classList).not.toContain('md-slide-toggle-focused'); - - dispatchFocusChangeEvent('focus', inputElement); - fixture.detectChanges(); - - expect(slideToggleElement.classList).toContain('md-slide-toggle-focused'); - }); - - it('should forward the required attribute', () => { - testComponent.isRequired = true; - fixture.detectChanges(); - - expect(inputElement.required).toBe(true); - - testComponent.isRequired = false; - fixture.detectChanges(); - - expect(inputElement.required).toBe(false); - }); - - }); - - describe('custom template', () => { - it('should not trigger the change event on initialization', async(() => { - let fixture = TestBed.createComponent(SlideToggleTestApp); - fixture.componentInstance.slideModel = true; - fixture.componentInstance.slideChecked = true; - fixture.detectChanges(); - - expect(fixture.componentInstance.lastEvent).toBeFalsy(); - })); - }); - - describe('with forms', () => { - - let fixture: ComponentFixture; - let testComponent: SlideToggleFormsTestApp; - let buttonElement: HTMLButtonElement; - let labelElement: HTMLLabelElement; - let inputElement: HTMLInputElement; - - // This initialization is async() because it needs to wait for ngModel to set the initial value. - beforeEach(async(() => { - fixture = TestBed.createComponent(SlideToggleFormsTestApp); - - testComponent = fixture.debugElement.componentInstance; - - fixture.detectChanges(); - - buttonElement = fixture.debugElement.query(By.css('button')).nativeElement; - labelElement = fixture.debugElement.query(By.css('label')).nativeElement; - inputElement = fixture.debugElement.query(By.css('input')).nativeElement; - })); - - it('should prevent the form from submit when being required', () => { - - if ('reportValidity' in inputElement === false) { - // If the browser does not report the validity then the tests will break. - // e.g Safari 8 on Mobile. - return; - } - - testComponent.isRequired = true; - - fixture.detectChanges(); - - buttonElement.click(); - fixture.detectChanges(); - - expect(testComponent.isSubmitted).toBe(false); - - testComponent.isRequired = false; - fixture.detectChanges(); - - buttonElement.click(); - fixture.detectChanges(); - - expect(testComponent.isSubmitted).toBe(true); - }); - - }); - - describe('with dragging', () => { - - let fixture: ComponentFixture; - - let testComponent: SlideToggleTestApp; - let slideToggle: MdSlideToggle; - let slideToggleElement: HTMLElement; - let slideToggleControl: NgControl; - let slideThumbContainer: HTMLElement; - - beforeEach(async(() => { - fixture = TestBed.createComponent(SlideToggleTestApp); - - testComponent = fixture.debugElement.componentInstance; - - fixture.detectChanges(); - - let slideToggleDebug = fixture.debugElement.query(By.css('md-slide-toggle')); - let thumbContainerDebug = slideToggleDebug.query(By.css('.md-slide-toggle-thumb-container')); - - slideToggle = slideToggleDebug.componentInstance; - slideToggleElement = slideToggleDebug.nativeElement; - slideToggleControl = slideToggleDebug.injector.get(NgControl); - slideThumbContainer = thumbContainerDebug.nativeElement; - })); - - it('should drag from start to end', fakeAsync(() => { - expect(slideToggle.checked).toBe(false); - - gestureConfig.emitEventForElement('slidestart', slideThumbContainer); - - expect(slideThumbContainer.classList).toContain('md-dragging'); - - gestureConfig.emitEventForElement('slide', slideThumbContainer, { - deltaX: 200 // Arbitrary, large delta that will be clamped to the end of the slide-toggle. - }); - - gestureConfig.emitEventForElement('slideend', slideThumbContainer); - - // Flush the timeout for the slide ending. - tick(); - - expect(slideToggle.checked).toBe(true); - expect(slideThumbContainer.classList).not.toContain('md-dragging'); - })); - - it('should drag from end to start', fakeAsync(() => { - slideToggle.checked = true; - - gestureConfig.emitEventForElement('slidestart', slideThumbContainer); - - expect(slideThumbContainer.classList).toContain('md-dragging'); - - gestureConfig.emitEventForElement('slide', slideThumbContainer, { - deltaX: -200 // Arbitrary, large delta that will be clamped to the end of the slide-toggle. - }); - - gestureConfig.emitEventForElement('slideend', slideThumbContainer); - - // Flush the timeout for the slide ending. - tick(); - - expect(slideToggle.checked).toBe(false); - expect(slideThumbContainer.classList).not.toContain('md-dragging'); - })); - - it('should not drag when disbaled', fakeAsync(() => { - slideToggle.disabled = true; - - expect(slideToggle.checked).toBe(false); - - gestureConfig.emitEventForElement('slidestart', slideThumbContainer); - - expect(slideThumbContainer.classList).not.toContain('md-dragging'); - - gestureConfig.emitEventForElement('slide', slideThumbContainer, { - deltaX: 200 // Arbitrary, large delta that will be clamped to the end of the slide-toggle. - }); - - gestureConfig.emitEventForElement('slideend', slideThumbContainer); - - // Flush the timeout for the slide ending. - tick(); - - expect(slideToggle.checked).toBe(false); - expect(slideThumbContainer.classList).not.toContain('md-dragging'); - })); - - it('should should emit a change event after drag', fakeAsync(() => { - expect(slideToggle.checked).toBe(false); - - gestureConfig.emitEventForElement('slidestart', slideThumbContainer); - - expect(slideThumbContainer.classList).toContain('md-dragging'); - - gestureConfig.emitEventForElement('slide', slideThumbContainer, { - deltaX: 200 // Arbitrary, large delta that will be clamped to the end of the slide-toggle. - }); - - gestureConfig.emitEventForElement('slideend', slideThumbContainer); - - // Flush the timeout for the slide ending. - tick(); - - expect(slideToggle.checked).toBe(true); - expect(slideThumbContainer.classList).not.toContain('md-dragging'); - expect(testComponent.lastEvent.checked).toBe(true); - })); - - }); - -}); - -/** - * Dispatches a focus change event from an element. - * @param eventName Name of the event, either 'focus' or 'blur'. - * @param element The element from which the event will be dispatched. - */ -function dispatchFocusChangeEvent(eventName: string, element: HTMLElement): void { - let event = document.createEvent('Event'); - event.initEvent(eventName, true, true); - element.dispatchEvent(event); -} - -@Component({ - selector: 'slide-toggle-test-app', - template: ` - - - Test Slide Toggle - - `, -}) -class SlideToggleTestApp { - isDisabled: boolean = false; - isRequired: boolean = false; - slideModel: boolean = false; - slideChecked: boolean = false; - slideColor: string; - slideId: string; - slideName: string; - slideLabel: string; - slideLabelledBy: string; - lastEvent: MdSlideToggleChange; - - onSlideClick(event: Event) {} - onSlideChange(event: MdSlideToggleChange) { - this.lastEvent = event; - } -} - - -@Component({ - selector: 'slide-toggle-forms-test-app', - template: ` -
- Required - -
` -}) -class SlideToggleFormsTestApp { - isSubmitted: boolean = false; - isRequired: boolean = false; -} +import {async, ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing'; +import {By, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser'; +import {Component} from '@angular/core'; +import {MdSlideToggle, MdSlideToggleChange, MdSlideToggleModule} from './slide-toggle'; +import {FormsModule, NgControl} from '@angular/forms'; +import {TestGestureConfig} from '../slider/test-gesture-config'; + +describe('MdSlideToggle', () => { + + let gestureConfig: TestGestureConfig; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [MdSlideToggleModule.forRoot(), FormsModule], + declarations: [SlideToggleTestApp, SlideToggleFormsTestApp], + providers: [ + {provide: HAMMER_GESTURE_CONFIG, useFactory: () => gestureConfig = new TestGestureConfig()} + ] + }); + + TestBed.compileComponents(); + })); + + describe('basic behavior', () => { + let fixture: ComponentFixture; + + let testComponent: SlideToggleTestApp; + let slideToggle: MdSlideToggle; + let slideToggleElement: HTMLElement; + let slideToggleControl: NgControl; + let labelElement: HTMLLabelElement; + let inputElement: HTMLInputElement; + + // This initialization is async() because it needs to wait for ngModel to set the initial value. + beforeEach(async(() => { + fixture = TestBed.createComponent(SlideToggleTestApp); + + testComponent = fixture.debugElement.componentInstance; + + // Enable jasmine spies on event functions, which may trigger at initialization + // of the slide-toggle component. + spyOn(fixture.debugElement.componentInstance, 'onSlideChange').and.callThrough(); + spyOn(fixture.debugElement.componentInstance, 'onSlideClick').and.callThrough(); + + // Initialize the slide-toggle component, by triggering the first change detection cycle. + fixture.detectChanges(); + + let slideToggleDebug = fixture.debugElement.query(By.css('md-slide-toggle')); + + slideToggle = slideToggleDebug.componentInstance; + slideToggleElement = slideToggleDebug.nativeElement; + slideToggleControl = slideToggleDebug.injector.get(NgControl); + inputElement = fixture.debugElement.query(By.css('input')).nativeElement; + labelElement = fixture.debugElement.query(By.css('label')).nativeElement; + })); + + // TODO(kara); update when core/testing adds fix + it('should update the model correctly', async(() => { + expect(slideToggleElement.classList).not.toContain('md-checked'); + + testComponent.slideModel = true; + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(slideToggleElement.classList).toContain('md-checked'); + }); + + })); + + it('should apply class based on color attribute', () => { + testComponent.slideColor = 'primary'; + fixture.detectChanges(); + + expect(slideToggleElement.classList).toContain('md-primary'); + + testComponent.slideColor = 'accent'; + fixture.detectChanges(); + + expect(slideToggleElement.classList).toContain('md-accent'); + }); + + it('should correctly update the disabled property', () => { + expect(inputElement.disabled).toBeFalsy(); + + testComponent.isDisabled = true; + fixture.detectChanges(); + + expect(inputElement.disabled).toBeTruthy(); + }); + + it('should correctly update the checked property', () => { + expect(slideToggle.checked).toBeFalsy(); + + testComponent.slideChecked = true; + fixture.detectChanges(); + + expect(inputElement.checked).toBeTruthy(); + }); + + it('should set the toggle to checked on click', () => { + expect(slideToggle.checked).toBe(false); + expect(slideToggleElement.classList).not.toContain('md-checked'); + + labelElement.click(); + fixture.detectChanges(); + + expect(slideToggleElement.classList).toContain('md-checked'); + expect(slideToggle.checked).toBe(true); + }); + + it('should not trigger the click event multiple times', () => { + // By default, when clicking on a label element, a generated click will be dispatched + // on the associated input element. + // Since we're using a label element and a visual hidden input, this behavior can led + // to an issue, where the click events on the slide-toggle are getting executed twice. + + expect(slideToggle.checked).toBe(false); + expect(slideToggleElement.classList).not.toContain('md-checked'); + + labelElement.click(); + fixture.detectChanges(); + + expect(slideToggleElement.classList).toContain('md-checked'); + expect(slideToggle.checked).toBe(true); + expect(testComponent.onSlideClick).toHaveBeenCalledTimes(1); + }); + + it('should trigger the change event properly', () => { + expect(inputElement.checked).toBe(false); + expect(slideToggleElement.classList).not.toContain('md-checked'); + + labelElement.click(); + fixture.detectChanges(); + + expect(inputElement.checked).toBe(true); + expect(slideToggleElement.classList).toContain('md-checked'); + expect(testComponent.onSlideChange).toHaveBeenCalledTimes(1); + }); + + it('should not trigger the change event by changing the native value', async(() => { + expect(inputElement.checked).toBe(false); + expect(slideToggleElement.classList).not.toContain('md-checked'); + + testComponent.slideChecked = true; + fixture.detectChanges(); + + expect(inputElement.checked).toBe(true); + expect(slideToggleElement.classList).toContain('md-checked'); + + // The change event shouldn't fire because the value change was not caused + // by any interaction. Use whenStable to ensure an event isn't fired asynchronously. + fixture.whenStable().then(() => { + expect(testComponent.onSlideChange).not.toHaveBeenCalled(); + }); + })); + + it('should not trigger the change event on initialization', async(() => { + expect(inputElement.checked).toBe(false); + expect(slideToggleElement.classList).not.toContain('md-checked'); + + testComponent.slideChecked = true; + fixture.detectChanges(); + + expect(inputElement.checked).toBe(true); + expect(slideToggleElement.classList).toContain('md-checked'); + + // The change event shouldn't fire, because the native input element is not focused. + // Use whenStable to ensure an event isn't fired asynchronously. + fixture.whenStable().then(() => { + expect(testComponent.onSlideChange).not.toHaveBeenCalled(); + }); + })); + + it('should add a suffix to the inputs id', () => { + testComponent.slideId = 'myId'; + fixture.detectChanges(); + + expect(inputElement.id).toBe('myId-input'); + + testComponent.slideId = 'nextId'; + fixture.detectChanges(); + + expect(inputElement.id).toBe('nextId-input'); + + testComponent.slideId = null; + fixture.detectChanges(); + + // Once the id input is falsy, we use a default prefix with a incrementing unique number. + expect(inputElement.id).toMatch(/md-slide-toggle-[0-9]+-input/g); + }); + + it('should forward the specified name to the input', () => { + testComponent.slideName = 'myName'; + fixture.detectChanges(); + + expect(inputElement.name).toBe('myName'); + + testComponent.slideName = 'nextName'; + fixture.detectChanges(); + + expect(inputElement.name).toBe('nextName'); + + testComponent.slideName = null; + fixture.detectChanges(); + + expect(inputElement.name).toBe(''); + }); + + it('should forward the aria-label attribute to the input', () => { + testComponent.slideLabel = 'ariaLabel'; + fixture.detectChanges(); + + expect(inputElement.getAttribute('aria-label')).toBe('ariaLabel'); + + testComponent.slideLabel = null; + fixture.detectChanges(); + + expect(inputElement.hasAttribute('aria-label')).toBeFalsy(); + }); + + it('should forward the aria-labelledby attribute to the input', () => { + testComponent.slideLabelledBy = 'ariaLabelledBy'; + fixture.detectChanges(); + + expect(inputElement.getAttribute('aria-labelledby')).toBe('ariaLabelledBy'); + + testComponent.slideLabelledBy = null; + fixture.detectChanges(); + + expect(inputElement.hasAttribute('aria-labelledby')).toBeFalsy(); + }); + + it('should be initially set to ng-pristine', () => { + expect(slideToggleElement.classList).toContain('ng-pristine'); + expect(slideToggleElement.classList).not.toContain('ng-dirty'); + }); + + it('should emit the new values properly', async(() => { + labelElement.click(); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + // We're checking the arguments type / emitted value to be a boolean, because sometimes the + // emitted value can be a DOM Event, which is not valid. + // See angular/angular#4059 + expect(testComponent.lastEvent.checked).toBe(true); + }); + })); + + it('should support subscription on the change observable', () => { + slideToggle.change.subscribe((event: MdSlideToggleChange) => { + expect(event.checked).toBe(true); + }); + + slideToggle.toggle(); + fixture.detectChanges(); + }); + + it('should have the correct control state initially and after interaction', () => { + // The control should start off valid, pristine, and untouched. + expect(slideToggleControl.valid).toBe(true); + expect(slideToggleControl.pristine).toBe(true); + expect(slideToggleControl.touched).toBe(false); + + // After changing the value programmatically, the control should + // become dirty (not pristine), but remain untouched. + slideToggle.checked = true; + fixture.detectChanges(); + + expect(slideToggleControl.valid).toBe(true); + expect(slideToggleControl.pristine).toBe(false); + expect(slideToggleControl.touched).toBe(false); + + // After a user interaction occurs (such as a click), the control should remain dirty and + // now also be touched. + labelElement.click(); + fixture.detectChanges(); + + expect(slideToggleControl.valid).toBe(true); + expect(slideToggleControl.pristine).toBe(false); + expect(slideToggleControl.touched).toBe(true); + }); + + it('should not set the control to touched when changing the state programmatically', () => { + // The control should start off with being untouched. + expect(slideToggleControl.touched).toBe(false); + + testComponent.slideChecked = true; + fixture.detectChanges(); + + expect(slideToggleControl.touched).toBe(false); + expect(slideToggleElement.classList).toContain('md-checked'); + + // After a user interaction occurs (such as a click), the control should remain dirty and + // now also be touched. + inputElement.click(); + fixture.detectChanges(); + + expect(slideToggleControl.touched).toBe(true); + expect(slideToggleElement.classList).not.toContain('md-checked'); + }); + + // TODO(kara): update when core/testing adds fix + it('should not set the control to touched when changing the model', async(() => { + // The control should start off with being untouched. + expect(slideToggleControl.touched).toBe(false); + + testComponent.slideModel = true; + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(slideToggleControl.touched).toBe(false); + expect(slideToggle.checked).toBe(true); + expect(slideToggleElement.classList).toContain('md-checked'); + }); + })); + + it('should correctly set the slide-toggle to checked on focus', () => { + expect(slideToggleElement.classList).not.toContain('md-slide-toggle-focused'); + + dispatchFocusChangeEvent('focus', inputElement); + fixture.detectChanges(); + + expect(slideToggleElement.classList).toContain('md-slide-toggle-focused'); + }); + + it('should forward the required attribute', () => { + testComponent.isRequired = true; + fixture.detectChanges(); + + expect(inputElement.required).toBe(true); + + testComponent.isRequired = false; + fixture.detectChanges(); + + expect(inputElement.required).toBe(false); + }); + + }); + + describe('custom template', () => { + it('should not trigger the change event on initialization', async(() => { + let fixture = TestBed.createComponent(SlideToggleTestApp); + fixture.componentInstance.slideModel = true; + fixture.componentInstance.slideChecked = true; + fixture.detectChanges(); + + expect(fixture.componentInstance.lastEvent).toBeFalsy(); + })); + }); + + describe('with forms', () => { + + let fixture: ComponentFixture; + let testComponent: SlideToggleFormsTestApp; + let buttonElement: HTMLButtonElement; + let labelElement: HTMLLabelElement; + let inputElement: HTMLInputElement; + + // This initialization is async() because it needs to wait for ngModel to set the initial value. + beforeEach(async(() => { + fixture = TestBed.createComponent(SlideToggleFormsTestApp); + + testComponent = fixture.debugElement.componentInstance; + + fixture.detectChanges(); + + buttonElement = fixture.debugElement.query(By.css('button')).nativeElement; + labelElement = fixture.debugElement.query(By.css('label')).nativeElement; + inputElement = fixture.debugElement.query(By.css('input')).nativeElement; + })); + + it('should prevent the form from submit when being required', () => { + + if ('reportValidity' in inputElement === false) { + // If the browser does not report the validity then the tests will break. + // e.g Safari 8 on Mobile. + return; + } + + testComponent.isRequired = true; + + fixture.detectChanges(); + + buttonElement.click(); + fixture.detectChanges(); + + expect(testComponent.isSubmitted).toBe(false); + + testComponent.isRequired = false; + fixture.detectChanges(); + + buttonElement.click(); + fixture.detectChanges(); + + expect(testComponent.isSubmitted).toBe(true); + }); + + }); + + describe('with dragging', () => { + + let fixture: ComponentFixture; + + let testComponent: SlideToggleTestApp; + let slideToggle: MdSlideToggle; + let slideToggleElement: HTMLElement; + let slideToggleControl: NgControl; + let slideThumbContainer: HTMLElement; + + beforeEach(async(() => { + fixture = TestBed.createComponent(SlideToggleTestApp); + + testComponent = fixture.debugElement.componentInstance; + + fixture.detectChanges(); + + let slideToggleDebug = fixture.debugElement.query(By.css('md-slide-toggle')); + let thumbContainerDebug = slideToggleDebug.query(By.css('.md-slide-toggle-thumb-container')); + + slideToggle = slideToggleDebug.componentInstance; + slideToggleElement = slideToggleDebug.nativeElement; + slideToggleControl = slideToggleDebug.injector.get(NgControl); + slideThumbContainer = thumbContainerDebug.nativeElement; + })); + + it('should drag from start to end', fakeAsync(() => { + expect(slideToggle.checked).toBe(false); + + gestureConfig.emitEventForElement('slidestart', slideThumbContainer); + + expect(slideThumbContainer.classList).toContain('md-dragging'); + + gestureConfig.emitEventForElement('slide', slideThumbContainer, { + deltaX: 200 // Arbitrary, large delta that will be clamped to the end of the slide-toggle. + }); + + gestureConfig.emitEventForElement('slideend', slideThumbContainer); + + // Flush the timeout for the slide ending. + tick(); + + expect(slideToggle.checked).toBe(true); + expect(slideThumbContainer.classList).not.toContain('md-dragging'); + })); + + it('should drag from end to start', fakeAsync(() => { + slideToggle.checked = true; + + gestureConfig.emitEventForElement('slidestart', slideThumbContainer); + + expect(slideThumbContainer.classList).toContain('md-dragging'); + + gestureConfig.emitEventForElement('slide', slideThumbContainer, { + deltaX: -200 // Arbitrary, large delta that will be clamped to the end of the slide-toggle. + }); + + gestureConfig.emitEventForElement('slideend', slideThumbContainer); + + // Flush the timeout for the slide ending. + tick(); + + expect(slideToggle.checked).toBe(false); + expect(slideThumbContainer.classList).not.toContain('md-dragging'); + })); + + it('should not drag when disbaled', fakeAsync(() => { + slideToggle.disabled = true; + + expect(slideToggle.checked).toBe(false); + + gestureConfig.emitEventForElement('slidestart', slideThumbContainer); + + expect(slideThumbContainer.classList).not.toContain('md-dragging'); + + gestureConfig.emitEventForElement('slide', slideThumbContainer, { + deltaX: 200 // Arbitrary, large delta that will be clamped to the end of the slide-toggle. + }); + + gestureConfig.emitEventForElement('slideend', slideThumbContainer); + + // Flush the timeout for the slide ending. + tick(); + + expect(slideToggle.checked).toBe(false); + expect(slideThumbContainer.classList).not.toContain('md-dragging'); + })); + + it('should should emit a change event after drag', fakeAsync(() => { + expect(slideToggle.checked).toBe(false); + + gestureConfig.emitEventForElement('slidestart', slideThumbContainer); + + expect(slideThumbContainer.classList).toContain('md-dragging'); + + gestureConfig.emitEventForElement('slide', slideThumbContainer, { + deltaX: 200 // Arbitrary, large delta that will be clamped to the end of the slide-toggle. + }); + + gestureConfig.emitEventForElement('slideend', slideThumbContainer); + + // Flush the timeout for the slide ending. + tick(); + + expect(slideToggle.checked).toBe(true); + expect(slideThumbContainer.classList).not.toContain('md-dragging'); + expect(testComponent.lastEvent.checked).toBe(true); + })); + + }); + +}); + +/** + * Dispatches a focus change event from an element. + * @param eventName Name of the event, either 'focus' or 'blur'. + * @param element The element from which the event will be dispatched. + */ +function dispatchFocusChangeEvent(eventName: string, element: HTMLElement): void { + let event = document.createEvent('Event'); + event.initEvent(eventName, true, true); + element.dispatchEvent(event); +} + +@Component({ + selector: 'slide-toggle-test-app', + template: ` + + + Test Slide Toggle + + `, +}) +class SlideToggleTestApp { + isDisabled: boolean = false; + isRequired: boolean = false; + slideModel: boolean = false; + slideChecked: boolean = false; + slideColor: string; + slideId: string; + slideName: string; + slideLabel: string; + slideLabelledBy: string; + lastEvent: MdSlideToggleChange; + + onSlideClick(event: Event) {} + onSlideChange(event: MdSlideToggleChange) { + this.lastEvent = event; + } +} + + +@Component({ + selector: 'slide-toggle-forms-test-app', + template: ` +
+ Required + +
` +}) +class SlideToggleFormsTestApp { + isSubmitted: boolean = false; + isRequired: boolean = false; +}