Skip to content

Commit

Permalink
feat(ripple): Implement subset of improved interaction response guide…
Browse files Browse the repository at this point in the history
…lines for ripple

Resolves #190
  • Loading branch information
cristobalchao@google.com committed Mar 2, 2017
1 parent d5873f6 commit 7eceb51
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 6 deletions.
35 changes: 29 additions & 6 deletions packages/mdc-ripple/foundation.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ const DEACTIVATION_ACTIVATION_PAIRS = {
blur: 'focus',
};

const DEACTIVATION_TIMEOUT_MS = 200;

export default class MDCRippleFoundation extends MDCFoundation {
static get cssClasses() {
return cssClasses;
Expand Down Expand Up @@ -110,6 +112,9 @@ export default class MDCRippleFoundation extends MDCFoundation {
wasElementMadeActive: false,
activationStartTime: 0,
activationEvent: null,
isProgrammatic: false,
isTimeoutDeactivated: false,
deactivationTimeout: null,
};
}

Expand Down Expand Up @@ -146,12 +151,20 @@ export default class MDCRippleFoundation extends MDCFoundation {

activationState.isActivated = true;
activationState.isProgrammatic = e === null;
activationState.isTimeoutDeactivated = false;
activationState.activationEvent = e;
activationState.wasActivatedByPointer = activationState.isProgrammatic ? false : (
e.type === 'mousedown' || e.type === 'touchstart' || e.type === 'pointerdown'
);

activationState.activationStartTime = Date.now();

activationState.deactivationTimeout = setTimeout(() => {
activationState.isTimeoutDeactivated = true;
this.deactivate_(e);
activationState.isActivated = false;
}, DEACTIVATION_TIMEOUT_MS);

requestAnimationFrame(() => {
// This needs to be wrapped in an rAF call b/c web browsers
// report active states inconsistently when they're called within
Expand Down Expand Up @@ -215,16 +228,18 @@ export default class MDCRippleFoundation extends MDCFoundation {
this.activationState_ = this.defaultActivationState_();
return;
}

const actualActivationType = DEACTIVATION_ACTIVATION_PAIRS[e.type];
const expectedActivationType = activationState.activationEvent.type;
// NOTE: Pointer events are tricky - https://patrickhlauke.github.io/touch/tests/results/
// Essentially, what we need to do here is decouple the deactivation UX from the actual
// deactivation state itself. This way, touch/pointer events in sequence do not trample one
// another.
const needsDeactivationUX = actualActivationType === expectedActivationType;
const needsDeactivationUX = actualActivationType === expectedActivationType ||
activationState.isTimeoutDeactivated;
let needsActualDeactivation = needsDeactivationUX;
if (activationState.wasActivatedByPointer) {
needsActualDeactivation = e.type === 'mouseup';
needsActualDeactivation = activationState.isTimeoutDeactivated || e.type === 'mouseup';
}

const state = Object.assign({}, activationState);
Expand All @@ -234,22 +249,30 @@ export default class MDCRippleFoundation extends MDCFoundation {
if (needsActualDeactivation) {
this.activationState_ = this.defaultActivationState_();
}

clearTimeout(activationState.deactivationTimeout);
}

deactivate() {
this.deactivate_(null);
}

animateDeactivation_(e, {wasActivatedByPointer, wasElementMadeActive, activationStartTime, isProgrammatic}) {
animateDeactivation_(e, {wasActivatedByPointer, wasElementMadeActive, activationStartTime, isProgrammatic,
isTimeoutDeactivated}) {
const {BG_ACTIVE} = MDCRippleFoundation.cssClasses;
if (wasActivatedByPointer || wasElementMadeActive) {
this.adapter_.removeClass(BG_ACTIVE);
const isPointerEvent = isProgrammatic ? false : (
e.type === 'touchend' || e.type === 'pointerup' || e.type === 'mouseup'
);


if (this.adapter_.isUnbounded()) {
this.animateUnboundedDeactivation_(this.getUnboundedDeactivationInfo_(activationStartTime));
} else {
const isPointerEventOnDeactivation = isTimeoutDeactivated &&
(e.type === 'touchstart' || e.type === 'pointerdown' || e.type === 'mousedown');

const isPointerEvent = isProgrammatic ? false : (
e.type === 'touchend' || e.type === 'pointerup' || e.type === 'mouseup' || isPointerEventOnDeactivation
);
this.animateBoundedDeactivation_(e, isPointerEvent);
}
}
Expand Down
37 changes: 37 additions & 0 deletions test/unit/mdc-ripple/foundation-deactivation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,43 @@ testFoundation('runs deactivation UX on public deactivate() call', ({foundation,
td.verify(adapter.addClass(cssClasses.FG_BOUNDED_ACTIVE_FILL));
});

testFoundation('runs deactivation UX mousedown pressed for longer than deactivation timeout',
({foundation, adapter, mockRaf}) => {
const handlers = captureHandlers(adapter);
foundation.init();
mockRaf.flush();

handlers.mousedown();
mockRaf.flush();

setTimeout(() => {
td.verify(adapter.removeClass(cssClasses.BG_ACTIVE));
td.verify(adapter.addClass(cssClasses.BG_BOUNDED_ACTIVE_FILL));
td.verify(adapter.addClass(cssClasses.FG_BOUNDED_ACTIVE_FILL));
}, foundation.DEACTIVATION_TIMEOUT_MS);
});

testFoundation('does not run deactivation UX on mouseup after mousedown pressed for longer than deactivation timeout',
({foundation, adapter, mockRaf}) => {
const handlers = captureHandlers(adapter);
foundation.init();
mockRaf.flush();

handlers.mousedown();
mockRaf.flush();

setTimeout(() => {
td.verify(adapter.removeClass(cssClasses.BG_ACTIVE));
td.verify(adapter.addClass(cssClasses.BG_BOUNDED_ACTIVE_FILL));
td.verify(adapter.addClass(cssClasses.FG_BOUNDED_ACTIVE_FILL));
handlers.mouseup();
mockRaf.flush();
td.verify(adapter.removeClass(cssClasses.BG_ACTIVE));
td.verify(adapter.addClass(cssClasses.BG_BOUNDED_ACTIVE_FILL));
td.verify(adapter.addClass(cssClasses.FG_BOUNDED_ACTIVE_FILL));
}, foundation.DEACTIVATION_TIMEOUT_MS);
});

testFoundation('only re-activates when there are no additional pointer events to be processed',
({foundation, adapter, mockRaf}) => {
const handlers = captureHandlers(adapter);
Expand Down

0 comments on commit 7eceb51

Please sign in to comment.