diff --git a/e2e/components/mdc-slide-toggle-e2e.spec.ts b/e2e/components/mdc-slide-toggle-e2e.spec.ts
index f80d2210c57..6d0817f5af2 100644
--- a/e2e/components/mdc-slide-toggle-e2e.spec.ts
+++ b/e2e/components/mdc-slide-toggle-e2e.spec.ts
@@ -1 +1,75 @@
-// TODO: copy tests from existing mat-slide-toggle, update as necessary to fix.
+import {browser, element, by, Key} from 'protractor';
+import {expectToExist} from '../util/index';
+
+
+describe('slide-toggle', () => {
+ const getInput = () => element(by.css('#normal-slide-toggle input'));
+ const getNormalToggle = () => element(by.css('#normal-slide-toggle'));
+
+ beforeEach(async () => await browser.get('mdc-slide-toggle'));
+
+ it('should render a slide-toggle', async () => {
+ await expectToExist('mat-slide-toggle');
+ });
+
+ it('should change the checked state on click', async () => {
+ const inputEl = getInput();
+
+ expect(await inputEl.getAttribute('checked'))
+ .toBeFalsy('Expect slide-toggle to be unchecked');
+
+ await getNormalToggle().click();
+
+ expect(await inputEl.getAttribute('checked'))
+ .toBeTruthy('Expect slide-toggle to be checked');
+ });
+
+ it('should change the checked state on click', async () => {
+ const inputEl = getInput();
+
+ expect(await inputEl.getAttribute('checked'))
+ .toBeFalsy('Expect slide-toggle to be unchecked');
+
+ await getNormalToggle().click();
+
+ expect(await inputEl.getAttribute('checked'))
+ .toBeTruthy('Expect slide-toggle to be checked');
+ });
+
+ it('should not change the checked state on click when disabled', async () => {
+ const inputEl = getInput();
+
+ expect(await inputEl.getAttribute('checked'))
+ .toBeFalsy('Expect slide-toggle to be unchecked');
+
+ await element(by.css('#disabled-slide-toggle')).click();
+
+ expect(await inputEl.getAttribute('checked'))
+ .toBeFalsy('Expect slide-toggle to be unchecked');
+ });
+
+ it('should move the thumb on state change', async () => {
+ const slideToggleEl = getNormalToggle();
+ const thumbEl = element(by.css('#normal-slide-toggle .mdc-switch__thumb-underlay'));
+ const previousPosition = await thumbEl.getLocation();
+
+ await slideToggleEl.click();
+
+ const position = await thumbEl.getLocation();
+
+ expect(position.x).not.toBe(previousPosition.x);
+ });
+
+ it('should toggle the slide-toggle on space key', async () => {
+ const inputEl = getInput();
+
+ expect(await inputEl.getAttribute('checked'))
+ .toBeFalsy('Expect slide-toggle to be unchecked');
+
+ await inputEl.sendKeys(Key.SPACE);
+
+ expect(await inputEl.getAttribute('checked'))
+ .toBeTruthy('Expect slide-toggle to be checked');
+ });
+
+});
diff --git a/src/dev-app/mdc-slide-toggle/mdc-slide-toggle-demo-module.ts b/src/dev-app/mdc-slide-toggle/mdc-slide-toggle-demo-module.ts
index ca9c0b9dc0f..1b1a39a956e 100644
--- a/src/dev-app/mdc-slide-toggle/mdc-slide-toggle-demo-module.ts
+++ b/src/dev-app/mdc-slide-toggle/mdc-slide-toggle-demo-module.ts
@@ -9,12 +9,14 @@
import {NgModule} from '@angular/core';
import {MatSlideToggleModule} from '@angular/material-experimental/mdc-slide-toggle';
import {RouterModule} from '@angular/router';
+import {FormsModule} from '@angular/forms';
import {MdcSlideToggleDemo} from './mdc-slide-toggle-demo';
@NgModule({
imports: [
MatSlideToggleModule,
RouterModule.forChild([{path: '', component: MdcSlideToggleDemo}]),
+ FormsModule,
],
declarations: [MdcSlideToggleDemo],
})
diff --git a/src/dev-app/mdc-slide-toggle/mdc-slide-toggle-demo.html b/src/dev-app/mdc-slide-toggle/mdc-slide-toggle-demo.html
index af5734fc032..105158669f2 100644
--- a/src/dev-app/mdc-slide-toggle/mdc-slide-toggle-demo.html
+++ b/src/dev-app/mdc-slide-toggle/mdc-slide-toggle-demo.html
@@ -1,2 +1,29 @@
-
-Not yet implemented.
+
+
+
+ Default Slide Toggle
+
+
+
+ Disabled Slide Toggle
+
+
+
+ Disable Bound
+
+
+
Example where the slide toggle is required inside of a form.
+
+
+
+
diff --git a/src/dev-app/mdc-slide-toggle/mdc-slide-toggle-demo.scss b/src/dev-app/mdc-slide-toggle/mdc-slide-toggle-demo.scss
index 670f248f709..18cfedd33d1 100644
--- a/src/dev-app/mdc-slide-toggle/mdc-slide-toggle-demo.scss
+++ b/src/dev-app/mdc-slide-toggle/mdc-slide-toggle-demo.scss
@@ -1 +1,9 @@
-// TODO: copy in demo styles from existing mat-slide-toggle demo.
+.demo-slide-toggle {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+
+ mat-slide-toggle {
+ margin: 6px 0;
+ }
+}
diff --git a/src/dev-app/mdc-slide-toggle/mdc-slide-toggle-demo.ts b/src/dev-app/mdc-slide-toggle/mdc-slide-toggle-demo.ts
index 5891e30f243..18d99d1e0c2 100644
--- a/src/dev-app/mdc-slide-toggle/mdc-slide-toggle-demo.ts
+++ b/src/dev-app/mdc-slide-toggle/mdc-slide-toggle-demo.ts
@@ -15,4 +15,9 @@ import {Component} from '@angular/core';
styleUrls: ['mdc-slide-toggle-demo.css'],
})
export class MdcSlideToggleDemo {
+ firstToggle: boolean;
+
+ onFormSubmit() {
+ alert(`You submitted the form.`);
+ }
}
diff --git a/src/e2e-app/mdc-slide-toggle/mdc-slide-toggle-e2e.html b/src/e2e-app/mdc-slide-toggle/mdc-slide-toggle-e2e.html
index 9d3d9f8d528..23eac9f2c5a 100644
--- a/src/e2e-app/mdc-slide-toggle/mdc-slide-toggle-e2e.html
+++ b/src/e2e-app/mdc-slide-toggle/mdc-slide-toggle-e2e.html
@@ -1 +1,2 @@
-
+Slide Toggle
+Disabled Slide Toggle
diff --git a/src/e2e-app/mdc-slide-toggle/mdc-slide-toggle-e2e.ts b/src/e2e-app/mdc-slide-toggle/mdc-slide-toggle-e2e.ts
index f0ac13d3e2f..9284cd286ac 100644
--- a/src/e2e-app/mdc-slide-toggle/mdc-slide-toggle-e2e.ts
+++ b/src/e2e-app/mdc-slide-toggle/mdc-slide-toggle-e2e.ts
@@ -13,6 +13,4 @@ import {Component} from '@angular/core';
selector: 'mdc-slide-toggle-e2e',
templateUrl: 'mdc-slide-toggle-e2e.html',
})
-export class MdcSlideToggleE2e {
- // TODO: copy implementation from existing mat-slide-toggle e2e page.
-}
+export class MdcSlideToggleE2e {}
diff --git a/src/material-experimental/mdc-slide-toggle/BUILD.bazel b/src/material-experimental/mdc-slide-toggle/BUILD.bazel
index 29aa0050db9..2c39465fa8a 100644
--- a/src/material-experimental/mdc-slide-toggle/BUILD.bazel
+++ b/src/material-experimental/mdc-slide-toggle/BUILD.bazel
@@ -10,6 +10,10 @@ ng_module(
module_name = "@angular/material-experimental/mdc-slide-toggle",
assets = [":slide_toggle_scss"] + glob(["**/*.html"]),
deps = [
+ "@npm//@angular/common",
+ "@npm//@angular/core",
+ "@npm//@angular/forms",
+ "@npm//@angular/animations",
"@npm//material-components-web",
] + CDK_TARGETS + MATERIAL_TARGETS,
)
@@ -20,10 +24,18 @@ sass_library(
deps = [
"//src/material/core:core_scss_lib",
"//src/material-experimental/mdc-helpers:mdc_helpers_scss_lib",
+ "//src/material-experimental/mdc-helpers:mdc_scss_deps_lib",
],
)
sass_binary(
name = "slide_toggle_scss",
src = "slide-toggle.scss",
+ include_paths = [
+ "external/npm/node_modules",
+ ],
+ deps = [
+ ":slide_toggle_scss_lib",
+ "//src/material-experimental/mdc-helpers:mdc_scss_deps_lib",
+ ]
)
diff --git a/src/material-experimental/mdc-slide-toggle/README.md b/src/material-experimental/mdc-slide-toggle/README.md
index 1ab1acdd7fa..e740302528c 100644
--- a/src/material-experimental/mdc-slide-toggle/README.md
+++ b/src/material-experimental/mdc-slide-toggle/README.md
@@ -1 +1,93 @@
-This is a placeholder for the MDC-based implementation of slide toggle.
+This is prototype of an alternate version of `` built on top of
+[MDC Web](https://github.com/material-components/material-components-web). It demonstrates how
+Angular Material could use MDC Web under the hood while still exposing the same API Angular users as
+the existing ``. This component is experimental and should not be used in production.
+
+## How to use
+Assuming your application is already up and running using Angular Material, you can add this
+component by following these steps:
+
+1. Install Angular Material Experimental & MDC WEB:
+
+ ```bash
+ npm i material-components-web @angular/material-experimental
+ ```
+
+2. In your `angular.json`, make sure `node_modules/` is listed as a Sass include path. This is
+ needed for the Sass compiler to be able to find the MDC Web Sass files.
+
+ ```json
+ ...
+ "styles": [
+ "src/styles.scss"
+ ],
+ "stylePreprocessorOptions": {
+ "includePaths": [
+ "node_modules/"
+ ]
+ },
+ ...
+ ```
+
+3. Import the experimental `MatSlideToggleModule` and add it to the module that declares your
+ component:
+
+ ```ts
+ import {MatSlideToggleModule} from '@angular/material-experimental/mdc-slide-toggle';
+
+ @NgModule({
+ declarations: [MyComponent],
+ imports: [MatSlideToggleModule],
+ })
+ export class MyModule {}
+ ```
+
+4. Add use `` in your component's template, just like you would the normal
+ ``:
+
+ ```html
+ Toggle me
+ ```
+
+5. Add the theme and typography mixins to your Sass. (There is currently no pre-built CSS option for
+ the experimental ``):
+
+ ```scss
+ @import '~@angular/material/theming';
+ @import '~@angular/material-experimental/mdc-slide-toggle';
+
+ $my-primary: mat-palette($mat-indigo);
+ $my-accent: mat-palette($mat-pink, A200, A100, A400);
+ $my-theme: mat-light-theme($my-primary, $my-accent);
+
+ @include mat-slide-toggle-theme-mdc($my-theme);
+ @include mat-slide-toggle-typography-mdc();
+ ```
+
+## API differences
+The experimental slide toggle API closely matches the
+[API of the standard slide toggle](https://material.angular.io/components/slide-toggle/api).
+`@angular/material-experimental/mdc-slide-toggle` exports symbols with the same name and public
+interface as all of the symbols found under `@angular/material/slide-toggle`, except for the
+following differences:
+
+* The MDC-based `mat-slide-toggle` drops the dependency on Hammer.js and as a result doesn't support
+dragging gestures.
+* As a result of dragging gestures not being supported, the `dragChange` event won't emit.
+
+## Replacing the standard slide toggle in an existing app
+Because the experimental API mirrors the API for the standard slide toggle, it can easily be swapped
+in by just changing the import paths. There is currently no schematic for this, but you can run the
+following string replace across your TypeScript files:
+
+```bash
+grep -lr --include="*.ts" --exclude-dir="node_modules" \
+ --exclude="*.d.ts" "['\"]@angular/material/slide-toggle['\"]" | xargs sed -i \
+ "s/['\"]@angular\/material\/slide-toggle['\"]/'@angular\/material-experimental\/mdc-slide-toggle'/g"
+```
+
+CSS styles and tests that depend on implementation details of mat-slide-toggle (such as getting
+elements from the template by class name) will need to be manually updated.
+
+There are some small visual differences between this slide and the standard mat-slide. This
+slide has a slightly larger ripple and different spacing between the label and the toggle.
diff --git a/src/material-experimental/mdc-slide-toggle/_mdc-slide-toggle.scss b/src/material-experimental/mdc-slide-toggle/_mdc-slide-toggle.scss
index e90515bcca0..3eea1af678e 100644
--- a/src/material-experimental/mdc-slide-toggle/_mdc-slide-toggle.scss
+++ b/src/material-experimental/mdc-slide-toggle/_mdc-slide-toggle.scss
@@ -1,13 +1,70 @@
+@import '@material/switch/mixins';
+@import '@material/switch/variables';
+@import '@material/form-field/mixins';
+@import '@material/ripple/variables';
+@import '@material/theme/functions';
@import '../mdc-helpers/mdc-helpers';
@mixin mat-slide-toggle-theme-mdc($theme) {
+ $primary: mat-color(map-get($theme, primary));
+ $accent: mat-color(map-get($theme, accent));
+ $warn: mat-color(map-get($theme, warn));
+
+ // Save original values of MDC global variables. We need to save these so we can restore the
+ // variables to their original values and prevent unintended side effects from using this mixin.
+ $orig-mdc-switch-baseline-theme-color: $mdc-switch-baseline-theme-color;
+
@include mat-using-mdc-theme($theme) {
- // TODO: MDC theme styles here.
+ $mdc-switch-baseline-theme-color: primary !global;
+
+ @include mdc-switch-without-ripple($query: $mat-theme-styles-query);
+ @include mdc-form-field-core-styles($query: $mat-theme-styles-query);
+
+ // MDC's switch doesn't support a `color` property. We add support
+ // for it by adding a CSS class for accent and warn style.
+ .mat-mdc-slide-toggle {
+ .mdc-switch__thumb-underlay::before, .mat-ripple-element {
+ background: $mdc-switch-toggled-off-ripple-color;
+ }
+
+ &.mat-accent {
+ $mdc-switch-baseline-theme-color: secondary !global;
+ @include mdc-switch-without-ripple($query: $mat-theme-styles-query);
+ }
+
+ &.mat-warn {
+ $mdc-switch-baseline-theme-color: error !global;
+ @include mdc-switch-without-ripple($query: $mat-theme-styles-query);
+ }
+ }
+
+ // The ripple color matches the palette only when it's checked.
+ .mat-mdc-slide-toggle-checked {
+ .mdc-switch__thumb-underlay::before, .mat-ripple-element {
+ background: $primary;
+ }
+
+ &.mat-accent {
+ .mdc-switch__thumb-underlay::before, .mat-ripple-element {
+ background: $accent;
+ }
+ }
+
+ &.mat-warn {
+ .mdc-switch__thumb-underlay::before, .mat-ripple-element {
+ background: $warn;
+ }
+ }
+ }
}
+
+ // Restore original values of MDC global variables.
+ $mdc-switch-baseline-theme-color: $orig-mdc-switch-baseline-theme-color !global;
}
@mixin mat-slide-toggle-typography-mdc($config) {
@include mat-using-mdc-typography($config) {
- // TODO: MDC typography styles here.
+ @include mdc-switch-without-ripple($query: $mat-typography-styles-query);
+ @include mdc-form-field-core-styles($query: $mat-typography-styles-query);
}
}
diff --git a/src/material-experimental/mdc-slide-toggle/module.ts b/src/material-experimental/mdc-slide-toggle/module.ts
index 240364437fe..b1e4ad5737c 100644
--- a/src/material-experimental/mdc-slide-toggle/module.ts
+++ b/src/material-experimental/mdc-slide-toggle/module.ts
@@ -8,11 +8,11 @@
import {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';
-import {MatCommonModule} from '@angular/material/core';
+import {MatCommonModule, MatRippleModule} from '@angular/material/core';
import {MatSlideToggle} from './slide-toggle';
@NgModule({
- imports: [MatCommonModule, CommonModule],
+ imports: [MatCommonModule, MatRippleModule, CommonModule],
exports: [MatSlideToggle, MatCommonModule],
declarations: [MatSlideToggle],
})
diff --git a/src/material-experimental/mdc-slide-toggle/public-api.ts b/src/material-experimental/mdc-slide-toggle/public-api.ts
index 3f2133d6121..8c10fb2b799 100644
--- a/src/material-experimental/mdc-slide-toggle/public-api.ts
+++ b/src/material-experimental/mdc-slide-toggle/public-api.ts
@@ -7,4 +7,5 @@
*/
export * from './slide-toggle';
+export * from './slide-toggle-config';
export * from './module';
diff --git a/src/material-experimental/mdc-slide-toggle/slide-toggle-config.ts b/src/material-experimental/mdc-slide-toggle/slide-toggle-config.ts
new file mode 100644
index 00000000000..e83bba485c9
--- /dev/null
+++ b/src/material-experimental/mdc-slide-toggle/slide-toggle-config.ts
@@ -0,0 +1,28 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+import {InjectionToken} from '@angular/core';
+
+
+/** Default `mat-slide-toggle` options that can be overridden. */
+export interface MatSlideToggleDefaultOptions {
+ /** Whether toggle action triggers value changes in slide toggle. */
+ disableToggleValue?: boolean;
+ /**
+ * Whether drag action triggers value changes in slide toggle.
+ * @deprecated No longer being used.
+ * @breaking-change 9.0.0.
+ */
+ disableDragValue?: boolean;
+}
+
+/** Injection token to be used to override the default options for `mat-slide-toggle`. */
+export const MAT_SLIDE_TOGGLE_DEFAULT_OPTIONS =
+ new InjectionToken('mat-slide-toggle-default-options', {
+ providedIn: 'root',
+ factory: () => ({disableToggleValue: false, disableDragValue: false})
+ });
diff --git a/src/material-experimental/mdc-slide-toggle/slide-toggle.html b/src/material-experimental/mdc-slide-toggle/slide-toggle.html
index 9c75ddb6f47..b617042356d 100644
--- a/src/material-experimental/mdc-slide-toggle/slide-toggle.html
+++ b/src/material-experimental/mdc-slide-toggle/slide-toggle.html
@@ -1 +1,37 @@
-
+
diff --git a/src/material-experimental/mdc-slide-toggle/slide-toggle.scss b/src/material-experimental/mdc-slide-toggle/slide-toggle.scss
index f61450904c9..c79e938a012 100644
--- a/src/material-experimental/mdc-slide-toggle/slide-toggle.scss
+++ b/src/material-experimental/mdc-slide-toggle/slide-toggle.scss
@@ -1 +1,43 @@
-// TODO: MDC core styles here.
+@import '@material/switch/functions';
+@import '@material/switch/mixins';
+@import '@material/form-field/mixins';
+@import '@material/ripple/variables';
+@import '../mdc-helpers/mdc-helpers';
+@import '../../material/core/style/layout-common';
+
+@include mdc-switch-without-ripple($query: $mat-base-styles-query);
+@include mdc-form-field-core-styles($query: $mat-base-styles-query);
+
+.mat-mdc-slide-toggle {
+ display: inline-block;
+
+ // The ripple needs extra specificity so the base ripple styling doesn't override its `position`.
+ .mat-mdc-slide-toggle-ripple, .mdc-switch__thumb-underlay::before {
+ @include mat-fill;
+ }
+
+ // The MDC switch styles related to the hover state are intertwined with the MDC ripple styles.
+ // We currently don't use the MDC ripple due to size concerns, therefore we need to add some
+ // additional styles to restore the hover state.
+ .mdc-switch__thumb-underlay::before {
+ border-radius: 50%;
+ content: '';
+ opacity: 0;
+ }
+
+ .mdc-switch:hover .mdc-switch__thumb-underlay::before {
+ opacity: map-get($mdc-ripple-dark-ink-opacities, hover);
+ transition: mdc-switch-transition-enter(opacity, 0, 75ms);
+ }
+
+ // Needs a little more specificity so the :hover styles don't override it.
+ &.mat-mdc-slide-toggle-focused .mdc-switch .mdc-switch__thumb-underlay::before {
+ opacity: map-get($mdc-ripple-dark-ink-opacities, focus);
+ }
+
+ // We use an Angular Material ripple rather than an MDC ripple due to size concerns, so we need to
+ // style it appropriately.
+ .mat-ripple-element {
+ opacity: map-get($mdc-ripple-dark-ink-opacities, press);
+ }
+}
diff --git a/src/material-experimental/mdc-slide-toggle/slide-toggle.spec.ts b/src/material-experimental/mdc-slide-toggle/slide-toggle.spec.ts
index f80d2210c57..8dfa7bc6662 100644
--- a/src/material-experimental/mdc-slide-toggle/slide-toggle.spec.ts
+++ b/src/material-experimental/mdc-slide-toggle/slide-toggle.spec.ts
@@ -1 +1,786 @@
-// TODO: copy tests from existing mat-slide-toggle, update as necessary to fix.
+import {BidiModule, Direction} from '@angular/cdk/bidi';
+import {dispatchFakeEvent} from '@angular/cdk/testing';
+import {Component} from '@angular/core';
+import {
+ ComponentFixture,
+ fakeAsync,
+ flush,
+ flushMicrotasks,
+ TestBed,
+ tick,
+} from '@angular/core/testing';
+import {FormControl, FormsModule, NgModel, ReactiveFormsModule} from '@angular/forms';
+import {By} from '@angular/platform-browser';
+import {MatSlideToggle, MatSlideToggleChange, MatSlideToggleModule} from './index';
+import {MAT_SLIDE_TOGGLE_DEFAULT_OPTIONS} from './slide-toggle-config';
+
+describe('MatSlideToggle without forms', () => {
+ beforeEach(fakeAsync(() => {
+ TestBed.configureTestingModule({
+ imports: [MatSlideToggleModule, BidiModule],
+ declarations: [
+ SlideToggleBasic,
+ SlideToggleWithTabindexAttr,
+ SlideToggleWithoutLabel,
+ SlideToggleProjectedLabel,
+ TextBindingComponent,
+ ]
+ });
+
+ TestBed.compileComponents();
+ }));
+
+ describe('basic behavior', () => {
+ let fixture: ComponentFixture;
+
+ let testComponent: SlideToggleBasic;
+ let slideToggle: MatSlideToggle;
+ let slideToggleElement: HTMLElement;
+ let labelElement: HTMLLabelElement;
+ let inputElement: HTMLInputElement;
+
+ beforeEach(fakeAsync(() => {
+ fixture = TestBed.createComponent(SlideToggleBasic);
+
+ // 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();
+
+ const slideToggleDebug = fixture.debugElement.query(By.css('mat-slide-toggle'));
+
+ testComponent = fixture.debugElement.componentInstance;
+ slideToggle = slideToggleDebug.componentInstance;
+ slideToggleElement = slideToggleDebug.nativeElement;
+ inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
+ labelElement = fixture.debugElement.query(By.css('label')).nativeElement;
+ }));
+
+ it('should apply class based on color attribute', () => {
+ testComponent.slideColor = 'primary';
+ fixture.detectChanges();
+
+ expect(slideToggleElement.classList).toContain('mat-primary');
+
+ testComponent.slideColor = 'accent';
+ fixture.detectChanges();
+
+ expect(slideToggleElement.classList).toContain('mat-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();
+ expect(inputElement.getAttribute('aria-checked')).toBe('false');
+
+ testComponent.slideChecked = true;
+ fixture.detectChanges();
+
+ expect(inputElement.checked).toBeTruthy();
+ expect(inputElement.getAttribute('aria-checked')).toBe('true');
+ });
+
+ it('should set the toggle to checked on click', () => {
+ expect(slideToggle.checked).toBe(false);
+ expect(inputElement.getAttribute('aria-checked')).toBe('false');
+ expect(slideToggleElement.classList).not.toContain('mat-mdc-slide-toggle-checked');
+
+ labelElement.click();
+ fixture.detectChanges();
+
+ expect(slideToggleElement.classList).toContain('mat-mdc-slide-toggle-checked');
+ expect(slideToggle.checked).toBe(true);
+ expect(inputElement.getAttribute('aria-checked')).toBe('true');
+ });
+
+ it('should trigger the change event properly', () => {
+ expect(inputElement.checked).toBe(false);
+ expect(slideToggleElement.classList).not.toContain('mat-mdc-slide-toggle-checked');
+
+ labelElement.click();
+ fixture.detectChanges();
+
+ expect(inputElement.checked).toBe(true);
+ expect(slideToggleElement.classList).toContain('mat-mdc-slide-toggle-checked');
+ expect(testComponent.onSlideChange).toHaveBeenCalledTimes(1);
+ });
+
+ it('should not trigger the change event by changing the native value', fakeAsync(() => {
+ expect(inputElement.checked).toBe(false);
+ expect(slideToggleElement.classList).not.toContain('mat-mdc-slide-toggle-checked');
+
+ testComponent.slideChecked = true;
+ fixture.detectChanges();
+
+ expect(inputElement.checked).toBe(true);
+ expect(slideToggleElement.classList).toContain('mat-mdc-slide-toggle-checked');
+ tick();
+
+ expect(testComponent.onSlideChange).not.toHaveBeenCalled();
+ }));
+
+ it('should not trigger the change event on initialization', fakeAsync(() => {
+ expect(inputElement.checked).toBe(false);
+ expect(slideToggleElement.classList).not.toContain('mat-mdc-slide-toggle-checked');
+
+ testComponent.slideChecked = true;
+ fixture.detectChanges();
+
+ expect(inputElement.checked).toBe(true);
+ expect(slideToggleElement.classList).toContain('mat-mdc-slide-toggle-checked');
+ tick();
+
+ expect(testComponent.onSlideChange).not.toHaveBeenCalled();
+ }));
+
+ it('should add a suffix to the inputs id', () => {
+ testComponent.slideId = 'myId';
+ fixture.detectChanges();
+
+ expect(slideToggleElement.id).toBe('myId');
+ expect(inputElement.id).toBe(`${slideToggleElement.id}-input`);
+
+ testComponent.slideId = 'nextId';
+ fixture.detectChanges();
+
+ expect(slideToggleElement.id).toBe('nextId');
+ expect(inputElement.id).toBe(`${slideToggleElement.id}-input`);
+
+ testComponent.slideId = null;
+ fixture.detectChanges();
+
+ // Once the id binding is set to null, the id property should auto-generate a unique id.
+ expect(inputElement.id).toMatch(/mat-slide-toggle-\d+-input/);
+ });
+
+ it('should forward the tabIndex to the underlying input', () => {
+ fixture.detectChanges();
+
+ expect(inputElement.tabIndex).toBe(0);
+
+ testComponent.slideTabindex = 4;
+ fixture.detectChanges();
+
+ expect(inputElement.tabIndex).toBe(4);
+ });
+
+ 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 set the `for` attribute to the id of the input element', () => {
+ expect(labelElement.getAttribute('for')).toBeTruthy();
+ expect(inputElement.getAttribute('id')).toBeTruthy();
+ expect(labelElement.getAttribute('for')).toBe(inputElement.getAttribute('id'));
+ });
+
+ it('should emit the new values properly', fakeAsync(() => {
+ labelElement.click();
+ fixture.detectChanges();
+ tick();
+
+ // 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', fakeAsync(() => {
+ const spy = jasmine.createSpy('change spy');
+ const subscription = slideToggle.change.subscribe(spy);
+
+ labelElement.click();
+ fixture.detectChanges();
+ tick();
+
+ expect(spy).toHaveBeenCalledWith(jasmine.objectContaining({checked: true}));
+ subscription.unsubscribe();
+ }));
+
+ 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);
+ });
+
+ it('should focus on underlying input element when focus() is called', () => {
+ expect(document.activeElement).not.toBe(inputElement);
+
+ slideToggle.focus();
+ fixture.detectChanges();
+
+ expect(document.activeElement).toBe(inputElement);
+ });
+
+ it('should set a element class if labelPosition is set to before', () => {
+ const formField = slideToggleElement.querySelector('.mdc-form-field')!;
+
+ expect(formField.classList).not.toContain('mdc-form-field--align-end');
+
+ testComponent.labelPosition = 'before';
+ fixture.detectChanges();
+
+ expect(formField.classList).toContain('mdc-form-field--align-end');
+ });
+
+ it('should show ripples on switch element', () => {
+ const rippleSelector = '.mat-ripple-element';
+ const switchElement = slideToggleElement.querySelector('.mdc-switch')!;
+
+ expect(slideToggleElement.querySelectorAll(rippleSelector).length).toBe(0);
+
+ dispatchFakeEvent(switchElement, 'mousedown');
+ dispatchFakeEvent(switchElement, 'mouseup');
+
+ expect(slideToggleElement.querySelectorAll(rippleSelector).length).toBe(1);
+ });
+
+ it('should not show ripples when disableRipple is set', () => {
+ const switchElement = slideToggleElement.querySelector('.mdc-switch')!;
+ const rippleSelector = '.mat-ripple-element';
+ testComponent.disableRipple = true;
+ fixture.detectChanges();
+
+ expect(slideToggleElement.querySelectorAll(rippleSelector).length).toBe(0);
+
+ dispatchFakeEvent(switchElement, 'mousedown');
+ dispatchFakeEvent(switchElement, 'mouseup');
+
+ expect(slideToggleElement.querySelectorAll(rippleSelector).length).toBe(0);
+ });
+ });
+
+ describe('custom template', () => {
+ it('should not trigger the change event on initialization', fakeAsync(() => {
+ const fixture = TestBed.createComponent(SlideToggleBasic);
+
+ fixture.componentInstance.slideChecked = true;
+ fixture.detectChanges();
+
+ expect(fixture.componentInstance.lastEvent).toBeFalsy();
+ }));
+
+ it('should be able to set the tabindex via the native attribute', fakeAsync(() => {
+ const fixture = TestBed.createComponent(SlideToggleWithTabindexAttr);
+
+ fixture.detectChanges();
+
+ const slideToggle = fixture.debugElement
+ .query(By.directive(MatSlideToggle)).componentInstance as MatSlideToggle;
+
+ expect(slideToggle.tabIndex)
+ .toBe(5, 'Expected tabIndex property to have been set based on the native attribute');
+ }));
+
+ it('should remove the tabindex from the host element', fakeAsync(() => {
+ const fixture = TestBed.createComponent(SlideToggleWithTabindexAttr);
+
+ fixture.detectChanges();
+
+ const slideToggle = fixture.debugElement.query(By.directive(MatSlideToggle)).nativeElement;
+ expect(slideToggle.hasAttribute('tabindex')).toBe(false);
+ }));
+
+ it('should remove the tabindex from the host element when disabled', fakeAsync(() => {
+ const fixture = TestBed.createComponent(SlideToggleWithTabindexAttr);
+
+ fixture.componentInstance.disabled = true;
+ fixture.detectChanges();
+
+ const slideToggle = fixture.debugElement.query(By.directive(MatSlideToggle)).nativeElement;
+ expect(slideToggle.hasAttribute('tabindex')).toBe(false);
+ }));
+ });
+
+ it('should not change value on click when click action is noop when using custom a ' +
+ 'action configuration', fakeAsync(() => {
+ TestBed
+ .resetTestingModule()
+ .configureTestingModule({
+ imports: [MatSlideToggleModule],
+ declarations: [SlideToggleBasic],
+ providers: [{
+ provide: MAT_SLIDE_TOGGLE_DEFAULT_OPTIONS, useValue: {disableToggleValue: true}
+ }]
+ });
+ const fixture = TestBed.createComponent(SlideToggleBasic);
+ fixture.detectChanges();
+
+ const testComponent = fixture.debugElement.componentInstance;
+ const slideToggleDebug = fixture.debugElement.query(By.css('mat-slide-toggle'));
+
+ const slideToggle = slideToggleDebug.componentInstance;
+ const inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
+ const labelElement = fixture.debugElement.query(By.css('label')).nativeElement;
+
+ expect(testComponent.toggleTriggered).toBe(0);
+ expect(testComponent.dragTriggered).toBe(0);
+ expect(slideToggle.checked).toBe(false, 'Expect slide toggle value not changed');
+
+ labelElement.click();
+ fixture.detectChanges();
+
+ expect(slideToggle.checked).toBe(false, 'Expect slide toggle value not changed');
+ expect(testComponent.toggleTriggered).toBe(1, 'Expect toggle once');
+ expect(testComponent.dragTriggered).toBe(0);
+
+ inputElement.click();
+ fixture.detectChanges();
+
+ expect(slideToggle.checked).toBe(false, 'Expect slide toggle value not changed');
+ expect(testComponent.toggleTriggered).toBe(2, 'Expect toggle twice');
+ expect(testComponent.dragTriggered).toBe(0);
+ }));
+});
+
+describe('MatSlideToggle with forms', () => {
+
+ beforeEach(fakeAsync(() => {
+ TestBed.configureTestingModule({
+ imports: [MatSlideToggleModule, FormsModule, ReactiveFormsModule],
+ declarations: [
+ SlideToggleWithForm,
+ SlideToggleWithModel,
+ SlideToggleWithFormControl,
+ SlideToggleWithModelAndChangeEvent,
+ ]
+ });
+
+ TestBed.compileComponents();
+ }));
+
+ describe('using ngModel', () => {
+ let fixture: ComponentFixture;
+
+ let testComponent: SlideToggleWithModel;
+ let slideToggle: MatSlideToggle;
+ let slideToggleElement: HTMLElement;
+ let slideToggleModel: NgModel;
+ let inputElement: HTMLInputElement;
+ let labelElement: HTMLLabelElement;
+
+ // This initialization is async() because it needs to wait for ngModel to set the initial value.
+ beforeEach(fakeAsync(() => {
+ fixture = TestBed.createComponent(SlideToggleWithModel);
+ fixture.detectChanges();
+
+ const slideToggleDebug = fixture.debugElement.query(By.directive(MatSlideToggle));
+
+ testComponent = fixture.debugElement.componentInstance;
+ slideToggle = slideToggleDebug.componentInstance;
+ slideToggleElement = slideToggleDebug.nativeElement;
+ slideToggleModel = slideToggleDebug.injector.get(NgModel);
+ inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
+ labelElement = fixture.debugElement.query(By.css('label')).nativeElement;
+ }));
+
+ it('should be initially set to ng-pristine', () => {
+ expect(slideToggleElement.classList).toContain('ng-pristine');
+ expect(slideToggleElement.classList).not.toContain('ng-dirty');
+ });
+
+ it('should update the model programmatically', fakeAsync(() => {
+ expect(slideToggleElement.classList).not.toContain('mat-mdc-slide-toggle-checked');
+
+ testComponent.modelValue = true;
+ fixture.detectChanges();
+
+ // Flush the microtasks because the forms module updates the model state asynchronously.
+ flushMicrotasks();
+
+ fixture.detectChanges();
+ expect(slideToggleElement.classList).toContain('mat-mdc-slide-toggle-checked');
+ }));
+
+ it('should have the correct control state initially and after interaction', fakeAsync(() => {
+ // The control should start off valid, pristine, and untouched.
+ expect(slideToggleModel.valid).toBe(true);
+ expect(slideToggleModel.pristine).toBe(true);
+ expect(slideToggleModel.touched).toBe(false);
+
+ // After changing the value from the view, the control should
+ // become dirty (not pristine), but remain untouched if focus is still there.
+ slideToggle.checked = true;
+
+ // Dispatch a change event on the input element to fake a user interaction that triggered
+ // the state change.
+ dispatchFakeEvent(inputElement, 'change');
+
+ expect(slideToggleModel.valid).toBe(true);
+ expect(slideToggleModel.pristine).toBe(false);
+ expect(slideToggleModel.touched).toBe(false);
+
+ // Once the input element loses focus, the control should remain dirty but should
+ // also turn touched.
+ dispatchFakeEvent(inputElement, 'blur');
+ fixture.detectChanges();
+ flushMicrotasks();
+
+ expect(slideToggleModel.valid).toBe(true);
+ expect(slideToggleModel.pristine).toBe(false);
+ expect(slideToggleModel.touched).toBe(true);
+ }));
+
+ it('should not throw an error when disabling while focused', fakeAsync(() => {
+ expect(() => {
+ // Focus the input element because after disabling, the `blur` event should automatically
+ // fire and not result in a changed after checked exception. Related: #12323
+ inputElement.focus();
+
+ // Flush the two nested timeouts from the FocusMonitor that are being created on `focus`.
+ flush();
+
+ slideToggle.disabled = true;
+ fixture.detectChanges();
+ flushMicrotasks();
+ }).not.toThrow();
+ }));
+
+ it('should not set the control to touched when changing the state programmatically',
+ fakeAsync(() => {
+
+ // The control should start off with being untouched.
+ expect(slideToggleModel.touched).toBe(false);
+
+ slideToggle.checked = true;
+ fixture.detectChanges();
+
+ expect(slideToggleModel.touched).toBe(false);
+ expect(slideToggleElement.classList).toContain('mat-mdc-slide-toggle-checked');
+
+ // Once the input element loses focus, the control should remain dirty but should
+ // also turn touched.
+ dispatchFakeEvent(inputElement, 'blur');
+ fixture.detectChanges();
+ flushMicrotasks();
+
+ expect(slideToggleModel.touched).toBe(true);
+ expect(slideToggleElement.classList).toContain('mat-mdc-slide-toggle-checked');
+ }));
+
+ it('should not set the control to touched when changing the model', fakeAsync(() => {
+ // The control should start off with being untouched.
+ expect(slideToggleModel.touched).toBe(false);
+
+ testComponent.modelValue = true;
+ fixture.detectChanges();
+
+ // Flush the microtasks because the forms module updates the model state asynchronously.
+ flushMicrotasks();
+
+ // The checked property has been updated from the model and now the view needs
+ // to reflect the state change.
+ fixture.detectChanges();
+
+ expect(slideToggleModel.touched).toBe(false);
+ expect(slideToggle.checked).toBe(true);
+ expect(slideToggleElement.classList).toContain('mat-mdc-slide-toggle-checked');
+ }));
+
+ it('should update checked state on click if control is checked initially', fakeAsync(() => {
+ fixture = TestBed.createComponent(SlideToggleWithModel);
+ slideToggle = fixture.debugElement.query(By.directive(MatSlideToggle)).componentInstance;
+ labelElement = fixture.debugElement.query(By.css('label')).nativeElement;
+
+ fixture.componentInstance.modelValue = true;
+ fixture.detectChanges();
+
+ // Flush the microtasks because the forms module updates the model state asynchronously.
+ flushMicrotasks();
+
+ // Now the new checked variable has been updated in the slide-toggle and the slide-toggle
+ // is marked for check because it still needs to update the underlying input.
+ fixture.detectChanges();
+
+ expect(slideToggle.checked)
+ .toBe(true, 'Expected slide-toggle to be checked initially');
+
+ labelElement.click();
+ fixture.detectChanges();
+ tick();
+
+ expect(slideToggle.checked)
+ .toBe(false, 'Expected slide-toggle to be no longer checked after label click.');
+ }));
+
+ it('should be pristine if initial value is set from NgModel', fakeAsync(() => {
+ fixture = TestBed.createComponent(SlideToggleWithModel);
+
+ fixture.componentInstance.modelValue = true;
+ fixture.detectChanges();
+
+ const debugElement = fixture.debugElement.query(By.directive(MatSlideToggle));
+ const modelInstance = debugElement.injector.get(NgModel);
+
+ // Flush the microtasks because the forms module updates the model state asynchronously.
+ flushMicrotasks();
+
+ expect(modelInstance.pristine).toBe(true);
+ }));
+
+ it('should set the model value when toggling via the `toggle` method', fakeAsync(() => {
+ expect(testComponent.modelValue).toBe(false);
+
+ fixture.debugElement.query(By.directive(MatSlideToggle)).componentInstance.toggle();
+ fixture.detectChanges();
+ flushMicrotasks();
+
+ fixture.detectChanges();
+ expect(testComponent.modelValue).toBe(true);
+ }));
+
+ });
+
+ describe('with a FormControl', () => {
+ let fixture: ComponentFixture;
+
+ let testComponent: SlideToggleWithFormControl;
+ let slideToggle: MatSlideToggle;
+ let inputElement: HTMLInputElement;
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SlideToggleWithFormControl);
+ fixture.detectChanges();
+
+ testComponent = fixture.debugElement.componentInstance;
+ slideToggle = fixture.debugElement.query(By.directive(MatSlideToggle)).componentInstance;
+ inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
+ });
+
+ it('should toggle the disabled state', () => {
+ expect(slideToggle.disabled).toBe(false);
+ expect(inputElement.disabled).toBe(false);
+
+ testComponent.formControl.disable();
+ fixture.detectChanges();
+
+ expect(slideToggle.disabled).toBe(true);
+ expect(inputElement.disabled).toBe(true);
+
+ testComponent.formControl.enable();
+ fixture.detectChanges();
+
+ expect(slideToggle.disabled).toBe(false);
+ expect(inputElement.disabled).toBe(false);
+ });
+ });
+
+ describe('with form element', () => {
+ let fixture: ComponentFixture;
+ let testComponent: SlideToggleWithForm;
+ let buttonElement: HTMLButtonElement;
+ let inputElement: HTMLInputElement;
+
+ // This initialization is async() because it needs to wait for ngModel to set the initial value.
+ beforeEach(fakeAsync(() => {
+ fixture = TestBed.createComponent(SlideToggleWithForm);
+ fixture.detectChanges();
+
+ testComponent = fixture.debugElement.componentInstance;
+ buttonElement = fixture.debugElement.query(By.css('button')).nativeElement;
+ inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
+ }));
+
+ it('should prevent the form from submit when being required', () => {
+ if (typeof (inputElement as any).reportValidity === 'undefined') {
+ // 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 model and change event', () => {
+ it('should report changes to NgModel before emitting change event', () => {
+ const fixture = TestBed.createComponent(SlideToggleWithModelAndChangeEvent);
+ fixture.detectChanges();
+
+ const labelEl = fixture.debugElement.query(By.css('label')).nativeElement;
+
+ spyOn(fixture.componentInstance, 'onChange').and.callFake(() => {
+ expect(fixture.componentInstance.checked)
+ .toBe(true, 'Expected the model value to have changed before the change event fired.');
+ });
+
+ labelEl.click();
+
+ expect(fixture.componentInstance.onChange).toHaveBeenCalledTimes(1);
+ });
+ });
+});
+
+@Component({
+ template: `
+
+ Test Slide Toggle
+ `,
+})
+class SlideToggleBasic {
+ isDisabled: boolean = false;
+ isRequired: boolean = false;
+ disableRipple: boolean = false;
+ slideChecked: boolean = false;
+ slideColor: string;
+ slideId: string | null;
+ slideName: string | null;
+ slideLabel: string | null;
+ slideLabelledBy: string | null;
+ slideTabindex: number;
+ lastEvent: MatSlideToggleChange;
+ labelPosition: string;
+ toggleTriggered: number = 0;
+ dragTriggered: number = 0;
+ direction: Direction = 'ltr';
+
+ onSlideClick: (event?: Event) => void = () => {};
+ onSlideChange = (event: MatSlideToggleChange) => this.lastEvent = event;
+ onSlideToggleChange = () => this.toggleTriggered++;
+ onSlideDragChange = () => this.dragTriggered++;
+}
+
+@Component({
+ template: `
+ `
+})
+class SlideToggleWithForm {
+ isSubmitted: boolean = false;
+ isRequired: boolean = false;
+}
+
+@Component({
+ template: ``
+})
+class SlideToggleWithModel {
+ modelValue = false;
+}
+
+@Component({
+ template: `
+
+ Test Slide Toggle
+ `,
+})
+class SlideToggleWithFormControl {
+ formControl = new FormControl();
+}
+
+@Component({template: ``})
+class SlideToggleWithTabindexAttr {
+ disabled = false;
+}
+
+@Component({
+ template: `{{label}}`
+})
+class SlideToggleWithoutLabel {
+ label: string;
+}
+
+@Component({
+ template: ``
+})
+class SlideToggleWithModelAndChangeEvent {
+ checked: boolean;
+ onChange: () => void = () => {};
+}
+
+@Component({
+ template: ``
+})
+class SlideToggleProjectedLabel {}
+
+@Component({
+ selector: 'some-text',
+ template: `{{text}}`
+})
+class TextBindingComponent {
+ text: string = 'Some text';
+}
diff --git a/src/material-experimental/mdc-slide-toggle/slide-toggle.ts b/src/material-experimental/mdc-slide-toggle/slide-toggle.ts
index 9a078493a80..6947473eb7d 100644
--- a/src/material-experimental/mdc-slide-toggle/slide-toggle.ts
+++ b/src/material-experimental/mdc-slide-toggle/slide-toggle.ts
@@ -6,7 +6,51 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ ViewEncapsulation,
+ AfterViewInit,
+ OnDestroy,
+ forwardRef,
+ ViewChild,
+ ElementRef,
+ Input,
+ Output,
+ EventEmitter,
+ ChangeDetectorRef,
+ Attribute,
+ Inject,
+ Optional,
+} from '@angular/core';
+import {MDCSwitchAdapter, MDCSwitchFoundation} from '@material/switch';
+import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
+import {coerceBooleanProperty, coerceNumberProperty} from '@angular/cdk/coercion';
+import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
+import {ThemePalette, RippleAnimationConfig} from '@angular/material/core';
+import {
+ MAT_SLIDE_TOGGLE_DEFAULT_OPTIONS,
+ MatSlideToggleDefaultOptions,
+} from './slide-toggle-config';
+
+// Increasing integer for generating unique ids for slide-toggle components.
+let nextUniqueId = 0;
+
+/** @docs-private */
+export const MAT_SLIDE_TOGGLE_VALUE_ACCESSOR: any = {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => MatSlideToggle),
+ multi: true
+};
+
+/** Change event object emitted by a MatSlideToggle. */
+export class MatSlideToggleChange {
+ constructor(
+ /** The source MatSlideToggle of the event. */
+ public source: MatSlideToggle,
+ /** The new `checked` value of the MatSlideToggle. */
+ public checked: boolean) { }
+}
@Component({
moduleId: module.id,
@@ -15,11 +59,261 @@ import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/co
styleUrls: ['slide-toggle.css'],
host: {
'class': 'mat-mdc-slide-toggle',
+ '[id]': 'id',
+ '[attr.tabindex]': 'null',
+ '[class.mat-primary]': 'color == "primary"',
+ '[class.mat-accent]': 'color == "accent"',
+ '[class.mat-warn]': 'color == "warn"',
+ '[class.mat-mdc-slide-toggle-focused]': '_focused',
+ '[class.mat-mdc-slide-toggle-checked]': 'checked',
+ '[class._mat-animation-noopable]': '_animationMode === "NoopAnimations"',
+ '(focus)': '_inputElement.nativeElement.focus()',
},
exportAs: 'matSlideToggle',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [MAT_SLIDE_TOGGLE_VALUE_ACCESSOR],
+
})
-export class MatSlideToggle {
- // TODO: set up MDC foundation class.
+export class MatSlideToggle implements ControlValueAccessor, AfterViewInit, OnDestroy {
+ private onChange = (_: any) => {};
+ private onTouched = () => {};
+
+ private _uniqueId: string = `mat-slide-toggle-${++nextUniqueId}`;
+ private _required: boolean = false;
+ private _checked: boolean = false;
+ private _foundation: MDCSwitchFoundation;
+ private _adapter: MDCSwitchAdapter = {
+ addClass: (className) => {
+ this._toggleClass(className, true);
+ },
+ removeClass: (className) => {
+ this._toggleClass(className, false);
+ },
+ setNativeControlChecked: (checked) => {
+ this._checked = checked;
+ },
+ setNativeControlDisabled: (disabled) => {
+ this._disabled = disabled;
+ },
+ };
+
+ /** Whether the slide toggle is currently focused. */
+ _focused: boolean;
+
+ /** The set of classes that should be applied to the native input. */
+ _classes: {[key: string]: boolean} = {'mdc-switch': true};
+
+ /** Configuration for the underlying ripple. */
+ _rippleAnimation: RippleAnimationConfig = {
+ // TODO(crisbeto): Use the MDC constants once they are exported separately from the
+ // foundation. Grabbing them off the foundation prevents the foundation class from being
+ // tree-shaken. There is an open PR for this:
+ // https://github.com/material-components/material-components-web/pull/4593
+ enterDuration: 225 /* MDCRippleFoundation.numbers.DEACTIVATION_TIMEOUT_MS */,
+ exitDuration: 150 /* MDCRippleFoundation.numbers.FG_DEACTIVATION_MS */,
+ };
+
+ /** The color palette for this slide toggle. */
+ @Input() color: ThemePalette = 'accent';
+
+ /** Name value will be applied to the input element if present. */
+ @Input() name: string | null = null;
+
+ /** A unique id for the slide-toggle input. If none is supplied, it will be auto-generated. */
+ @Input() id: string = this._uniqueId;
+
+ /** Tabindex for the input element. */
+ @Input()
+ get tabIndex(): number { return this._tabIndex; }
+ set tabIndex(value: number) {
+ this._tabIndex = coerceNumberProperty(value);
+ }
+ private _tabIndex: number;
+
+ /** Whether the label should appear after or before the slide-toggle. Defaults to 'after'. */
+ @Input() labelPosition: 'before' | 'after' = 'after';
+
+ /** Used to set the aria-label attribute on the underlying input element. */
+ @Input('aria-label') ariaLabel: string | null = null;
+
+ /** Used to set the aria-labelledby attribute on the underlying input element. */
+ @Input('aria-labelledby') ariaLabelledby: string | null = null;
+
+ /** Whether the slide-toggle is required. */
+ @Input()
+ get required(): boolean { return this._required; }
+ set required(value) { this._required = coerceBooleanProperty(value); }
+
+ /** Whether the slide-toggle element is checked or not. */
+ @Input()
+ get checked(): boolean { return this._checked; }
+ set checked(value) {
+ this._checked = coerceBooleanProperty(value);
+
+ if (this._foundation) {
+ this._foundation.setChecked(this._checked);
+ }
+
+ this._changeDetectorRef.markForCheck();
+ }
+
+ /** Whether to disable the ripple on this checkbox. */
+ @Input()
+ get disableRipple(): boolean {
+ return this._disableRipple;
+ }
+ set disableRipple(disableRipple: boolean) {
+ this._disableRipple = coerceBooleanProperty(disableRipple);
+ }
+ private _disableRipple = false;
+
+ /** Whether the slide toggle is disabled. */
+ @Input()
+ get disabled(): boolean {
+ return this._disabled;
+ }
+ set disabled(disabled) {
+ this._disabled = coerceBooleanProperty(disabled);
+
+ if (this._foundation) {
+ this._foundation.setDisabled(this._disabled);
+ }
+
+ this._changeDetectorRef.markForCheck();
+ }
+ private _disabled = false;
+
+ /** An event will be dispatched each time the slide-toggle changes its value. */
+ @Output() readonly change: EventEmitter =
+ new EventEmitter();
+
+ /** Event will be dispatched each time the slide-toggle input is toggled. */
+ @Output() readonly toggleChange: EventEmitter = new EventEmitter();
+
+ /**
+ * An event will be dispatched each time the slide-toggle is dragged.
+ * This event is always emitted when the user drags the slide toggle to make a change greater
+ * than 50%. It does not mean the slide toggle's value is changed. The event is not emitted when
+ * the user toggles the slide toggle to change its value.
+ * @deprecated No longer being used.
+ * @breaking-change 9.0.0
+ */
+ @Output() readonly dragChange: EventEmitter = new EventEmitter();
+
+ /** Returns the unique id for the visual hidden input. */
+ get inputId(): string { return `${this.id || this._uniqueId}-input`; }
+
+ /** Reference to the underlying input element. */
+ @ViewChild('input', {static: false}) _inputElement: ElementRef;
+
+ constructor(private _changeDetectorRef: ChangeDetectorRef,
+ @Attribute('tabindex') tabIndex: string,
+ @Inject(MAT_SLIDE_TOGGLE_DEFAULT_OPTIONS)
+ public defaults: MatSlideToggleDefaultOptions,
+ @Optional() @Inject(ANIMATION_MODULE_TYPE) public _animationMode?: string) {
+ this.tabIndex = parseInt(tabIndex) || 0;
+ }
+
+ ngAfterViewInit() {
+ const foundation = this._foundation = new MDCSwitchFoundation(this._adapter);
+ foundation.setDisabled(this.disabled);
+ foundation.setChecked(this.checked);
+ }
+
+ ngOnDestroy() {
+ if (this._foundation) {
+ this._foundation.destroy();
+ }
+ }
+
+ /** Method being called whenever the underlying input emits a change event. */
+ _onChangeEvent(event: Event) {
+ // We always have to stop propagation on the change event.
+ // Otherwise the change event, from the input element, will bubble up and
+ // emit its event object to the component's `change` output.
+ event.stopPropagation();
+ this.toggleChange.emit();
+ this._foundation.handleChange(event);
+
+ // When the slide toggle's config disabled toggle change event by setting
+ // `disableToggleValue: true`, the slide toggle's value does not change,
+ // and the checked state of the underlying input needs to be changed back.
+ if (this.defaults.disableToggleValue) {
+ this._inputElement.nativeElement.checked = this.checked;
+ return;
+ }
+
+ // Sync the value from the underlying input element with the component instance.
+ this.checked = this._inputElement.nativeElement.checked;
+
+ // Emit our custom change event only if the underlying input emitted one. This ensures that
+ // there is no change event, when the checked state changes programmatically.
+ this.onChange(this.checked);
+ this.change.emit(new MatSlideToggleChange(this, this.checked));
+ }
+
+ /** Method being called whenever the slide-toggle has been clicked. */
+ _onInputClick(event: Event) {
+ // We have to stop propagation for click events on the visual hidden input element.
+ // By default, when a user clicks on a label element, a generated click event will be
+ // dispatched on the associated input element. Since we are using a label element as our
+ // root container, the click event on the `slide-toggle` will be executed twice.
+ // The real click event will bubble up, and the generated click event also tries to bubble up.
+ // This will lead to multiple click events.
+ // Preventing bubbling for the second event will solve that issue.
+ event.stopPropagation();
+ }
+
+ /** Implemented as part of ControlValueAccessor. */
+ writeValue(value: any): void {
+ this.checked = !!value;
+ }
+
+ /** Implemented as part of ControlValueAccessor. */
+ registerOnChange(fn: any): void {
+ this.onChange = fn;
+ }
+
+ /** Implemented as part of ControlValueAccessor. */
+ registerOnTouched(fn: any): void {
+ this.onTouched = fn;
+ }
+
+ /** Implemented as a part of ControlValueAccessor. */
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ this._changeDetectorRef.markForCheck();
+ }
+
+ /** Focuses the slide-toggle. */
+ focus(): void {
+ this._inputElement.nativeElement.focus();
+ }
+
+ /** Toggles the checked state of the slide-toggle. */
+ toggle(): void {
+ this.checked = !this.checked;
+ this.onChange(this.checked);
+ }
+
+ /** Handles blur events on the native input. */
+ _onBlur() {
+ // When a focused element becomes disabled, the browser *immediately* fires a blur event.
+ // Angular does not expect events to be raised during change detection, so any state change
+ // (such as a form control's 'ng-touched') will cause a changed-after-checked error.
+ // See https://github.com/angular/angular/issues/17793. To work around this, we defer
+ // telling the form control it has been touched until the next tick.
+ Promise.resolve().then(() => {
+ this._focused = false;
+ this.onTouched();
+ this._changeDetectorRef.markForCheck();
+ });
+ }
+
+ /** Toggles a class on the switch element. */
+ private _toggleClass(cssClass: string, active: boolean) {
+ this._classes[cssClass] = active;
+ this._changeDetectorRef.markForCheck();
+ }
}
diff --git a/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.html b/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.html
index 42ccf0b3fb8..738d63d81ca 100644
--- a/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.html
+++ b/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.html
@@ -31,5 +31,7 @@ Not yet implemented.
MDC slide-toggle
-
-Not yet implemented.
+
+
+
+with a label