-
Notifications
You must be signed in to change notification settings - Fork 6.8k
fix(material/core): ripples persisting when container is removed from DOM while fading-in #24482
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
/** | ||
* @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 {NgModule} from '@angular/core'; | ||
import {ExampleViewerModule} from '../example-viewer/example-viewer-module'; | ||
import {SelectE2e} from './select-e2e'; | ||
|
||
@NgModule({ | ||
imports: [ExampleViewerModule], | ||
declarations: [SelectE2e], | ||
}) | ||
export class SelectE2eModule {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
/** | ||
* @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 {Component} from '@angular/core'; | ||
|
||
@Component({ | ||
selector: 'select-demo', | ||
template: `<example-list-viewer [ids]="examples"></example-list-viewer>`, | ||
}) | ||
export class SelectE2e { | ||
examples = ['select-overview']; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -176,6 +176,10 @@ export class RippleRenderer implements EventListenerObject { | |
if (!animationForciblyDisabledThroughCss && (enterDuration || animationConfig.exitDuration)) { | ||
this._ngZone.runOutsideAngular(() => { | ||
ripple.addEventListener('transitionend', () => this._finishRippleTransition(rippleRef)); | ||
// If the transition is cancelled (e.g. due to DOM removal), we destroy the ripple | ||
// directly as otherwise we would keep it part of the ripple container forever. | ||
// https://www.w3.org/TR/css-transitions-1/#:~:text=no%20longer%20in%20the%20document. | ||
ripple.addEventListener('transitioncancel', () => this._destroyRipple(rippleRef)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should these event listeners be removed once the ripple is destroyed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good question. Not sure. If the ripple element is not referenced anywhere, it shouldn't matter? |
||
}); | ||
} | ||
|
||
|
@@ -190,19 +194,8 @@ export class RippleRenderer implements EventListenerObject { | |
|
||
/** Fades out a ripple reference. */ | ||
fadeOutRipple(rippleRef: RippleRef) { | ||
const wasActive = this._activeRipples.delete(rippleRef); | ||
|
||
if (rippleRef === this._mostRecentTransientRipple) { | ||
this._mostRecentTransientRipple = null; | ||
} | ||
|
||
// Clear out the cached bounding rect if we have no more ripples. | ||
if (!this._activeRipples.size) { | ||
this._containerRect = null; | ||
} | ||
|
||
// For ripples that are not active anymore, don't re-run the fade-out animation. | ||
if (!wasActive) { | ||
// For ripples already fading out or hidden, this should be a noop. | ||
if (rippleRef.state === RippleState.FADING_OUT || rippleRef.state === RippleState.HIDDEN) { | ||
return; | ||
} | ||
|
||
|
@@ -303,6 +296,19 @@ export class RippleRenderer implements EventListenerObject { | |
|
||
/** Destroys the given ripple by removing it from the DOM and updating its state. */ | ||
private _destroyRipple(rippleRef: RippleRef) { | ||
this._activeRipples.delete(rippleRef); | ||
|
||
// Clear out the cached bounding rect if we have no more ripples. | ||
if (!this._activeRipples.size) { | ||
this._containerRect = null; | ||
} | ||
|
||
// If the current ref is the most recent transient ripple, unset it | ||
// avoid memory leaks. | ||
if (rippleRef === this._mostRecentTransientRipple) { | ||
this._mostRecentTransientRipple = null; | ||
} | ||
|
||
rippleRef.state = RippleState.HIDDEN; | ||
rippleRef.element.remove(); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import {browser, by, element, ExpectedConditions} from 'protractor'; | ||
import {getElement} from '../../cdk/testing/private/e2e'; | ||
|
||
const presenceOf = ExpectedConditions.presenceOf; | ||
const not = ExpectedConditions.not; | ||
|
||
describe('select', () => { | ||
beforeEach(async () => await browser.get('/select?animations=true')); | ||
|
||
// Regression test which ensures that ripples within the select are not persisted | ||
// accidentally. This could happen because the select panel is removed from DOM | ||
// immediately when an option is clicked. Usually ripples still fade-in at that point. | ||
it('should not accidentally persist ripples', async () => { | ||
const select = getElement('.mat-select'); | ||
const options = element.all(by.css('.mat-option')); | ||
const ripples = element.all(by.css('.mat-ripple-element')); | ||
|
||
// Wait for select to be rendered. | ||
await browser.wait(presenceOf(select)); | ||
|
||
// Opent the select and wait for options to be displayed. | ||
await select.click(); | ||
await browser.wait(presenceOf(options.get(0))); | ||
|
||
// Click the first option and wait for the select to be closed. | ||
await options.get(0).click(); | ||
await browser.wait(not(presenceOf(options.get(0)))); | ||
|
||
// Re-open the select and wait for it to be rendered. | ||
await select.click(); | ||
await browser.wait(presenceOf(options.get(0))); | ||
|
||
// Expect no ripples to be showing up without an option click. | ||
expect(await ripples.isPresent()).toBe(false); | ||
}); | ||
}); |
Uh oh!
There was an error while loading. Please reload this page.