Skip to content

Commit b0635a5

Browse files
authored
fix(material/button): fix disableRipple regression (#27527)
* fix(material/button): fix disableRipple regression * fixup! fix(material/button): fix disableRipple regression * fixup! fix(material/button): fix disableRipple regression * fixup! fix(material/button): fix disableRipple regression
1 parent 00c5a17 commit b0635a5

File tree

5 files changed

+134
-119
lines changed

5 files changed

+134
-119
lines changed

src/material/button/button-base.ts

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77
*/
88

99
import {FocusMonitor, FocusOrigin} from '@angular/cdk/a11y';
10+
import {coerceBooleanProperty} from '@angular/cdk/coercion';
1011
import {Platform} from '@angular/cdk/platform';
1112
import {
1213
AfterViewInit,
1314
Directive,
1415
ElementRef,
1516
inject,
1617
NgZone,
17-
OnChanges,
1818
OnDestroy,
1919
OnInit,
2020
} from '@angular/core';
@@ -27,7 +27,7 @@ import {
2727
mixinDisabled,
2828
mixinDisableRipple,
2929
} from '@angular/material/core';
30-
import {MAT_BUTTON_RIPPLE_UNINITIALIZED, MatButtonLazyLoader} from './button-lazy-loader';
30+
import {MatButtonLazyLoader} from './button-lazy-loader';
3131

3232
/** Inputs common to all buttons. */
3333
export const MAT_BUTTON_INPUTS = ['disabled', 'disableRipple', 'color'];
@@ -43,7 +43,6 @@ export const MAT_BUTTON_HOST = {
4343
// Add a class that applies to all buttons. This makes it easier to target if somebody
4444
// wants to target all Material buttons.
4545
'[class.mat-mdc-button-base]': 'true',
46-
[MAT_BUTTON_RIPPLE_UNINITIALIZED]: '',
4746
};
4847

4948
/** List of classes to add to buttons instances based on host attribute selector. */
@@ -94,7 +93,7 @@ export const _MatButtonMixin = mixinColor(
9493
@Directive()
9594
export class MatButtonBase
9695
extends _MatButtonMixin
97-
implements CanDisable, CanColor, CanDisableRipple, AfterViewInit, OnChanges, OnDestroy
96+
implements CanDisable, CanColor, CanDisableRipple, AfterViewInit, OnDestroy
9897
{
9998
private readonly _focusMonitor = inject(FocusMonitor);
10099

@@ -113,17 +112,33 @@ export class MatButtonBase
113112
* @breaking-change 17.0.0
114113
*/
115114
get ripple(): MatRipple {
116-
if (!this._ripple && this._rippleLoader) {
117-
this._ripple = this._rippleLoader._createMatRipple(this._elementRef.nativeElement);
118-
}
119-
return this._ripple!;
115+
return this._rippleLoader?.getRipple(this._elementRef.nativeElement)!;
120116
}
121117
set ripple(v: MatRipple) {
122-
this._ripple = v;
118+
this._rippleLoader?.attachRipple(this._elementRef.nativeElement, v);
123119
}
124120

125-
/** @docs-private Reference to the MatRipple instance of the button. */
126-
protected _ripple?: MatRipple;
121+
// We override `disableRipple` and `disabled` so we can hook into
122+
// their setters and update the ripple disabled state accordingly.
123+
124+
/** Whether the ripple effect is disabled or not. */
125+
override get disableRipple(): boolean {
126+
return this._disableRipple;
127+
}
128+
override set disableRipple(value: any) {
129+
this._disableRipple = coerceBooleanProperty(value);
130+
this._updateRippleDisabled();
131+
}
132+
private _disableRipple: boolean = false;
133+
134+
override get disabled(): boolean {
135+
return this._disabled;
136+
}
137+
override set disabled(value: any) {
138+
this._disabled = coerceBooleanProperty(value);
139+
this._updateRippleDisabled();
140+
}
141+
private _disabled: boolean = false;
127142

128143
constructor(
129144
elementRef: ElementRef,
@@ -133,6 +148,10 @@ export class MatButtonBase
133148
) {
134149
super(elementRef);
135150

151+
this._rippleLoader?.configureRipple(this._elementRef.nativeElement, {
152+
className: 'mat-mdc-button-ripple',
153+
});
154+
136155
const classList = (elementRef.nativeElement as HTMLElement).classList;
137156

138157
// For each of the variant selectors that is present in the button's host
@@ -150,12 +169,6 @@ export class MatButtonBase
150169
this._focusMonitor.monitor(this._elementRef, true);
151170
}
152171

153-
ngOnChanges() {
154-
if (this._ripple) {
155-
this._ripple.disabled = this.disableRipple || this.disabled;
156-
}
157-
}
158-
159172
ngOnDestroy() {
160173
this._focusMonitor.stopMonitoring(this._elementRef);
161174
}
@@ -173,6 +186,13 @@ export class MatButtonBase
173186
private _hasHostAttributes(...attributes: string[]) {
174187
return attributes.some(attribute => this._elementRef.nativeElement.hasAttribute(attribute));
175188
}
189+
190+
private _updateRippleDisabled(): void {
191+
this._rippleLoader?.setDisabled(
192+
this._elementRef.nativeElement,
193+
this.disableRipple || this.disabled,
194+
);
195+
}
176196
}
177197

178198
/** Shared inputs by buttons using the `<a>` tag */
@@ -195,7 +215,6 @@ export const MAT_ANCHOR_HOST = {
195215
// Add a class that applies to all buttons. This makes it easier to target if somebody
196216
// wants to target all Material buttons.
197217
'[class.mat-mdc-button-base]': 'true',
198-
[MAT_BUTTON_RIPPLE_UNINITIALIZED]: '',
199218
};
200219

201220
/**

src/material/button/button-lazy-loader.ts

Lines changed: 95 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -15,30 +15,32 @@ import {
1515
OnDestroy,
1616
inject,
1717
} from '@angular/core';
18-
import {
19-
MAT_RIPPLE_GLOBAL_OPTIONS,
20-
MatRipple,
21-
RippleConfig,
22-
RippleGlobalOptions,
23-
RippleRenderer,
24-
RippleTarget,
25-
} from '@angular/material/core';
2618
import {Platform} from '@angular/cdk/platform';
19+
import {MAT_RIPPLE_GLOBAL_OPTIONS, MatRipple} from '@angular/material/core';
2720

2821
/** The options for the MatButtonRippleLoader's event listeners. */
2922
const eventListenerOptions = {capture: true};
3023

3124
/** The events that should trigger the initialization of the ripple. */
3225
const rippleInteractionEvents = ['focus', 'click', 'mouseenter', 'touchstart'];
3326

34-
/** The attribute attached to a mat-button whose ripple has not yet been initialized. */
35-
export const MAT_BUTTON_RIPPLE_UNINITIALIZED = 'mat-button-ripple-uninitialized';
27+
/** The attribute attached to a component whose ripple has not yet been initialized. */
28+
const matRippleUninitialized = 'mat-ripple-loader-uninitialized';
29+
30+
/** Additional classes that should be added to the ripple when it is rendered. */
31+
const matRippleClassName = 'mat-ripple-loader-class-name';
32+
33+
/** Whether the ripple should be centered. */
34+
const matRippleCentered = 'mat-ripple-loader-centered';
35+
36+
/** Whether the ripple should be disabled. */
37+
const matRippleDisabled = 'mat-ripple-loader-disabled';
3638

3739
/**
38-
* Handles attaching the MatButton's ripple on demand.
40+
* Handles attaching ripples on demand.
3941
*
40-
* This service allows us to avoid eagerly creating & attaching the MatButton's ripple.
41-
* It works by creating & attaching the ripple only when a MatButton is first interacted with.
42+
* This service allows us to avoid eagerly creating & attaching MatRipples.
43+
* It works by creating & attaching a ripple only when a component is first interacted with.
4244
*/
4345
@Injectable({providedIn: 'root'})
4446
export class MatButtonLazyLoader implements OnDestroy {
@@ -62,50 +64,93 @@ export class MatButtonLazyLoader implements OnDestroy {
6264
}
6365
}
6466

65-
/** Handles creating and attaching button internals when a button is initially interacted with. */
66-
private _onInteraction = (event: Event) => {
67-
if (event.target === this._document) {
68-
return;
67+
/**
68+
* Configures the ripple that will be rendered by the ripple loader.
69+
*
70+
* Stores the given information about how the ripple should be configured on the host
71+
* element so that it can later be retrived & used when the ripple is actually created.
72+
*/
73+
configureRipple(
74+
host: HTMLElement,
75+
config: {
76+
className?: string;
77+
centered?: boolean;
78+
disabled?: boolean;
79+
},
80+
): void {
81+
// Indicates that the ripple has not yet been rendered for this component.
82+
host.setAttribute(matRippleUninitialized, '');
83+
84+
// Store the additional class name(s) that should be added to the ripple element.
85+
if (config.className || !host.hasAttribute(matRippleClassName)) {
86+
host.setAttribute(matRippleClassName, config.className || '');
6987
}
70-
const eventTarget = event.target as Element;
7188

72-
// TODO(wagnermaciel): Consider batching these events to improve runtime performance.
89+
// Store whether the ripple should be centered.
90+
if (config.centered) {
91+
host.setAttribute(matRippleCentered, '');
92+
}
7393

74-
const button = eventTarget.closest(`[${MAT_BUTTON_RIPPLE_UNINITIALIZED}]`);
75-
if (button) {
76-
button.removeAttribute(MAT_BUTTON_RIPPLE_UNINITIALIZED);
77-
this._appendRipple(button as HTMLElement);
94+
if (config.disabled) {
95+
host.setAttribute(matRippleDisabled, '');
7896
}
79-
};
97+
}
8098

81-
/** Creates a MatButtonRipple and appends it to the given button element. */
82-
private _appendRipple(button: HTMLElement): void {
83-
if (!this._document) {
84-
return;
99+
/** Returns the ripple instance for the given host element. */
100+
getRipple(host: HTMLElement): MatRipple | undefined {
101+
if ((host as any).matRipple) {
102+
return (host as any).matRipple;
85103
}
86-
const ripple = this._document.createElement('span');
87-
ripple.classList.add('mat-mdc-button-ripple');
104+
return this.createRipple(host);
105+
}
88106

89-
const target = new MatButtonRippleTarget(
90-
button,
91-
this._globalRippleOptions ? this._globalRippleOptions : undefined,
92-
this._animationMode ? this._animationMode : undefined,
93-
);
94-
target.rippleConfig.centered = button.hasAttribute('mat-icon-button');
107+
/** Sets the disabled state on the ripple instance corresponding to the given host element. */
108+
setDisabled(host: HTMLElement, disabled: boolean): void {
109+
const ripple = (host as any).matRipple as MatRipple | undefined;
95110

96-
const rippleRenderer = new RippleRenderer(target, this._ngZone, ripple, this._platform);
97-
rippleRenderer.setupTriggerEvents(button);
98-
button.append(ripple);
111+
// If the ripple has already been instantiated, just disable it.
112+
if (ripple) {
113+
ripple.disabled = disabled;
114+
return;
115+
}
116+
117+
// Otherwise, set an attribute so we know what the
118+
// disabled state should be when the ripple is initialized.
119+
if (disabled) {
120+
host.setAttribute(matRippleDisabled, '');
121+
} else {
122+
host.removeAttribute(matRippleDisabled);
123+
}
99124
}
100125

101-
_createMatRipple(button: HTMLElement): MatRipple | undefined {
126+
/** Handles creating and attaching component internals when a component it is initially interacted with. */
127+
private _onInteraction = (event: Event) => {
128+
if (!(event.target instanceof HTMLElement)) {
129+
return;
130+
}
131+
const eventTarget = event.target as HTMLElement;
132+
133+
// TODO(wagnermaciel): Consider batching these events to improve runtime performance.
134+
135+
const element = eventTarget.closest(`[${matRippleUninitialized}]`);
136+
if (element) {
137+
this.createRipple(element as HTMLElement);
138+
}
139+
};
140+
141+
/** Creates a MatRipple and appends it to the given element. */
142+
createRipple(host: HTMLElement): MatRipple | undefined {
102143
if (!this._document) {
103144
return;
104145
}
105-
button.querySelector('.mat-mdc-button-ripple')?.remove();
106-
button.removeAttribute(MAT_BUTTON_RIPPLE_UNINITIALIZED);
146+
147+
// Create the ripple element.
148+
host.querySelector('.mat-ripple')?.remove();
107149
const rippleEl = this._document!.createElement('span');
108-
rippleEl.classList.add('mat-mdc-button-ripple');
150+
rippleEl.classList.add('mat-ripple', host.getAttribute(matRippleClassName)!);
151+
host.append(rippleEl);
152+
153+
// Create the MatRipple.
109154
const ripple = new MatRipple(
110155
new ElementRef(rippleEl),
111156
this._ngZone,
@@ -114,38 +159,15 @@ export class MatButtonLazyLoader implements OnDestroy {
114159
this._animationMode ? this._animationMode : undefined,
115160
);
116161
ripple._isInitialized = true;
117-
ripple.trigger = button;
118-
button.append(rippleEl);
162+
ripple.trigger = host;
163+
ripple.centered = host.hasAttribute(matRippleCentered);
164+
ripple.disabled = host.hasAttribute(matRippleDisabled);
165+
this.attachRipple(host, ripple);
119166
return ripple;
120167
}
121-
}
122-
123-
/**
124-
* The RippleTarget for the lazily rendered MatButton ripple.
125-
* It handles ripple configuration and disabled state for ripples interactions.
126-
*
127-
* Note that this configuration is usually handled by the MatRipple, but the MatButtonLazyLoader does not use the
128-
* MatRipple Directive. In order to create & attach a ripple on demand, it uses the "lower level" RippleRenderer.
129-
*/
130-
class MatButtonRippleTarget implements RippleTarget {
131-
rippleConfig: RippleConfig & RippleGlobalOptions;
132-
133-
constructor(
134-
private _button: HTMLElement,
135-
private _globalRippleOptions?: RippleGlobalOptions,
136-
animationMode?: string,
137-
) {
138-
this._setRippleConfig(_globalRippleOptions, animationMode);
139-
}
140-
141-
private _setRippleConfig(globalRippleOptions?: RippleGlobalOptions, animationMode?: string) {
142-
this.rippleConfig = globalRippleOptions || {};
143-
if (animationMode === 'NoopAnimations') {
144-
this.rippleConfig.animation = {enterDuration: 0, exitDuration: 0};
145-
}
146-
}
147168

148-
get rippleDisabled(): boolean {
149-
return this._button.hasAttribute('disabled') || !!this._globalRippleOptions?.disabled;
169+
attachRipple(host: Element, ripple: MatRipple): void {
170+
host.removeAttribute(matRippleUninitialized);
171+
(host as any).matRipple = ripple;
150172
}
151173
}

src/material/button/button.spec.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {By} from '@angular/platform-browser';
44
import {MatButtonModule, MatButton, MatFabDefaultOptions, MAT_FAB_DEFAULT_OPTIONS} from './index';
55
import {MatRipple, ThemePalette} from '@angular/material/core';
66
import {createMouseEvent, dispatchEvent} from '@angular/cdk/testing/private';
7-
import {MAT_BUTTON_RIPPLE_UNINITIALIZED} from './button-lazy-loader';
87

98
describe('MDC-based MatButton', () => {
109
beforeEach(waitForAsync(() => {
@@ -317,9 +316,6 @@ describe('MDC-based MatButton', () => {
317316
const fab = fixture.debugElement.query(By.css('button[mat-fab]'))!;
318317
let ripple = fab.nativeElement.querySelector('.mat-mdc-button-ripple');
319318
expect(ripple).withContext('Expect ripple to be absent before user interaction').toBeNull();
320-
expect(fab.nativeElement.hasAttribute(MAT_BUTTON_RIPPLE_UNINITIALIZED))
321-
.withContext('Expect mat-button to have the "uninitialized" attr before user interaction')
322-
.toBeTrue();
323319

324320
// Referencing the ripple should instantiate the ripple.
325321
expect(fab.componentInstance.ripple).toBeDefined();
@@ -328,11 +324,6 @@ describe('MDC-based MatButton', () => {
328324
expect(ripple)
329325
.withContext('Expect ripple to be present after user interaction')
330326
.not.toBeNull();
331-
expect(fab.nativeElement.hasAttribute(MAT_BUTTON_RIPPLE_UNINITIALIZED))
332-
.withContext(
333-
'Expect mat-button NOT to have the "uninitialized" attr after user interaction',
334-
)
335-
.toBeFalse();
336327
});
337328

338329
// Ensure each of these events triggers the initialization of the button ripple.
@@ -341,12 +332,10 @@ describe('MDC-based MatButton', () => {
341332
const fab = fixture.debugElement.query(By.css('button[mat-fab]'))!;
342333
let ripple = fab.nativeElement.querySelector('.mat-mdc-button-ripple');
343334
expect(ripple).toBeNull();
344-
expect(fab.nativeElement.hasAttribute(MAT_BUTTON_RIPPLE_UNINITIALIZED)).toBeTrue();
345335

346336
dispatchEvent(fab.nativeElement, createMouseEvent(event));
347337
ripple = fab.nativeElement.querySelector('.mat-mdc-button-ripple');
348338
expect(ripple).not.toBeNull();
349-
expect(fab.nativeElement.hasAttribute(MAT_BUTTON_RIPPLE_UNINITIALIZED)).toBeFalse();
350339
});
351340
}
352341
});

0 commit comments

Comments
 (0)