Skip to content

Commit 89ee76f

Browse files
committed
fixup! feat(material/button): make button ripples lazy
1 parent 03b3ee7 commit 89ee76f

File tree

5 files changed

+77
-18
lines changed

5 files changed

+77
-18
lines changed

src/material/button/button-base.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ export const _MatButtonMixin = mixinColor(
9191
/** Base class for all buttons. */
9292
@Directive({
9393
host: {
94+
'mat-button-ripple-uninitialized': 'true',
95+
'mat-button-internals-uninitialized': 'true',
9496
'[attr.mat-button-disabled]': '_isRippleDisabled()',
9597
'[attr.data-mat-button-is-fab]': '_isFab',
9698
},

src/material/button/button-lazy-ripple-renderer.ts renamed to src/material/button/button-lazy-loader.ts

Lines changed: 69 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,25 @@ import {
2424
} from '../core';
2525
import {Platform} from '@angular/cdk/platform';
2626

27+
/** The options for the MatButtonRippleLoader's event listeners. */
28+
const OPTIONS = {passive: true, capture: true};
29+
30+
/** The attribute attached to a mat-button whose ripple has not yet been initialized. */
31+
const MAT_BUTTON_RIPPLE_UNINITIALIZED = 'mat-button-ripple-uninitialized';
32+
33+
/** The attribute attached to a mat-button whose internals (excluding the ripple) have not yet been initialized. */
34+
const MAT_BUTTON_INTERNALS_UNINITIALIZED = 'mat-button-internals-uninitialized';
35+
2736
@Injectable({providedIn: 'root'})
28-
export class MatButtonRippleLoader implements OnDestroy {
37+
export class MatButtonLazyLoader implements OnDestroy {
2938
private _document: Document;
3039

40+
/** A batch of actions to run. */
41+
private _actionQueue: Function[] = [];
42+
43+
/** A timeout for when the action queue will be emptied / ran. */
44+
private _runActionsTimeout: any | null = null;
45+
3146
constructor(
3247
private _platform: Platform,
3348
private _ngZone: NgZone,
@@ -40,46 +55,84 @@ export class MatButtonRippleLoader implements OnDestroy {
4055
this._document = document;
4156

4257
this._ngZone.runOutsideAngular(() => {
43-
const options = {passive: true, capture: true};
44-
this._document.addEventListener('focus', this.onInteraction, options);
45-
this._document.addEventListener('mouseenter', this.onInteraction, options);
58+
this._document.addEventListener('focus', this.onInteraction, OPTIONS);
59+
this._document.addEventListener('mouseenter', this.onInteraction, OPTIONS);
4660
});
4761
}
4862

4963
ngOnDestroy() {
5064
this._ngZone.runOutsideAngular(() => {
51-
document.removeEventListener('focus', this.onInteraction);
52-
document.removeEventListener('mouseenter', this.onInteraction);
65+
this._document.removeEventListener('focus', this.onInteraction, OPTIONS);
66+
this._document.removeEventListener('mouseenter', this.onInteraction, OPTIONS);
5367
});
5468
}
5569

70+
/** Handles creating and attaching button internals when a button is initially interacted with. */
5671
private onInteraction = (event: Event) => {
5772
if (!(event.target instanceof Element)) {
5873
return;
5974
}
6075

61-
const button = event.target.closest('.mat-mdc-button-base:not([data-mat-button-interacted])');
76+
const button = this._closest(event.target);
6277
if (!button) {
6378
return;
6479
}
6580

81+
button.removeAttribute(MAT_BUTTON_INTERNALS_UNINITIALIZED);
82+
this._actionQueue.push(() => this._attachButtonInternals(button as HTMLButtonElement));
83+
84+
// Immediately run all of the queued actions if a focus event occurs.
85+
86+
if (event.type === 'focus') {
87+
this._runActions();
88+
} else if (event.type === 'mouseenter') {
89+
this._runActionsTimeout = setTimeout(() => this._runActions(), 50);
90+
}
91+
};
92+
93+
/** Runs all of the actions that have been queued up. */
94+
private _runActions(): void {
95+
if (this._runActionsTimeout !== null) {
96+
clearTimeout(this._runActionsTimeout);
97+
this._runActionsTimeout = null;
98+
}
99+
for (const callback of this._actionQueue) {
100+
callback();
101+
}
102+
this._actionQueue = [];
103+
}
104+
105+
/**
106+
* Traverses the element and its parents (heading toward the document root)
107+
* until it finds a mat-button that has not been initialized.
108+
*/
109+
private _closest(element: Element): Element | null {
110+
let el: Element | null = element;
111+
while (el) {
112+
if (el.hasAttribute(MAT_BUTTON_INTERNALS_UNINITIALIZED)) {
113+
return el;
114+
}
115+
el = el.parentElement;
116+
}
117+
return null;
118+
}
119+
120+
private _attachButtonInternals(button: HTMLButtonElement): void {
66121
button.prepend(this._createSpan(this._getPersistentRippleClassName(button)));
67122

68-
// A separate flag is used for the ripple because the ripple can
69-
// be rendered separately from the rest of the button DOM internals
70-
// if it is interacted with via the MatButton's ripple API.
71-
if (!button.hasAttribute('data-mat-button-ripple-rendered')) {
123+
if (button.hasAttribute(MAT_BUTTON_RIPPLE_UNINITIALIZED)) {
124+
button.removeAttribute(MAT_BUTTON_RIPPLE_UNINITIALIZED);
72125
button.append(this._createSpan('mat-mdc-focus-indicator'));
73-
this._appendRipple(button as HTMLButtonElement);
74-
button.setAttribute('data-mat-button-ripple-rendered', '');
126+
this._appendRipple(button);
75127
} else {
76128
const rippleEl = button.querySelector('.mat-mdc-button-ripple');
77129
rippleEl!.before(this._createSpan('mat-mdc-focus-indicator'));
78130
}
79131

80-
button.append(this._createSpan('mat-mdc-button-touch-target'));
81-
button.setAttribute('data-mat-button-interacted', '');
82-
};
132+
// Move the touch target to the correct location in the button.
133+
const touchTarget = button.querySelector('.mat-mdc-button-touch-target')!;
134+
button.appendChild(touchTarget);
135+
}
83136

84137
private _appendRipple(button: HTMLButtonElement): void {
85138
const ripple = this._document.createElement('span');

src/material/button/button-ripple.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
} from '../core';
2424
import {DOCUMENT} from '@angular/common';
2525
import {Platform} from '@angular/cdk/platform';
26-
import {MatButtonRippleLoader} from './button-lazy-ripple-renderer';
26+
import {MatButtonLazyLoader} from './button-lazy-loader';
2727

2828
/**
2929
* The MatButtonRipple directive is an extention of the MatRipple
@@ -53,7 +53,7 @@ export class MatButtonRipple extends MatRipple {
5353
@Optional() @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) globalOptions?: RippleGlobalOptions,
5454
@Optional() @Inject(ANIMATION_MODULE_TYPE) _animationMode?: string,
5555
@Optional() @Inject(DOCUMENT) document?: Document,
56-
@Inject(MatButtonRippleLoader) _rippleLoader?: MatButtonRippleLoader,
56+
@Inject(MatButtonLazyLoader) _rippleLoader?: MatButtonLazyLoader,
5757
) {
5858
super(_elementRef, ngZone, platform, globalOptions, _animationMode, document);
5959
this._buttonEl = _elementRef.nativeElement;

src/material/button/button.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@
55

66
<ng-content select=".material-icons[iconPositionEnd], mat-icon[iconPositionEnd], [matButtonIcon][iconPositionEnd]">
77
</ng-content>
8+
9+
<span class="mat-mdc-button-touch-target"></span>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
<ng-content></ng-content>
2+
3+
<span class="mat-mdc-button-touch-target"></span>

0 commit comments

Comments
 (0)