From 127faf58331d69d3d4214bde7130ebb58ca8bb12 Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Thu, 23 Feb 2023 20:30:48 -0500 Subject: [PATCH] fixup! feat(material/button): make button ripples lazy --- src/material/button/button-base.ts | 2 + ...pple-renderer.ts => button-lazy-loader.ts} | 85 +++++++++++++++---- src/material/button/button-ripple.ts | 4 +- src/material/button/button.html | 2 + src/material/button/icon-button.html | 2 + 5 files changed, 77 insertions(+), 18 deletions(-) rename src/material/button/{button-lazy-ripple-renderer.ts => button-lazy-loader.ts} (56%) diff --git a/src/material/button/button-base.ts b/src/material/button/button-base.ts index 6e7daa712dfc..03a39dc0bc9a 100644 --- a/src/material/button/button-base.ts +++ b/src/material/button/button-base.ts @@ -91,6 +91,8 @@ export const _MatButtonMixin = mixinColor( /** Base class for all buttons. */ @Directive({ host: { + 'mat-button-ripple-uninitialized': 'true', + 'mat-button-internals-uninitialized': 'true', '[attr.mat-button-disabled]': '_isRippleDisabled()', '[attr.data-mat-button-is-fab]': '_isFab', }, diff --git a/src/material/button/button-lazy-ripple-renderer.ts b/src/material/button/button-lazy-loader.ts similarity index 56% rename from src/material/button/button-lazy-ripple-renderer.ts rename to src/material/button/button-lazy-loader.ts index 67cb78cc36b6..fc7f082062b3 100644 --- a/src/material/button/button-lazy-ripple-renderer.ts +++ b/src/material/button/button-lazy-loader.ts @@ -24,10 +24,25 @@ import { } from '../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'; + +/** The attribute attached to a mat-button whose internals (excluding the ripple) have not yet been initialized. */ +const MAT_BUTTON_INTERNALS_UNINITIALIZED = 'mat-button-internals-uninitialized'; + @Injectable({providedIn: 'root'}) -export class MatButtonRippleLoader implements OnDestroy { +export class MatButtonLazyLoader implements OnDestroy { private _document: Document; + /** A batch of actions to run. */ + private _actionQueue: Function[] = []; + + /** A timeout for when the action queue will be emptied / ran. */ + private _runActionsTimeout: any | null = null; + constructor( private _platform: Platform, private _ngZone: NgZone, @@ -40,46 +55,84 @@ export class MatButtonRippleLoader implements OnDestroy { this._document = document; this._ngZone.runOutsideAngular(() => { - const options = {passive: true, capture: true}; - this._document.addEventListener('focus', this.onInteraction, options); - this._document.addEventListener('mouseenter', this.onInteraction, options); + this._document.addEventListener('focus', this.onInteraction, OPTIONS); + this._document.addEventListener('mouseenter', this.onInteraction, OPTIONS); }); } ngOnDestroy() { this._ngZone.runOutsideAngular(() => { - document.removeEventListener('focus', this.onInteraction); - document.removeEventListener('mouseenter', this.onInteraction); + this._document.removeEventListener('focus', this.onInteraction, OPTIONS); + this._document.removeEventListener('mouseenter', 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; } - const button = event.target.closest('.mat-mdc-button-base:not([data-mat-button-interacted])'); + const button = this._closest(event.target); if (!button) { return; } + button.removeAttribute(MAT_BUTTON_INTERNALS_UNINITIALIZED); + this._actionQueue.push(() => this._attachButtonInternals(button as HTMLButtonElement)); + + // Immediately run all of the queued actions if a focus event occurs. + + if (event.type === 'focus') { + this._runActions(); + } else if (event.type === 'mouseenter') { + this._runActionsTimeout = setTimeout(() => this._runActions(), 50); + } + }; + + /** Runs all of the actions that have been queued up. */ + private _runActions(): void { + if (this._runActionsTimeout !== null) { + clearTimeout(this._runActionsTimeout); + this._runActionsTimeout = null; + } + for (const callback of this._actionQueue) { + callback(); + } + this._actionQueue = []; + } + + /** + * 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_INTERNALS_UNINITIALIZED)) { + return el; + } + el = el.parentElement; + } + return null; + } + + private _attachButtonInternals(button: HTMLButtonElement): void { button.prepend(this._createSpan(this._getPersistentRippleClassName(button))); - // A separate flag is used for the ripple because the ripple can - // be rendered separately from the rest of the button DOM internals - // if it is interacted with via the MatButton's ripple API. - if (!button.hasAttribute('data-mat-button-ripple-rendered')) { + if (button.hasAttribute(MAT_BUTTON_RIPPLE_UNINITIALIZED)) { + button.removeAttribute(MAT_BUTTON_RIPPLE_UNINITIALIZED); button.append(this._createSpan('mat-mdc-focus-indicator')); - this._appendRipple(button as HTMLButtonElement); - button.setAttribute('data-mat-button-ripple-rendered', ''); + this._appendRipple(button); } else { const rippleEl = button.querySelector('.mat-mdc-button-ripple'); rippleEl!.before(this._createSpan('mat-mdc-focus-indicator')); } - button.append(this._createSpan('mat-mdc-button-touch-target')); - button.setAttribute('data-mat-button-interacted', ''); - }; + // Move the touch target to the correct location in the button. + const touchTarget = button.querySelector('.mat-mdc-button-touch-target')!; + button.appendChild(touchTarget); + } private _appendRipple(button: HTMLButtonElement): void { const ripple = this._document.createElement('span'); diff --git a/src/material/button/button-ripple.ts b/src/material/button/button-ripple.ts index 81705e2e7053..5476b579c815 100644 --- a/src/material/button/button-ripple.ts +++ b/src/material/button/button-ripple.ts @@ -23,7 +23,7 @@ import { } from '../core'; import {DOCUMENT} from '@angular/common'; import {Platform} from '@angular/cdk/platform'; -import {MatButtonRippleLoader} from './button-lazy-ripple-renderer'; +import {MatButtonLazyLoader} from './button-lazy-loader'; /** * The MatButtonRipple directive is an extention of the MatRipple @@ -53,7 +53,7 @@ export class MatButtonRipple extends MatRipple { @Optional() @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) globalOptions?: RippleGlobalOptions, @Optional() @Inject(ANIMATION_MODULE_TYPE) _animationMode?: string, @Optional() @Inject(DOCUMENT) document?: Document, - @Inject(MatButtonRippleLoader) _rippleLoader?: MatButtonRippleLoader, + @Inject(MatButtonLazyLoader) _rippleLoader?: MatButtonLazyLoader, ) { super(_elementRef, ngZone, platform, globalOptions, _animationMode, document); this._buttonEl = _elementRef.nativeElement; diff --git a/src/material/button/button.html b/src/material/button/button.html index 07af25b40fde..6b36e94cfc86 100644 --- a/src/material/button/button.html +++ b/src/material/button/button.html @@ -5,3 +5,5 @@ + + diff --git a/src/material/button/icon-button.html b/src/material/button/icon-button.html index 6dbc74306383..b5ecb57ac272 100644 --- a/src/material/button/icon-button.html +++ b/src/material/button/icon-button.html @@ -1 +1,3 @@ + +