Skip to content
This repository was archived by the owner on Jan 13, 2025. It is now read-only.

Commit d83d5bd

Browse files
authored
fix(ripple): Only deduplicate events on parents whose children activated (#2160)
BREAKING CHANGE: Adds `containsEventTarget(target)` API to the ripple adapter.
1 parent c09aeae commit d83d5bd

File tree

7 files changed

+50
-11
lines changed

7 files changed

+50
-11
lines changed

packages/mdc-ripple/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ Method Signature | Description
153153
| `isSurfaceDisabled() => boolean` | Whether or not the ripple is attached to a disabled component |
154154
| `addClass(className: string) => void` | Adds a class to the ripple surface |
155155
| `removeClass(className: string) => void` | Removes a class from the ripple surface |
156+
| `containsEventTarget(target: EventTarget) => boolean` | Whether or not the ripple surface contains the given event target |
156157
| `registerInteractionHandler(evtType: string, handler: EventListener) => void` | Registers an event handler on the ripple surface |
157158
| `deregisterInteractionHandler(evtType: string, handler: EventListener) => void` | Unregisters an event handler on the ripple surface |
158159
| `registerDocumentInteractionHandler(evtType: string, handler: EventListener) => void` | Registers an event handler on the documentElement |

packages/mdc-ripple/adapter.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ class MDCRippleAdapter {
5757
/** @param {string} className */
5858
removeClass(className) {}
5959

60+
/** @param {!EventTarget} target */
61+
containsEventTarget(target) {}
62+
6063
/**
6164
* @param {string} evtType
6265
* @param {!Function} handler

packages/mdc-ripple/foundation.js

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,9 @@ const ACTIVATION_EVENT_TYPES = ['touchstart', 'pointerdown', 'mousedown', 'keydo
6666
// Deactivation events registered on documentElement when a pointer-related down event occurs
6767
const POINTER_DEACTIVATION_EVENT_TYPES = ['touchend', 'pointerup', 'mouseup'];
6868

69-
// Tracks whether an activation has occurred on the current frame, to avoid multiple nested activations
70-
let isActivating = false;
69+
// Tracks activations that have occurred on the current frame, to avoid simultaneous nested activations
70+
/** @type {!Array<!EventTarget>} */
71+
let activatedTargets = [];
7172

7273
/**
7374
* @extends {MDCFoundation<!MDCRippleAdapter>}
@@ -93,6 +94,7 @@ class MDCRippleFoundation extends MDCFoundation {
9394
isSurfaceDisabled: () => /* boolean */ {},
9495
addClass: (/* className: string */) => {},
9596
removeClass: (/* className: string */) => {},
97+
containsEventTarget: (/* target: !EventTarget */) => {},
9698
registerInteractionHandler: (/* evtType: string, handler: EventListener */) => {},
9799
deregisterInteractionHandler: (/* evtType: string, handler: EventListener */) => {},
98100
registerDocumentInteractionHandler: (/* evtType: string, handler: EventListener */) => {},
@@ -284,7 +286,13 @@ class MDCRippleFoundation extends MDCFoundation {
284286
* @private
285287
*/
286288
activate_(e) {
287-
if (isActivating || this.adapter_.isSurfaceDisabled()) {
289+
if (this.adapter_.isSurfaceDisabled()) {
290+
return;
291+
}
292+
293+
const hasActivatedChild =
294+
e && activatedTargets.length > 0 && activatedTargets.some((target) => this.adapter_.containsEventTarget(target));
295+
if (hasActivatedChild) {
288296
return;
289297
}
290298

@@ -308,10 +316,10 @@ class MDCRippleFoundation extends MDCFoundation {
308316
);
309317

310318
if (e) {
319+
activatedTargets.push(/** @type {!EventTarget} */ (e.target));
311320
this.registerDeactivationHandlers_(e);
312321
}
313322

314-
isActivating = true;
315323
requestAnimationFrame(() => {
316324
// This needs to be wrapped in an rAF call b/c web browsers
317325
// report active states inconsistently when they're called within
@@ -326,8 +334,8 @@ class MDCRippleFoundation extends MDCFoundation {
326334
this.activationState_ = this.defaultActivationState_();
327335
}
328336

329-
// Reset flag on next frame to avoid any ancestors from also triggering ripple from the same interaction
330-
isActivating = false;
337+
// Reset array on next frame after the current event has had a chance to bubble to prevent ancestor ripples
338+
activatedTargets = [];
331339
});
332340
}
333341

packages/mdc-ripple/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ class MDCRipple extends MDCComponent {
6363
isSurfaceDisabled: () => instance.disabled,
6464
addClass: (className) => instance.root_.classList.add(className),
6565
removeClass: (className) => instance.root_.classList.remove(className),
66+
containsEventTarget: (target) => instance.root_.contains(target),
6667
registerInteractionHandler: (evtType, handler) =>
6768
instance.root_.addEventListener(evtType, handler, util.applyPassive()),
6869
deregisterInteractionHandler: (evtType, handler) =>

test/unit/mdc-ripple/foundation-activation.test.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -318,17 +318,16 @@ testFoundation('removes deactivation classes on activate to ensure ripples can b
318318
td.verify(adapter.removeClass(cssClasses.FG_DEACTIVATION));
319319
});
320320

321-
testFoundation('will not activate multiple ripples on same frame',
321+
testFoundation('will not activate multiple ripples on same frame if one surface descends from another',
322322
({foundation, adapter, mockRaf}) => {
323323
const secondRipple = setupTest();
324324
const firstHandlers = captureHandlers(adapter, 'registerInteractionHandler');
325325
const secondHandlers = captureHandlers(secondRipple.adapter, 'registerInteractionHandler');
326+
td.when(secondRipple.adapter.containsEventTarget(td.matchers.anything())).thenReturn(true);
326327
foundation.init();
327328
secondRipple.foundation.init();
328329
mockRaf.flush();
329330

330-
// Simulate use case where a child and parent are both ripple surfaces, and the same event propagates to the
331-
// parent after being handled on the child
332331
firstHandlers.mousedown();
333332
secondHandlers.mousedown();
334333
mockRaf.flush();
@@ -337,6 +336,24 @@ testFoundation('will not activate multiple ripples on same frame',
337336
td.verify(secondRipple.adapter.addClass(cssClasses.FG_ACTIVATION), {times: 0});
338337
});
339338

339+
testFoundation('will activate multiple ripples on same frame for surfaces without an ancestor/descendant relationship',
340+
({foundation, adapter, mockRaf}) => {
341+
const secondRipple = setupTest();
342+
const firstHandlers = captureHandlers(adapter, 'registerInteractionHandler');
343+
const secondHandlers = captureHandlers(secondRipple.adapter, 'registerInteractionHandler');
344+
td.when(secondRipple.adapter.containsEventTarget(td.matchers.anything())).thenReturn(false);
345+
foundation.init();
346+
secondRipple.foundation.init();
347+
mockRaf.flush();
348+
349+
firstHandlers.mousedown();
350+
secondHandlers.mousedown();
351+
mockRaf.flush();
352+
353+
td.verify(adapter.addClass(cssClasses.FG_ACTIVATION));
354+
td.verify(secondRipple.adapter.addClass(cssClasses.FG_ACTIVATION));
355+
});
356+
340357
testFoundation('displays the foreground ripple on activation when unbounded', ({foundation, adapter, mockRaf}) => {
341358
const handlers = captureHandlers(adapter, 'registerInteractionHandler');
342359
td.when(adapter.computeBoundingRect()).thenReturn({width: 100, height: 100, left: 0, top: 0});

test/unit/mdc-ripple/foundation.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ test('numbers returns constants.numbers', () => {
4040
test('defaultAdapter returns a complete adapter implementation', () => {
4141
verifyDefaultAdapter(MDCRippleFoundation, [
4242
'browserSupportsCssVars', 'isUnbounded', 'isSurfaceActive', 'isSurfaceDisabled',
43-
'addClass', 'removeClass', 'registerInteractionHandler', 'deregisterInteractionHandler',
43+
'addClass', 'removeClass', 'containsEventTarget', 'registerInteractionHandler', 'deregisterInteractionHandler',
4444
'registerDocumentInteractionHandler', 'deregisterDocumentInteractionHandler',
4545
'registerResizeHandler', 'deregisterResizeHandler', 'updateCssVariable',
4646
'computeBoundingRect', 'getWindowPageOffset',

test/unit/mdc-ripple/mdc-ripple.test.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,15 @@ test('adapter#removeClass removes a class from the root', () => {
137137
assert.isNotOk(root.classList.contains('foo'));
138138
});
139139

140+
test('adapter#containsEventTarget returns true if the passed element is a descendant of the root element', () => {
141+
const {root, component} = setupTest();
142+
const child = bel`<div></div>`;
143+
const notChild = bel`<div></div>`;
144+
root.appendChild(child);
145+
assert.isTrue(component.getDefaultFoundation().adapter_.containsEventTarget(child));
146+
assert.isFalse(component.getDefaultFoundation().adapter_.containsEventTarget(notChild));
147+
});
148+
140149
test('adapter#registerInteractionHandler proxies to addEventListener on the root element', () => {
141150
const {root, component} = setupTest();
142151
const handler = td.func('interactionHandler');
@@ -180,7 +189,7 @@ test('adapter#registerResizeHandler uses the handler as a window resize listener
180189
window.removeEventListener('resize', handler);
181190
});
182191

183-
test('adapter#registerResizeHandler unlistens the handler for window resize', () => {
192+
test('adapter#deregisterResizeHandler unlistens the handler for window resize', () => {
184193
const {component} = setupTest();
185194
const handler = td.func('resizeHandler');
186195
window.addEventListener('resize', handler);

0 commit comments

Comments
 (0)