mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
prototype(slide-toggle): create prototype menu based on MDC Web (#15893)
Creates a prototype of `mat-slide-toggle` that uses MDC web.
This commit is contained in:
committed by
mmalerba
parent
fde980c8ad
commit
b730ba856c
@@ -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');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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],
|
||||
})
|
||||
|
||||
@@ -1,2 +1,29 @@
|
||||
<!-- TODO: copy in demo template from existing mat-slide-toggle demo. -->
|
||||
Not yet implemented.
|
||||
<div class="demo-slide-toggle">
|
||||
|
||||
<mat-slide-toggle color="primary" [(ngModel)]="firstToggle">
|
||||
Default Slide Toggle
|
||||
</mat-slide-toggle>
|
||||
|
||||
<mat-slide-toggle [(ngModel)]="firstToggle" disabled>
|
||||
Disabled Slide Toggle
|
||||
</mat-slide-toggle>
|
||||
|
||||
<mat-slide-toggle [disabled]="firstToggle">
|
||||
Disable Bound
|
||||
</mat-slide-toggle>
|
||||
|
||||
<p>Example where the slide toggle is required inside of a form.</p>
|
||||
|
||||
<form #form="ngForm" (ngSubmit)="onFormSubmit()" ngNativeValidate>
|
||||
|
||||
<mat-slide-toggle name="slideToggle" required ngModel>
|
||||
Slide Toggle
|
||||
</mat-slide-toggle>
|
||||
|
||||
<p>
|
||||
<button mat-raised-button type="submit">Submit Form</button>
|
||||
</p>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
<!-- TODO: copy implementation from existing mat-slide-toggle e2e page. -->
|
||||
<mat-slide-toggle id="normal-slide-toggle">Slide Toggle</mat-slide-toggle>
|
||||
<mat-slide-toggle id="disabled-slide-toggle" disabled>Disabled Slide Toggle</mat-slide-toggle>
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1 +1,93 @@
|
||||
This is a placeholder for the MDC-based implementation of slide toggle.
|
||||
This is prototype of an alternate version of `<mat-slide-toggle>` 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 `<mat-slide-toggle>`. 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 `<mat-slide-toggle>` in your component's template, just like you would the normal
|
||||
`<mat-slide-toggle>`:
|
||||
|
||||
```html
|
||||
<mat-slide-toggle [checked]="isChecked">Toggle me</mat-slide-toggle>
|
||||
```
|
||||
|
||||
5. Add the theme and typography mixins to your Sass. (There is currently no pre-built CSS option for
|
||||
the experimental `<mat-slide-toggle>`):
|
||||
|
||||
```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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
})
|
||||
|
||||
@@ -7,4 +7,5 @@
|
||||
*/
|
||||
|
||||
export * from './slide-toggle';
|
||||
export * from './slide-toggle-config';
|
||||
export * from './module';
|
||||
|
||||
@@ -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<MatSlideToggleDefaultOptions>('mat-slide-toggle-default-options', {
|
||||
providedIn: 'root',
|
||||
factory: () => ({disableToggleValue: false, disableDragValue: false})
|
||||
});
|
||||
@@ -1 +1,37 @@
|
||||
<!-- TODO: MDC template here. -->
|
||||
<div class="mdc-form-field"
|
||||
[class.mdc-form-field--align-end]="labelPosition == 'before'">
|
||||
<div [ngClass]="_classes" #switch>
|
||||
<div class="mdc-switch__track"></div>
|
||||
<div class="mdc-switch__thumb-underlay">
|
||||
<div class="mat-mdc-slide-toggle-ripple" mat-ripple
|
||||
[matRippleTrigger]="switch"
|
||||
[matRippleDisabled]="disableRipple || disabled"
|
||||
[matRippleCentered]="true"
|
||||
[matRippleRadius]="24"
|
||||
[matRippleAnimation]="_rippleAnimation"></div>
|
||||
<div class="mdc-switch__thumb">
|
||||
<input #input class="mdc-switch__native-control" type="checkbox"
|
||||
role="switch"
|
||||
[id]="inputId"
|
||||
[required]="required"
|
||||
[tabIndex]="tabIndex"
|
||||
[checked]="checked"
|
||||
[disabled]="disabled"
|
||||
[attr.name]="name"
|
||||
[attr.aria-checked]="checked.toString()"
|
||||
[attr.aria-label]="ariaLabel"
|
||||
[attr.aria-labelledby]="ariaLabelledby"
|
||||
(change)="_onChangeEvent($event)"
|
||||
(click)="_onInputClick($event)"
|
||||
(blur)="_onBlur()"
|
||||
(focus)="_focused = true">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label #label
|
||||
[for]="inputId"
|
||||
(click)="$event.stopPropagation()">
|
||||
<ng-content></ng-content>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<any>;
|
||||
|
||||
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<SlideToggleWithModel>;
|
||||
|
||||
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>(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>(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<SlideToggleWithFormControl>;
|
||||
|
||||
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<any>;
|
||||
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: `
|
||||
<mat-slide-toggle [dir]="direction" [required]="isRequired"
|
||||
[disabled]="isDisabled"
|
||||
[color]="slideColor"
|
||||
[id]="slideId"
|
||||
[checked]="slideChecked"
|
||||
[name]="slideName"
|
||||
[aria-label]="slideLabel"
|
||||
[aria-labelledby]="slideLabelledBy"
|
||||
[tabIndex]="slideTabindex"
|
||||
[labelPosition]="labelPosition"
|
||||
[disableRipple]="disableRipple"
|
||||
(toggleChange)="onSlideToggleChange()"
|
||||
(dragChange)="onSlideDragChange()"
|
||||
(change)="onSlideChange($event)"
|
||||
(click)="onSlideClick($event)">
|
||||
<span>Test Slide Toggle</span>
|
||||
</mat-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: `
|
||||
<form ngNativeValidate (ngSubmit)="isSubmitted = true">
|
||||
<mat-slide-toggle name="slide" ngModel [required]="isRequired">Required</mat-slide-toggle>
|
||||
<button type="submit"></button>
|
||||
</form>`
|
||||
})
|
||||
class SlideToggleWithForm {
|
||||
isSubmitted: boolean = false;
|
||||
isRequired: boolean = false;
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `<mat-slide-toggle [(ngModel)]="modelValue"></mat-slide-toggle>`
|
||||
})
|
||||
class SlideToggleWithModel {
|
||||
modelValue = false;
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<mat-slide-toggle [formControl]="formControl">
|
||||
<span>Test Slide Toggle</span>
|
||||
</mat-slide-toggle>`,
|
||||
})
|
||||
class SlideToggleWithFormControl {
|
||||
formControl = new FormControl();
|
||||
}
|
||||
|
||||
@Component({template: `<mat-slide-toggle tabindex="5" [disabled]="disabled"></mat-slide-toggle>`})
|
||||
class SlideToggleWithTabindexAttr {
|
||||
disabled = false;
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `<mat-slide-toggle>{{label}}</mat-slide-toggle>`
|
||||
})
|
||||
class SlideToggleWithoutLabel {
|
||||
label: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `<mat-slide-toggle [(ngModel)]="checked" (change)="onChange()"></mat-slide-toggle>`
|
||||
})
|
||||
class SlideToggleWithModelAndChangeEvent {
|
||||
checked: boolean;
|
||||
onChange: () => void = () => {};
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `<mat-slide-toggle><some-text></some-text></mat-slide-toggle>`
|
||||
})
|
||||
class SlideToggleProjectedLabel {}
|
||||
|
||||
@Component({
|
||||
selector: 'some-text',
|
||||
template: `<span>{{text}}</span>`
|
||||
})
|
||||
class TextBindingComponent {
|
||||
text: string = 'Some text';
|
||||
}
|
||||
|
||||
@@ -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<MatSlideToggleChange> =
|
||||
new EventEmitter<MatSlideToggleChange>();
|
||||
|
||||
/** Event will be dispatched each time the slide-toggle input is toggled. */
|
||||
@Output() readonly toggleChange: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
/**
|
||||
* 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<void> = new EventEmitter<void>();
|
||||
|
||||
/** 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<HTMLInputElement>;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,5 +31,7 @@ Not yet implemented.
|
||||
|
||||
<h2>MDC slide-toggle</h2>
|
||||
|
||||
<!-- TODO: Copy kitchen sink examples for mat-slide-toggle here -->
|
||||
Not yet implemented.
|
||||
<mat-slide-toggle></mat-slide-toggle>
|
||||
<mat-slide-toggle checked></mat-slide-toggle>
|
||||
<mat-slide-toggle checked disabled></mat-slide-toggle>
|
||||
<mat-slide-toggle>with a label</mat-slide-toggle>
|
||||
|
||||
Reference in New Issue
Block a user