From 8864e9aefeed11296824e3808757dc9aa71c20e0 Mon Sep 17 00:00:00 2001
From: dan <dan.abramov@gmail.com>
Date: Tue, 16 Apr 2024 22:47:33 +0100
Subject: Restructure feed hover state machine code (#3550)

---
 src/components/ProfileHoverCard/index.web.tsx | 134 +++++++++++++++-----------
 1 file changed, 77 insertions(+), 57 deletions(-)

(limited to 'src/components/ProfileHoverCard/index.web.tsx')

diff --git a/src/components/ProfileHoverCard/index.web.tsx b/src/components/ProfileHoverCard/index.web.tsx
index 370baccbb..d7036e779 100644
--- a/src/components/ProfileHoverCard/index.web.tsx
+++ b/src/components/ProfileHoverCard/index.web.tsx
@@ -59,9 +59,9 @@ type Action =
   | 'pressed'
   | 'hovered'
   | 'unhovered'
-  | 'show-timer-elapsed'
-  | 'hide-timer-elapsed'
-  | 'hide-animation-completed'
+  | 'hovered-long-enough'
+  | 'unhovered-long-enough'
+  | 'finished-animating-hide'
 
 const SHOW_DELAY = 350
 const SHOW_DURATION = 300
@@ -76,90 +76,110 @@ export function ProfileHoverCardInner(props: ProfileHoverCardProps) {
   const [currentState, dispatch] = React.useReducer(
     // Tip: console.log(state, action) when debugging.
     (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') {
+        return hidden()
+      }
+
+      // --- Hidden ---
+      // In the beginning, the card is not displayed.
+      function hidden(): State {
         return {stage: 'hidden'}
       }
 
+      // The user can kick things off by hovering a target.
       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') {
-          return {
-            stage: 'might-show',
-            effect() {
-              const id = setTimeout(
-                () => dispatch('show-timer-elapsed'),
-                SHOW_DELAY,
-              )
-              return () => {
-                clearTimeout(id)
-              }
-            },
-          }
+          return mightShow(SHOW_DELAY)
         }
       }
 
+      // --- 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') {
-        // 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') {
-          return {stage: 'hidden'}
+          return hidden()
         }
-        if (action === 'show-timer-elapsed') {
-          return {stage: 'showing'}
+        if (action === 'hovered-long-enough') {
+          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') {
-        // We're showing the card now.
-        // If the user unhovers, we'll start a grace period before hiding the card.
         if (action === 'unhovered') {
-          return {
-            stage: 'might-hide',
-            effect() {
-              const id = setTimeout(
-                () => dispatch('hide-timer-elapsed'),
-                HIDE_DELAY,
-              )
-              return () => clearTimeout(id)
-            },
-          }
+          return mightHide(HIDE_DELAY)
+        }
+      }
+
+      // --- 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') {
-        // 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') {
-          return {stage: 'showing'}
+          return showing()
         }
-        if (action === 'hide-timer-elapsed') {
-          return {
-            stage: 'hiding',
-            effect() {
-              const id = setTimeout(
-                () => dispatch('hide-animation-completed'),
-                HIDE_DURATION,
-              )
-              return () => clearTimeout(id)
-            },
-          }
+        if (action === 'unhovered-long-enough') {
+          return hiding(HIDE_DURATION)
+        }
+      }
+
+      // --- 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') {
-        // We're currently playing the hiding animation.
-        // We'll ignore all inputs now and wait for the animation to finish.
-        // At that point, we'll hide the entire thing, going back to square one.
-        if (action === 'hide-animation-completed') {
-          return {stage: 'hidden'}
+        if (action === 'finished-animating-hide') {
+          return hidden()
         }
       }
 
-      // Something else happened. Keep calm and carry on.
       return state
     },
     {stage: 'hidden'},
-- 
cgit 1.4.1

