-
Notifications
You must be signed in to change notification settings - Fork 6.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(material/button): make button ripples lazy
- Loading branch information
1 parent
24fab99
commit 3d72aca
Showing
14 changed files
with
353 additions
and
40 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
/** | ||
* @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 {DOCUMENT} from '@angular/common'; | ||
import { | ||
ANIMATION_MODULE_TYPE, | ||
Inject, | ||
Injectable, | ||
NgZone, | ||
OnDestroy, | ||
Optional, | ||
} from '@angular/core'; | ||
import { | ||
MAT_RIPPLE_GLOBAL_OPTIONS, | ||
RippleConfig, | ||
RippleGlobalOptions, | ||
RippleRenderer, | ||
RippleTarget, | ||
} from '@angular/material/core'; | ||
import {Platform} from '@angular/cdk/platform'; | ||
|
||
/** The options for the MatButtonRippleLoader's event listeners. */ | ||
const OPTIONS = {passive: true, capture: true}; | ||
|
||
/** The attribute attached to a mat-button whose ripple has not yet been initialized. */ | ||
const MAT_BUTTON_RIPPLE_UNINITIALIZED = 'mat-button-ripple-uninitialized'; | ||
|
||
@Injectable({providedIn: 'root'}) | ||
export class MatButtonLazyLoader implements OnDestroy { | ||
private _document: Document; | ||
|
||
constructor( | ||
private _platform: Platform, | ||
private _ngZone: NgZone, | ||
@Optional() @Inject(DOCUMENT) document: any, | ||
@Optional() @Inject(ANIMATION_MODULE_TYPE) private _animationMode?: string, | ||
@Optional() | ||
@Inject(MAT_RIPPLE_GLOBAL_OPTIONS) | ||
private _globalRippleOptions?: RippleGlobalOptions, | ||
) { | ||
this._document = document; | ||
|
||
this._ngZone.runOutsideAngular(() => { | ||
this._document.addEventListener('focus', this._onInteraction, OPTIONS); | ||
this._document.addEventListener('click', this._onInteraction, OPTIONS); | ||
this._document.addEventListener('mouseenter', this._onInteraction, OPTIONS); | ||
this._document.addEventListener('touchstart', this._onInteraction, OPTIONS); | ||
}); | ||
} | ||
|
||
ngOnDestroy() { | ||
this._ngZone.runOutsideAngular(() => { | ||
this._document.removeEventListener('focus', this._onInteraction, OPTIONS); | ||
this._document.removeEventListener('click', this._onInteraction, OPTIONS); | ||
this._document.removeEventListener('mouseenter', this._onInteraction, OPTIONS); | ||
this._document.removeEventListener('touchstart', this._onInteraction, OPTIONS); | ||
}); | ||
} | ||
|
||
/** Handles creating and attaching button internals when a button is initially interacted with. */ | ||
private _onInteraction = (event: Event) => { | ||
if (!(event.target instanceof Element)) { | ||
return; | ||
} | ||
|
||
// TODO(wagnermaciel): Consider batching these events to improve runtime performance. | ||
|
||
const button = this._closest(event.target); | ||
if (button) { | ||
button.removeAttribute(MAT_BUTTON_RIPPLE_UNINITIALIZED); | ||
this._appendRipple(button as HTMLButtonElement); | ||
} | ||
}; | ||
|
||
/** | ||
* Traverses the element and its parents (heading toward the document root) | ||
* until it finds a mat-button that has not been initialized. | ||
*/ | ||
private _closest(element: Element): Element | null { | ||
let el: Element | null = element; | ||
while (el) { | ||
if (el.hasAttribute(MAT_BUTTON_RIPPLE_UNINITIALIZED)) { | ||
return el; | ||
} | ||
el = el.parentElement; | ||
} | ||
return null; | ||
} | ||
|
||
/** Creates a MatButtonRipple and appends it to the given button element. */ | ||
private _appendRipple(button: HTMLButtonElement): void { | ||
const ripple = this._document.createElement('span'); | ||
ripple.classList.add('mat-mdc-button-ripple'); | ||
|
||
const target = new MatButtonRippleTarget( | ||
button, | ||
this._globalRippleOptions, | ||
this._animationMode, | ||
); | ||
target.rippleConfig.centered = button.hasAttribute('mat-icon-button'); | ||
|
||
const rippleRenderer = new RippleRenderer(target, this._ngZone, ripple, this._platform); | ||
rippleRenderer.setupTriggerEvents(button); | ||
button.append(ripple); | ||
} | ||
} | ||
|
||
class MatButtonRippleTarget implements RippleTarget { | ||
rippleConfig: RippleConfig & RippleGlobalOptions; | ||
|
||
constructor( | ||
private _button: HTMLButtonElement, | ||
private _globalRippleOptions?: RippleGlobalOptions, | ||
animationMode?: string, | ||
) { | ||
this._setRippleConfig(_globalRippleOptions, animationMode); | ||
} | ||
|
||
private _setRippleConfig(globalRippleOptions?: RippleGlobalOptions, animationMode?: string) { | ||
this.rippleConfig = globalRippleOptions || {}; | ||
if (animationMode === 'NoopAnimations') { | ||
this.rippleConfig.animation = {enterDuration: 0, exitDuration: 0}; | ||
} | ||
} | ||
|
||
get rippleDisabled(): boolean { | ||
return this._button.disabled || !!this._globalRippleOptions?.disabled; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
/** | ||
* @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 { | ||
ANIMATION_MODULE_TYPE, | ||
Directive, | ||
ElementRef, | ||
Inject, | ||
NgZone, | ||
Optional, | ||
} from '@angular/core'; | ||
import { | ||
MAT_RIPPLE_GLOBAL_OPTIONS, | ||
MatRipple, | ||
RippleGlobalOptions, | ||
RippleRenderer, | ||
RippleTarget, | ||
} from '@angular/material/core'; | ||
import {DOCUMENT} from '@angular/common'; | ||
import {Platform} from '@angular/cdk/platform'; | ||
import {MatButtonLazyLoader} from './button-lazy-loader'; | ||
|
||
/** | ||
* The MatButtonRipple directive is an extention of the MatRipple | ||
* which allows us to lazily append the DOM node for the button ripple. | ||
* | ||
* The MatButtonRipple directive allows us to not immediately attach the ripple node to the button by providing a separate render function | ||
*/ | ||
@Directive({ | ||
selector: '[mat-button-ripple], [matButtonRipple]', | ||
exportAs: 'matButtonRipple', | ||
standalone: true, | ||
}) | ||
export class MatButtonRipple extends MatRipple { | ||
/** The host button element. */ | ||
private _buttonEl: HTMLElement; | ||
|
||
/** The element that the MatRipple is attached to. */ | ||
private _rippleEl: HTMLElement; | ||
|
||
/** Whether this ripple has already been attached. */ | ||
_isAttached = false; | ||
|
||
constructor( | ||
_elementRef: ElementRef<HTMLElement>, | ||
readonly ngZone: NgZone, | ||
readonly platform: Platform, | ||
@Optional() @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) globalOptions?: RippleGlobalOptions, | ||
@Optional() @Inject(ANIMATION_MODULE_TYPE) _animationMode?: string, | ||
@Optional() @Inject(DOCUMENT) document?: Document, | ||
@Inject(MatButtonLazyLoader) _rippleLoader?: MatButtonLazyLoader, | ||
) { | ||
super(_elementRef, ngZone, platform, globalOptions, _animationMode, document); | ||
this._buttonEl = _elementRef.nativeElement; | ||
this._buttonEl.classList.remove('mat-ripple'); | ||
} | ||
|
||
/** | ||
* Creates a new RippleRenderer. | ||
* The MatButtonRipple overrides the MatRipple's createRippleRenderer so that we can change the element the ripple is attached to. | ||
*/ | ||
protected override _createRippleRenderer( | ||
_: RippleTarget, | ||
__: NgZone, | ||
___: ElementRef, | ||
____: Platform, | ||
): RippleRenderer | undefined { | ||
return undefined; | ||
} | ||
|
||
/** Initializes the event listeners of the ripple and attaches it to the button. */ | ||
_renderRipple(): void { | ||
if (this._isAttached) { | ||
return; | ||
} | ||
|
||
// A ripple may have already been rendered by the MatButtonRippleLoader if | ||
// the user interacts with the button before the MatButton's ripple is referenced. | ||
const existingRipple = this._buttonEl.querySelector('.mat-mdc-button-ripple'); | ||
if (existingRipple) { | ||
existingRipple.remove(); | ||
} | ||
|
||
this._rippleEl = this._document!.createElement('span'); | ||
this._rippleEl.classList.add('mat-mdc-button-ripple'); | ||
this._rippleRenderer = new RippleRenderer(this, this.ngZone, this._rippleEl, this.platform); | ||
this._rippleRenderer!.setupTriggerEvents(this._buttonEl); | ||
this._buttonEl.append(this._rippleEl); | ||
this._buttonEl.removeAttribute('mat-button-ripple-uninitialized'); | ||
this._isAttached = true; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.