Skip to content

Commit

Permalink
fixup! feat(material/button): make button ripples lazy
Browse files Browse the repository at this point in the history
  • Loading branch information
wagnermaciel committed Feb 24, 2023
1 parent 1d6d46b commit 127faf5
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 18 deletions.
2 changes: 2 additions & 0 deletions src/material/button/button-base.ts
Expand Up @@ -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',
},
Expand Down
Expand Up @@ -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,
Expand All @@ -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');
Expand Down
4 changes: 2 additions & 2 deletions src/material/button/button-ripple.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/material/button/button.html
Expand Up @@ -5,3 +5,5 @@

<ng-content select=".material-icons[iconPositionEnd], mat-icon[iconPositionEnd], [matButtonIcon][iconPositionEnd]">
</ng-content>

<span class="mat-mdc-button-touch-target"></span>
2 changes: 2 additions & 0 deletions src/material/button/icon-button.html
@@ -1 +1,3 @@
<ng-content></ng-content>

<span class="mat-mdc-button-touch-target"></span>

0 comments on commit 127faf5

Please sign in to comment.