Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Scrolling while target is hovered and card is visible should hide the card (#3586)

* Don't remove the effect, it's not needed here (and wrong)

* Differentiate between hovering target and card

* Group related code closer

* Hide on scroll away

* Use named arguments

* Inline defaults

* Track reason we're showing

* Only hide on scroll away while hovering target

authored by

dan and committed by
GitHub
1e26654a 4a771b93

+78 -36
+78 -36
src/components/ProfileHoverCard/index.web.tsx
··· 50 50 return isTouchDevice ? props.children : <ProfileHoverCardInner {...props} /> 51 51 } 52 52 53 - type State = { 54 - stage: 'hidden' | 'might-show' | 'showing' | 'might-hide' | 'hiding' 55 - effect?: () => () => any 56 - } 53 + type State = 54 + | { 55 + stage: 'hidden' | 'might-hide' | 'hiding' 56 + effect?: () => () => any 57 + } 58 + | { 59 + stage: 'might-show' | 'showing' 60 + effect?: () => () => any 61 + reason: 'hovered-target' | 'hovered-card' 62 + } 57 63 58 64 type Action = 59 65 | 'pressed' 60 - | 'hovered' 61 - | 'unhovered' 66 + | 'scrolled-while-showing' 67 + | 'hovered-target' 68 + | 'unhovered-target' 69 + | 'hovered-card' 70 + | 'unhovered-card' 62 71 | 'hovered-long-enough' 63 72 | 'unhovered-long-enough' 64 73 | 'finished-animating-hide' ··· 87 96 function hidden(): State { 88 97 return {stage: 'hidden'} 89 98 } 90 - 91 - // The user can kick things off by hovering a target. 92 99 if (state.stage === 'hidden') { 93 - if (action === 'hovered') { 94 - return mightShow(SHOW_DELAY) 100 + // The user can kick things off by hovering a target. 101 + if (action === 'hovered-target') { 102 + return mightShow({ 103 + reason: action, 104 + }) 95 105 } 96 106 } 97 107 98 108 // --- Might Show --- 99 109 // The card is not visible yet but we're considering showing it. 100 - function mightShow(waitMs: number): State { 110 + function mightShow({ 111 + waitMs = SHOW_DELAY, 112 + reason, 113 + }: { 114 + waitMs?: number 115 + reason: 'hovered-target' | 'hovered-card' 116 + }): State { 101 117 return { 102 118 stage: 'might-show', 119 + reason, 103 120 effect() { 104 121 const id = setTimeout(() => dispatch('hovered-long-enough'), waitMs) 105 122 return () => { ··· 108 125 }, 109 126 } 110 127 } 111 - 112 - // We'll make a decision at the end of a grace period timeout. 113 128 if (state.stage === 'might-show') { 114 - if (action === 'unhovered') { 129 + // We'll make a decision at the end of a grace period timeout. 130 + if (action === 'unhovered-target' || action === 'unhovered-card') { 115 131 return hidden() 116 132 } 117 133 if (action === 'hovered-long-enough') { 118 - return showing() 134 + return showing({ 135 + reason: state.reason, 136 + }) 119 137 } 120 138 } 121 139 122 140 // --- Showing --- 123 141 // The card is beginning to show up and then will remain visible. 124 - function showing(): State { 125 - return {stage: 'showing'} 142 + function showing({ 143 + reason, 144 + }: { 145 + reason: 'hovered-target' | 'hovered-card' 146 + }): State { 147 + return { 148 + stage: 'showing', 149 + reason, 150 + effect() { 151 + function onScroll() { 152 + dispatch('scrolled-while-showing') 153 + } 154 + window.addEventListener('scroll', onScroll) 155 + return () => window.removeEventListener('scroll', onScroll) 156 + }, 157 + } 126 158 } 127 - 128 - // If the user moves the pointer away, we'll begin to consider hiding it. 129 159 if (state.stage === 'showing') { 130 - if (action === 'unhovered') { 131 - return mightHide(HIDE_DELAY) 160 + // If the user moves the pointer away, we'll begin to consider hiding it. 161 + if (action === 'unhovered-target' || action === 'unhovered-card') { 162 + return mightHide() 163 + } 164 + // Scrolling away if the hover is on the target instantly hides without a delay. 165 + // If the hover is already on the card, we won't this. 166 + if ( 167 + state.reason === 'hovered-target' && 168 + action === 'scrolled-while-showing' 169 + ) { 170 + return hiding() 132 171 } 133 172 } 134 173 135 174 // --- Might Hide --- 136 175 // The user has moved hover away from a visible card. 137 - function mightHide(waitMs: number): State { 176 + function mightHide({waitMs = HIDE_DELAY}: {waitMs?: number} = {}): State { 138 177 return { 139 178 stage: 'might-hide', 140 179 effect() { ··· 146 185 }, 147 186 } 148 187 } 149 - 150 - // We'll make a decision based on whether it received hover again in time. 151 188 if (state.stage === 'might-hide') { 152 - if (action === 'hovered') { 153 - return showing() 189 + // We'll make a decision based on whether it received hover again in time. 190 + if (action === 'hovered-target' || action === 'hovered-card') { 191 + return showing({ 192 + reason: action, 193 + }) 154 194 } 155 195 if (action === 'unhovered-long-enough') { 156 - return hiding(HIDE_DURATION) 196 + return hiding() 157 197 } 158 198 } 159 199 160 200 // --- Hiding --- 161 201 // The user waited enough outside that we're hiding the card. 162 - function hiding(animationDurationMs: number): State { 202 + function hiding({ 203 + animationDurationMs = HIDE_DURATION, 204 + }: { 205 + animationDurationMs?: number 206 + } = {}): State { 163 207 return { 164 208 stage: 'hiding', 165 209 effect() { ··· 171 215 }, 172 216 } 173 217 } 174 - 175 - // While hiding, we don't want to be interrupted by anything else. 176 - // When the animation finishes, we loop back to the initial hidden state. 177 218 if (state.stage === 'hiding') { 219 + // While hiding, we don't want to be interrupted by anything else. 220 + // When the animation finishes, we loop back to the initial hidden state. 178 221 if (action === 'finished-animating-hide') { 179 222 return hidden() 180 223 } ··· 188 231 React.useEffect(() => { 189 232 if (currentState.effect) { 190 233 const effect = currentState.effect 191 - delete currentState.effect // Mark as completed 192 234 return effect() 193 235 } 194 236 }, [currentState]) ··· 204 246 205 247 const onPointerEnterTarget = React.useCallback(() => { 206 248 prefetchIfNeeded() 207 - dispatch('hovered') 249 + dispatch('hovered-target') 208 250 }, [prefetchIfNeeded]) 209 251 210 252 const onPointerLeaveTarget = React.useCallback(() => { 211 - dispatch('unhovered') 253 + dispatch('unhovered-target') 212 254 }, []) 213 255 214 256 const onPointerEnterCard = React.useCallback(() => { 215 - dispatch('hovered') 257 + dispatch('hovered-card') 216 258 }, []) 217 259 218 260 const onPointerLeaveCard = React.useCallback(() => { 219 - dispatch('unhovered') 261 + dispatch('unhovered-card') 220 262 }, []) 221 263 222 264 const onPress = React.useCallback(() => {