-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
feat(ripple): Implement subset of improved interaction response guide… #363
Changes from 1 commit
7eceb51
7911662
1cc19ac
ef7a680
94b5edf
645e58f
d876879
e6d6064
9bf03c9
2a1df0d
5ae91bd
008f39b
b760901
c6970ad
e1683f3
e2beadf
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 |
---|---|---|
|
@@ -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; | ||
|
@@ -110,6 +112,9 @@ export default class MDCRippleFoundation extends MDCFoundation { | |
wasElementMadeActive: false, | ||
activationStartTime: 0, | ||
activationEvent: null, | ||
isProgrammatic: false, | ||
isTimeoutDeactivated: false, | ||
deactivationTimeout: null, | ||
}; | ||
} | ||
|
||
|
@@ -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(() => { | ||
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. same comment as above |
||
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 | ||
|
@@ -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); | ||
|
@@ -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); | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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', | ||
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. so for both of these, the tests will actually exit before the To prevent this, you can use lolex, which we use in our codebase to mock JS timers. You can see examples of this in the following tests: |
||
({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)); | ||
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. Not exactly sure how these tests are different from the ones above... Should this be verifying that these adapter methods are not called before |
||
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); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this, as well as
isProgrammatic
, need to be added to the return value ofdefaultActivationState_()