feat(ripples): support updating global ripple options at runtime (#14705)

* feat(ripples): support updating global ripple options at runtime

* Allows updating any global ripple option at runtime. This makes it possible for developers to disable ripples at runtime.

Closes #9729

* fixup! feat(ripples): support updating global ripple options at runtime

Address feedback
This commit is contained in:
Paul Gschwendtner
2019-01-16 19:37:54 +01:00
committed by vivian-hu
parent f6d0ea80ca
commit 4f755cf10c
11 changed files with 183 additions and 96 deletions
+3
View File
@@ -12,6 +12,7 @@ import {CommonModule} from '@angular/common';
import {HttpClientModule} from '@angular/common/http';
import {NgModule} from '@angular/core';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MAT_RIPPLE_GLOBAL_OPTIONS} from '@angular/material';
import {ExampleModule} from '@angular/material-examples';
import {BrowserModule} from '@angular/platform-browser';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
@@ -50,6 +51,7 @@ import {ProgressBarDemo} from './progress-bar/progress-bar-demo';
import {ProgressSpinnerDemo} from './progress-spinner/progress-spinner-demo';
import {RadioDemo} from './radio/radio-demo';
import {RippleDemo} from './ripple/ripple-demo';
import {DevAppRippleOptions} from './ripple/ripple-options';
import {DEV_APP_ROUTES} from './routes';
import {ScreenTypeDemo} from './screen-type/screen-type-demo';
import {SelectDemo} from './select/select-demo';
@@ -140,6 +142,7 @@ import {VirtualScrollDemo} from './virtual-scroll/virtual-scroll-demo';
],
providers: [
{provide: OverlayContainer, useClass: FullscreenOverlayContainer},
{provide: MAT_RIPPLE_GLOBAL_OPTIONS, useExisting: DevAppRippleOptions},
],
entryComponents: [
ContentElementDialog,
+3
View File
@@ -34,6 +34,9 @@
<mat-icon>fullscreen</mat-icon>
</button>
<button mat-button (click)="toggleTheme()">{{dark ? 'Light' : 'Dark'}} theme</button>
<button mat-button (click)="rippleOptions.disabled = !rippleOptions.disabled">
{{rippleOptions.disabled ? 'Enable' : 'Disable'}} ripples
</button>
<button mat-button (click)="root.dir = (root.dir === 'rtl' ? 'ltr' : 'rtl')"
title="Toggle between RTL and LTR">
{{root.dir.toUpperCase()}}
+3 -1
View File
@@ -8,6 +8,7 @@
import {OverlayContainer} from '@angular/cdk/overlay';
import {Component, ElementRef, ViewEncapsulation} from '@angular/core';
import {DevAppRippleOptions} from './ripple/ripple-options';
/** Root component for the dev-app demos. */
@Component({
@@ -69,7 +70,8 @@ export class DevAppComponent {
constructor(
private _element: ElementRef<HTMLElement>,
private _overlayContainer: OverlayContainer) {}
private _overlayContainer: OverlayContainer,
public rippleOptions: DevAppRippleOptions) {}
toggleFullscreen() {
// Cast to `any`, because the typings don't include the browser-prefixed methods.
+21
View File
@@ -0,0 +1,21 @@
/**
* @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 {Injectable} from '@angular/core';
import {RippleGlobalOptions} from '@angular/material';
/**
* Global ripple options for the dev-app. The ripple options are used as a class
* so that the global options can be changed at runtime.
*/
@Injectable({providedIn: 'root'})
export class DevAppRippleOptions implements RippleGlobalOptions {
/** Whether ripples should be disabled */
disabled: boolean = false;
}
+1
View File
@@ -51,6 +51,7 @@ ng_test_library(
"@angular//packages/platform-browser/animations",
"//src/cdk/a11y",
"//src/cdk/bidi",
"//src/lib/core",
"//src/cdk/keycodes",
"//src/cdk/platform",
"//src/cdk/testing",
+15 -4
View File
@@ -3,6 +3,7 @@ import {BACKSPACE, DELETE, SPACE} from '@angular/cdk/keycodes';
import {createKeyboardEvent, dispatchFakeEvent} from '@angular/cdk/testing';
import {Component, DebugElement} from '@angular/core';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {MAT_RIPPLE_GLOBAL_OPTIONS, RippleGlobalOptions} from '@angular/material/core';
import {By} from '@angular/platform-browser';
import {Subject} from 'rxjs';
import {MatChip, MatChipEvent, MatChipSelectionChange, MatChipsModule} from './index';
@@ -13,19 +14,22 @@ describe('Chips', () => {
let chipDebugElement: DebugElement;
let chipNativeElement: HTMLElement;
let chipInstance: MatChip;
let globalRippleOptions: RippleGlobalOptions;
let dir = 'ltr';
beforeEach(async(() => {
globalRippleOptions = {};
TestBed.configureTestingModule({
imports: [MatChipsModule],
declarations: [BasicChip, SingleChip],
providers: [{
provide: Directionality, useFactory: () => ({
providers: [
{provide: MAT_RIPPLE_GLOBAL_OPTIONS, useFactory: () => globalRippleOptions},
{provide: Directionality, useFactory: () => ({
value: dir,
change: new Subject()
})
}]
})},
]
});
TestBed.compileComponents();
@@ -203,6 +207,13 @@ describe('Chips', () => {
subscription.unsubscribe();
});
it('should be able to disable ripples through ripple global options at runtime', () => {
expect(chipInstance.rippleDisabled).toBe(false, 'Expected chip ripples to be enabled.');
globalRippleOptions.disabled = true;
expect(chipInstance.rippleDisabled).toBe(true, 'Expected chip ripples to be disabled.');
});
});
describe('keyboard behavior', () => {
+8 -16
View File
@@ -123,21 +123,20 @@ export class MatChip extends _MatChipMixinBase implements FocusableOption, OnDes
/** Reference to the RippleRenderer for the chip. */
private _chipRipple: RippleRenderer;
/** Whether the ripples are globally disabled through the RippleGlobalOptions */
private _ripplesGloballyDisabled = false;
/**
* Ripple configuration for ripples that are launched on pointer down.
* Ripple configuration for ripples that are launched on pointer down. The ripple config
* is set to the global ripple options since we don't have any configurable options for
* the chip ripples.
* @docs-private
*/
rippleConfig: RippleConfig = {};
rippleConfig: RippleConfig & RippleGlobalOptions;
/**
* Whether ripples are disabled on interaction
* @docs-private
*/
get rippleDisabled(): boolean {
return this.disabled || this.disableRipple || this._ripplesGloballyDisabled;
return this.disabled || this.disableRipple || !!this.rippleConfig.disabled;
}
/** Whether the chip has focus. */
@@ -225,22 +224,15 @@ export class MatChip extends _MatChipMixinBase implements FocusableOption, OnDes
constructor(public _elementRef: ElementRef,
private _ngZone: NgZone,
platform: Platform,
@Optional() @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) globalOptions: RippleGlobalOptions) {
@Optional() @Inject(MAT_RIPPLE_GLOBAL_OPTIONS)
globalRippleOptions: RippleGlobalOptions | null) {
super(_elementRef);
this._addHostClassName();
this._chipRipple = new RippleRenderer(this, _ngZone, _elementRef, platform);
this._chipRipple.setupTriggerEvents(_elementRef.nativeElement);
if (globalOptions) {
// TODO(paul): Do not copy each option manually. Allow dynamic global option changes: #9729
this._ripplesGloballyDisabled = !!globalOptions.disabled;
this.rippleConfig = {
animation: globalOptions.animation,
terminateOnPointerUp: globalOptions.terminateOnPointerUp,
};
}
this.rippleConfig = globalRippleOptions || {};
}
_addHostClassName() {
+46 -6
View File
@@ -20,7 +20,7 @@ and calling its `launch` method.
### Ripple trigger
By default ripples will fade in on interaction with the directive's host element.
In some situations, developers may want to show ripples on interaction with *some other* element,
In some situations, developers may want to show ripples on interaction with *some other* element,
but still want to have the ripples placed in another location. This can be done by specifying
the `matRippleTrigger` option that expects a reference to an `HTMLElement`.
@@ -29,7 +29,7 @@ the `matRippleTrigger` option that expects a reference to an `HTMLElement`.
<div matRipple [matRippleTrigger]="trigger" class="my-ripple-container">
<!-- This is the ripple container, but not the trigger element for ripples. -->
</div>
<div #trigger></div>
</div>
```
@@ -43,14 +43,14 @@ class MyComponent {
/** Reference to the directive instance of the ripple. */
@ViewChild(MatRipple) ripple: MatRipple;
/** Shows a centered and persistent ripple. */
launchRipple() {
const rippleRef = this.ripple.launch({
persistent: true,
centered: true
});
// Fade out the ripple later.
rippleRef.fadeOut();
}
@@ -91,7 +91,7 @@ const globalRippleConfig: RippleGlobalOptions = {
@NgModule({
providers: [
{provide: MAT_RIPPLE_GLOBAL_OPTIONS, useValue: globalRippleConfig}
{provide: MAT_RIPPLE_GLOBAL_OPTIONS, useValue: globalRippleConfig}
]
})
```
@@ -100,7 +100,7 @@ All available global options can be seen in the `RippleGlobalOptions` interface.
### Disabling animation
The animation of ripples can be disabled by using the `animation` global option. If the
The animation of ripples can be disabled by using the `animation` global option. If the
`enterDuration` and `exitDuration` is being set to `0`, ripples will just appear without any
animation.
@@ -139,3 +139,43 @@ const globalRippleConfig: RippleGlobalOptions = {
terminateOnPointerUp: true
};
```
### Updating global options at runtime
To change global ripple options at runtime, just inject the `MAT_RIPPLE_GLOBAL_OPTIONS`
provider and update the desired options.
There are various ways of injecting the global options. In order to make it easier to
inject and update options at runtime, it's recommended to create a service that implements
the `RippleGlobalOptions` interface.
```ts
@Injectable({providedIn: 'root'})
export class AppGlobalRippleOptions implements RippleGlobalOptions {
/** Whether ripples should be disabled globally. */
disabled: boolean = false;
}
```
```ts
@NgModule({
providers: [
{provide: MAT_RIPPLE_GLOBAL_OPTIONS, useExisting: AppGlobalRippleOptions},
]
})
export class MyModule {...}
```
Now that the global ripple options are set to a service we can inject, the service can be
used update any global ripple option at runtime.
```ts
@Component(...)
export class MyComponent {
constructor(private _appRippleOptions: AppGlobalRippleOptions) {}
disableRipples() {
this._appRippleOptions.disabled = true;
}
}
```
+1 -1
View File
@@ -120,7 +120,7 @@ export class MatRipple implements OnInit, OnDestroy, RippleTarget {
constructor(private _elementRef: ElementRef<HTMLElement>,
ngZone: NgZone,
platform: Platform,
@Optional() @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) globalOptions: RippleGlobalOptions,
@Optional() @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) globalOptions?: RippleGlobalOptions,
@Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string) {
this._globalOptions = globalOptions || {};
+73 -51
View File
@@ -1,5 +1,6 @@
import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
import {Component, ViewChild, ViewChildren, QueryList} from '@angular/core';
import {MAT_RIPPLE_GLOBAL_OPTIONS, RippleGlobalOptions} from '@angular/material/core';
import {By} from '@angular/platform-browser';
import {dispatchFakeEvent, dispatchMouseEvent} from '@angular/cdk/testing';
import {Direction, Directionality} from '@angular/cdk/bidi';
@@ -10,8 +11,11 @@ import {MatTabLink, MatTabNav, MatTabsModule} from '../index';
describe('MatTabNavBar', () => {
let dir: Direction = 'ltr';
let dirChange = new Subject();
let globalRippleOptions: RippleGlobalOptions;
beforeEach(async(() => {
globalRippleOptions = {};
TestBed.configureTestingModule({
imports: [MatTabsModule],
declarations: [
@@ -21,10 +25,9 @@ describe('MatTabNavBar', () => {
TabLinkWithNativeTabindexAttr,
],
providers: [
{provide: Directionality, useFactory: () => ({
value: dir,
change: dirChange.asObservable()
})},
{provide: MAT_RIPPLE_GLOBAL_OPTIONS, useFactory: () => globalRippleOptions},
{provide: Directionality, useFactory: () =>
({value: dir, change: dirChange.asObservable()})},
]
});
@@ -113,29 +116,6 @@ describe('MatTabNavBar', () => {
.toBe(true, 'Expected aria-disabled to be set to "true" if link is disabled.');
});
it('should disable the ripples on all tabs when they are disabled on the nav bar', () => {
expect(fixture.componentInstance.tabLinks.toArray().every(tabLink => !tabLink.rippleDisabled))
.toBe(true, 'Expected every tab link to have ripples enabled');
fixture.componentInstance.disableRippleOnBar = true;
fixture.detectChanges();
expect(fixture.componentInstance.tabLinks.toArray().every(tabLink => tabLink.rippleDisabled))
.toBe(true, 'Expected every tab link to have ripples disabled');
});
it('should have the `disableRipple` from the tab take precendence over the nav bar', () => {
const firstTab = fixture.componentInstance.tabLinks.first;
expect(firstTab.rippleDisabled).toBe(false, 'Expected ripples to be enabled on first tab');
firstTab.disableRipple = true;
fixture.componentInstance.disableRippleOnBar = false;
fixture.detectChanges();
expect(firstTab.rippleDisabled).toBe(true, 'Expected ripples to be disabled on first tab');
});
it('should update the tabindex if links are disabled', () => {
const tabLinkElements = fixture.debugElement.queryAll(By.css('a'))
.map(tabLinkDebugEl => tabLinkDebugEl.nativeElement);
@@ -161,30 +141,6 @@ describe('MatTabNavBar', () => {
expect(getComputedStyle(tabLinkElement).pointerEvents).toBe('none');
});
it('should show ripples for tab links', () => {
const tabLink = fixture.debugElement.nativeElement.querySelector('.mat-tab-link');
dispatchMouseEvent(tabLink, 'mousedown');
dispatchMouseEvent(tabLink, 'mouseup');
expect(tabLink.querySelectorAll('.mat-ripple-element').length)
.toBe(1, 'Expected one ripple to show up if user clicks on tab link.');
});
it('should be able to disable ripples on a tab link', () => {
const tabLinkDebug = fixture.debugElement.query(By.css('a'));
const tabLinkElement = tabLinkDebug.nativeElement;
const tabLinkInstance = tabLinkDebug.injector.get<MatTabLink>(MatTabLink);
tabLinkInstance.disableRipple = true;
dispatchMouseEvent(tabLinkElement, 'mousedown');
dispatchMouseEvent(tabLinkElement, 'mouseup');
expect(tabLinkElement.querySelectorAll('.mat-ripple-element').length)
.toBe(0, 'Expected no ripple to show up if ripples are disabled.');
});
it('should re-align the ink bar when the direction changes', () => {
const inkBar = fixture.componentInstance.tabNavBar._inkBar;
@@ -285,6 +241,72 @@ describe('MatTabNavBar', () => {
expect(tabLink.tabIndex).toBe(3, 'Expected the tabIndex to be have been set to 3.');
});
describe('ripples', () => {
let fixture: ComponentFixture<SimpleTabNavBarTestApp>;
beforeEach(() => {
fixture = TestBed.createComponent(SimpleTabNavBarTestApp);
fixture.detectChanges();
});
it('should be disabled on all tab links when they are disabled on the nav bar', () => {
expect(fixture.componentInstance.tabLinks.toArray().every(tabLink => !tabLink.rippleDisabled))
.toBe(true, 'Expected every tab link to have ripples enabled');
fixture.componentInstance.disableRippleOnBar = true;
fixture.detectChanges();
expect(fixture.componentInstance.tabLinks.toArray().every(tabLink => tabLink.rippleDisabled))
.toBe(true, 'Expected every tab link to have ripples disabled');
});
it('should have the `disableRipple` from the tab take precedence over the nav bar', () => {
const firstTab = fixture.componentInstance.tabLinks.first;
expect(firstTab.rippleDisabled).toBe(false, 'Expected ripples to be enabled on first tab');
firstTab.disableRipple = true;
fixture.componentInstance.disableRippleOnBar = false;
fixture.detectChanges();
expect(firstTab.rippleDisabled).toBe(true, 'Expected ripples to be disabled on first tab');
});
it('should show up for tab link elements on mousedown', () => {
const tabLink = fixture.debugElement.nativeElement.querySelector('.mat-tab-link');
dispatchMouseEvent(tabLink, 'mousedown');
dispatchMouseEvent(tabLink, 'mouseup');
expect(tabLink.querySelectorAll('.mat-ripple-element').length)
.toBe(1, 'Expected one ripple to show up if user clicks on tab link.');
});
it('should be able to disable ripples on an individual tab link', () => {
const tabLinkDebug = fixture.debugElement.query(By.css('a'));
const tabLinkElement = tabLinkDebug.nativeElement;
const tabLinkInstance = tabLinkDebug.injector.get<MatTabLink>(MatTabLink);
tabLinkInstance.disableRipple = true;
dispatchMouseEvent(tabLinkElement, 'mousedown');
dispatchMouseEvent(tabLinkElement, 'mouseup');
expect(tabLinkElement.querySelectorAll('.mat-ripple-element').length)
.toBe(0, 'Expected no ripple to show up if ripples are disabled.');
});
it('should be able to disable ripples through global options at runtime', () => {
expect(fixture.componentInstance.tabLinks.toArray().every(tabLink => !tabLink.rippleDisabled))
.toBe(true, 'Expected every tab link to have ripples enabled');
globalRippleOptions.disabled = true;
expect(fixture.componentInstance.tabLinks.toArray().every(tabLink => tabLink.rippleDisabled))
.toBe(true, 'Expected every tab link to have ripples disabled');
});
});
});
@Component({
+9 -17
View File
@@ -191,9 +191,6 @@ export class MatTabLink extends _MatTabLinkMixinBase
/** Reference to the RippleRenderer for the tab-link. */
protected _tabLinkRipple: RippleRenderer;
/** Whether the ripples are globally disabled through the RippleGlobalOptions */
private _ripplesGloballyDisabled = false;
/** Whether the link is active. */
@Input()
get active(): boolean { return this._isActive; }
@@ -205,25 +202,28 @@ export class MatTabLink extends _MatTabLinkMixinBase
}
/**
* Ripple configuration for ripples that are launched on pointer down.
* Ripple configuration for ripples that are launched on pointer down. The ripple config
* is set to the global ripple options since we don't have any configurable options for
* the tab link ripples.
* @docs-private
*/
rippleConfig: RippleConfig = {};
rippleConfig: RippleConfig & RippleGlobalOptions;
/**
* Whether ripples are disabled on interaction
* Whether ripples are disabled on interaction.
* @docs-private
*/
get rippleDisabled(): boolean {
return this.disabled || this.disableRipple || this._tabNavBar.disableRipple ||
this._ripplesGloballyDisabled;
!!this.rippleConfig.disabled;
}
constructor(private _tabNavBar: MatTabNav,
public _elementRef: ElementRef,
ngZone: NgZone,
platform: Platform,
@Optional() @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) globalOptions: RippleGlobalOptions,
@Optional() @Inject(MAT_RIPPLE_GLOBAL_OPTIONS)
globalRippleOptions: RippleGlobalOptions | null,
@Attribute('tabindex') tabIndex: string,
/**
* @deprecated
@@ -234,18 +234,10 @@ export class MatTabLink extends _MatTabLinkMixinBase
this._tabLinkRipple = new RippleRenderer(this, ngZone, _elementRef, platform);
this._tabLinkRipple.setupTriggerEvents(_elementRef.nativeElement);
this.rippleConfig = globalRippleOptions || {};
this.tabIndex = parseInt(tabIndex) || 0;
if (globalOptions) {
// TODO(paul): Do not copy each option manually. Allow dynamic global option changes: #9729
this._ripplesGloballyDisabled = !!globalOptions.disabled;
this.rippleConfig = {
terminateOnPointerUp: globalOptions.terminateOnPointerUp,
animation: globalOptions.animation,
};
}
if (_focusMonitor) {
_focusMonitor.monitor(_elementRef);
}