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 targetzio/stable
parent
4a771b9350
commit
1e26654a9b
|
@ -50,15 +50,24 @@ export function ProfileHoverCard(props: ProfileHoverCardProps) {
|
||||||
return isTouchDevice ? props.children : <ProfileHoverCardInner {...props} />
|
return isTouchDevice ? props.children : <ProfileHoverCardInner {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = {
|
type State =
|
||||||
stage: 'hidden' | 'might-show' | 'showing' | 'might-hide' | 'hiding'
|
| {
|
||||||
effect?: () => () => any
|
stage: 'hidden' | 'might-hide' | 'hiding'
|
||||||
}
|
effect?: () => () => any
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
stage: 'might-show' | 'showing'
|
||||||
|
effect?: () => () => any
|
||||||
|
reason: 'hovered-target' | 'hovered-card'
|
||||||
|
}
|
||||||
|
|
||||||
type Action =
|
type Action =
|
||||||
| 'pressed'
|
| 'pressed'
|
||||||
| 'hovered'
|
| 'scrolled-while-showing'
|
||||||
| 'unhovered'
|
| 'hovered-target'
|
||||||
|
| 'unhovered-target'
|
||||||
|
| 'hovered-card'
|
||||||
|
| 'unhovered-card'
|
||||||
| 'hovered-long-enough'
|
| 'hovered-long-enough'
|
||||||
| 'unhovered-long-enough'
|
| 'unhovered-long-enough'
|
||||||
| 'finished-animating-hide'
|
| 'finished-animating-hide'
|
||||||
|
@ -87,19 +96,27 @@ export function ProfileHoverCardInner(props: ProfileHoverCardProps) {
|
||||||
function hidden(): State {
|
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') {
|
||||||
if (action === 'hovered') {
|
// The user can kick things off by hovering a target.
|
||||||
return mightShow(SHOW_DELAY)
|
if (action === 'hovered-target') {
|
||||||
|
return mightShow({
|
||||||
|
reason: action,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Might Show ---
|
// --- Might Show ---
|
||||||
// The card is not visible yet but we're considering showing it.
|
// The card is not visible yet but we're considering showing it.
|
||||||
function mightShow(waitMs: number): State {
|
function mightShow({
|
||||||
|
waitMs = SHOW_DELAY,
|
||||||
|
reason,
|
||||||
|
}: {
|
||||||
|
waitMs?: number
|
||||||
|
reason: 'hovered-target' | 'hovered-card'
|
||||||
|
}): State {
|
||||||
return {
|
return {
|
||||||
stage: 'might-show',
|
stage: 'might-show',
|
||||||
|
reason,
|
||||||
effect() {
|
effect() {
|
||||||
const id = setTimeout(() => dispatch('hovered-long-enough'), waitMs)
|
const id = setTimeout(() => dispatch('hovered-long-enough'), waitMs)
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -108,33 +125,55 @@ export function ProfileHoverCardInner(props: ProfileHoverCardProps) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We'll make a decision at the end of a grace period timeout.
|
|
||||||
if (state.stage === 'might-show') {
|
if (state.stage === 'might-show') {
|
||||||
if (action === 'unhovered') {
|
// We'll make a decision at the end of a grace period timeout.
|
||||||
|
if (action === 'unhovered-target' || action === 'unhovered-card') {
|
||||||
return hidden()
|
return hidden()
|
||||||
}
|
}
|
||||||
if (action === 'hovered-long-enough') {
|
if (action === 'hovered-long-enough') {
|
||||||
return showing()
|
return showing({
|
||||||
|
reason: state.reason,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Showing ---
|
// --- Showing ---
|
||||||
// The card is beginning to show up and then will remain visible.
|
// The card is beginning to show up and then will remain visible.
|
||||||
function showing(): State {
|
function showing({
|
||||||
return {stage: 'showing'}
|
reason,
|
||||||
|
}: {
|
||||||
|
reason: 'hovered-target' | 'hovered-card'
|
||||||
|
}): State {
|
||||||
|
return {
|
||||||
|
stage: 'showing',
|
||||||
|
reason,
|
||||||
|
effect() {
|
||||||
|
function onScroll() {
|
||||||
|
dispatch('scrolled-while-showing')
|
||||||
|
}
|
||||||
|
window.addEventListener('scroll', onScroll)
|
||||||
|
return () => window.removeEventListener('scroll', onScroll)
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user moves the pointer away, we'll begin to consider hiding it.
|
|
||||||
if (state.stage === 'showing') {
|
if (state.stage === 'showing') {
|
||||||
if (action === 'unhovered') {
|
// If the user moves the pointer away, we'll begin to consider hiding it.
|
||||||
return mightHide(HIDE_DELAY)
|
if (action === 'unhovered-target' || action === 'unhovered-card') {
|
||||||
|
return mightHide()
|
||||||
|
}
|
||||||
|
// Scrolling away if the hover is on the target instantly hides without a delay.
|
||||||
|
// If the hover is already on the card, we won't this.
|
||||||
|
if (
|
||||||
|
state.reason === 'hovered-target' &&
|
||||||
|
action === 'scrolled-while-showing'
|
||||||
|
) {
|
||||||
|
return hiding()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Might Hide ---
|
// --- Might Hide ---
|
||||||
// The user has moved hover away from a visible card.
|
// The user has moved hover away from a visible card.
|
||||||
function mightHide(waitMs: number): State {
|
function mightHide({waitMs = HIDE_DELAY}: {waitMs?: number} = {}): State {
|
||||||
return {
|
return {
|
||||||
stage: 'might-hide',
|
stage: 'might-hide',
|
||||||
effect() {
|
effect() {
|
||||||
|
@ -146,20 +185,25 @@ export function ProfileHoverCardInner(props: ProfileHoverCardProps) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We'll make a decision based on whether it received hover again in time.
|
|
||||||
if (state.stage === 'might-hide') {
|
if (state.stage === 'might-hide') {
|
||||||
if (action === 'hovered') {
|
// We'll make a decision based on whether it received hover again in time.
|
||||||
return showing()
|
if (action === 'hovered-target' || action === 'hovered-card') {
|
||||||
|
return showing({
|
||||||
|
reason: action,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if (action === 'unhovered-long-enough') {
|
if (action === 'unhovered-long-enough') {
|
||||||
return hiding(HIDE_DURATION)
|
return hiding()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Hiding ---
|
// --- Hiding ---
|
||||||
// The user waited enough outside that we're hiding the card.
|
// The user waited enough outside that we're hiding the card.
|
||||||
function hiding(animationDurationMs: number): State {
|
function hiding({
|
||||||
|
animationDurationMs = HIDE_DURATION,
|
||||||
|
}: {
|
||||||
|
animationDurationMs?: number
|
||||||
|
} = {}): State {
|
||||||
return {
|
return {
|
||||||
stage: 'hiding',
|
stage: 'hiding',
|
||||||
effect() {
|
effect() {
|
||||||
|
@ -171,10 +215,9 @@ export function ProfileHoverCardInner(props: ProfileHoverCardProps) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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') {
|
||||||
|
// 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 (action === 'finished-animating-hide') {
|
if (action === 'finished-animating-hide') {
|
||||||
return hidden()
|
return hidden()
|
||||||
}
|
}
|
||||||
|
@ -188,7 +231,6 @@ export function ProfileHoverCardInner(props: ProfileHoverCardProps) {
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (currentState.effect) {
|
if (currentState.effect) {
|
||||||
const effect = currentState.effect
|
const effect = currentState.effect
|
||||||
delete currentState.effect // Mark as completed
|
|
||||||
return effect()
|
return effect()
|
||||||
}
|
}
|
||||||
}, [currentState])
|
}, [currentState])
|
||||||
|
@ -204,19 +246,19 @@ export function ProfileHoverCardInner(props: ProfileHoverCardProps) {
|
||||||
|
|
||||||
const onPointerEnterTarget = React.useCallback(() => {
|
const onPointerEnterTarget = React.useCallback(() => {
|
||||||
prefetchIfNeeded()
|
prefetchIfNeeded()
|
||||||
dispatch('hovered')
|
dispatch('hovered-target')
|
||||||
}, [prefetchIfNeeded])
|
}, [prefetchIfNeeded])
|
||||||
|
|
||||||
const onPointerLeaveTarget = React.useCallback(() => {
|
const onPointerLeaveTarget = React.useCallback(() => {
|
||||||
dispatch('unhovered')
|
dispatch('unhovered-target')
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onPointerEnterCard = React.useCallback(() => {
|
const onPointerEnterCard = React.useCallback(() => {
|
||||||
dispatch('hovered')
|
dispatch('hovered-card')
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onPointerLeaveCard = React.useCallback(() => {
|
const onPointerLeaveCard = React.useCallback(() => {
|
||||||
dispatch('unhovered')
|
dispatch('unhovered-card')
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onPress = React.useCallback(() => {
|
const onPress = React.useCallback(() => {
|
||||||
|
|
Loading…
Reference in New Issue