Restructure feed hover state machine code (#3550)
parent
69d3768006
commit
8864e9aefe
|
@ -59,9 +59,9 @@ type Action =
|
||||||
| 'pressed'
|
| 'pressed'
|
||||||
| 'hovered'
|
| 'hovered'
|
||||||
| 'unhovered'
|
| 'unhovered'
|
||||||
| 'show-timer-elapsed'
|
| 'hovered-long-enough'
|
||||||
| 'hide-timer-elapsed'
|
| 'unhovered-long-enough'
|
||||||
| 'hide-animation-completed'
|
| 'finished-animating-hide'
|
||||||
|
|
||||||
const SHOW_DELAY = 350
|
const SHOW_DELAY = 350
|
||||||
const SHOW_DURATION = 300
|
const SHOW_DURATION = 300
|
||||||
|
@ -76,90 +76,110 @@ export function ProfileHoverCardInner(props: ProfileHoverCardProps) {
|
||||||
const [currentState, dispatch] = React.useReducer(
|
const [currentState, dispatch] = React.useReducer(
|
||||||
// Tip: console.log(state, action) when debugging.
|
// Tip: console.log(state, action) when debugging.
|
||||||
(state: State, action: Action): State => {
|
(state: State, action: Action): State => {
|
||||||
// Regardless of which stage we're in, pressing always hides the card.
|
// Pressing within a card should always hide it.
|
||||||
|
// No matter which stage we're in.
|
||||||
if (action === 'pressed') {
|
if (action === 'pressed') {
|
||||||
|
return hidden()
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Hidden ---
|
||||||
|
// In the beginning, the card is not displayed.
|
||||||
|
function hidden(): State {
|
||||||
return {stage: 'hidden'}
|
return {stage: 'hidden'}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The user can kick things off by hovering a target.
|
||||||
if (state.stage === 'hidden') {
|
if (state.stage === 'hidden') {
|
||||||
// Our story starts when the card is hidden.
|
|
||||||
// If the user hovers, we kick off a grace period before showing the card.
|
|
||||||
if (action === 'hovered') {
|
if (action === 'hovered') {
|
||||||
return {
|
return mightShow(SHOW_DELAY)
|
||||||
stage: 'might-show',
|
|
||||||
effect() {
|
|
||||||
const id = setTimeout(
|
|
||||||
() => dispatch('show-timer-elapsed'),
|
|
||||||
SHOW_DELAY,
|
|
||||||
)
|
|
||||||
return () => {
|
|
||||||
clearTimeout(id)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Might Show ---
|
||||||
|
// The card is not visible yet but we're considering showing it.
|
||||||
|
function mightShow(waitMs: number): State {
|
||||||
|
return {
|
||||||
|
stage: 'might-show',
|
||||||
|
effect() {
|
||||||
|
const id = setTimeout(() => dispatch('hovered-long-enough'), waitMs)
|
||||||
|
return () => {
|
||||||
|
clearTimeout(id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll make a decision at the end of a grace period timeout.
|
||||||
if (state.stage === 'might-show') {
|
if (state.stage === 'might-show') {
|
||||||
// We're in the grace period when we decide whether to show the card.
|
|
||||||
// At this point, two things can happen. Either the user unhovers, and
|
|
||||||
// we go back to hidden--or they linger enough that we'll show the card.
|
|
||||||
if (action === 'unhovered') {
|
if (action === 'unhovered') {
|
||||||
return {stage: 'hidden'}
|
return hidden()
|
||||||
}
|
}
|
||||||
if (action === 'show-timer-elapsed') {
|
if (action === 'hovered-long-enough') {
|
||||||
return {stage: 'showing'}
|
return showing()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Showing ---
|
||||||
|
// The card is beginning to show up and then will remain visible.
|
||||||
|
function showing(): State {
|
||||||
|
return {stage: 'showing'}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user moves the pointer away, we'll begin to consider hiding it.
|
||||||
if (state.stage === 'showing') {
|
if (state.stage === 'showing') {
|
||||||
// We're showing the card now.
|
|
||||||
// If the user unhovers, we'll start a grace period before hiding the card.
|
|
||||||
if (action === 'unhovered') {
|
if (action === 'unhovered') {
|
||||||
return {
|
return mightHide(HIDE_DELAY)
|
||||||
stage: 'might-hide',
|
|
||||||
effect() {
|
|
||||||
const id = setTimeout(
|
|
||||||
() => dispatch('hide-timer-elapsed'),
|
|
||||||
HIDE_DELAY,
|
|
||||||
)
|
|
||||||
return () => clearTimeout(id)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Might Hide ---
|
||||||
|
// The user has moved hover away from a visible card.
|
||||||
|
function mightHide(waitMs: number): State {
|
||||||
|
return {
|
||||||
|
stage: 'might-hide',
|
||||||
|
effect() {
|
||||||
|
const id = setTimeout(
|
||||||
|
() => dispatch('unhovered-long-enough'),
|
||||||
|
waitMs,
|
||||||
|
)
|
||||||
|
return () => clearTimeout(id)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll make a decision based on whether it received hover again in time.
|
||||||
if (state.stage === 'might-hide') {
|
if (state.stage === 'might-hide') {
|
||||||
// We're in the grace period when we decide whether to hide the card.
|
|
||||||
// At this point, two things can happen. Either the user hovers, and
|
|
||||||
// we go back to showing it--or they linger enough that we'll start hiding the card.
|
|
||||||
if (action === 'hovered') {
|
if (action === 'hovered') {
|
||||||
return {stage: 'showing'}
|
return showing()
|
||||||
}
|
}
|
||||||
if (action === 'hide-timer-elapsed') {
|
if (action === 'unhovered-long-enough') {
|
||||||
return {
|
return hiding(HIDE_DURATION)
|
||||||
stage: 'hiding',
|
|
||||||
effect() {
|
|
||||||
const id = setTimeout(
|
|
||||||
() => dispatch('hide-animation-completed'),
|
|
||||||
HIDE_DURATION,
|
|
||||||
)
|
|
||||||
return () => clearTimeout(id)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Hiding ---
|
||||||
|
// The user waited enough outside that we're hiding the card.
|
||||||
|
function hiding(animationDurationMs: number): State {
|
||||||
|
return {
|
||||||
|
stage: 'hiding',
|
||||||
|
effect() {
|
||||||
|
const id = setTimeout(
|
||||||
|
() => dispatch('finished-animating-hide'),
|
||||||
|
animationDurationMs,
|
||||||
|
)
|
||||||
|
return () => clearTimeout(id)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// While hiding, we don't want to be interrupted by anything else.
|
||||||
|
// When the animation finishes, we loop back to the initial hidden state.
|
||||||
if (state.stage === 'hiding') {
|
if (state.stage === 'hiding') {
|
||||||
// We're currently playing the hiding animation.
|
if (action === 'finished-animating-hide') {
|
||||||
// We'll ignore all inputs now and wait for the animation to finish.
|
return hidden()
|
||||||
// At that point, we'll hide the entire thing, going back to square one.
|
|
||||||
if (action === 'hide-animation-completed') {
|
|
||||||
return {stage: 'hidden'}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Something else happened. Keep calm and carry on.
|
|
||||||
return state
|
return state
|
||||||
},
|
},
|
||||||
{stage: 'hidden'},
|
{stage: 'hidden'},
|
||||||
|
|
Loading…
Reference in New Issue