···5959 | 'pressed'
6060 | 'hovered'
6161 | 'unhovered'
6262- | 'show-timer-elapsed'
6363- | 'hide-timer-elapsed'
6464- | 'hide-animation-completed'
6262+ | 'hovered-long-enough'
6363+ | 'unhovered-long-enough'
6464+ | 'finished-animating-hide'
65656666const SHOW_DELAY = 350
6767const SHOW_DURATION = 300
···7676 const [currentState, dispatch] = React.useReducer(
7777 // Tip: console.log(state, action) when debugging.
7878 (state: State, action: Action): State => {
7979- // Regardless of which stage we're in, pressing always hides the card.
7979+ // Pressing within a card should always hide it.
8080+ // No matter which stage we're in.
8081 if (action === 'pressed') {
8282+ return hidden()
8383+ }
8484+8585+ // --- Hidden ---
8686+ // In the beginning, the card is not displayed.
8787+ function hidden(): State {
8188 return {stage: 'hidden'}
8289 }
83909191+ // The user can kick things off by hovering a target.
8492 if (state.stage === 'hidden') {
8585- // Our story starts when the card is hidden.
8686- // If the user hovers, we kick off a grace period before showing the card.
8793 if (action === 'hovered') {
8888- return {
8989- stage: 'might-show',
9090- effect() {
9191- const id = setTimeout(
9292- () => dispatch('show-timer-elapsed'),
9393- SHOW_DELAY,
9494- )
9595- return () => {
9696- clearTimeout(id)
9797- }
9898- },
9999- }
9494+ return mightShow(SHOW_DELAY)
9595+ }
9696+ }
9797+9898+ // --- Might Show ---
9999+ // The card is not visible yet but we're considering showing it.
100100+ function mightShow(waitMs: number): State {
101101+ return {
102102+ stage: 'might-show',
103103+ effect() {
104104+ const id = setTimeout(() => dispatch('hovered-long-enough'), waitMs)
105105+ return () => {
106106+ clearTimeout(id)
107107+ }
108108+ },
100109 }
101110 }
102111112112+ // We'll make a decision at the end of a grace period timeout.
103113 if (state.stage === 'might-show') {
104104- // We're in the grace period when we decide whether to show the card.
105105- // At this point, two things can happen. Either the user unhovers, and
106106- // we go back to hidden--or they linger enough that we'll show the card.
107114 if (action === 'unhovered') {
108108- return {stage: 'hidden'}
115115+ return hidden()
109116 }
110110- if (action === 'show-timer-elapsed') {
111111- return {stage: 'showing'}
117117+ if (action === 'hovered-long-enough') {
118118+ return showing()
112119 }
113120 }
114121122122+ // --- Showing ---
123123+ // The card is beginning to show up and then will remain visible.
124124+ function showing(): State {
125125+ return {stage: 'showing'}
126126+ }
127127+128128+ // If the user moves the pointer away, we'll begin to consider hiding it.
115129 if (state.stage === 'showing') {
116116- // We're showing the card now.
117117- // If the user unhovers, we'll start a grace period before hiding the card.
118130 if (action === 'unhovered') {
119119- return {
120120- stage: 'might-hide',
121121- effect() {
122122- const id = setTimeout(
123123- () => dispatch('hide-timer-elapsed'),
124124- HIDE_DELAY,
125125- )
126126- return () => clearTimeout(id)
127127- },
128128- }
131131+ return mightHide(HIDE_DELAY)
129132 }
130133 }
131134135135+ // --- Might Hide ---
136136+ // The user has moved hover away from a visible card.
137137+ function mightHide(waitMs: number): State {
138138+ return {
139139+ stage: 'might-hide',
140140+ effect() {
141141+ const id = setTimeout(
142142+ () => dispatch('unhovered-long-enough'),
143143+ waitMs,
144144+ )
145145+ return () => clearTimeout(id)
146146+ },
147147+ }
148148+ }
149149+150150+ // We'll make a decision based on whether it received hover again in time.
132151 if (state.stage === 'might-hide') {
133133- // We're in the grace period when we decide whether to hide the card.
134134- // At this point, two things can happen. Either the user hovers, and
135135- // we go back to showing it--or they linger enough that we'll start hiding the card.
136152 if (action === 'hovered') {
137137- return {stage: 'showing'}
153153+ return showing()
138154 }
139139- if (action === 'hide-timer-elapsed') {
140140- return {
141141- stage: 'hiding',
142142- effect() {
143143- const id = setTimeout(
144144- () => dispatch('hide-animation-completed'),
145145- HIDE_DURATION,
146146- )
147147- return () => clearTimeout(id)
148148- },
149149- }
155155+ if (action === 'unhovered-long-enough') {
156156+ return hiding(HIDE_DURATION)
157157+ }
158158+ }
159159+160160+ // --- Hiding ---
161161+ // The user waited enough outside that we're hiding the card.
162162+ function hiding(animationDurationMs: number): State {
163163+ return {
164164+ stage: 'hiding',
165165+ effect() {
166166+ const id = setTimeout(
167167+ () => dispatch('finished-animating-hide'),
168168+ animationDurationMs,
169169+ )
170170+ return () => clearTimeout(id)
171171+ },
150172 }
151173 }
152174175175+ // While hiding, we don't want to be interrupted by anything else.
176176+ // When the animation finishes, we loop back to the initial hidden state.
153177 if (state.stage === 'hiding') {
154154- // We're currently playing the hiding animation.
155155- // We'll ignore all inputs now and wait for the animation to finish.
156156- // At that point, we'll hide the entire thing, going back to square one.
157157- if (action === 'hide-animation-completed') {
158158- return {stage: 'hidden'}
178178+ if (action === 'finished-animating-hide') {
179179+ return hidden()
159180 }
160181 }
161182162162- // Something else happened. Keep calm and carry on.
163183 return state
164184 },
165185 {stage: 'hidden'},